Microsoft Sentinel's workspace settings and entity page content as code: UEBA, entity analytics, anomalies, EyesOn, and custom entity activities, over azapi.
These are the Sentinel toggles that every estate sets by hand in the portal, because no Terraform
provider exposes them: Microsoft.SecurityInsights/settings and entityQueries are absent from the
AzureRM provider entirely, and absent from every stable API version to date. They are also the
settings nobody can later account for, because nothing recorded who turned them on.
- Four settings kinds, each a workspace singleton.
Ueba,EntityAnalytics,AnomaliesandEyesOn. Each resource's name IS its kind, so a duplicate is impossible by construction. Theenabled_settingsoutput lists what is on, which is the line worth reviewing in a pull request because each is a workspace-wide behaviour change. - Presence is the toggle, and that has a consequence. A PUT enables a setting and a DELETE
disables it;
isEnabledis read only. So settinganomalies_enabled = falsedoes not turn anything off unless this module turned it on, because Terraform only deletes what it created. To take over a setting enabled by hand, enable it here once so the module owns it, then set it to false. - Custom entity activities.
entity_queriescreates the queries an analyst sees on the timeline of an entity page when they pivot onto a user, host or IP, which is where a triage decision actually gets made. Only theActivitykind is creatable; Expansion and Insight queries are Microsoft supplied templates and read only. - Stable names without a random provider. Entity queries are GUID-named in the portal. Here the
map key stays the readable handle and the deployed name derives from it with
uuidv5, so it is stable across plans and applies, and overridable per query when adopting an activity that already exists. - The subtle bug is caught statically.
required_input_fields_setsis a list of ALTERNATIVE field sets: the activity runs when the entity satisfies any one of them. So a field the query substitutes must appear in EVERY set, or the substitution renders empty whenever the entity matched through a set that omits it. The query still runs and the timeline row still appears, just wrong, which is worse than not appearing at all. Acheckblock reports it, advisory rather than fatal because a deliberately optional substitution is legitimate.
api_version defaults to 2025-10-01-preview. Both resource types are preview only: neither
appears in any stable version, including 2025-09-01, so there is no stable version to pin instead.
The version is a variable, so a consumer is never pinned to this module's release cadence.
securityMLAnalyticsSettings(built-in anomaly rules and their observation tuning) is already covered byterraform-azurerm-sentinel-alert-rulethroughazurerm_sentinel_alert_rule_anomaly_built_inand_duplicate. Theanomalies_enabledinput here is only the workspace level switch, not the per-rule surface.- Incident tasks are a child of an incident, which is a runtime object with a lifetime measured in days. They are created by an automation rule or a playbook, not declared as infrastructure.
module "sentinel_settings" {
source = "libre-devops/sentinel-settings/azapi"
version = "~> 1.0"
workspace_id = module.law.workspace_ids["log-ldo-uks-prd-001"]
sentinel_onboarding_id = module.law.sentinel_onboarding_ids["log-ldo-uks-prd-001"]
entity_analytics = {
entity_providers = ["AzureActiveDirectory"]
}
ueba = {
data_sources = ["SigninLogs", "AuditLogs", "SecurityEvent"]
}
anomalies_enabled = true
}Pass sentinel_onboarding_id on a workspace this run also onboards. It threads a graph edge rather
than data, so every setting waits for onboarding to complete; without it the first call races the
onboarding and answers 400 not-onboarded.
examples/minimal- a Sentinel-onboarded workspace and entity analytics, the cheapest of the four settings to turn on and off.examples/complete- every setting kind plus two custom entity activities, applying and destroying in one run.
Local work needs PowerShell 7+ and just, because the recipes
wrap the LibreDevOpsHelpers
PowerShell module (the same engine the libre-devops/terraform-azure action runs in CI). Install
just with brew install just, or uv tool add rust-just then uv run just <recipe>.
Run just to list recipes: just update-ldo-pwsh (install or force-update LibreDevOpsHelpers from
PSGallery), just validate, just scan (Trivy only), just pwsh-analyze (PSScriptAnalyzer only),
just plan, just apply, just destroy, just e2e, just test, and just docs (the
plan/apply/destroy recipes mirror the action, including the storage firewall dance; just e2e
applies an example then always destroys it, defaulting to minimal, so nothing is left running).
Releasing is also just:
just increment-release [patch|minor|major] bumps, tags, and publishes a GitHub release, and the
Terraform Registry picks up the tag.
The test suite is plan-time and offline: terraform init -backend=false && terraform test needs no
credentials. Note that terraform test treats a check block failure as a run failure, so the two
advisory checks cannot be asserted from a passing run; they are exercised by hand against a
deliberately broken configuration when they change.
This module is scanned with Trivy; HIGH and CRITICAL
findings fail the build. Any waiver is a deliberate, reviewed decision, never a way to quiet a
finding that should be fixed. Waivers live in .trivyignore.yaml (the
machine-applied source of truth, passed to Trivy with --ignorefile) and are mirrored in the table
below so the reason is auditable.
| Trivy ID | Resource | Finding | Justification |
|---|---|---|---|
| None |
To add an exception: add an entry to .trivyignore.yaml (id, optional paths to scope it, and a
statement recording why), then add a matching row here. Where the finding is out of this module's
scope, point the justification at the Libre DevOps module that does address it (for example the
private-endpoint module). Both the file and this table are reviewed in the pull request.
The Requirements, Providers, Inputs, Outputs, and Resources below are generated by terraform-docs.
| Name | Version |
|---|---|
| terraform | >= 1.9.0, < 2.0.0 |
| azapi | >= 2.0.0, < 3.0.0 |
| Name | Version |
|---|---|
| azapi | >= 2.0.0, < 3.0.0 |
No modules.
| Name | Type |
|---|---|
| azapi_resource.anomalies | resource |
| azapi_resource.entity_analytics | resource |
| azapi_resource.entity_queries | resource |
| azapi_resource.eyes_on | resource |
| azapi_resource.ueba | resource |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| anomalies_enabled | Whether the Anomalies setting is enabled. This is the workspace level switch for Sentinel's built-in anomaly detection; the individual anomaly rules and their observation tuning belong to the sentinel-alert-rule module, not here. The setting carries no writable properties: it exists or it does not. See the note above about false not disabling something this module never enabled. |
bool |
false |
no |
| api_version | API version for Microsoft.SecurityInsights/settings and entityQueries. Both are PREVIEW ONLY. They are absent from every stable version to date, including 2025-09-01, so there is no stable version to pin instead and a preview version is not a shortcut here. Variablised so consumers are never pinned to this module's release cadence. |
string |
"2025-10-01-preview" |
no |
| entity_analytics | Entity analytics, or null to leave it unmanaged. entity_providers are the identity directories synced into the IdentityInfo table, which is what gives an entity page its context. AzureActiveDirectory syncs Entra ID. ActiveDirectory syncs on-premises AD, and needs Defender for Identity connected; naming it without that connection yields an empty sync rather than an error. This is the setting UEBA depends on for identity enrichment, so enabling ueba without it is usually a half-configuration. |
object({ |
null |
no |
| entity_queries | Custom entity activities, keyed by a caller chosen handle. These are the queries an analyst sees on the timeline of an entity page when they pivot onto a user, host or IP, which is where a triage decision actually gets made and is otherwise configured by hand in every estate. Only the Activity kind is creatable. Expansion and Insight queries are Microsoft supplied templates and are read only. query runs with the source entity's fields substituted in, referenced as {{Entity.FieldName}}. required_input_fields_sets is a list of alternative field sets: the activity shows when the entity carries EVERY field in ANY one set, so [["Name","NTDomain"],["AadUserId"]] means an account resolved either way is enough. Get this wrong and the activity silently never appears, which is the single most common reason a custom activity looks broken. Because the sets are alternatives, any field the query or content SUBSTITUTES should appear in every one of them. A field named in only some sets is absent whenever the entity matched through one of the others, and the substitution renders empty rather than failing. A check block reports this; it is advisory, because a deliberately optional substitution is legitimate. content is the one line rendered on the timeline, and it may reference query result columns the same way. Keep it short: it is a timeline row, not a report. name overrides the generated resource name, which is otherwise derived deterministically from the map key. Set it only when adopting an activity that already exists. |
map(object({ |
{} |
no |
| eyes_on_enabled | Whether the EyesOn setting is enabled. EyesOn permits Microsoft engineers to access workspace data when investigating a support case or improving detections. This is a data governance decision rather than a detection one, so it is off by default and should be a deliberate, recorded choice in any regulated estate. |
bool |
false |
no |
| resource_timeouts | Per-operation timeouts applied to every resource this module creates. | 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 while 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 sentinel module's onboarding_id, or the log-analytics-workspace module's sentinel_onboarding_ids output). Passing it makes every setting wait for onboarding to complete, closing the race where a freshly onboarded workspace answers 400 not-onboarded to the first call. Omit for a workspace that is already onboarded. | string |
null |
no |
| ueba | User and Entity Behaviour Analytics, or null to leave it unmanaged. data_sources are the tables UEBA enriches. Each one has to actually be flowing into the workspace: naming a table with no connector behind it is accepted by the API and simply produces no behavioural data, which looks identical to UEBA not working. UEBA requires an Entra ID P2 or equivalent licence, and writes to the BehaviorAnalytics and IdentityInfo tables. Enabling it changes ingestion volume, so it is a cost decision as well as a detection one. |
object({ |
null |
no |
| workspace_id | Resource id of the Sentinel-onboarded Log Analytics workspace the settings live under (the azapi parent_id). | string |
n/a | yes |
| Name | Description |
|---|---|
| anomalies_id | Resource id of the Anomalies setting, or null when it is not enabled by this module. |
| enabled_settings | The settings kinds this module currently has enabled, sorted. This is the line worth reviewing in a pull request, because each of these is a workspace-wide behaviour change. |
| entity_analytics_id | Resource id of the entity analytics setting, or null when unmanaged. |
| entity_analytics_providers | The identity providers synced into IdentityInfo, or an empty list when unmanaged. |
| entity_queries | The full entity query objects, keyed by the caller's handle. |
| entity_query_ids | Map of the caller's handle to the entity query resource id. |
| entity_query_names | Map of the caller's handle to the deployed resource name, which is a GUID derived from the handle unless overridden. Needed to import or cross-reference an activity. |
| eyes_on_id | Resource id of the EyesOn setting, or null when it is not enabled by this module. |
| ueba_data_sources | The data sources UEBA enriches, or an empty list when unmanaged. Useful as the input to a check that a connector exists for each one. |
| ueba_id | Resource id of the UEBA setting, or null when unmanaged. |