Detection rules › Kusto

Azure Security Benchmark Posture Changed

Severity
medium
Time window
7d
Group by
AssessedResourceId, ComplianceDomain, Failed, Passed, PassedControlsPercentage, RecommendationName, Total
Source
github.com/Azure/Azure-Sentinel

This rule monitors Azure policies aligned with the Azure Security Benchmark regulatory compliance initiative and triggers when policy compliance falls below 70% within a 7-day time window.

MITRE ATT&CK coverage

TacticTechniques
DiscoveryT1082 System Information Discovery

Rule body kusto

id: 0610e72f-ceaf-42d1-879e-952a1bd8d07a
name: Azure Security Benchmark Posture Changed
description: This rule monitors Azure policies aligned with the Azure Security Benchmark regulatory compliance initiative
  and triggers when policy compliance falls below 70% within a 7-day time window.
severity: Medium
requiredDataConnectors:
- connectorId: AzureSecurityCenter
  dataTypes:
  - SecurityRecommendation
  - SecurityRegulatoryCompliance
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: | 
  let ComplianceDomainLookup = SecurityRecommendation
  | join kind=fullouter (SecurityRegulatoryCompliance | where ComplianceStandard == "Azure-Security-Benchmark") on RecommendationName
  | summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationName
  | extend ComplianceDomain = iff(ComplianceControl contains "NS.", "Network Security", iff(ComplianceControl contains "IM.", "Identity Management", iff(ComplianceControl contains "PA.", "Privileged Access", iff(ComplianceControl contains "DP.", "Data Protection", iff(ComplianceControl contains "AM.", "Asset Management", iff(ComplianceControl contains "LT.", "Logging & Threat Detection", iff(ComplianceControl contains "IR.", "Incident Response", iff(ComplianceControl contains "PV.", "Posture & Vulnerability Management", iff(ComplianceControl contains "ES.", "Endpoint Security", iff(ComplianceControl contains "BR.", "Backup & Recovery", iff(ComplianceControl startswith "DS.", "DevOps Security", iff(ComplianceControl contains "GS.", "Governance & Strategy", "Other"))))))))))));
  SecurityRecommendation
  | join kind=fullouter (SecurityRegulatoryCompliance | where ComplianceStandard == "Azure-Security-Benchmark") on RecommendationName
  | summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationName
  | extend ComplianceDomain = iff(ComplianceControl contains "NS.", "Network Security", iff(ComplianceControl contains "IM.", "Identity Management", iff(ComplianceControl contains "PA.", "Privileged Access", iff(ComplianceControl contains "DP.", "Data Protection", iff(ComplianceControl contains "AM.", "Asset Management", iff(ComplianceControl contains "LT.", "Logging & Threat Detection", iff(ComplianceControl contains "IR.", "Incident Response", iff(ComplianceControl contains "PV.", "Posture & Vulnerability Management", iff(ComplianceControl contains "ES.", "Endpoint Security", iff(ComplianceControl contains "BR.", "Backup & Recovery", iff(ComplianceControl startswith "DS.", "DevOps Security", iff(ComplianceControl contains "GS.", "Governance & Strategy", "Other"))))))))))))
  | summarize Failed = countif(RecommendationState == "Unhealthy"), Passed = countif(RecommendationState == "Healthy"), Total = countif(RecommendationState == "Healthy" or RecommendationState == "Unhealthy") by ComplianceDomain
  | extend PassedControlsPercentage = iff(Total > 0, (Passed / todouble(Total)) * 100.0, real(null))
  | join kind=leftouter (ComplianceDomainLookup) on ComplianceDomain
  | project ComplianceDomain, Total, PassedControlsPercentage, Passed, Failed, LastEvaluated = TimeGenerated
  | summarize arg_max(LastEvaluated, *) by ComplianceDomain, Total, PassedControlsPercentage, Passed, Failed
  | where PassedControlsPercentage < 70
  | sort by PassedControlsPercentage asc, Passed desc
  | extend RemediationLink = strcat("https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22")
entityMappings:
- entityType: URL
  fieldMappings:
  - identifier: Url
    columnName: RemediationLink
alertDetailsOverride:
  alertDisplayNameFormat: Azure Security Benchmark posture below threshold for {{ComplianceDomain}}
customDetails:
  ComplianceDomain: ComplianceDomain
  TotalControls: Total
  PassedControls: Passed
  FailedControls: Failed
version: 1.0.2
kind: Scheduled

Stages and Predicates

