Enables Microsoft's machine-learning behaviour model, which learns normal activity in the workspace and alerts on deviations (
azurerm_sentinel_alert_rule_machine_learning_behavior_analytics). Targetshashicorp/azurerm ~> 4.0.
- 🧠 Microsoft's behaviour model. It learns normal activity in the workspace and alerts on deviations — anomalous SSH and RDP sign-ins being the canonical cases.
- ℹ️ You supply no logic and no thresholds — the template is the detection, and this resource turns it on.
- 🔴 The model needs a learning period on the workspace's own data. Early silence is not a misconfiguration, and nothing in Terraform reports when the model is ready.
- 🔁
enabledis the only argument that updates in place; the other three are force-new. - 🧪 The module's surface is as small as the resource's — no invented knobs for a model you cannot configure.
- 🔒 No credential is accepted and none is emitted.
Four arguments, and the only interesting one is the one that is not there.
💡 Why it matters: The mechanics take a minute. The thing worth knowing is operational: a correctly configured behaviour-analytics rule is legitimately quiet for days while the model trains, which is exactly the situation in which someone reaches for a fix that is not needed.
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["MACHINE-LEARNING BEHAVIOUR ANALYTICS IS A MICROSOFT-AUTHORED MODEL. It learns normal behaviour in the workspace and alerts on deviations - anomalous SSH and RDP sign-ins being the canonical cases."]
tmpl["you supply NO LOGIC AND NO THRESHOLDS: the alert_rule_template_guid IS the detection. Four arguments, three of them force-new, and enabled is the only one that updates in place."]
learn["AND THE MODEL NEEDS A LEARNING PERIOD on the workspace's own data before it produces anything meaningful. EARLY SILENCE IS NOT A MISCONFIGURATION - and nothing in Terraform reports when the model is ready."]
caveat["so the caveat sits next to is_active, the flag someone would check: a correctly configured rule that is legitimately quiet for days is exactly the case where an operator reaches for a fix that is not needed"]
small["THE MODULE'S SURFACE IS DELIBERATELY AS SMALL AS THE RESOURCE'S. There is no synthetic input for anything the model does, because none of it is configurable - inventing knobs would misrepresent what the caller controls."]
silent["and the family risk still applies: enabled false leaves the detection present and SILENT, the state that survives a review asking IS IT CONFIGURED while failing one asking IS IT WORKING. enabled KEEPS the provider's true default because for a detection the closed value is the dangerous one."]
name["name is an ARM name rather than something an analyst sees - the detection's presentation comes from the template. It is force-new, so renaming replaces the rule and Sentinel treats the new one as unrelated."]
ws["and like every module in this family, log_analytics_workspace_id wants the workspace's ARM RESOURCE ID and is wired from the ONBOARDING module's workspace_id output, which is what orders onboarding first"]
this["terraform-azurerm-sentinel-alert-rule-machine-learning-behavior-analytics"]
keystone["azurerm_sentinel_alert_rule_machine_learning_behavior_analytics.this"]
what -->|"so"| tmpl
tmpl -->|"but"| learn
learn -->|"therefore"| caveat
caveat -->|"posture"| this
small -->|"scope"| this
silent -->|"default"| this
name -->|"and"| ws
ws -->|"wiring"| this
this -->|"enables Microsoft's behaviour model"| 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,learn,caveat,small,silent,name,ws sib;
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_sentinel_alert_rule_machine_learning_behavior_analytics.this |
1 | The keystone. The template is the detection. |
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 |
| 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:
enabledis the only argument that updates in place.name,log_analytics_workspace_idandalert_rule_template_guidare all force-new.nameis an ARM name, not something an analyst sees — the detection's presentation comes from the template.- The model requires a learning period, which is a service behaviour rather than a schema fact and is reported nowhere in Terraform.
- Force-new:
name,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 behaviour-analytics template in the workspace's gallery, and its GUID.
- The data the model learns from already flowing — sign-in data for the SSH and RDP detections. A model with no history has nothing to learn from.
- Patience. Expect a learning period before the rule produces meaningful alerts.
terraform-azurerm-sentinel-alert-rule-machine-learning-behavior-analytics/
├── providers.tf # required_version + the pinned azurerm provider. No provider block.
├── variables.tf # log_analytics_workspace_id, name, alert_rule_template_guid,
# # enabled, timeouts
├── 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_mlba" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-alert-rule-machine-learning-behavior-analytics.git?ref=v1.0.0"
# ⚠️ From the ONBOARDING module, not the workspace module. See example 1.
log_analytics_workspace_id = module.sentinel.workspace_id
name = "anomalous-ssh-login"
alert_rule_template_guid = "737a2ce1-70a3-4968-9e90-3e6aca836abf"
}ℹ️ The caller configures the provider, its authentication, and the mandatory
features {}block. This module declares none of them.
⚠️ Read example 3 before judging whether this rule is working — early silence is expected.
Consumes
| Input | Type | Source module |
|---|---|---|
log_analytics_workspace_id |
string |
the Sentinel onboarding module → workspace_id |
name |
string |
caller — an ARM name, force-new |
alert_rule_template_guid |
string |
the behaviour-analytics template in the workspace's gallery |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The rule's Resource ID. | review, imports |
name |
The ARM name. Force-new. | review |
log_analytics_workspace_id |
The workspace. Force-new. | review |
alert_rule_template_guid |
The template, which is the detection. Force-new. | 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 with nothing appended. 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 = "737a2ce1-70a3-4968-9e90-3e6aca836abf"ℹ️ You supply no logic. The model is Microsoft's — what counts as normal, how deviation is scored, and what threshold triggers an alert are all internal to it. There is nothing to configure and nothing to tune, which is why this module has four arguments.
⚠️ 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 · 🔴 The learning period, and why silence is not a fault
Enable the rule -> the model trains on the workspace's own history
-> ...for a while...
-> meaningful alerts
🔴 A newly enabled behaviour-analytics rule is not immediately useful. The model builds a picture of normal activity from the workspace's own data first, so early silence is expected — not a sign that the rule is misconfigured.
⚠️ And nothing in Terraform reports when the model is ready. There is no output, no attribute and no plan line that distinguishes "still learning" from "trained and quiet" from "broken".
💡 So the caveat sits next to
is_active, which is the flag someone would check:
output "check" { value = module.rule_mlba.is_active } # true, and still possibly silent
⚠️ This is the situation in which an operator reaches for a fix that is not needed — disabling and re-enabling the rule, or repointing the template, both of which are counterproductive while the model is training.
💡 What to do instead: record when the rule was enabled, and give the model time before concluding anything. If the workspace is new, the model has little history to learn from and will take proportionally longer.
4 · 🧪 Why the module has four arguments and no more
log_analytics_workspace_id = module.sentinel.workspace_id
name = "anomalous-ssh-login"
alert_rule_template_guid = "737a2ce1-70a3-4968-9e90-3e6aca836abf"
enabled = trueNOT exposed, because the resource does not have them:
severity the template decides
query there is no query — it is a model, not KQL
thresholds internal to the model
tactics the template's classification
entity mapping the template's
frequency the model's
ℹ️ The module's surface is deliberately as small as the resource's. There is no synthetic input for anything the model does, because none of it is configurable.
💡 Inventing knobs would misrepresent what the caller controls. A
severityvariable that this module accepted and then discarded would read as a setting, and this suite would rather be honest about a four-argument resource than pad it to look substantial.
⚠️ Contrast the anomaly-built-in module, which also wraps a Microsoft-authored detection but emits eleven read-only facts about it — description, frequency, tactics, required data connectors, observation settings. This resource exposes none of those, so there is nothing to surface.
💡 The practical consequence: if you need to know what this detection looks for or what data it needs, the template gallery is the source, not Terraform.
5 · `name` is an ARM name, not a display name
name = "anomalous-ssh-login"ℹ️ Unlike the scheduled and NRT rule modules, there is no
display_namehere. The detection's presentation — what an analyst sees in the incident queue — comes from the template.
⚠️ nameis force-new, so renaming replaces the rule and Sentinel treats the new one as unrelated to the old. Alert history does not carry over.
💡 Use a stable, descriptive identifier and leave it alone. Since it is invisible to analysts, there is no presentational reason to change it — which makes any rename almost certainly accidental.
ℹ️ Validated as non-empty only. Naming rules for Sentinel alert rules are a service concern.
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_mlba.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 hereis_active = trueis especially far from "producing alerts": the model may still be learning (example 3).
7 · 🔁 Only `enabled` updates in place
Force-new: name 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 {
mlba_rules = {
ssh = "737a2ce1-70a3-4968-9e90-3e6aca836abf"
rdp = "fa118b98-de46-4e94-87f9-8e6d5060b60b"
}
}
module "rule_mlba_all" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-alert-rule-machine-learning-behavior-analytics.git?ref=v1.0.0"
for_each = local.mlba_rules
log_analytics_workspace_id = module.sentinel.workspace_id
name = "mlba-${each.key}"
alert_rule_template_guid = each.value
}
output "active" {
value = { for k, m in module.rule_mlba_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.
ℹ️ Each instance trains independently, so enabling several at once means several learning periods running in parallel — not one.
9 · What a review should assert
output "rule_mlba_review" {
value = {
active = module.rule_mlba.is_active # the assertion
template = module.rule_mlba.alert_rule_template_guid
name = module.rule_mlba.name
}
}🔒
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. The model may still be learning (example 3), and nothing here distinguishes that from a trained model with nothing to report.
💡
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_mlba.azurerm_sentinel_alert_rule_machine_learning_behavior_analytics.this' \
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sentinel/providers/Microsoft.OperationalInsights/workspaces/law-sentinel-prod/providers/Microsoft.SecurityInsights/alertRules/anomalous-ssh-login"
⚠️ 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.
ℹ️ 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
}
# ── Microsoft's behaviour models — several, each training independently ─────
locals {
mlba_rules = {
ssh = "737a2ce1-70a3-4968-9e90-3e6aca836abf"
rdp = "fa118b98-de46-4e94-87f9-8e6d5060b60b"
}
}
module "rule_mlba" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-alert-rule-machine-learning-behavior-analytics.git?ref=v1.0.0"
for_each = local.mlba_rules
log_analytics_workspace_id = module.sentinel.workspace_id
name = "mlba-${each.key}"
alert_rule_template_guid = each.value
# enabled left at true. Expect a learning period before anything appears (example 3).
}
# ── 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 = {
# Active, which is necessary and not sufficient — the models train first.
mlba_active = { for k, m in module.rule_mlba : k => m.is_active }
# And the rule we own outright.
scheduled_gaps = module.rule_impossible_travel.detection_gaps
}
}🔒 What the composition gets right: both behaviour models enabled, keyed by a readable slug rather than by GUID, and an output that asserts activation while the surrounding comment records that activation is not the same as producing.
⚠️ What no plan will tell you: whether either model has finished learning, whether the workspace has enough sign-in history for it to learn from, or what either detection actually looks for — that is in the template gallery, not here.
💡 Record the date you enabled these. It is the only way to judge later whether silence is a learning period or a problem.
| Input | Type | Default | Notes |
|---|---|---|---|
log_analytics_workspace_id |
string |
— | Required. Force-new. Shape-validated; from the onboarding module. |
name |
string |
— | Required. Force-new. An ARM name, not a display name. |
alert_rule_template_guid |
string |
— | Required. Force-new. GUID-validated. |
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 "alert_rule_template_guid" {
type = string
# The template IS the detection: the model, its notion of normal, and its thresholds are all
# internal to it. Force-new, because repointing it is a different detection.
validation {
condition = can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.alert_rule_template_guid))
error_message = "alert_rule_template_guid must be a GUID, for example 737a2ce1-70a3-4968-9e90-3e6aca836abf."
}
}
variable "enabled" {
type = bool
default = true
# The provider's default, kept deliberately: for a detection the closed value is the dangerous
# one. Note that active is not the same as producing — the model needs a learning period first.
}| Output | Description | Kind |
|---|---|---|
id |
The Resource ID of the rule | Passthrough |
name |
The rule's ARM name | Passthrough |
log_analytics_workspace_id |
The workspace the rule belongs to | Passthrough |
alert_rule_template_guid |
The template this rule instantiates | Passthrough |
enabled |
Whether the rule is enabled | Passthrough |
is_active |
Derived, and the one to assert on: whether this detection will actually fire | Derived |
only_enabled_is_updatable |
Always true | Constant |
import_checks_the_alert_rule_kind |
Always true | Constant |
template_guid_is_not_validated_against_the_workspace |
Always true | Constant |
requires_the_workspace_to_be_sentinel_onboarded |
Always true, and enforced by nothing offline | Constant |
detection_logic_is_microsoft_authored |
Always true, and the reason this resource has only four arguments | Constant |
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 learning period is documented next to the flag someone would check. A correctly configured rule that is legitimately quiet for days is precisely the case where an operator reaches for a fix that makes things worse — disabling and re-enabling, or repointing the template. So the caveat lives on
enabledand onis_active, not only in a prose section. -
The module's surface is deliberately as small as the resource's. No synthetic inputs for severity, thresholds, tactics or frequency, because none of them is configurable on this resource. Inventing them would misrepresent what the caller controls, and this suite would rather document a four-argument resource honestly than pad it.
-
The contrast with the anomaly-built-in module is drawn explicitly. Both wrap a Microsoft-authored detection, but that resource exposes eleven read-only facts — description, frequency, required data connectors, observation settings — and this one exposes none. Knowing which module tells you what saves a search through both.
-
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. There is little to leave unvalidated here: the GUID is shape-checked and
nameis checked for emptiness. Naming rules for Sentinel alert rules are a service concern, so nothing stricter is asserted.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Silent detections | is_active emitted; enabled defaults to true |
false, knowingly |
| Premature intervention | the learning period documented next to is_active |
— |
| Invented settings | the module's surface kept as small as the resource's | — |
| 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 enabling: confirm the data the model learns from is already flowing.
- After enabling: record the date, and give the model time before concluding anything.
- Before disabling and re-enabling a quiet rule: that resets nothing useful and may restart learning.
- Before looking for what this detection covers: the template gallery, not Terraform.
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. - ⏳ Expect a learning period before this rule produces anything. Do not treat early silence as a fault.
- ℹ️ 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;nameis non-empty;- 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 the model has finished learning — there is no attribute, output or plan line for it;
- whether the workspace has enough history for the model to learn from;
- what the detection actually looks for, or what data it needs — this resource exposes neither.
Outputs:
alert_rule_template_guid = "737a2ce1-70a3-4968-9e90-3e6aca836abf"
enabled = true
id = "/subscriptions/00000000-.../providers/Microsoft.SecurityInsights/alertRules/anomalous-ssh-login"
is_active = true
log_analytics_workspace_id = "/subscriptions/00000000-.../workspaces/law-sentinel-prod"
name = "anomalous-ssh-login"
✅
is_active = true— the rule fires. Whether the model has finished learning is not visible here, and cannot be (example 3).
ℹ️ Six lines is the whole surface. Compare the anomaly-built-in module, which emits eleven read-only facts about its detection; this resource exposes none (example 4).
⚠️ There is nodisplay_name. What an analyst sees comes from the template (example 5).
| 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). |
| The rule is enabled and nothing has fired for days | Usually the model is still learning. | Expected; record the enable date and wait (example 3). |
| Disabling and re-enabling did not help | It may have restarted the learning period. | Leave it alone (example 3). |
| Wanted to set a severity or threshold | The resource exposes neither. | Not configurable (example 4). |
| Wanted to know what this rule detects | This resource exposes no description. | Check the template gallery (example 4). |
| A rename replaced the rule and lost its history | name is force-new. |
Expected (example 5). |
| 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_machine_learning_behavior_analytics— provider documentation.- Advanced multistage attack detection and ML analytics — the built-in ML detections this module enables.
- Anomalous RDP and SSH login detection — including the learning period of example 3.
- 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."