Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

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

Repository files navigation

☁️ Azure Security Center Setting Terraform Module

Manages one Defender for Cloud data-access setting for a subscription β€” whether findings flow to Defender for Endpoint, Defender for Cloud Apps or Microsoft Sentinel. One boolean whose safe direction depends on which setting you picked. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Posture


🧩 Overview

  • βš™οΈ Creates one azurerm_security_center_setting, named this, for the subscription the caller's provider points at.
  • πŸ”΄ The provider accepts a sixth setting name the apply cannot use. Its validator is built from an API enum containing current, which the provider has no request mapping for β€” so current passes plan-time validation and then fails during apply. This module rejects it at parse time. That is why it validates a narrower set than the provider.
  • πŸ”΄ enabled has no safe default, and the module says so rather than inventing one. For the three WDATP settings, true switches on Defender for Endpoint integration and is the stronger posture. For MCAS and Sentinel, true starts an outbound data flow. One boolean, two meanings.
  • πŸ”΄ Destroy writes false; it removes nothing. The provider implements destroy as an update, so removing this module switches the setting off and leaves the record in place.
  • πŸ”΄ Whether a fresh apply succeeds depends on invisible subscription state. The provider raises its must-be-imported error only when the setting is already enabled.
  • ⚠️ One Terraform resource, two API models. The setting name decides which β€” and the provider's own source notes it ought to be split into two resources.
  • ⚠️ Owner on the subscription is required, per the provider's note.

πŸ’‘ Why it matters: three arguments, and almost every one of them hides something. A value the provider accepts but cannot apply, a boolean whose direction of safety flips with its neighbour, a destroy that is actually a write, and a create whose success depends on state you cannot see from the configuration.


❀️ Support this project

If this module saves you time:


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

This diagram is shared with the two server-vulnerability-assessment modules authored alongside it β€” the node text carries the difference. This module is the ...-security-center-setting node, the one whose edge runs to the Log Analytics workspace.

flowchart TB
  PLAN["terraform-azurerm-security-center-subscription-pricing<br/>the Defender for Servers plan"]
  VMMOD["terraform-azurerm-linux-virtual-machine<br/>terraform-azurerm-windows-virtual-machine"]
  VMA["terraform-azurerm-security-center-server-vulnerability-assessment-virtual-machine<br/>one record per machine, Qualys era"]
  SET["terraform-azurerm-security-center-server-vulnerability-assessments-setting<br/>subscription wide, selects MdeTvm"]
  DAS["terraform-azurerm-security-center-setting<br/>data access and alert sync"]
  POL["terraform-azurerm-security-center-assessment-policy"]
  ASM["terraform-azurerm-security-center-assessment"]
  LAW["terraform-azurerm-log-analytics-workspace<br/>holds the tags this family cannot"]

  PLAN -->|"prerequisite the modules cannot verify"| VMA
  PLAN -->|"prerequisite the modules cannot verify"| SET
  VMMOD -->|"id becomes virtual_machine_id"| VMA
  SET -.->|"supersedes the per machine record"| VMA
  DAS -->|"the Sentinel setting syncs alerts onward"| LAW
  POL -->|"id becomes assessment_policy_id"| ASM

  classDef batch fill:#0078D4,stroke:#004578,color:#ffffff
  classDef anchor fill:#004578,stroke:#002d4d,color:#ffffff
  classDef ext fill:#f0f3f7,stroke:#9aa7b5,color:#1b2733
  class VMA,SET,DAS batch
  class PLAN anchor
  class VMMOD,POL,ASM,LAW ext
Loading

That edge is the Sentinel setting: switching it on syncs Defender for Cloud alerts onward to Microsoft Sentinel, which sits on a Log Analytics workspace this module does not own.


🧬 What this module builds

flowchart TB
  IN1["setting_name<br/>five documented values, force new"]
  IN2["enabled<br/>required, no safe default exists"]
  TO["timeouts<br/>all four operations"]
  T["azurerm_security_center_setting.this"]
  ID["id<br/>arm_model"]
  CAT["governs_data_sharing_outward<br/>governs_defender_for_endpoint_integration<br/>enabling_this_shares_data_with_another_product"]
  TRAP["provider_accepts_a_setting_name_the_apply_cannot_map<br/>a_fresh_apply_is_refused_only_when_the_setting_is_already_on"]
  DEL["destroy_writes_enabled_false_it_does_not_remove_anything"]

  IN1 --> T
  IN2 --> T
  TO --> T
  T --> ID
  IN1 --> CAT
  IN2 --> CAT
  T --> TRAP
  T --> DEL

  classDef keystone fill:#004578,stroke:#002d4d,color:#ffffff
  classDef io fill:#0078D4,stroke:#004578,color:#ffffff
  classDef note fill:#f0f3f7,stroke:#9aa7b5,color:#1b2733
  class T keystone
  class IN1,IN2,TO,ID io
  class CAT,TRAP,DEL note
Loading
Resource Cardinality Notes
azurerm_security_center_setting.this single Named by setting_name; one per name per subscription
timeouts 0–1 All four operations β€” enabled updates in place

βœ… Provider / Versions

Item Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None here. The caller configures the provider, its auth, and features {}
API Microsoft.Security 2022-05-01

Schema notes that bite

  • πŸ”΄ The provider accepts current; the apply cannot map it. Its ValidateFunc is built from settings.PossibleValuesForSettingName(), which returns six values β€” the five documented ones plus current. The provider's expand function has no case for current and falls through to failed to deduce the kind from its name, at apply time. This module enforces the documented five instead, turning a deferred failure into a plan-time error. (Provider Go source, the validator and the expand switch's default branch; provider documentation, which lists five.)
  • πŸ”΄ One resource, two API models. MCAS and the three WDATP names write a data-export settings body; Sentinel writes an alert-sync settings body. The provider's source carries a note that the resource ought to be split into a data-export setting and an alert-sync setting. (Provider Go source, the file's leading comment and the expand switch.)
  • πŸ”΄ Deletion disables the setting. The provider's destroy calls the update operation with false β€” its error path even names the operation disabling. Nothing is removed. (Provider documentation, ~> note; provider Go source, the delete function.)
  • πŸ”΄ A fresh apply is refused only when the setting is already on. These are platform settings that always exist, so the provider's read always succeeds; its must-be-imported guard fires only when the existing value is Enabled. A first apply against a setting currently off simply writes the new value. (Provider Go source, the create path's existence check.)
  • πŸ”΄ setting_name is force-new; enabled is not. The name is the Resource ID's last segment. Changing it destroys one setting and creates another β€” and because destroy writes false, the old setting is switched off in the process. (Provider documentation, per argument; provider Go source, ForceNew: true.)
  • πŸ”΄ The subscription comes from provider configuration. The provider builds the ID from its own account subscription, so there is no subscription input and the module cannot report which subscription it changed. (Provider Go source.)
  • ⚠️ enabled is required. There is no empty call for this module to make safe, so the secure-by-default rule cannot apply β€” and no single default would be correct across the five names anyway. (Binary schema.)
  • ⚠️ The value set is case-sensitive with irregular casing. MCAS and the WDATP family are upper case; Sentinel is title case. The provider's validator compares case-sensitively. (Provider Go source, StringInSlice(..., false).)
  • ⚠️ The expand function tolerates an all-caps SENTINEL the validator refuses. It carries a bare "SENTINEL" case alongside the enum constant, but a configuration can never reach it, because validation rejects that spelling first. Dead code, not a second accepted value. (Provider Go source, the expand switch.)
  • ⚠️ The resource has a v0-to-v1 state migration. State written by much older provider versions is upgraded on first use. (Provider Go source, SchemaVersion: 1 and its state upgrader.)
  • ⚠️ No tags, no location, no resource_group_name. Subscription-scoped, so no universal tags tail. (Binary schema.)
  • ⚠️ The provider's Go source declares no CustomizeDiff, ConflictsWith, RequiredWith, ExactlyOneOf or AtLeastOneOf. There is no published rule linking setting_name and enabled β€” every combination is legal β€” so this module deliberately does not group them into one variable. (Provider Go source.)

πŸ”‘ Required Azure RBAC Roles / Permissions

Principal Permission Scope Why
The Terraform identity πŸ”΄ Owner The subscription The provider documents that this resource requires Owner on the subscription
The Terraform identity Microsoft.Security/settings/write The subscription Create, update, and disable on destroy
The Terraform identity Microsoft.Security/settings/read The subscription Refresh and plan
A data reviewer (no Azure role limits this) β€” Enabling MCAS or Sentinel starts an outbound data flow. Nothing in RBAC gates where that data goes

πŸ”΄ Owner includes role-assignment rights, so a pipeline permitted to apply this module can grant itself anything else in the subscription. That is a governance question about the pipeline rather than about this resource.

⚠️ The last row is not an Azure permission, and that is the point. Switching on MCAS or Sentinel sends Defender for Cloud data to another product. No Azure role constrains the retention, residency or onward use of that data once it arrives. Whoever owns data protection should see that decision β€” it is not settled by having Owner.

Plan access is not credential access. The only computed attribute is the Resource ID.


Azure Prerequisites

  • Microsoft.Security registered on the subscription.
  • πŸ”΄ Owner on the subscription, per the table above.
  • The destination product, where one applies. MCAS needs Defender for Cloud Apps; Sentinel needs a Sentinel workspace to receive the alerts; the WDATP settings need the Defender for Endpoint integration. None of them is created here.
  • πŸ”΄ Certainty about which subscription the provider points at. This module cannot report it.
  • πŸ”΄ Knowledge of the setting's current value, if a first apply must not fail: the provider refuses only when the setting is already enabled.
  • For MCAS and Sentinel, a decision recorded somewhere other than Terraform about the outbound data flow.
  • No resource group and no region to choose.

πŸ“ Module Structure

terraform-azurerm-security-center-setting/
β”œβ”€β”€ providers.tf    # required_version + the pinned azurerm; no provider block
β”œβ”€β”€ variables.tf    # setting_name (5 checks) + enabled + timeouts
β”œβ”€β”€ main.tf         # the keystone `this` + the API-model and category derivations
β”œβ”€β”€ outputs.tf      # id first, then the derived category, then the traps
β”œβ”€β”€ README.md       # this file
β”œβ”€β”€ SCOPE.md        # the cross-module contract
β”œβ”€β”€ LICENSE         # MIT
└── .gitignore

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "defender_endpoint_integration" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "WDATP"
  enabled      = true
}

ℹ️ The caller owns the provider, its authentication, and the mandatory features {} block β€” and with it, the choice of subscription.

πŸ”΄ Both arguments are required. There is no empty call, and no default for enabled would be right for all five setting names.


πŸ”Œ Cross-Module Contract

Consumes

Nothing. Both inputs are a name and a boolean; the subscription comes from provider configuration. What the setting points at β€” Sentinel, Defender for Cloud Apps, Defender for Endpoint β€” is not referenced by ID.

Emits

Output Description Consumed by
id The setting's Resource ID, subscription-scoped Audit
arm_model AlertSyncSettings or DataExportSettings Design review
governs_data_sharing_outward true for MCAS and Sentinel check blocks
governs_defender_for_endpoint_integration true for the WDATP family check blocks
enabling_this_shares_data_with_another_product The conjunction worth reviewing Data-flow sign-off
destroy_writes_enabled_false_it_does_not_remove_anything Constant true Change review
provider_accepts_a_setting_name_the_apply_cannot_map Constant true Design review

πŸ“š Example Library

1 Β· Minimal β€” Defender for Endpoint integration on
module "wdatp" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "WDATP"
  enabled      = true
}

πŸ’‘ For the WDATP family, true is the stronger posture β€” it switches on the integration that the vulnerability scanner depends on.

2 Β· The unified Defender for Endpoint solution
module "wdatp_unified" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "WDATP_UNIFIED_SOLUTION"
  enabled      = true
}

ℹ️ A separate record from WDATP, with its own Resource ID. The two do not replace one another.

3 Β· Microsoft Sentinel alert sync β€” the other API model
module "sentinel_alert_sync" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "Sentinel"
  enabled      = true
}

output "which_model" {
  value = module.sentinel_alert_sync.arm_model
}

⚠️ Sentinel is the only name that writes an alert-sync body; the other four write a data-export body. Note the title-case spelling β€” the validator is case-sensitive, and SENTINEL is rejected.

4 Β· Defender for Cloud Apps, switched off deliberately
module "mcas" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "MCAS"
  enabled      = false
}

output "sharing_outward" {
  value       = module.mcas.enabling_this_shares_data_with_another_product
  description = "False here, because the setting is off. The output is the conjunction, not the category."
}

πŸ”’ Declaring false explicitly is a real configuration, not a no-op: it holds the setting off against a portal change, and it records the decision in code.

5 Β· Asserting that no outbound sharing is switched on
module "mcas" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "MCAS"
  enabled      = false
}

check "no_unreviewed_outbound_data_flow" {
  assert {
    condition     = !module.mcas.enabling_this_shares_data_with_another_product
    error_message = "An outbound Defender for Cloud data flow is enabled. Data-protection sign-off is required before this is switched on."
  }
}

πŸ’‘ The assertion uses the conjunction rather than enabled, because enabled = true on a WDATP setting is not a data-sharing decision at all.

6 Β· All five settings, keyed by name
variable "settings" {
  type = map(bool)
  default = {
    WDATP                              = true
    WDATP_UNIFIED_SOLUTION             = true
    WDATP_EXCLUDE_LINUX_PUBLIC_PREVIEW = false
    MCAS                               = false
    Sentinel                           = false
  }
}

module "security_center_settings" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"
  for_each = var.settings

  setting_name = each.key
  enabled      = each.value
}

πŸ’‘ A keyed map whose keys are the setting names β€” which is exactly what the Resource ID uses, so the map key and the resource identity agree. The posture choice per setting is visible in one place.

7 Β· Asserting across the whole map
variable "settings" {
  type = map(bool)
}

module "security_center_settings" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"
  for_each = var.settings

  setting_name = each.key
  enabled      = each.value
}

check "endpoint_integration_is_on" {
  assert {
    condition     = anytrue([for k, m in module.security_center_settings : m.governs_defender_for_endpoint_integration && m.enabled])
    error_message = "No Defender for Endpoint data-access setting is enabled, so the vulnerability scanner has no agent feeding it."
  }
}

check "outbound_sharing_is_accounted_for" {
  assert {
    condition     = length([for k, m in module.security_center_settings : k if m.enabling_this_shares_data_with_another_product]) == 0
    error_message = "At least one outbound data-sharing setting is enabled without sign-off."
  }
}

πŸ’‘ Iterating a for_each module map is the shape that scales. The two assertions separate the posture question from the data-flow question, because one boolean cannot answer both.

8 Β· Why `current` is rejected here but accepted by the provider
# This fails at PLAN time, in this module:
#
#   setting_name = "current"
#
# Without the module's check it would pass the provider's own validation - `current`
# is in the API enum the validator is built from - and then fail during APPLY with
# "failed to deduce the kind from its name". The five documented values are the ones
# the provider can actually map.

module "wdatp" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "WDATP"
  enabled      = true
}

πŸ”΄ This is the one place this module is deliberately stricter than the provider. The narrower set is the enforceable one, because the provider's own apply path rejects everything outside it.

9 Β· The casing that catches people
# Rejected at plan time, each with a message naming the exact spelling:
#
#   setting_name = "sentinel"   -> matches only when case is ignored; use "Sentinel"
#   setting_name = "SENTINEL"   -> likewise. The provider's expand function carries a
#                                  "SENTINEL" case, but validation rejects it first,
#                                  so that branch is unreachable from configuration
#   setting_name = "mcas"       -> use "MCAS"

module "sentinel" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "Sentinel"
  enabled      = true
}

⚠️ The casing is irregular on purpose: MCAS and the WDATP family are upper case, Sentinel is title case.

10 Β· Adopting a setting that is already enabled
import {
  to = module.wdatp.azurerm_security_center_setting.this
  id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Security/settings/WDATP"
}

module "wdatp" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "WDATP"
  enabled      = true
}

πŸ”΄ Import is needed only when the setting is already enabled β€” that is the sole case where the provider refuses a fresh apply. Against a setting currently off, the same apply succeeds and simply writes the value. The behaviour depends on subscription state that is invisible in the configuration.

11 Β· Timeouts, and what `delete` actually times
module "wdatp" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "WDATP"
  enabled      = true

  timeouts = {
    create = "15m"
    read   = "5m"
    update = "15m"
    delete = "15m"
  }
}

⚠️ The delete timeout bounds a write, not a removal β€” destroy issues an update that sets false. All four operations exist because enabled genuinely updates in place.

12 Β· Two subscriptions, two aliased providers
provider "azurerm" {
  features {}
}

provider "azurerm" {
  alias           = "platform"
  subscription_id = "11111111-1111-1111-1111-111111111111"
  features {}
}

module "wdatp_workload" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"

  setting_name = "WDATP"
  enabled      = true
}

module "wdatp_platform" {
  source    = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"
  providers = { azurerm = azurerm.platform }

  setting_name = "WDATP"
  enabled      = true
}

πŸ”΄ The same setting_name in two subscriptions is two different records. Within one subscription it would be one record and a silent collision, so name the blocks after the subscription.

13 Β· πŸ—οΈ End-to-end composition
provider "azurerm" {
  features {}
}

variable "settings" {
  type = map(bool)
  default = {
    WDATP                  = true
    WDATP_UNIFIED_SOLUTION = true
    MCAS                   = false
    Sentinel               = false
  }
}

# The plan that makes the posture settings meaningful.
module "defender_plan" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"

  resource_type = "VirtualMachines"
  tier          = "Standard"
}

# The scanner selection for the subscription.
module "server_va_setting" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-server-vulnerability-assessments-setting.git?ref=v1.0.0"
}

# The data-access settings, one record per name.
module "security_center_settings" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-setting.git?ref=v1.0.0"
  for_each = var.settings

  setting_name = each.key
  enabled      = each.value
}

# Where the estate's security data lives, and where the tags this family cannot carry belong.
module "law" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-log-analytics-workspace.git?ref=v1.0.0"

  name                = "law-security"
  resource_group_name = "rg-observability"
  location            = "eastus2"

  tags = {
    owner   = "security-engineering"
    purpose = "defender-for-cloud"
  }
}

check "scanner_has_its_agent" {
  assert {
    condition     = module.server_va_setting.selects_defender_vulnerability_management && module.security_center_settings["WDATP"].enabled
    error_message = "Defender Vulnerability Management is selected but the Defender for Endpoint data-access setting is off, leaving the scanner without its agent."
  }
}

check "outbound_sharing_needs_sign_off" {
  assert {
    condition     = length([for k, m in module.security_center_settings : k if m.enabling_this_shares_data_with_another_product]) == 0
    error_message = "An outbound Defender for Cloud data flow is enabled. Route this to whoever owns data protection before applying."
  }
}

output "settings_by_model" {
  value = { for k, m in module.security_center_settings : k => m.arm_model }
}

output "workspace_id" {
  value = module.law.id
}

πŸ”’ The composition separates the two questions this resource conflates: posture (is the endpoint integration on) and data flow (is anything being shared outward). Each gets its own assertion, and the workspace carries the tags none of these subscription-level records can.


πŸ“₯ Inputs

Name Type Required Notes
setting_name string yes One of five documented values. Force-new. Five checks
enabled bool yes No default is offered β€” see below
timeouts object no All four operations
Full schemas
variable "setting_name" {
  type = string
  # 5 validations: non-blank; a member of the documented five; `current`
  # rejected with the apply-time-failure explanation; a case-only near miss
  # naming the exact spellings; and a Resource ID passed where a name belongs.
}

variable "enabled" {
  type = bool
  # No validation is possible on a bare boolean, and no default is offered.
  # For WDATP settings, true is the stronger posture. For MCAS and Sentinel,
  # true starts an outbound data flow. The module reports the category instead.
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
}

⚠️ setting_name and enabled are deliberately not grouped into one variable. Grouping is this suite's tool for enforcing a cross-field rule, and the provider declares none here β€” every combination of name and boolean is legal. Grouping them would impose a relationship the provider does not have.

⚠️ The member check is the broad one. The current check and the case-only check are each contained in its negation, so a bad value reports two errors. That overlap is structural; the narrow messages exist because the generic one does not explain why current in particular is a trap.


🧾 Outputs

Output Type Notes
id string The setting's Resource ID. First
setting_name string The input, trimmed
enabled bool As configured
arm_model string AlertSyncSettings or DataExportSettings
one_terraform_resource_spans_two_api_models bool Constant true
provider_accepts_a_setting_name_the_apply_cannot_map bool Constant true. Read this
governs_data_sharing_outward bool true for MCAS and Sentinel
governs_defender_for_endpoint_integration bool true for the WDATP family
enabling_this_shares_data_with_another_product bool The conjunction. Assert on this
the_secure_direction_depends_on_the_setting_name bool Constant true
secure_default_cannot_apply_because_enabled_is_required bool Constant true
targets_a_public_preview_setting bool true for WDATP_EXCLUDE_LINUX_PUBLIC_PREVIEW
subscription_is_taken_from_provider_configuration_not_an_input bool Constant true
is_a_singleton_per_setting_name_per_subscription bool Constant true
the_setting_exists_before_terraform_creates_it bool Constant true
a_fresh_apply_is_refused_only_when_the_setting_is_already_on bool Constant true
destroy_writes_enabled_false_it_does_not_remove_anything bool Constant true
setting_name_is_force_new_but_enabled_is_not bool Constant true
accepts_no_credential bool Constant true

Nothing here is sensitive. No output is conditional.


🧠 Architecture Notes

The module validates a narrower set than the provider, on purpose. This suite's usual rule when two published sources disagree is to enforce the wider set and merely report the narrower one β€” because the deciding question is whether the narrower claim is backed by an actual rejection. Here it is. The provider's ValidateFunc is built from the API's SettingName enum, which includes current; but the provider's expand function has no case for it and returns failed to deduce the kind from its name. So current passes plan and fails apply. The documented five are the values the provider can genuinely act on, and enforcing them converts a deferred, confusing failure into a clear parse-time error.

One boolean, two meanings, and therefore no default. The provider makes enabled required, so there is no empty call to harden β€” and even if there were, no single value would be right. For WDATP, WDATP_UNIFIED_SOLUTION and WDATP_EXCLUDE_LINUX_PUBLIC_PREVIEW, true switches on Defender for Endpoint integration and is the stronger posture. For MCAS and Sentinel, true starts a flow of Defender for Cloud data into another product β€” a residency and retention decision that is not the security team's alone. Defaulting either way would state a position the module is not entitled to state for all five names at once. What the module does instead is derive the category, and emit the conjunction enabling_this_shares_data_with_another_product so a composition can assert on the state that actually warrants review.

The sensitive thing here is the data, not a credential. Nothing this module accepts or emits is secret; both inputs are a name and a flag. But MCAS and Sentinel govern where security data β€” which routinely contains information about real systems and the people using them β€” is sent. No Azure role limits retention or onward use once it arrives. That question belongs to whoever owns data protection, and the RBAC table names it as a row with no role in it precisely so it is not mistaken for something Owner settles.

One resource, two API models, and the provider says it should be two resources. The setting name decides whether the request body is a data-export settings object or an alert-sync settings object, and the provider's source carries a note that the resource ought to be split. arm_model surfaces which one a given call writes β€” useful now for understanding a plan, and useful later if that split arrives and a migration is needed.

Three sibling resources, three different existence-check behaviours. This module's provider places its must-be-imported guard correctly but conditions it on the setting already being enabled β€” and because these are platform settings that always exist, that means a fresh apply succeeds or fails depending on subscription state that is invisible in the configuration. The per-machine assessment resource in the same family errors whenever a record exists. The subscription-level vulnerability-assessments setting never errors at all, because its guard is unreachable. An expectation formed on one of the three is a liability on the other two.

Destroy is a write. The provider implements it as an update setting false, and its own error path calls the operation disabling. Removing this module from a configuration therefore switches a setting off rather than tidying anything away β€” and for a WDATP setting that is a loss of integration, not a cleanup. lifecycle blocks are not valid inside a module block, so a caller cannot add prevent_destroy; the control has to be process or policy.


🧱 Design Principles