Let binding: ComplianceDomainLookup

let ComplianceDomainLookup = SecurityRecommendation
| join kind=fullouter (SecurityRegulatoryCompliance | where ComplianceStandard == "Azure-Security-Benchmark") on RecommendationName
| summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationName
| extend ComplianceDomain = iff(ComplianceControl contains "NS.", "Network Security", iff(ComplianceControl contains "IM.", "Identity Management", iff(ComplianceControl contains "PA.", "Privileged Access", iff(ComplianceControl contains "DP.", "Data Protection", iff(ComplianceControl contains "AM.", "Asset Management", iff(ComplianceControl contains "LT.", "Logging & Threat Detection", iff(ComplianceControl contains "IR.", "Incident Response", iff(ComplianceControl contains "PV.", "Posture & Vulnerability Management", iff(ComplianceControl contains "ES.", "Endpoint Security", iff(ComplianceControl contains "BR.", "Backup & Recovery", iff(ComplianceControl startswith "DS.", "DevOps Security", iff(ComplianceControl contains "GS.", "Governance & Strategy", "Other"))))))))))));

Stage 1: source

SecurityRecommendation

Stage 2: join

| join kind=fullouter (SecurityRegulatoryCompliance | where ComplianceStandard == "Azure-Security-Benchmark") on RecommendationName

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationName

Stage 4: extend

| extend ComplianceDomain = iff(ComplianceControl contains "NS.", "Network Security", iff(ComplianceControl contains "IM.", "Identity Management", iff(ComplianceControl contains "PA.", "Privileged Access", iff(ComplianceControl contains "DP.", "Data Protection", iff(ComplianceControl contains "AM.", "Asset Management", iff(ComplianceControl contains "LT.", "Logging & Threat Detection", iff(ComplianceControl contains "IR.", "Incident Response", iff(ComplianceControl contains "PV.", "Posture & Vulnerability Management", iff(ComplianceControl contains "ES.", "Endpoint Security", iff(ComplianceControl contains "BR.", "Backup & Recovery", iff(ComplianceControl startswith "DS.", "DevOps Security", iff(ComplianceControl contains "GS.", "Governance & Strategy", "Other"))))))))))))
ComplianceDomain =
ifComplianceControl contains "NS.""Network Security"
elseiff((ComplianceControl contains "IM."), "Identity Management", iff((ComplianceControl contains "PA."), "Privileged Access", iff((ComplianceControl contains "DP."), "Data Protection", iff((ComplianceControl contains "AM."), "Asset Management", iff((ComplianceControl contains "LT."), "Logging & Threat Detection", iff((ComplianceControl contains "IR."), "Incident Response", iff((ComplianceControl contains "PV."), "Posture & Vulnerability Management", iff((ComplianceControl contains "ES."), "Endpoint Security", iff((ComplianceControl contains "BR."), "Backup & Recovery", iff((ComplianceControl startswith "DS."), "DevOps Security", iff((ComplianceControl contains "GS."), "Governance & Strategy", "Other")))))))))))

Stage 5: summarize

| summarize Failed = countif(RecommendationState == "Unhealthy"), Passed = countif(RecommendationState == "Healthy"), Total = countif(RecommendationState == "Healthy" or RecommendationState == "Unhealthy") by ComplianceDomain

Stage 6: extend

| extend PassedControlsPercentage = iff(Total > 0, (Passed / todouble(Total)) * 100.0, real(null))
PassedControlsPercentage =
ifTotal > 0((Passed / todouble(Total)) * 100.0)
elsereal(null)

Stage 7: join

| join kind=leftouter (ComplianceDomainLookup) on ComplianceDomain

Stage 8: project

| project ComplianceDomain, Total, PassedControlsPercentage, Passed, Failed, LastEvaluated = TimeGenerated

Stage 9: summarize

| summarize arg_max(LastEvaluated, *) by ComplianceDomain, Total, PassedControlsPercentage, Passed, Failed

Stage 10: where

| where PassedControlsPercentage < 70

Stage 11: sort

| sort by PassedControlsPercentage asc, Passed desc

Stage 12: extend

| extend RemediationLink = strcat("https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22")

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
ComplianceStandardeq
  • Azure-Security-Benchmark transforms: cased
PassedControlsPercentagelt
  • 70 transforms: cased

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
ComplianceDomainsummarize
Failedsummarize
Passedsummarize
PassedControlsPercentagesummarize
Totalsummarize
RemediationLinkextend