Skip to content

Repository files navigation

Terraform AzAPI Sentinel Automation Rules

Microsoft Sentinel automation rules as code, via the Azure/azapi provider, exposing the full Microsoft.SecurityInsights/automationRules surface that the azurerm resource does not: every condition kind as a typed object (including grouped boolean logic and the array conditions), incident tasks alongside property changes and playbooks, trigger expiry, and a variablised API version.

CI Release Terraform Registry License


Why azapi for this resource

azurerm_sentinel_automation_rule covers the common path but not the full resource: conditions arrive only as a raw condition_json blob, and the action surface stops at incident property changes and playbooks. This module speaks the current stable API (2025-09-01 by default, a variable, so consumers are never pinned to this module's release cadence) with the whole surface typed:

  • Conditions as objects, not JSON. Every condition kind the API knows, each a typed one-of: property (the 57 supported incident/entity properties with the 8 operators), property_array (custom-details checks, AnyItem or AllItems), property_changed (severity, status, or owner, ChangedFrom or ChangedTo), property_array_changed (alerts, labels, tactics, or comments Added), and boolean (And/Or groups of the property kinds). A raw_json escape hatch per condition passes anything the typed surface cannot say (deeper boolean nesting among them) without giving up the typed path for everything else.
  • All three action kinds. modify_properties_actions (severity, status, classification with reason and comment, labels, owner), run_playbook_actions (tenant defaulted to the deploying tenant), and add_incident_task_actions, plus raw_actions_json appended verbatim. Only the keys you set are sent; nothing lands as null.
  • The trigger, whole. Incidents or Alerts, Created or Updated (the impossible Alerts+Updated combination fails at plan), enable/disable, and expiration_time_utc.

Naming: stable GUIDs without a random provider

Automation rules are GUID-named children in the portal. Here the map key is the readable handle you compose with, and the deployed name derives from it deterministically (uuidv5), so plans are stable, applies are idempotent, and no random provider joins the graph. A per-rule name override exists for adopting rules that already exist.

What the module enforces

At plan: the trigger enums (and the impossible Alerts+Updated combination), the 1 to 1000 order range, at least one action per rule (the API rejects an empty actions list), exactly one kind set per condition (and per boolean inner condition, and per array item condition), and the Alerts-trigger action rule the API enforces at PUT: an Alerts-triggered rule can only run playbooks, since ModifyProperties and AddIncidentTask act on incidents (caught live by this repo's CI, now failing at plan instead). An advisory check warns when two rules share an order, which deploys fine but evaluates in an unspecified sequence. Unset optionals are dropped from every body rather than sent as null.

Notes worth knowing

  • The workspace arrives as its resource id (workspace_id, the azapi parent_id), per the estate's pass-ids principle. It must be Sentinel-onboarded, and when the same configuration creates the onboarding, pass sentinel_onboarding_id (the log-analytics-workspace module's sentinel_onboarding_ids output) too: it threads the dependency so the rules wait for onboarding, closing a race this repo's own CI caught live (the first rule read answers 400 not-onboarded otherwise).
  • A rule with triggers_when = "Updated" should carry a property_changed or property_array_changed condition declaring what changed; the portal requires one, and the estate's live SOC rules follow it (an any-status-transition property_changed on IncidentStatus is the standard shape).
  • This module is the full-surface sibling of the estate's libre-devops/sentinel-automation-rule/azurerm module: that one speaks the azurerm resource with condition_json blobs, this one speaks the API directly with everything typed.
  • A RunPlaybook action only works once the Sentinel service principal (Azure Security Insights) holds the Microsoft Sentinel Automation Contributor role on the playbook's resource group; the API accepts the rule regardless and the action fails at run time, so wire the grant with the estate role-assignment module alongside this one.
  • expiration_time_utc is a strict RFC3339 instant (for example 2026-12-31T23:59:59Z).
  • The old estate pattern's ignore_null_property has no azapi 2.x equivalent and is not needed: bodies here are assembled with null-dropping merges, so unset keys are never sent at all.
  • Migrating from condition_json blobs: each blob entry is one raw_json condition here, and can then be converted to its typed form one condition at a time.

Security scan exceptions

CI runs a trivy config scan that gates on HIGH and CRITICAL findings. Waivers live in .trivyignore.yaml, and every waiver is recorded here. There are currently none.

ID Where Justification

Requirements

Name Version
terraform >= 1.9.0, < 2.0.0
azapi >= 2.0.0, < 3.0.0

Providers

Name Version
azapi 2.11.0

Modules

No modules.

Resources

Name Type
azapi_resource.automation_rules resource
azapi_client_config.current data source

Inputs

Name Description Type Default Required
api_version API version for Microsoft.SecurityInsights/automationRules. 2025-09-01 is the current stable version; variablised so consumers are never pinned to this module's release cadence. string "2025-09-01" no
automation_rules Sentinel automation rules, keyed by a stable logical name. The key is the handle you compose with
(outputs, state addresses); the deployed resource name is a GUID derived deterministically from the
key (overridable per rule with name), matching how the portal names these children.

Each rule is the full 2025-09-01 surface, typed:

display_name / order The rule as it appears in Sentinel; order is 1 to 1000 and drives
evaluation sequence (keep orders unique across the workspace).
enabled isEnabled on the trigger; default true.
triggers_on Incidents (default) or Alerts.
triggers_when Created (default) or Updated (Incidents only; Alerts support Created).
expiration_time_utc Optional RFC3339 instant (2026-12-31T23:59:59Z); the rule disables
itself at that time. The API is strict about the format.

conditions Ordered list; every entry sets EXACTLY ONE of:
property { name, operator, values }: an incident/entity property check. name is
any AutomationRulePropertyConditionSupportedProperty (IncidentTitle,
IncidentSeverity, AccountName, IPAddress, Url, ...); operator is one of
Equals, NotEquals, Contains, NotContains, StartsWith, NotStartsWith,
EndsWith, NotEndsWith.
property_array { array_type, array_condition_type, item_conditions }: a check over
CustomDetails or CustomDetailValues, AnyItem or AllItems, with
property-shaped item conditions.
property_changed { name, change_type, operator, values }: fires on IncidentSeverity,
IncidentStatus, or IncidentOwner changing, ChangedFrom or ChangedTo.
property_array_changed { array_type, change_type }: fires when Alerts, Labels, Tactics, or
Comments have items Added.
boolean { operator = And|Or, inner_conditions }: groups inner conditions (the
property kinds, one level deep; use raw_json for deeper nesting).
raw_json The condition object as a jsonencode()d string ({ conditionType,
conditionProperties }), the escape hatch for shapes the typed surface
cannot say (deeper boolean nesting among them). A string rather than an
object so heterogeneous escape hatches type-check inside the list.

Actions (at least one across the four lists; order sequences them):
modify_properties_actions Incident changes: severity, status, classification (with reason
and comment), labels (plain strings), owner (object_id, email,
assigned_to, user_principal_name, owner_type). Only the keys you
set are sent.
run_playbook_actions { order, logic_app_resource_id, tenant_id }: runs a Logic App
playbook; tenant_id defaults to the deploying tenant. The Sentinel
service principal (Azure Security Insights) needs the Microsoft
Sentinel Automation Contributor role on the playbook's resource
group or the action fails at run time.
add_incident_task_actions { order, title, description }: adds an incident task.
raw_actions_json Action objects as jsonencode()d strings, appended after the
typed lists.
map(object({
name = optional(string)
display_name = string
order = number
enabled = optional(bool, true)
triggers_on = optional(string, "Incidents")
triggers_when = optional(string, "Created")
expiration_time_utc = optional(string)

conditions = optional(list(object({
property = optional(object({
name = string
operator = string
values = list(string)
}))
property_array = optional(object({
array_type = string
array_condition_type = optional(string, "AnyItem")
item_conditions = optional(list(object({
property = optional(object({
name = string
operator = string
values = list(string)
}))
raw_json = optional(string)
})), [])
}))
property_changed = optional(object({
name = string
change_type = string
operator = string
values = list(string)
}))
property_array_changed = optional(object({
array_type = string
change_type = optional(string, "Added")
}))
boolean = optional(object({
operator = string
inner_conditions = list(object({
property = optional(object({
name = string
operator = string
values = list(string)
}))
property_changed = optional(object({
name = string
change_type = string
operator = string
values = list(string)
}))
property_array_changed = optional(object({
array_type = string
change_type = optional(string, "Added")
}))
raw_json = optional(string)
}))
}))
raw_json = optional(string)
})), [])

modify_properties_actions = optional(list(object({
order = number
severity = optional(string)
status = optional(string)
classification = optional(string)
classification_reason = optional(string)
classification_comment = optional(string)
labels = optional(list(string))
owner = optional(object({
object_id = optional(string)
email = optional(string)
assigned_to = optional(string)
user_principal_name = optional(string)
owner_type = optional(string)
}))
})), [])

run_playbook_actions = optional(list(object({
order = number
logic_app_resource_id = string
tenant_id = optional(string)
})), [])

add_incident_task_actions = optional(list(object({
order = number
title = string
description = optional(string)
})), [])

raw_actions_json = optional(list(string), [])
}))
n/a yes
resource_timeouts Timeouts for the automation rule resources. Rules create fast, so a short create surfaces real errors quickly; delete stays generous.
object({
create = optional(string, "10m")
read = optional(string, "5m")
update = optional(string, "10m")
delete = optional(string, "30m")
})
{} no
retry_error_message_regex Optional azapi retry: a list of regexes matched against error messages that should be retried (Sentinel occasionally answers a transient conflict during workspace onboarding settles). list(string) null no
schema_validation_enabled azapi's embedded schema validation. Left on, plans catch typos early; the embedded schema can lag a brand-new api_version, so turn it off if a plan rejects a property the live API accepts. bool true no
sentinel_onboarding_id Optional: the id of the workspace's Sentinel onboarding state resource (the log-analytics-workspace module's sentinel_onboarding_ids output). Passing it makes every rule wait for onboarding to complete, closing the race where a freshly onboarded workspace answers 400 not-onboarded to the first rule read (caught live in this repo's CI). Omit for a workspace that is already onboarded. string null no
workspace_id Resource id of the Sentinel-onboarded Log Analytics workspace the rules live under (the azapi parent_id). string n/a yes

Outputs

Name Description
automation_rule_ids Map of logical rule key to the automation rule's resource id.
automation_rule_ids_zipmap Map of logical rule key to {name, id} for easy composition; name is the deployed GUID name.
automation_rule_names Map of logical rule key to the deployed GUID name (derived deterministically from the key unless overridden).
workspace_id The workspace the rules live under, passed through for composition.

About

🛡️ Terraform azapi module for Microsoft Sentinel automation rules: the full typed surface, conditions and all

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages