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.
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), andboolean(And/Or groups of the property kinds). Araw_jsonescape 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), andadd_incident_task_actions, plusraw_actions_jsonappended 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.
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.
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.
- The workspace arrives as its resource id (
workspace_id, the azapiparent_id), per the estate's pass-ids principle. It must be Sentinel-onboarded, and when the same configuration creates the onboarding, passsentinel_onboarding_id(the log-analytics-workspace module'ssentinel_onboarding_idsoutput) 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 aproperty_changedorproperty_array_changedcondition declaring what changed; the portal requires one, and the estate's live SOC rules follow it (an any-status-transitionproperty_changedon IncidentStatus is the standard shape). - This module is the full-surface sibling of the estate's
libre-devops/sentinel-automation-rule/azurermmodule: that one speaks the azurerm resource withcondition_jsonblobs, this one speaks the API directly with everything typed. - A
RunPlaybookaction 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_utcis a strict RFC3339 instant (for example2026-12-31T23:59:59Z).- The old estate pattern's
ignore_null_propertyhas 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_jsonblobs: each blob entry is oneraw_jsoncondition here, and can then be converted to its typed form one condition at a time.
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 |
|---|
| Name | Version |
|---|---|
| terraform | >= 1.9.0, < 2.0.0 |
| azapi | >= 2.0.0, < 3.0.0 |
| Name | Version |
|---|---|
| azapi | 2.11.0 |
No modules.
| Name | Type |
|---|---|
| azapi_resource.automation_rules | resource |
| azapi_client_config.current | data source |
| 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({ |
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({ |
{} |
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 |
| 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. |