Skip to content

Repository files navigation

Terraform Azure Sentinel Settings

Microsoft Sentinel's workspace settings and entity page content as code: UEBA, entity analytics, anomalies, EyesOn, and custom entity activities, over azapi.

CI Release Terraform Registry License


Overview

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, Anomalies and EyesOn. Each resource's name IS its kind, so a duplicate is impossible by construction. The enabled_settings output 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; isEnabled is read only. So setting anomalies_enabled = false does 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_queries creates 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 the Activity kind 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_sets is 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. A check block reports it, advisory rather than fatal because a deliberately optional substitution is legitimate.

Preview API, deliberately

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.

What is deliberately not here

  • securityMLAnalyticsSettings (built-in anomaly rules and their observation tuning) is already covered by terraform-azurerm-sentinel-alert-rule through azurerm_sentinel_alert_rule_anomaly_built_in and _duplicate. The anomalies_enabled input 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.

Usage

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

  • 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.

Developing

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.

Security scan exceptions

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.

Reference

The Requirements, Providers, Inputs, Outputs, and Resources below are generated by terraform-docs.

Requirements

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

Providers

Name Version
azapi >= 2.0.0, < 3.0.0

Modules

No modules.

Resources

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

Inputs

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({
entity_providers = list(string)
})
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({
title = string
content = string
query = string
input_entity_type = string

required_input_fields_sets = list(list(string))

description = optional(string)
entities_filter = optional(map(list(string)))
enabled = optional(bool, true)
name = optional(string)
}))
{} 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({
create = optional(string, "30m")
read = optional(string, "5m")
update = optional(string, "30m")
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 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({
data_sources = list(string)
})
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

Outputs

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.

About

🛡️ Microsoft Sentinel workspace settings and entity page content as code: UEBA, entity analytics, anomalies, EyesOn and custom entity activities, over azapi

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages