Enables Microsoft's multi-stage attack detection, which correlates low-fidelity signals from many sources into one high-confidence incident (
azurerm_sentinel_alert_rule_fusion). Targetshashicorp/azurerm ~> 4.0.
- 🧩 Microsoft's multi-stage attack detection. Fusion correlates low-fidelity signals from many sources into a single high-confidence incident.
- ℹ️ You do not write the correlation logic — the template is the detection. This resource enables it and, optionally, narrows which source signals feed it.
⚠️ Everysource_signalsentry is a reduction in coverage. The module's default is to supply none.- 🔴
nameis deprecated and removed in provider v5.0, so this module neither accepts nor emits it — the one place in this family offering deliberately less than the pinned schema allows. ⚠️ The API version for this resource is a preview one.- 🔁
enabledis the only argument that updates in place. - 🔒 No credential is accepted and none is emitted.
💡 Why it matters: Fusion's value is precisely the breadth of what it correlates, which makes every tuning knob a way to make it worse. The module therefore defaults to no tuning at all, and emits the signals you turned off so the reduction is visible.
Microsoft states that after March 31, 2027, Microsoft Sentinel will no longer be supported in the Azure portal and will be available only in the Microsoft Defender portal, and that customers using Microsoft Sentinel in the Azure portal will be redirected to the Defender portal. Since July 2025 many new customers are onboarded and redirected to the Defender portal automatically.
This does not change what this module manages.
azurerm_sentinel_*are ARM resources and Terraform talks to ARM, not to a portal, so these resources continue to exist and stay manageable from code across that date. What moves is the management experience - runbooks, screenshots, analyst training, and any procedure that ends in a human clicking through Sentinel in the Azure portal. Plan that transition on its own schedule; this module emitsmanagement_moves_to_the_defender_portalso the date reaches plan output and inventory reports rather than living only in documentation.
If this module saves you time, please consider supporting its continued development:
- ⭐ Star the repository on GitHub.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
law["terraform-azurerm-log-analytics-workspace: Sentinel has no workspace of its own. Everything here lives on a Log Analytics workspace, and its RETENTION decides how far back any detection can look."]
onboard["terraform-azurerm-sentinel-log-analytics-workspace-onboarding: THE GATE. Nothing else in this family works until it exists, and DESTROYING IT OFFBOARDS SENTINEL from the workspace."]
wsid["AND WIRE log_analytics_workspace_id FROM THIS MODULE'S workspace_id OUTPUT, not from the workspace module's id. Both carry the same value, but reading it through here makes Terraform ORDER ONBOARDING FIRST. The provider's own examples chain it this way."]
gotcha["NOTE THE FIELD NAME TRAP: azurerm_log_analytics_workspace exports BOTH id AND an attribute literally named workspace_id. Every module in this family wants the ARM RESOURCE ID, not the GUID. A bare GUID is rejected at plan time."]
cmk["and customer_managed_key_enabled is PERMANENT FOR THE WORKSPACE: once onboarded with it true, the workspace cannot be onboarded again with it false. It is also force-new, so a plan that looks like an ordinary replacement can leave the workspace offboarded and unable to come back the way you asked."]
sched["terraform-azurerm-sentinel-alert-rule-scheduled: THE WORKHORSE. Your KQL, on a schedule. query_period must be at least query_frequency or events between runs are never examined."]
nrt["terraform-azurerm-sentinel-alert-rule-nrt: the same shape MINUS the schedule, evaluated continuously. event_grouping is REQUIRED here, entity mappings cap at 5 combined not 10, and alert_rule_template_version is force-new."]
nrtq["BUT NRT RESTRICTS THE QUERY: joins, wide unions and long aggregations are the usual casualties, and NOTHING CHECKS THAT AT PLAN TIME. Build it in the analytics-rule wizard first; porting a scheduled rule's query straight in is the common way to get a rule that will not run."]
ms["terraform-azurerm-sentinel-alert-rule-ms-security-incident: PROMOTES alerts another Microsoft product already raised. Detects nothing itself. product_filter takes LEGACY product names, so Microsoft Defender for Endpoint is REJECTED and Microsoft Defender Advanced Threat Protection is accepted."]
fusion["terraform-azurerm-sentinel-alert-rule-fusion: Microsoft's MULTI-STAGE attack correlation. Its name argument is DEPRECATED and removed in provider v5.0, so this module neither accepts nor emits it."]
mlba["terraform-azurerm-sentinel-alert-rule-machine-learning-behavior-analytics: Microsoft's behaviour model. Needs a LEARNING PERIOD, so early silence is not a misconfiguration."]
ti["terraform-azurerm-sentinel-alert-rule-threat-intelligence: matches logs against threat-intelligence INDICATORS, and supplies none. With no indicator source it is a detection that CANNOT FIRE, and nothing about it looks wrong."]
tmpl["these three are TEMPLATE-DRIVEN: the alert_rule_template_guid IS the detection, so repointing it is a different detection rather than a retune, and enabled is the only thing that updates in place"]
anom["terraform-azurerm-sentinel-alert-rule-anomaly-built-in: TOGGLES a rule Microsoft ships. It creates nothing, so REMOVING THIS RESOURCE DISABLES THE RULE rather than releasing it."]
dup["terraform-azurerm-sentinel-alert-rule-anomaly-duplicate: a TUNED COPY, because a built-in rule's thresholds are read-only. Anything you do not override is INHERITED from the built-in rule."]
pair["so the usual shape is a PAIR, and it needs a decision the provider will not make: running the original AND a tuned duplicate in Production means the same detection active twice with two sets of thresholds"]
mode["and BOTH anomaly modules take a REQUIRED mode: Production or FLIGHTING. Flighting runs in evaluation - enabled, configured, and not contributing. Right while tuning, wrong to leave behind."]
gaps["THE FAMILY'S REAL RISK IS NOT AN OPEN DOOR, IT IS A SILENT DETECTION. enabled false, create_incident_enabled false, suppression_enabled true, mode Flighting, a trigger threshold above zero, a severity_filter missing High - each looks reasonable alone and each leaves a rule that passes a review asking IS IT CONFIGURED and fails one asking DOES IT WORK."]
emit["which is why every module here emits derived posture flags - is_active, creates_incidents, is_production_mode - and the two query modules gather them into a single detection_gaps output"]
law -->|"id"| onboard
cmk -->|"decide before the first apply"| onboard
onboard -->|"workspace_id"| wsid
gotcha -->|"read this first"| wsid
wsid -->|"log_analytics_workspace_id"| sched
wsid -->|"log_analytics_workspace_id"| nrt
wsid -->|"log_analytics_workspace_id"| ms
wsid -->|"log_analytics_workspace_id"| fusion
wsid -->|"log_analytics_workspace_id"| mlba
wsid -->|"log_analytics_workspace_id"| ti
wsid -->|"log_analytics_workspace_id"| anom
wsid -->|"log_analytics_workspace_id"| dup
nrtq -->|"unvalidatable"| nrt
tmpl -->|"shape"| fusion
tmpl -->|"shape"| mlba
tmpl -->|"shape"| ti
anom -->|"settings_definition_id becomes built_in_rule_id"| dup
pair -->|"disable or flight the original"| dup
mode -->|"required"| anom
mode -->|"required"| dup
gaps -->|"so"| emit
emit -->|"assert on these"| sched
emit -->|"assert on these"| nrt
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class onboard keystone;
class sched,nrt,ms,fusion,mlba,ti,anom,dup me;
class law,wsid,gotcha,cmk,nrtq,tmpl,pair,mode,gaps,emit sib;
flowchart TB
what["FUSION IS MICROSOFT'S MULTI-STAGE ATTACK DETECTION. It correlates LOW-FIDELITY signals from many sources into a single high-confidence incident."]
tmpl["you do not write the correlation logic - THE TEMPLATE IS THE DETECTION. This resource enables it and, optionally, narrows which source signals feed it. Repointing alert_rule_template_guid is a DIFFERENT DETECTION, not a retune."]
narrow["SO TREAT EVERY source_signals ENTRY AS A REDUCTION IN COVERAGE unless it only restates a default. Setting a source's enabled to false, or trimming a sub-type's severities_allowed, REMOVES SIGNAL FROM THE CORRELATION - and the incidents you stop seeing are not logged anywhere as suppressed."]
sev["severities_allowed is the field most likely to be set too narrowly. Fusion correlates WEAK signals, so excluding Low and Informational from a subtype can remove precisely the evidence the correlation needed."]
empty["which is why the module's DEFAULT IS TO SUPPLY NO SOURCE TUNING AT ALL - the template's own configuration is the widest coverage - and why disabled_source_signal_names is emitted on its own"]
dep["THE name ARGUMENT IS DEPRECATED AND REMOVED IN PROVIDER v5.0. The schema carries a deprecated flag, and merely READING the attribute back raises a deprecation warning at terraform validate."]
gone["SO THIS MODULE NEITHER ACCEPTS NOR EMITS IT. A field that disappears at the next major version is a migration handed to every caller, and the service names the rule perfectly well without one. Import still works: the ARM name is part of the Resource ID either way."]
less["this is the ONE PLACE IN THIS FAMILY where a module deliberately offers LESS than the pinned schema allows"]
preview["and note the API version for this resource is a PREVIEW one, which is worth knowing before depending on its stability"]
names["source and sub_type names come from the FUSION TEMPLATE, not a fixed list, and are left UNVALIDATED for that reason - the set grows as Microsoft adds detections, so an enum would eventually reject a valid current value"]
silent["and the family risk applies: enabled false leaves the multi-stage detection present in the workspace and SILENT - the state that survives a review asking IS IT CONFIGURED while failing one asking IS IT WORKING"]
this["terraform-azurerm-sentinel-alert-rule-fusion"]
keystone["azurerm_sentinel_alert_rule_fusion.this"]
what -->|"so"| tmpl
tmpl -->|"therefore"| narrow
narrow -->|"especially"| sev
sev -->|"so"| empty
empty -->|"posture"| this
dep -->|"therefore"| gone
gone -->|"note"| less
less -->|"and"| preview
preview -->|"typing"| this
names -->|"validation"| this
silent -->|"emitted as is_active"| this
this -->|"enables the multi-stage correlation"| keystone
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class this me;
class keystone keystone;
class what,tmpl,narrow,sev,empty,dep,gone,less,preview,names,silent sib;
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_sentinel_alert_rule_fusion.this |
1 | The keystone. The template is the detection. |
source blocks |
0..n | Optional per-signal tuning, with nested sub_type blocks. |
timeouts block |
0..1 | All four operations. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Azure resource provider | Microsoft.SecurityInsights, on a Log Analytics workspace — a preview API version for this resource |
| Provider block | None in this module. The caller configures provider "azurerm", including the mandatory features {} block, and supplies authentication. |
Schema notes that bite — confirmed against the live provider schema and its documentation:
- 🔴
nameis DEPRECATED on this resource and removed in provider v5.0. The schema carries the flag, and merely reading the attribute back raises a deprecation warning atterraform validate. This module neither accepts nor emits it. - The ARM API version for this resource is a PREVIEW version at the time of authoring, which is worth knowing before depending on its stability.
sourceandsub_typenames come from the Fusion template, not from a fixed list.sub_type.severities_allowedis required and must be non-empty.- Force-new:
log_analytics_workspace_id,alert_rule_template_guid. enabledis the only argument that updates in place. Everything else is force-new.- No
tags. Tag the workspace. lifecycleis not valid inside amoduleblock, so a caller cannot addignore_changesorprevent_destroy.
| Operation | Role | Scope |
|---|---|---|
| Creating, updating or deleting an analytics rule | Microsoft Sentinel Contributor | the resource group containing the Log Analytics workspace, or the workspace |
| Reading rules and their results | Microsoft Sentinel Reader | the same scope |
Contributor or Owner at the same scope also work, and are broader than needed.
ℹ️ Sentinel's roles are workspace-scoped, and there is no per-rule permission — so a team that may enable this detection may also disable every other one.
- The Log Analytics workspace already onboarded to Microsoft Sentinel — wire
log_analytics_workspace_idfrom that module'sworkspace_idoutput so Terraform orders the two correctly. - The alert rule template present in the workspace's template gallery, and its GUID. Templates are supplied by Microsoft and by installed content solutions.
- The Fusion template in the workspace's gallery. There is normally one per workspace, so this is usually a single fixed GUID.
- Data flowing from the sources Fusion correlates. Its value comes from breadth: a Fusion rule in a workspace with one connector has very little to correlate.
terraform-azurerm-sentinel-alert-rule-fusion/
├── providers.tf # required_version + the pinned azurerm provider. No provider block.
├── variables.tf # log_analytics_workspace_id, alert_rule_template_guid,
# # enabled, source_signals, timeouts. NO name — deprecated.
├── main.tf # the keystone, one resource
├── outputs.tf # id first, then the template facts, then is_active
├── README.md # this document
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT
└── .gitignore
provider "azurerm" {
features {}
}
module "rule_fusion" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-alert-rule-fusion.git?ref=v1.0.0"
# ⚠️ From the ONBOARDING module, not the workspace module. See example 1.
log_analytics_workspace_id = module.sentinel.workspace_id
alert_rule_template_guid = "f71aba3d-28fb-450b-b192-4e76a83015c8"
# source_signals deliberately omitted: the template's own configuration is the
# widest coverage. See example 3.
}ℹ️ The caller configures the provider, its authentication, and the mandatory
features {}block. This module declares none of them.
⚠️ Read example 3 before adding anysource_signals— every entry narrows the detection.
Consumes
| Input | Type | Source module |
|---|---|---|
log_analytics_workspace_id |
string |
the Sentinel onboarding module → workspace_id |
alert_rule_template_guid |
string |
the Fusion template in the workspace's gallery |
source_signals |
list(object) |
caller — optional, and every entry narrows the correlation |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The rule's Resource ID. | review, imports |
log_analytics_workspace_id |
The workspace. Force-new. | review |
alert_rule_template_guid |
The template, which is the detection. Force-new. | review |
source_signal_names / tunes_source_signals |
Whether the correlation has been narrowed. | security review |
disabled_source_signal_names |
🔴 Source signals explicitly turned off. | security review |
enabled / is_active |
Whether the detection fires. | security review |
No credential is accepted and none is emitted.
1 · ⚠️ Wire the workspace from the onboarding module
# ✅ This creates the dependency edge.
log_analytics_workspace_id = module.sentinel.workspace_id# ⚠️ Identical string. No dependency on onboarding.
log_analytics_workspace_id = module.law.id💡 Both carry the same value, so both work once Sentinel is onboarded. The difference is ordering: reading it through the onboarding module makes Terraform onboard Sentinel before creating this rule, and a rule created against a workspace that is not yet a Sentinel workspace fails at apply. The provider's own examples chain it this way.
⚠️ And note what the field is not:
# ❌ Rejected at plan time.
log_analytics_workspace_id = azurerm_log_analytics_workspace.example.workspace_id🔴
azurerm_log_analytics_workspaceexports both anidand an attribute literally namedworkspace_id. This argument wants the ARM Resource ID; the similarly-named attribute is the customer GUID.
Error: Invalid value for variable
log_analytics_workspace_id must be a Microsoft.OperationalInsights/workspaces
ARM Resource ID of the form
/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.OperationalInsights/workspaces/<name>.
THE CHECK IS ANCHORED AT BOTH ENDS: nothing may be appended AND nothing may be
prepended. The end anchor matters because this rule OWN ID extends the same path
with /providers/Microsoft.SecurityInsights/alertRules/<name>. Prefer the Sentinel onboarding module's
`workspace_id` output; a bare GUID is the workspace's customer ID and is not
accepted.
2 · The template IS the detection
alert_rule_template_guid = "f71aba3d-28fb-450b-b192-4e76a83015c8"ℹ️ You supply no logic. Fusion's correlation logic is Microsoft's, and it spans sources you have not enumerated. That breadth is the point: the individual signals it combines are usually too weak to alert on alone.
⚠️ alert_rule_template_guidis force-new, and that is semantically right: repointing it is a different detection, not a reconfiguration of this one. Sentinel treats the replacement as unrelated to the original.
ℹ️ Validated as a GUID shape only. Whether the GUID names a template of this kind — as opposed to a scheduled, NRT or other template — is not knowable at plan time, and using the wrong kind fails at apply.
💡 So keep the GUID somewhere it can be looked up rather than retyped. In a small estate a
localsblock of named template GUIDs is enough; in a larger one it belongs alongside whatever records which content solutions are installed.
3 · ⚠️ Every `source_signals` entry narrows the detection
# ✅ The recommended state: no tuning at all.
# source_signals = []# ⚠️ Each of these removes signal from the correlation.
source_signals = [
{
name = "Alerts"
enabled = true
sub_type = [{
name = "MicrosoftDefenderForEndpoint"
severities_allowed = ["High", "Medium"] # ⚠️ Low and Informational dropped
}]
},
{
name = "Anomalies"
enabled = false # 🔴 this whole source no longer feeds Fusion
},
]🔴 Treat every entry as a reduction in coverage unless it only restates a default. Setting a source's
enabledtofalse, or trimming a sub-type'sseverities_allowed, removes signal from the correlation — and the incidents you stop seeing are not logged anywhere as suppressed.
⚠️ severities_allowedis the field most likely to be set too narrowly. Fusion correlates weak signals, so excludingLowandInformationalfrom a subtype can remove precisely the evidence the correlation needed. A signal that is uninteresting alone is exactly what a multi-stage detection is built from.
ℹ️ Modelled as an ordered list, not this suite's usual keyed map: the provider's
sourceandsub_typeare positional list blocks, and the signal names come from the Fusion template rather than from the caller — so there is no caller-chosen key to build a map on.
💡 The narrowest, most consequential output in the module exists for this:
output "turned_off" { value = module.rule_fusion.disabled_source_signal_names }Each entry is a class of evidence the multi-stage detection no longer sees.
4 · 🔴 `name` is deprecated — and this module drops it
The pinned provider's schema still accepts a `name` on this resource:
name | deprecated = true
"the `name` is deprecated and will be removed in v5.0 version of the provider."
🔴 Merely reading the attribute back raises a deprecation warning at
terraform validate— not at apply, at plan. So a module that emitted anameoutput would make every caller's validate noisy.
✅ So this module neither accepts nor emits it. No
nameinput, nonameoutput.
💡 Why drop it rather than pass it through: a field that disappears at the next major version is a migration handed to every caller, and the service names the rule perfectly well without one. Import still works — the ARM name is part of the Resource ID either way (example 10).
ℹ️ This is the one place in this family where a module deliberately offers less than the pinned schema allows. The library pins
azurerm ~> 4.0on purpose and migrates per-module; declining to expose a field already marked for removal is what makes that migration cheap here.
⚠️ If you need the ARM name, take the last segment ofid.
5 · ⚠️ A preview API version
This resource's ARM API version is a PREVIEW version.
⚠️ Worth knowing before depending on its stability. Preview API versions can change behaviour between provider releases in ways that a stable version would not, and the deprecation ofname(example 4) is a visible instance of exactly that churn.
💡 The practical implication is not "avoid this resource" — Fusion is valuable and there is no stable alternative. It is: pin the provider version, read the changelog for this resource before upgrading, and expect the argument surface to move more than a stable resource's would.
ℹ️ This library pins
azurerm ~> 4.0precisely so that a provider upgrade is a deliberate, per-module decision rather than something that happens on aterraform init.
6 · 🔴 `enabled` keeps the provider's `true` default
enabled = true # the defaultℹ️ This module keeps the provider's default deliberately. Everywhere else in this suite the secure default is the closed one, but a detection inverts that: the risky configuration is a rule that exists and does not fire.
🔴
falsehere means the detection is present in the workspace and silent — exactly the state that survives a review asking "is the rule configured?" while failing one asking "is it working?".
ℹ️
is_activeis emitted so the distinction is visible in a state review:
output "check" { value = module.rule_fusion.is_active } # expect true💡 The general principle, worth stating because it recurs across this family: follow the secure-default convention by understanding what "secure" means for the resource, not by mechanically choosing the closed value.
⚠️ And for Fusion specifically,is_active = trueis necessary but not sufficient: a Fusion rule in a workspace with few connectors has little to correlate, however enabled it is.
7 · 🔁 Only `enabled` updates in place
Force-new: log_analytics_workspace_id alert_rule_template_guid
Updatable: enabled
⚠️ Everything that identifies the rule replaces it. That is unusual compared with the scheduled rule module, where even the query updates in place — but it follows from the shape of a template-driven rule: there is nothing to tune, so there is nothing to update.
ℹ️ Replacing a rule resets its alert history in Sentinel. The new rule is unrelated to the old one as far as the service is concerned, so incident continuity is lost.
⚠️ A caller cannot addignore_changesorprevent_destroy—lifecycleis not valid inside amoduleblock. Reading the plan is the safeguard.
💡 In practice this makes the module comfortable to live with: there is very little that can drift, and the one field that changes is the one you would want to change.
8 · Enabling several template-driven rules
locals {
fusion_templates = {
# Normally one per workspace, so this is usually a single entry.
default = "f71aba3d-28fb-450b-b192-4e76a83015c8"
}
}
module "rule_fusion_all" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-alert-rule-fusion.git?ref=v1.0.0"
for_each = local.fusion_templates
log_analytics_workspace_id = module.sentinel.workspace_id
alert_rule_template_guid = each.value
}
output "active" {
value = { for k, m in module.rule_fusion_all : k => m.is_active }
}ℹ️
for_eachlives in the caller — the resource addresses one template, and the module reflects that.
💡 Keying by a short slug rather than by GUID keeps the configuration readable and the keys stable. The GUIDs are opaque and easy to transpose; a name-keyed map makes a review possible.
⚠️ Afor_eachmap also makes it easy to disable a lot of detections in one edit. A review of this configuration should read theactiveoutput, not the module block.
ℹ️ Fusion normally has one template per workspace, so this pattern matters less here than for the other template-driven rules — it is included for consistency with them.
9 · What a review should assert
output "rule_fusion_review" {
value = {
active = module.rule_fusion.is_active # the assertion
template = module.rule_fusion.alert_rule_template_guid
tuned = module.rule_fusion.tunes_source_signals # false is the wider posture
disabled = module.rule_fusion.disabled_source_signal_names # 🔴 should be empty
}
}🔒
activeis the assertion. It is the only thing about this module that can be wrong in a way that matters, and it is a single boolean — so there is no excuse for not checking it.
⚠️ But active is not the same as producing. A Fusion rule correlates across sources; with few data connectors configured there is little to correlate, and no output here can tell you that.
💡
templatebelongs in a review because the GUID is opaque: a review that records which template each rule instantiates is the only cheap way to notice that two rules point at the same detection, or that one points somewhere unexpected.
10 · Importing a rule enabled from the portal
terraform import 'module.rule_fusion.azurerm_sentinel_alert_rule_fusion.this' \
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sentinel/providers/Microsoft.OperationalInsights/workspaces/law-sentinel-prod/providers/Microsoft.SecurityInsights/alertRules/f71aba3d-28fb-450b-b192-4e76a83015c8"
⚠️ Setalert_rule_template_guidto match before the first plan. It is force-new, so a mismatch proposes a replacement — which resets the rule's alert history.
💡 Enabling template-driven rules from the portal is the common starting point, so importing is often more appropriate than creating. Import, confirm an empty plan, and only then change anything.
ℹ️ There is no
nameto reconcile (example 4), which makes an import here simpler than for the other rule modules — the GUID and the source tuning are the whole surface.
ℹ️ The ID's last segment is the rule's ARM
name, which is a convenient way to learn it if the rule was created outside Terraform.
11 · What a destroy actually removes
terraform destroy on THIS module:
removes the alert rule -> the detection stops
-> the TEMPLATE is untouched, and can be re-instantiated
-> nothing else in the workspace is affected
ℹ️ A clean removal. The rule is an instance of a Microsoft-supplied template, so destroying it takes away your instance and leaves the template in the gallery. Re-applying recreates the rule.
⚠️ What is lost is the rule's alert and incident history in Sentinel, because a recreated rule is a new rule as far as the service is concerned. That is the same cost as any force-new change here (example 7).
💡 Worth contrasting with the anomaly-built-in module, where a destroy behaves quite differently: a built-in anomaly rule cannot be deleted, so removing that resource disables the detection rather than removing an instance of it. Two modules in the same family, two destroy semantics — which is why each says so explicitly.
ℹ️ A caller cannot add
prevent_destroy(lifecycleis not valid inside amoduleblock). If this detection matters enough to protect, aCanNotDeletelock at the workspace scope covers the whole Sentinel estate.
12 · 🏗️ End-to-end composition
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-sentinel-prod"
location = "eastus"
}
module "law" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-log-analytics-workspace.git?ref=v1.0.0"
name = "law-sentinel-prod"
resource_group_name = module.rg.name
location = module.rg.location
sku = "PerGB2018"
retention_in_days = 90
}
# ── The gate ──────────────────────────────────────────────────
module "sentinel" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-log-analytics-workspace-onboarding.git?ref=v1.0.0"
workspace_id = module.law.id
}
# ── Fusion: no source tuning, which is the widest coverage (example 3) ───────
module "rule_fusion" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-alert-rule-fusion.git?ref=v1.0.0"
log_analytics_workspace_id = module.sentinel.workspace_id
alert_rule_template_guid = "f71aba3d-28fb-450b-b192-4e76a83015c8"
# No source_signals: Fusion's value is the breadth of what it correlates.
# No name: deprecated and removed in v5.0 (example 4).
}
# ── A rule we wrote ourselves, for contrast ────────────────────────────
module "rule_impossible_travel" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-alert-rule-scheduled.git?ref=v1.0.0"
name = "impossible-travel"
log_analytics_workspace_id = module.sentinel.workspace_id
display_name = "Impossible travel sign-in"
severity = "High"
query = file("${path.module}/queries/impossible_travel.kql")
query_frequency = "PT15M"
query_period = "PT1H"
tactics = ["InitialAccess"]
incident = { create_incident_enabled = true }
}
output "detection_posture" {
value = {
fusion_active = module.rule_fusion.is_active # expect true
fusion_narrowed = module.rule_fusion.tunes_source_signals # expect false
fusion_disabled = module.rule_fusion.disabled_source_signal_names # expect []
# And the rule we own outright.
scheduled_gaps = module.rule_impossible_travel.detection_gaps
}
}🔒 What the composition gets right: the Fusion template enabled with no source tuning, no
name, and an output that asserts the correlation has not been narrowed.
⚠️ What no plan will tell you: whether enough data connectors are configured for Fusion to have anything to correlate — its value is entirely in that breadth, and no plan can see it.
💡
fusion_disabledbeing empty is the line worth keeping. A non-empty list is a class of evidence the multi-stage detection no longer sees.
| Input | Type | Default | Notes |
|---|---|---|---|
log_analytics_workspace_id |
string |
— | Required. Force-new. Shape-validated; from the onboarding module. |
alert_rule_template_guid |
string |
— | Required. Force-new. GUID-validated. |
source_signals |
list(object) |
[] |
|
enabled |
bool |
true |
Matches the provider; the closed value is the dangerous one. |
timeouts |
object(...) |
null |
All four operations. |
There is no tags variable — the provider exposes none on Sentinel alert rules.
Full schemas
variable "source_signals" {
type = list(object({
name = string
enabled = optional(bool)
sub_type = optional(list(object({
name = string
enabled = optional(bool)
severities_allowed = list(string) # High | Medium | Low | Informational
})), [])
}))
default = []
# An ordered list, not a keyed map: the provider's blocks are positional and the signal names come
# from the Fusion template rather than from the caller.
validation {
condition = alltrue(flatten([
for s in var.source_signals : [for t in coalesce(s.sub_type, []) : length(t.severities_allowed) > 0]
]))
error_message = "Every sub_type must allow at least one severity. An empty severities_allowed would silently remove that subtype's signal from the Fusion correlation."
}
# ...plus: non-empty names, no duplicate source names, and severities from the four legal values.
}
# DELIBERATELY NOT EXPOSED: `name`. Deprecated in the pinned schema and removed in v5.0; reading the
# attribute is enough to raise a validate warning. See example 4.| Output | Description | Kind |
|---|---|---|
id |
The Resource ID of the Fusion alert rule | Passthrough |
log_analytics_workspace_id |
The workspace the rule belongs to | Passthrough |
alert_rule_template_guid |
The Fusion template this rule instantiates | Passthrough |
enabled |
Whether the rule is enabled | Passthrough |
is_active |
Derived, and the one to assert on: whether this Fusion detection will actually fire | Derived |
source_signal_names |
The Fusion source signals this rule tunes explicitly, in order | Derived |
tunes_source_signals |
Derived | Passthrough |
disabled_source_signal_names |
Derived | Derived |
management_moves_to_the_defender_portal |
Always true - Sentinel's Azure portal experience retires 2027-03-31 and moves to the Microsoft Defender portal; the ARM resources are unaffected | Constant |
🔒 No credential is accepted and none is emitted.
-
The deprecated
nameis removed from the module's surface rather than passed through. Reading it raises a validate warning, and it disappears in provider v5.0 — so exposing it would hand every caller a migration and a noisy validate. This is the one place in this family where a module deliberately offers less than the pinned schema allows, and it is what makes this module's v5.0 migration a non-event. -
source_signalsdefaults to empty, and the documentation frames every entry as a coverage reduction. For most resources an empty optional collection is simply a default; here it is the recommended state. Saying so is the difference between a caller tuning deliberately and tuning because the field exists. -
disabled_source_signal_namesis emitted separately from the general list. It is the narrowest and most consequential thing this module can express — each entry is a class of evidence the multi-stage detection no longer sees, and folding it into a general list would have hidden it. -
The preview API version is disclosed rather than glossed. It explains why this resource's argument surface moves more than a stable one's, and the
namedeprecation is the visible evidence. -
enabledkeeps the provider'struedefault, inverting this suite's usual convention. The rule "default to the closed value" assumes the closed value is the safe one; for a detection it is not. The convention is followed by understanding it rather than applying it mechanically — the same reasoning as every other rule module in this family. -
The module's surface is kept as small as the resource's. There is no synthetic input for anything the detection does, because none of it is configurable. Inventing knobs would misrepresent what the caller controls, and this suite would rather be honest about a four-argument resource than pad it.
-
alert_rule_template_guidis force-new, and that is documented as semantically correct rather than as a limitation. The template is the detection, so repointing it is a different detection — a replacement is the accurate representation, not an inconvenience. -
Where a value set belongs to the service, it is left unvalidated and the reason is stated. Fusion source and sub-type names come from the template, and the set grows as Microsoft adds detections — so an enum here would eventually reject a valid current value.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Silent detections | is_active emitted; enabled defaults to true |
false, knowingly |
| Narrowed correlation | source_signals defaults to empty, documented as the widest coverage |
tune it, knowingly |
| Invisible reductions | disabled_source_signal_names emitted on its own |
— |
| Deprecated fields | name not exposed at all, so no caller inherits its removal |
— |
| Preview-API churn | disclosed, with the provider pin named as the mitigation | — |
| Wrong-value pastes | the workspace's GUID attribute rejected at plan | — |
| Malformed template GUIDs | rejected at plan; the wrong kind of template cannot be checked | — |
| Surprise replacements | the force-new set documented, and explained as semantically right | — |
| Secrets | none accepted, none emitted | — |
- Before adding
source_signals: the template's own configuration is the widest coverage. - Before trimming
severities_allowed: Fusion correlates weak signals, soLowmay be the evidence. - Before upgrading the provider: this resource is on a preview API, and
nameis already gone in v5.0. - Before reporting Fusion as active: check how many data connectors it actually has to correlate.
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the source to a tag —
?ref=v1.0.0— never a branch. - Plan-only from here. A human applies from CI.
- ✅
enabledupdates in place. Turning a detection on or off is an ordinary apply. ⚠️ Everything else is force-new, and replacing a rule resets its alert history in Sentinel.- ℹ️ Assert
is_activein CI. It is a single boolean and the only thing here that can be wrong in a way that matters. - ℹ️ There is no
nameargument — deprecated in the pinned schema, removed in v5.0. - ℹ️ A destroy removes the detection. Nothing else in the workspace is affected.
terraform validate and terraform fmt -check are the offline gate. They confirm:
log_analytics_workspace_idis aMicrosoft.OperationalInsights/workspacesARM Resource ID — a bare GUID is rejected;alert_rule_template_guidis a GUID;- every
source_signalsentry has a non-empty name, with no duplicate source names; - every
sub_typehas a non-empty name and at least one allowed severity, drawn from the four legal values; - no output is sensitive, because nothing sensitive is accepted;
- the module declares no
providerblock.
💡 These were proved by evaluating the conditions in
terraform consoleinside the module — which does fire root-module variable validations, unliketerraform validateon a calling configuration. A bare GUID in the workspace field and a non-GUID template value both fail.
What only plan and apply exercise:
- whether the workspace exists and is onboarded to Sentinel;
- whether the template GUID names a template of the right kind, and whether it exists in this workspace;
- whether the identity holds Microsoft Sentinel Contributor.
What no Terraform command checks at any stage:
- whether enough data connectors exist for Fusion to correlate anything — its entire value is that breadth;
- whether a source or sub-type name is one the Fusion template recognises;
- which incidents a narrowed correlation is no longer producing — nothing reports it.
Outputs:
alert_rule_template_guid = "f71aba3d-28fb-450b-b192-4e76a83015c8"
disabled_source_signal_names = []
enabled = true
id = "/subscriptions/00000000-.../providers/Microsoft.SecurityInsights/alertRules/f71aba3d-28fb-450b-b192-4e76a83015c8"
is_active = true
log_analytics_workspace_id = "/subscriptions/00000000-.../workspaces/law-sentinel-prod"
source_signal_names = []
tunes_source_signals = false
✅
tunes_source_signals = falseanddisabled_source_signal_names = []— the widest coverage, which is the recommended posture.
ℹ️ Note the absence of a
nameoutput. Deprecated in the pinned schema and removed in v5.0 (example 4).
⚠️ is_active = truesays the rule fires. It does not say Fusion has enough sources to correlate.
| Symptom | Cause | Fix |
|---|---|---|
Plan rejects log_analytics_workspace_id |
The workspace's workspace_id GUID attribute was passed. |
Use the onboarding module's workspace_id (example 1). |
| A first apply fails saying the workspace is not onboarded | Nothing ordered onboarding first. | Wire from the onboarding module (example 1). |
Plan rejects alert_rule_template_guid |
It is not a GUID. | Copy it from the template gallery (example 2). |
| Apply fails saying the template was not found | The GUID is wrong, or the template is not in this workspace. | Check the gallery, and any content solution it needs (example 2). |
| Apply fails on the template's kind | The GUID names a template of a different type. | Only this rule type's templates work here (example 2). |
| A template-GUID change replaced the rule | It is force-new — a different template is a different detection. | Expected (examples 2, 7). |
Plan rejects a sub_type |
Empty severities_allowed, or an unknown severity. |
At least one of the four (example 3). |
Plan rejects source_signals |
Duplicate source names, or an empty name. | One entry per source (example 3). |
| Apply fails on a source name | It is not a signal this Fusion template recognises. | Unvalidatable here — check the template (example 3). |
A deprecation warning at validate |
Something read the resource's name. |
This module does not; check your own outputs (example 4). |
| Fusion produces very few incidents | It may have little to correlate. | Check how many data connectors feed the workspace (example 9). |
| The rule is enabled and nothing appears | See the notes above — active is not the same as producing. | Check is_active, then the upstream (example 9). |
Wanted ignore_changes or prevent_destroy |
lifecycle is not valid inside a module block. |
Not available; read the plan. |
| Wanted to tag this resource | The provider exposes no tags. | Tag the workspace. |
azurerm_sentinel_alert_rule_fusion— provider documentation.- Advanced multistage attack detection (Fusion) — what Fusion correlates, and the source signals of example 3.
- Configure multistage attack detection — tuning source signals from the portal's side.
- Create custom analytics rules — how rule templates are instantiated in the service.
- Microsoft Sentinel roles and permissions — the RBAC table above.
- Sibling modules:
terraform-azurerm-sentinel-log-analytics-workspace-onboarding,...-alert-rule-scheduled,...-alert-rule-nrt,...-alert-rule-ms-security-incident,...-alert-rule-fusion,...-alert-rule-machine-learning-behavior-analytics,...-alert-rule-threat-intelligence,...-alert-rule-anomaly-built-in,...-alert-rule-anomaly-duplicate. - This module's
SCOPE.md.
💙 "Infrastructure as Code should be standardized, consistent, and secure."

