Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Sentinel Microsoft Purview Insider Risk Management Data Connector Terraform Module

Connects Microsoft Purview Insider Risk Management (the product the resource calls Office IRM) to Microsoft Sentinel, so Insider Risk Management's alerts about risky user activity become available (azurerm_sentinel_data_connector_office_irm). Targets hashicorp/azurerm ~> 4.0.

Terraform Provider Module Type Resources Lifecycle

🧩 Overview

  • πŸ”Œ Brings Microsoft Purview Insider Risk Management alerts into the Sentinel workspace.
  • πŸ”΄ Nothing is updatable. All three arguments are force-new, and for a connector a destroy-and-recreate is a gap in collection, not a metadata edit.
  • ⚠️ tenant_id looks like a cross-tenant switch and is not β€” only the running account's tenant is supported, which is exactly what the computed default resolves to. Omit it.
  • πŸ”΄ This connector is the upstream a detection depends on. Destroying it breaks no rule's configuration and makes every rule built on its data permanently silent.
  • ⚠️ A preview API version, so expect the argument surface to move.
  • 🧬 One of nine identical tenant-scoped connector modules β€” the shape is shared, the data is not.
  • πŸ”’ No credential is accepted and none is emitted.

πŸ’‘ Why it matters: Insider risk is the one category in this batch where the subject of the detection is an employee rather than an attacker β€” which makes the data unusually sensitive and its handling a question for more than the security team. Worth a deliberate decision before enabling, not after.

πŸ“… Azure portal retirement - March 31, 2027

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 emits management_moves_to_the_defender_portal so the date reaches plan output and inventory reports rather than living only in documentation.

Reference: https://learn.microsoft.com/azure/sentinel/overview#microsoft-sentinel-in-the-azure-portal-retirement-timeline

❀️ Support this project

If this module saves you time, please consider supporting its continued development:


πŸ—ΊοΈ Where this fits in the family

flowchart LR
  law["terraform-azurerm-log-analytics-workspace: every connector writes into this workspace, and its RETENTION bounds how far back any detection built on the data can look"]
  onboard["terraform-azurerm-sentinel-log-analytics-workspace-onboarding: THE GATE. No connector works until Sentinel is onboarded, and DESTROYING IT OFFBOARDS SENTINEL."]
  wsid["and every connector takes log_analytics_workspace_id FROM THAT MODULE'S workspace_id OUTPUT, not from the workspace module's id - same string, but reading it through the onboarding module is what makes Terraform ORDER ONBOARDING FIRST"]
  clones["THE NINE TENANT-SCOPED CONNECTORS, all with an IDENTICAL three-argument shape: name, workspace, and an optional tenant_id. NOTHING is updatable, so every argument is force-new and the schema omits the update timeout."]
  list["azure-active-directory. azure-advanced-threat-protection. dynamics-365. microsoft-defender-advanced-threat-protection. microsoft-threat-protection. office-365-project. office-atp. office-irm. office-power-bi."]
  tenant["AND tenant_id IS A TRAP: it is optional and computed, so it defaults to the running account's tenant - and the provider states that ONLY the same tenant is allowed, because cross-tenant collection IS NOT SUPPORTED. An argument that looks like it enables multi-tenant collection and does not."]
  preview["all nine sit on a PREVIEW API version, which is worth knowing before depending on their argument surface"]
  others["THE OTHER NINE CONNECTORS ARE NOT CLONES: azure-security-center and iot take a SUBSCRIPTION id instead. microsoft-cloud-app-security and office-365 carry per-stream TOGGLES. microsoft-threat-intelligence and threat-intelligence take a LOOKBACK DATE. aws-cloud-trail and aws-s3 take an AWS ROLE ARN. threat-intelligence-taxii takes a URL, a collection and CREDENTIALS."]
  rules["AND CONNECTORS ARE THE UPSTREAM THAT DECIDES WHETHER A DETECTION CAN FIRE AT ALL"]
  ms["terraform-azurerm-sentinel-alert-rule-ms-security-incident: its product_filter takes LEGACY product names, and three of them map exactly onto connectors in this batch - Azure Advanced Threat Protection, Microsoft Defender Advanced Threat Protection, and Office 365 Advanced Threat Protection."]
  sched["terraform-azurerm-sentinel-alert-rule-scheduled and -nrt: a query against a table no connector is filling DEPLOYS CLEANLY AND NEVER FIRES"]
  anom["terraform-azurerm-sentinel-alert-rule-anomaly-built-in: emits required_data_connector, which is Microsoft's own statement of which connectors its detection needs. Cross-check it against what is actually deployed."]
  silent["SO THE FAMILY RISK IS THE SAME ONE AS THE ALERT RULES, ARRIVING FROM UPSTREAM: a perfectly configured detection with no connector behind it is SILENT, and silence is indistinguishable from the good outcome."]

  law -->|"id"| onboard
  onboard -->|"workspace_id"| wsid
  wsid -->|"log_analytics_workspace_id"| clones
  wsid -->|"log_analytics_workspace_id"| others
  clones -->|"which nine"| list
  tenant -->|"read this first"| clones
  preview -->|"stability"| clones
  clones -->|"fills tables"| rules
  others -->|"fills tables"| rules
  rules -->|"product_filter upstream"| ms
  rules -->|"query tables"| sched
  rules -->|"required_data_connector"| anom
  silent -->|"the shared failure mode"| rules

  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 clones me;
  class law,wsid,list,tenant,preview,others,rules,ms,sched,anom,silent sib;
Loading

🧬 What this module builds

flowchart TB
  identical["READ THIS FIRST: NINE CONNECTOR MODULES SHARE THIS EXACT SHAPE. name, log_analytics_workspace_id, and an optional tenant_id. Nothing else. So this diagram is the same in all nine READMEs, and what differs between them is only WHICH DATA ARRIVES."]
  nothing["NOTHING IS UPDATABLE. All three arguments are force-new and the provider's schema OMITS THE UPDATE TIMEOUT, so declaring one fails at plan. Every change to a connector is a destroy-and-recreate."]
  gap["and a destroy-and-recreate is a GAP IN COLLECTION, not a metadata edit. Whatever the connector would have ingested between the destroy and the create is simply not there, and no detection built on that table will ever know."]
  tenant["tenant_id IS A TRAP. It is optional AND computed, so omitting it takes the running account's tenant - and the provider states that ONLY THE SAME TENANT IS ALLOWED because cross-tenant collection IS NOT SUPPORTED YET."]
  looks["SO IT IS AN ARGUMENT THAT LOOKS LIKE IT ENABLES MULTI-TENANT COLLECTION AND DOES NOT. Setting it to another tenant's id is accepted by the type system and fails at apply, which is why these modules validate the GUID SHAPE and document the same-tenant restriction rather than pretending to check it."]
  omit["and the recommendation is therefore to OMIT IT: the computed default is the only value that can work, and an omitted argument cannot drift from it"]
  ws["log_analytics_workspace_id wants the workspace's ARM RESOURCE ID, and azurerm_log_analytics_workspace ALSO exports an attribute literally named workspace_id which is the customer GUID. A bare GUID is rejected at plan time."]
  order["and it should be wired from the ONBOARDING module's workspace_id output, not the workspace module's id - same string, but that is what makes Terraform ORDER ONBOARDING BEFORE THE CONNECTOR"]
  preview["ALL NINE SIT ON A PREVIEW API VERSION, so expect their argument surface to move more than a stable resource's would. The provider pin is the mitigation."]
  upstream["AND A CONNECTOR IS THE UPSTREAM THAT DECIDES WHETHER A DETECTION CAN FIRE. A scheduled rule querying a table this connector was supposed to fill deploys cleanly and never fires - so the connector, not the rule, is where that silence starts."]
  this["one of the nine identical tenant-scoped data connector modules"]
  keystone["the azurerm_sentinel_data_connector resource for that one product, named this"]

  identical -->|"and"| nothing
  nothing -->|"consequence"| gap
  gap -->|"lifecycle"| this
  tenant -->|"so"| looks
  looks -->|"therefore"| omit
  omit -->|"recommended"| this
  ws -->|"and"| order
  order -->|"wiring"| this
  preview -->|"stability"| this
  upstream -->|"why it matters"| this
  this -->|"brings one product's data into the workspace"| 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 identical,nothing,gap,tenant,looks,omit,ws,order,preview,upstream sib;
Loading

ℹ️ That diagram is shared by all nine tenant-scoped connector modules, deliberately: they have the same schema, the same lifecycle and the same failure modes. What differs between them is which data arrives β€” which is what the Overview and example 2 are for.

Resource inventory

Resource Count Notes
azurerm_sentinel_data_connector_office_irm.this 1 The keystone. Three arguments, none updatable.
timeouts block 0..1 Create, read and delete only β€” there is no update operation.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Azure resource provider Microsoft.SecurityInsights on a Log Analytics workspace β€” a preview API version
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:

  • ⚠️ tenant_id is optional AND computed, and only the same tenant as the running account is allowed β€” the provider states that cross-tenant collection is not supported yet.
  • πŸ”΄ All three arguments are force-new, and the schema omits the update timeout. Declaring one fails at plan.
  • The API version is a preview one.
  • The Resource ID ends /dataConnectors/<name> under the workspace.
  • One connector of this type per workspace in normal use.
  • No tags. Tag the workspace.
  • lifecycle is not valid inside a module block, so a caller cannot add prevent_destroy.

πŸ”‘ Required Azure RBAC Roles / Permissions

Operation Role Scope
Creating or deleting a data connector Microsoft Sentinel Contributor the resource group containing the Log Analytics workspace, or the workspace
Reading connectors Microsoft Sentinel Reader the same scope

Contributor or Owner at the same scope also work, and are broader than needed.

⚠️ Azure RBAC is only half of it. Connecting this workload usually also requires tenant-level rights on the product side β€” a Global Administrator, or an Insider Risk Management role in Purview β€” which are not Azure roles and cannot be granted through this module. A caller with Sentinel Contributor and nothing else can fail at apply.

Azure Prerequisites

  • The Log Analytics workspace already onboarded to Microsoft Sentinel β€” wire log_analytics_workspace_id from that module's workspace_id output so Terraform orders the two correctly.
  • An Insider Risk Management licence, typically via Microsoft 365 E5 or the E5 compliance add-on.
  • Insider risk policies already configured in Microsoft Purview. The connector carries alerts those policies raise; with no policies there are no alerts.
  • Retention long enough to be useful. Sentinel has no storage of its own, so the workspace's retention bounds what any detection on this data can look back at.

πŸ“ Module Structure

terraform-azurerm-sentinel-data-connector-office-irm/
β”œβ”€β”€ providers.tf   # required_version + the pinned azurerm provider. No provider block.
β”œβ”€β”€ variables.tf   # name, log_analytics_workspace_id, tenant_id, timeouts (no update)
β”œβ”€β”€ main.tf        # the keystone, one resource
β”œβ”€β”€ outputs.tf     # id first, then the tenant facts, then `collects` and the upstream flag
β”œβ”€β”€ README.md      # this document
β”œβ”€β”€ SCOPE.md       # the cross-module contract and the tenant rationale
β”œβ”€β”€ LICENSE        # MIT
└── .gitignore

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "dc_irm" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-data-connector-office-irm.git?ref=v1.0.0"

  name = "insider-risk"

  # ⚠️ From the ONBOARDING module, not the workspace module. See example 1.
  log_analytics_workspace_id = module.sentinel.workspace_id

  # tenant_id deliberately omitted β€” the computed default is the only workable
  # value, because cross-tenant collection is not supported. See example 3.
}

ℹ️ The caller configures the provider, its authentication, and the mandatory features {} block. This module declares none of them.

⚠️ Read example 4 before your first apply β€” there is no update path, so every later change is a gap in collection.

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
name string caller β€” an ARM name, force-new
log_analytics_workspace_id string the Sentinel onboarding module β†’ workspace_id
tenant_id string omit it β€” the computed default is the only workable value

Emits

Output Description Consumed by
id The connector's Resource ID. review, imports
name The ARM name. Force-new. review
log_analytics_workspace_id The workspace it writes into. Force-new. review
tenant_id The tenant collected from; computed when omitted. review
tenant_id_was_explicit Derived β€” whether a tenant was stated rather than defaulted. review
collects A constant naming what arrives. coverage review
is_upstream_of_detections Always true. dependency review

No credential is accepted and none is emitted.

πŸ“š Example Library

The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:

variable "sentinel_dev_workspace_id" {
  description = "workspace id of an existing sentinel dev that these examples reference but do not create."
  type        = string
}

variable "sentinel_prod_workspace_id" {
  description = "workspace id of an existing sentinel prod that these examples reference but do not create."
  type        = string
}
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 connector, and a connector created against a workspace that is not yet a Sentinel workspace fails at apply.