Concern This module's position Why
Secure by default Cannot apply, and says so. enabled is required and its safe direction differs per setting Defaulting it would invent a posture position for five different settings at once
Closed value sets The documented five, enforced case-sensitively β€” narrower than the provider The provider's own apply path rejects current, so the narrow set is the enforceable one
Probable mistakes current, case variants and a Resource ID are each rejected by name The generic value-set message does not explain why current passes validation and then fails
Grouping Declined. setting_name and enabled stay separate variables Grouping enforces a cross-field rule; the provider declares none, and every combination is legal
Derived reporting The category, and the dangerous conjunction, are emitted Neither input answers "is this an outbound data flow" on its own
Data, not secrets The data-protection question is named, with no Azure role attached to it No RBAC role limits where shared security data goes once it leaves
Irreversible-looking operations Destroy semantics stated in an output and in Troubleshooting Destroy is a write, and no plan output says so in those words
Universal tail No tags, no location, no resource_group_name Subscription-scoped. Tag the Log Analytics workspace instead

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the source to a tag β€” ?ref=v1.0.0 β€” never a branch. Everything above is plan-only and static; a human applies from CI.

A change to setting_name is a destroy-then-create, and the destroy writes false to the old setting. Review that plan as two posture changes rather than one rename.


πŸ§ͺ Testing

What the offline gate covers:

  • terraform validate β€” the configuration parses and the types line up.
  • terraform fmt -check β€” formatting.
  • terraform console with .tfvars fixtures β€” all five setting_name checks and the timeouts duration check were proven to fire by line number. Seven good fixtures were confirmed to fire nothing: one for each of the five setting names, one padded with whitespace, and one with all four timeouts. The derived values were then evaluated one expression at a time β€” MCAS resolving to DataExportSettings, Sentinel to AlertSyncSettings, and enabling_this_shares_data_with_another_product returning true only for MCAS with enabled = true, false for Sentinel with enabled = false, and false for WDATP with enabled = true.

What only a real plan or apply against Azure can exercise:

  • Which subscription the provider is pointed at.
  • Whether the setting is currently enabled, and therefore whether a fresh apply is refused.
  • Whether the destination product exists and is receiving data.

πŸ’¬ Example Output

$ terraform output
settings_by_model = {
  "MCAS"                   = "DataExportSettings"
  "Sentinel"               = "AlertSyncSettings"
  "WDATP"                  = "DataExportSettings"
  "WDATP_UNIFIED_SOLUTION" = "DataExportSettings"
}
workspace_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-observability/providers/Microsoft.OperationalInsights/workspaces/law-security"

πŸ” Troubleshooting

Symptom Cause Fix
failed to deduce the kind from its name "current" during apply current is in the API enum the provider validates against but has no request mapping Use one of the five documented names. This module rejects current at plan time, so you see this only without it
setting_name must be one of MCAS, Sentinel, ... plus a second error about current The narrow and broad checks both fired Expected. Both messages describe the same bad value
setting_name matches a legal value only when case is ignored sentinel, SENTINEL or mcas was passed Use the exact spellings. Sentinel is title case; the rest are upper case
AuthorizationFailed despite holding Security Admin This resource requires Owner on the subscription Grant Owner at subscription scope, and review whether the pipeline should hold it
A first apply failed with a must-be-imported error, but the same code worked on another subscription The provider refuses only when the setting is already enabled Import it β€” see example 10. The difference is subscription state, not your configuration
A destroy switched off Defender for Endpoint integration Destroy writes false; it does not remove the record Intended provider behaviour. Re-apply with enabled = true. prevent_destroy cannot be set from a module block
Changing setting_name produced a destroy and a create setting_name is the Resource ID's last segment and is force-new Expected. Note the destroy switches the old setting off on the way
Two blocks with the same setting_name in one subscription, an alternating plan Both resolve to one Resource ID One block per name per subscription. Use for_each keyed by name
Data appeared in Sentinel or Defender for Cloud Apps unexpectedly An MCAS or Sentinel setting is enabled Assert on enabling_this_shares_data_with_another_product β€” see example 5 β€” and route the decision to data protection

πŸ”— Related Docs


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