⚠️ 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_workspace exports both an id and an attribute literally named workspace_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 Β· πŸ”Œ What this connector actually brings in
Brings in:  Insider Risk Management ALERTS about risky user activity
            (data exfiltration, departing-employee activity, policy violations)

⚠️ THIS DATA IS ABOUT EMPLOYEES, NOT ATTACKERS. That makes it categorically different from the other eight connectors in this batch, and worth pausing over: insider-risk alerts name individuals and describe their behaviour, and bringing them into a workspace that a wider security team can query changes who can see them.

πŸ’‘ So enabling this connector is a decision for more than the security team. In most organisations it needs the agreement of the people who own employee-privacy and works-council obligations, and in some jurisdictions the collection itself is constrained. Nothing in Terraform will raise that question β€” which is why it is raised here.

ℹ️ Purview already applies pseudonymisation controls to insider-risk data on its own side, and those settings are configured in Purview rather than here. Whether they carry through to what lands in the workspace is a product-side question worth verifying before you rely on it.

πŸ’‘ Practical consequence for Sentinel: consider whether these alerts belong in the same workspace as the rest β€” Sentinel RBAC is workspace-scoped, so anyone who can read the workspace can read these alerts.

ℹ️ The resource is named office_irm β€” the product is Microsoft Purview Insider Risk Management.

3 · ⚠️ `tenant_id` looks like a cross-tenant switch and is not
# βœ… Recommended: omit it.
# tenant_id = ...
# ⚠️ Legal, and adds nothing.
tenant_id = "00000000-0000-0000-0000-000000000000" # your own tenant
# ❌ Accepted by the type system. Fails at apply.
tenant_id = "11111111-1111-1111-1111-111111111111" # someone else's tenant

⚠️ The provider states the restriction plainly: only the same tenant as the running account is allowed, because cross-tenant collection is not supported yet. The field is optional and computed, so omitting it resolves to that same tenant β€” the only value that can work.

πŸ’‘ So omitting it is strictly better than setting it. An explicit value adds a way to be wrong without adding a capability, and because the field is force-new, correcting a wrong value is a destroy-and-recreate β€” which for a connector means a gap in collection (example 4).

ℹ️ The restriction is documented, not enforced. This module cannot see which tenant the running account belongs to, so a validation would either be wrong or reject legal input. The GUID shape is checked and the restriction is stated:

Error: Invalid value for variable

  tenant_id must be a GUID when set. Omit the argument entirely to use the running
  account's tenant, which is the only tenant this connector can collect from.

πŸ’‘ tenant_id_was_explicit is emitted so a review can find the configurations that took the risk (example 8).

4 Β· πŸ”΄ Nothing is updatable β€” and that means gaps
Force-new: name  log_analytics_workspace_id  tenant_id
Updatable: nothing
timeouts = { create = "30m", read = "5m", delete = "30m" } # βœ…
timeouts = { create = "30m", update = "30m" }               # ❌ fails at plan

πŸ”΄ Every change to this connector is a destroy-and-recreate, and for a connector that is not a metadata edit β€” it is a gap in collection. Whatever would have been ingested between the destroy and the create is simply absent from the workspace, and no detection built on the table will report the hole.

⚠️ So a rename β€” the most innocuous-looking change imaginable β€” costs data. Settle name before the first apply.

ℹ️ The absent update timeout is a schema fact, not an omission: with nothing updatable there is no update operation, so writing update into the RESOURCE's timeouts block is a validate-time error.

⚠️ But a stray update in this module's timeouts VARIABLE is silently discarded, not rejected. Terraform's object-type conversion drops attributes the declared type does not carry, so a four-field timeouts block copied from a sibling module applies three fields and warns about nothing β€” the one place in this module where a wrong input produces no error at all.

πŸ’‘ A caller cannot add prevent_destroy (lifecycle is not valid inside a module block). A CanNotDelete lock at the workspace scope protects the whole Sentinel estate, which is the available control:

resource "azurerm_management_lock" "sentinel" {
  name       = "sentinel-no-delete"
  scope      = module.law.id
  lock_level = "CanNotDelete"
  notes      = "Deleting connectors leaves gaps in collection that nothing reports"
}
5 Β· πŸ”΄ This connector is where a detection's silence starts
NO CONNECTOR                            CONNECTOR PRESENT
   |                                        |
   v                                        v
the table is empty                      the table fills
   |                                        |
   v                                        v
a scheduled rule querying it            the rule fires when it should
DEPLOYS CLEANLY AND NEVER FIRES

πŸ”΄ The provider models no relationship between a connector and the rules that depend on it. Terraform cannot order them, no output on either side reveals the dependency, and a rule whose data never arrives is indistinguishable from a rule with nothing to report.

⚠️ So destroying this connector breaks no rule's configuration β€” every rule stays valid, enabled and apparently healthy, and goes permanently quiet.

πŸ’‘ That asymmetry is why the module emits a constant:

output "upstream" { value = module.dc_irm.is_upstream_of_detections } # always true

ℹ️ Three modules in this family make the dependency visible from the other side, and are worth knowing about: the anomaly-built-in module emits required_data_connector β€” Microsoft's own statement of which connectors its detection needs; the ms-security-incident module's product_filter names the product whose connector must exist; and the threat-intelligence rule module documents that with no indicator source it cannot fire at all.

⚠️ And for this connector specifically, think about who consumes it. A sentinel-alert-rule-ms-security-incident module promoting these alerts puts named-employee findings into the general incident queue. That may be exactly right, and it should be a decision.

6 Β· One connector per workspace, and what a second one does
# βœ… One per workspace.
module "dc_irm" {
  name                       = "insider-risk"
  log_analytics_workspace_id = module.sentinel.workspace_id
}

ℹ️ A second connector of the same type in the same workspace is redundant rather than additive. It does not double the data or widen the collection; the service already collects everything this connector type covers for the tenant.

⚠️ Where it is legitimate: several workspaces, each with its own connector β€” see example 7.

πŸ’‘ If you find yourself wanting two, the thing you are reaching for is probably one of the other connectors: this family has eighteen resource types, and the nine that are not clones take subscription IDs, per-stream toggles, lookback dates, AWS role ARNs or TAXII credentials.

7 Β· Several workspaces, each with its own connector
locals {
  workspaces = {
    prod = var.sentinel_prod_workspace_id
    dev  = var.sentinel_dev_workspace_id
  }
}

module "dc_irm" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-data-connector-office-irm.git?ref=v1.0.0"
  for_each = local.workspaces

  name                       = "insider-risk-${each.key}"
  log_analytics_workspace_id = each.value
}

ℹ️ for_each lives in the caller β€” the resource is one-per-workspace, and the module reflects that.

⚠️ Note that each workspace's connector collects the same tenant's data. Cross-tenant is not supported (example 3), so several workspaces means several copies of the same collection, not broader coverage β€” and the ingestion cost is duplicated.

πŸ’‘ Keying by environment rather than by workspace ID keeps the keys stable if a workspace is ever replaced.

⚠️ name includes the key because a connector's ARM name must be unique within its workspace β€” and because a force-new rename later costs data (example 4), it is worth getting the pattern right first.

8 Β· What a review should assert
output "dc_irm_review" {
  value = {
    id             = module.dc_irm.id
    collects       = module.dc_irm.collects                  # what arrives
    tenant         = module.dc_irm.tenant_id                 # the computed default
    tenant_stated  = module.dc_irm.tenant_id_was_explicit    # expect false
    upstream       = module.dc_irm.is_upstream_of_detections  # always true
  }
}

πŸ”’ collects is the line that makes a coverage review possible. A Resource ID and an ARM name say nothing about the data; this output names it, so a list of connectors reads as a list of what you can detect on.

⚠️ tenant_stated should normally be false. true is not wrong, but it means a configuration set the one value that cannot vary β€” adding a way to be wrong without adding a capability (example 3).

ℹ️ upstream is a constant. It is in the outputs so that "what goes quiet if we remove this?" has an answer in the state review rather than only in this document.

πŸ’‘ Nothing here is sensitive, and nothing needs to be. None of the nine clone connectors takes a credential β€” authorisation is the service's, through the tenant the running account belongs to.

9 Β· Importing a connector enabled from the portal
terraform import 'module.dc_irm.azurerm_sentinel_data_connector_office_irm.this' \
  "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sentinel/providers/Microsoft.OperationalInsights/workspaces/law-sentinel-prod/providers/Microsoft.SecurityInsights/dataConnectors/insider-risk"

πŸ’‘ Importing is usually the right move here. Connectors are normally enabled from the Sentinel data-connectors gallery, and since every argument is force-new, creating a duplicate in Terraform rather than adopting the existing one means deleting and recreating β€” a gap in collection (example 4).

⚠️ Match name exactly. It is the last segment of the Resource ID, it is force-new, and a mismatch proposes a replacement rather than an error.

ℹ️ Leave tenant_id unset when importing. It is computed, so the imported state carries the real value and an omitted argument matches it. Stating a value invites a needless diff.

πŸ’‘ Import, confirm an empty plan, and only then change anything.

10 · ⚠️ A preview API version
This resource's ARM API version is a PREVIEW version.

⚠️ Worth knowing before depending on the argument surface. Preview API versions can change behaviour between provider releases in ways a stable version would not, and this family has already seen that churn: the Fusion alert rule's name argument is deprecated and removed in provider v5.0.

πŸ’‘ The practical implication is not "avoid these resources" β€” there is no stable alternative, and the data is the point. It is: pin the provider version, read the changelog for these resources before upgrading, and expect more movement than a stable resource would show.

ℹ️ This library pins azurerm ~> 4.0 precisely so that a provider upgrade is a deliberate, per-module decision rather than something that happens on a terraform init.

11 Β· What a destroy actually removes
terraform destroy on this module:
  removes the connector    ->  collection STOPS
                           ->  the workspace, its tables and existing data SURVIVE
                           ->  every rule built on this data stays valid and goes SILENT

ℹ️ Existing data is not deleted. The workspace keeps whatever was already ingested, subject to retention β€” so a destroy is not data loss, it is the end of new data.

πŸ”΄ But the detections do not fail; they go quiet (example 5). Nothing in Azure raises an alert for "a table stopped filling", and a rule with no results looks exactly like a rule with nothing to report.

⚠️ And re-creating the connector does not backfill. The gap between destroy and create is permanent β€” which is the same cost as any force-new change here (example 4).

πŸ’‘ So the order for a deliberate removal is: decide what detections depend on this data, say so out loud, then remove the connector. And if the intent is only "stop managing this with Terraform", the operation you want is terraform state rm, which leaves collection running.

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 # bounds every detection built on this connector's data
}

# ── 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
}

# ── THE CONNECTOR: the upstream that decides whether the rules below can fire ──
module "dc_irm" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-sentinel-data-connector-office-irm.git?ref=v1.0.0"

  name                       = "insider-risk"
  log_analytics_workspace_id = module.sentinel.workspace_id

  # tenant_id omitted: the computed default is the only workable value (example 3).
}

resource "azurerm_management_lock" "sentinel" {
  name       = "sentinel-no-delete"
  scope      = module.law.id
  lock_level = "CanNotDelete"
  notes      = "Deleting connectors leaves gaps in collection that nothing reports"
}

# ── No promotion rule in this example, deliberately ────────────────────
#
# Insider-risk alerts name individuals. Promoting them into the general incident
# queue puts employee-behaviour findings in front of everyone with workspace access,
# so this composition brings the data in and leaves the routing decision explicit
# rather than making it by default.

output "collection_posture" {
  value = {
    # What arrives, and from where.
    collects      = module.dc_irm.collects
    tenant_stated = module.dc_irm.tenant_id_was_explicit # expect false
    upstream      = module.dc_irm.is_upstream_of_detections

    # Deliberately no promotion rule here β€” see the comment above.

  }
}

πŸ”’ What the composition gets right: the connector declared alongside the detections that depend on it so the relationship is legible even though Terraform cannot model it, the workspace wired from the onboarding module, tenant_id omitted, a CanNotDelete lock over the estate, and retention stated where it can be seen next to the data it bounds.

⚠️ What no plan will tell you: whether the product-side prerequisites are satisfied, whether data is actually arriving, or whether the detections below have anything to work with yet.

πŸ’‘ depends_on would not help here. The provider models no relationship between a connector and a rule, and creation order is not the issue β€” a rule created before its data simply finds nothing until the data arrives. The dependency is operational, not ordering.

πŸ“₯ Inputs

Input Type Default Notes
name string β€” Required. Force-new. ⚠️ A rename costs data β€” see example 4.
log_analytics_workspace_id string β€” Required. Force-new. Shape-validated; from the onboarding module.
tenant_id string null Force-new, computed. ⚠️ Omit it β€” see example 3.
timeouts object(...) null Create, read, delete only β€” there is no update operation.

There is no tags variable β€” the provider exposes none on Sentinel data connectors.

Full schemas
variable "tenant_id" {
  type    = string
  default = null
  # Optional AND computed, and only the running account's tenant is supported β€” cross-tenant
  # collection is not available. So the computed default is the only workable value, and omitting
  # the argument is strictly better than setting it. The restriction is DOCUMENTED, not enforced:
  # this module cannot see which tenant the running account belongs to.
  validation {
    condition     = var.tenant_id == null || 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}$", coalesce(var.tenant_id, "x")))
    error_message = "tenant_id must be a GUID when set. Omit the argument entirely to use the running account's tenant, which is the only tenant this connector can collect from."
  }
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    delete = optional(string)
  })
  default = null
  # No `update`: all three arguments are force-new, so the schema omits it.
}

🧾 Outputs

Output Description Kind
id The Resource ID of the data connector Passthrough
name The connector's ARM name Passthrough
log_analytics_workspace_id The workspace this connector writes into Passthrough
tenant_id The tenant being collected from Passthrough
tenant_id_was_explicit Derived Derived
collects A constant naming what this connector brings into the workspace Derived
is_upstream_of_detections Always true, and emitted because it is the fact most easily forgotten: detections built on this connector's tables cannot fire without it Constant
every_argument_is_force_new Always true Constant
import_checks_the_connector_kind Always true, and an unusually protective import guard Constant
requires_the_workspace_to_be_sentinel_onboarded Always true, and enforced by nothing offline Constant
applies_are_not_serialised_by_the_provider Always true 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.

🧠 Architecture Notes

  • The absence of an update path is treated as the module's headline fact, because for a connector it has a cost that a force-new field usually does not. Replacing a virtual machine is disruptive and visible; replacing a connector is quiet and leaves a hole in the data that no detection reports. So the documentation says "gap in collection" rather than "force-new" wherever it can.

  • tenant_id is documented as a non-capability. It is optional, computed, force-new, and the only legal value is the one the default resolves to β€” so the recommendation is to omit it, and tenant_id_was_explicit exists to show which configurations did not. An argument that looks like a cross-tenant switch is worth more words than an argument that is one.

  • The tenant restriction is validated for shape and not for correctness, with the reason stated. This module cannot see which tenant the running account belongs to, so a check would either be wrong or reject legal input. Where a constraint cannot be verified, this suite states it rather than approximating it.

  • collects is emitted as a constant string, which is unusual and earns its place. A connector's Resource ID and ARM name say nothing about the data, and "what can we detect on?" is the question a coverage review actually asks. Putting the answer in the outputs turns a list of connectors into a list of capabilities.

  • is_upstream_of_detections is a constant true for the fact it makes visible, not for the value. Destroying a connector breaks no rule's configuration and makes every rule built on its data permanently silent β€” an asymmetry the provider models nowhere, so the module says it.

  • The nine clones deliberately share one generated shape, one shape diagram and one example skeleton. They have the same schema, lifecycle and failure modes; differentiating their documentation beyond what actually differs would invent distinctions a reader would then have to check. What varies is stated where it varies β€” the data, the product name, the prerequisites and the detections that consume it.

  • depends_on is explicitly not recommended for the connector-to-rule relationship. Creation order is not the problem: a rule created before its data simply finds nothing until data arrives. Suggesting depends_on would imply the dependency is an ordering one when it is operational.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Silent collection gaps the no-update-path consequence documented in five sections β€”
Illusory capability tenant_id recommended omitted; tenant_id_was_explicit emitted state it, knowingly
Unverifiable constraints the same-tenant rule documented, not faked as a validation β€”
Invisible coverage collects emitted so a connector list reads as a capability list β€”
Invisible dependencies is_upstream_of_detections emitted; the silence asymmetry explained β€”
Wrong-value pastes the workspace's GUID attribute rejected at plan β€”
Preview-API churn disclosed, with the provider pin named as the mitigation β€”
Destroy protection a workspace-scope CanNotDelete lock recommended destroy anyway, knowingly
Secrets none accepted, none emitted β€”
  • Before the first apply: settle name. A later rename costs data.
  • Before the first apply: check the product-side prerequisites β€” Azure RBAC is only half of it.
  • Before setting tenant_id: it cannot vary, so omitting it is strictly better.
  • Before a destroy: work out which detections go quiet, and say so out loud.

πŸš€ Runbook

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.
  • πŸ”΄ Any change is a destroy-and-recreate, and therefore a gap in collection. Read plans carefully.
  • ⚠️ A destroy stops collection and leaves every dependent rule valid and silent. Existing data survives.
  • ℹ️ Prefer terraform import over creating a connector that already exists in the portal.
  • ℹ️ To stop managing without stopping collection, use terraform state rm.
  • ℹ️ After the first apply, confirm data is arriving in the Sentinel data-connectors blade. No Terraform output can tell you.

πŸ§ͺ Testing

terraform validate and terraform fmt -check are the offline gate. They confirm:

  • log_analytics_workspace_id is a Microsoft.OperationalInsights/workspaces ARM Resource ID β€” a bare GUID is rejected;
  • name is non-empty;
  • tenant_id, when set, is a GUID;
  • the timeouts variable's object type declares only create, read and delete;
  • no output is sensitive, because nothing sensitive is accepted;
  • the module declares no provider block.

πŸ’‘ These were proved by evaluating the conditions in terraform console inside the module β€” which does fire root-module variable validations, unlike terraform validate on a calling configuration. A bare GUID in the workspace field and a non-GUID tenant_id both fail; a workspace ARM Resource ID with tenant_id omitted passes and the tenant reads back as null until apply.

What only plan and apply exercise:

  • whether the workspace exists and is onboarded to Sentinel;
  • whether the identity holds Microsoft Sentinel Contributor;
  • whether the tenant supplied is the running account's β€” the restriction this module documents rather than checks.

What no Terraform command checks at any stage:

  • πŸ”΄ whether data is actually arriving β€” the only thing that makes a connector useful;
  • whether the product-side prerequisites are satisfied β€” licensing, consent and tenant-level rights are not Azure roles;
  • which detections depend on this connector's tables, and would go silent without it;
  • whether workspace retention suits the data this connector brings.

πŸ’¬ Example Output

Outputs:

collects                   = "Microsoft Purview Insider Risk Management alerts"
id                         = "/subscriptions/00000000-.../workspaces/law-sentinel-prod/providers/Microsoft.SecurityInsights/dataConnectors/insider-risk"
is_upstream_of_detections  = true
log_analytics_workspace_id = "/subscriptions/00000000-.../workspaces/law-sentinel-prod"
name                       = "insider-risk"
tenant_id                  = "00000000-0000-0000-0000-000000000000"
tenant_id_was_explicit     = false

βœ… tenant_id_was_explicit = false with a populated tenant_id β€” the recommended shape: the value was computed from the running account's tenant rather than stated (example 3).

πŸ’‘ collects is the line a coverage review reads. Everything else identifies the resource; this one says what it is for.

⚠️ is_upstream_of_detections = true is a constant. Read it as "and this is what goes quiet if the connector is removed".

πŸ” Troubleshooting

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 tenant_id It is not a GUID. Omit it entirely (example 3).
Apply fails on the tenant A different tenant was supplied; cross-tenant is unsupported. Omit tenant_id (example 3).
An update timeout you set is silently ignored, with no error This module's timeouts is a typed object with no update attribute, and Terraform's object-type conversion discards undeclared keys without raising anything. The Unsupported argument: update diagnostic exists, but only inside a resource block's own timeouts -- which a module caller never writes. Do not set update; there is no update operation to time out. Nothing will warn you, so check the key is absent rather than relying on a plan error.
A small edit shows a destroy and recreate Every argument is force-new. Expected β€” and it costs data (example 4).
Data is missing for a period A destroy-and-recreate left a gap; re-creating does not backfill. Avoid force-new edits after the first apply (examples 4, 11).
Apply fails on permissions despite Sentinel Contributor Product-side rights are not Azure roles. See the RBAC note above.
The connector exists and no data arrives Product-side prerequisites, licensing or consent. Check the Sentinel data-connectors blade.
Detections built on this data never fire The connector may be absent or not yet collecting. Check here first, not the rule (example 5).
A duplicate connector was created instead of adopted Every argument is force-new, so a mismatch replaces. Import the existing one (example 9).
An import proposes a replacement name does not match the Resource ID's last segment. Match it exactly (example 9).
An import shows a tenant_id diff A value was stated in the configuration. Leave it unset (example 9).
Wanted to collect from another tenant Cross-tenant collection is not supported. Not possible (example 3).
Wanted to tag this resource The provider exposes no tags. Tag the workspace.

πŸ”— Related Docs

πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."