Query Details

Conditional Access Baseline Gap Detected Due Policy Change

Query

id: 02e0bfee-2e8d-4081-a8e1-fcc468ad7a74
name: Conditional Access Baseline Gap Detected Due to Policy Change
version: 1.0.0
kind: Scheduled
description: A Conditional Access policy is modified and, using "What If"-feature in Maester checks, identifies if the change creates a critical gap in the Conditional Access baseline design. It correlates audit events with negative changed Maester results to alert on deviations from the recommended baseline.
severity: Medium
queryFrequency: 1d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- InitialAccess
query: |+
  let ChangedPolicies = AuditLogs
      | where TimeGenerated > ago(24h)
      | where OperationName has "conditional access policy"
      | where Result =~ "success"
      | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
      | extend InitiatingAppId = tostring(InitiatedBy.app.appId)
      | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
      | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
      | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
      | extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
      | extend CaPolicyName = tostring(TargetResources[0].displayName)
      | extend CaPolicyId = tostring(TargetResources[0].id)
      | extend NewPolicyValues = TargetResources[0].modifiedProperties[0].newValue
      | extend OldPolicyValues = TargetResources[0].modifiedProperties[0].oldValue
      | extend
          InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]),
          InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
      | project-reorder
          TimeGenerated,
          OperationName,
          CaPolicyId,
          CaPolicyName,
          InitiatingAppId,
          InitiatingAppName,
          InitiatingAppServicePrincipalId,
          InitiatingUserPrincipalName,
          InitiatingAadUserId,
          InitiatingIPAddress,
          NewPolicyValues,
          OldPolicyValues;
  let PreviousCheckResults = Maester_CL
      | where Block == "Conditional Access Baseline Policies"
      | summarize arg_min(TimeGenerated, *) by Id;
  let NewestCheckResults = Maester_CL
      | where Block == "Conditional Access Baseline Policies"
      | summarize arg_max(TimeGenerated, *) by Id;
  let FailedChecks = NewestCheckResults
      | join kind=inner PreviousCheckResults on Id
      | where Result != Result1
      | extend PreviousResultDetail = tostring(parse_json(ResultDetail1))
      | extend AffectedCaPolicyIds = extract_all(@'([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})', PreviousResultDetail)
      | project
          Id,
          Title,
          CurrentResult = Result,
          CurrentResultTimestamp = TimeGenerated,
          PreviousResult = Result1,
          PreviousResultTimestamp = TimeGenerated1,
          ResultDetail,
          PreviousResultDetail,
          AffectedCaPolicyIds;
  ChangedPolicies
  | join kind=inner (
      FailedChecks
      | mv-expand parse_json(AffectedCaPolicyIds)
      | project-rename MaesterId = Id
      | extend CaPolicyId = tostring(AffectedCaPolicyIds)
      | project
          MaesterId,
          CaPolicyId,
          TestTitle = parse_json(PreviousResultDetail).TestTitle,
          CurrentResult,
          TestFinding = ResultDetail.TestResult,
          PreviousTestFinding = parse_json(PreviousResultDetail).TestResult
      )
      on CaPolicyId
  | extend FailedChecks = bag_pack_columns(MaesterId, TestTitle, CurrentResult, TestFinding, PreviousTestFinding)
  | project
      TimeGenerated,
      OperationName,
      CaPolicyId,
      CaPolicyName,
      InitiatingUserPrincipalName,
      InitiatingAadUserId,
      InitiatingIPAddress,
      Id,
      LoggedByService,
      ActivityDisplayName,
      parse_json(FailedChecks),
      PreviousCaPolicy = parse_json(OldPolicyValues),
      CurrentCaPolicy = parse_json(NewPolicyValues),
      TestTitle,
      TestFinding

suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: Selected
    groupByEntities:
    - Account
    groupByAlertDetails: []
    groupByCustomDetails:
    - CaPolicyId
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDescriptionFormat: |-
    Conditional Access policy "{{CaPolicyName}}" has been modified, resulting in a potential policy baseline gap and failed Maester check:

    {{{TestTitle}} 
    {{TestFinding}}
  alertDynamicProperties: []
customDetails:
  CaPolicyId: CaPolicyId
  CaPolicyName: CaPolicyName
  Activity: ActivityDisplayName
  FailedChecks: FailedChecks
  PreviousCaPolicy: PreviousCaPolicy
  CurrentCaPolicy: CurrentCaPolicy
  TestTitle: TestTitle
  TestFinding: TestFinding
entityMappings:
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: InitiatingIPAddress
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: InitiatingUserPrincipalName
suppressionDuration: 5h

Explanation

This query is designed to monitor changes in Conditional Access policies within an organization and identify any critical gaps that these changes might introduce. Here's a simplified breakdown of what the query does:

  1. Monitor Policy Changes: It looks at audit logs from the past 24 hours to find successful modifications to Conditional Access policies. It captures details about who made the change, what was changed, and the new and old values of the policy.

  2. Check Against Baseline: It uses a tool called Maester to compare the current state of Conditional Access policies against a baseline (a set of recommended security configurations). It identifies any discrepancies or failures in maintaining this baseline.

  3. Identify Failures: The query identifies policies that have failed the baseline check by comparing the most recent check results with previous ones. It extracts details about which policies are affected.

  4. Correlate Changes and Failures: It correlates the identified policy changes with the failed baseline checks to determine if the changes are responsible for the failures.

  5. Alert Generation: If a policy change results in a baseline failure, an alert is generated. The alert includes details about the policy change, the failed check, and the specific gap introduced.

  6. Incident Management: The query is configured to create incidents for these alerts, grouping them by account and policy ID. This helps in managing and tracking the incidents effectively.

  7. Severity and Frequency: The severity of the alert is set to medium, and the query runs daily, looking back over the past two days to ensure timely detection of issues.

Overall, this query helps organizations maintain their security posture by ensuring that any changes to Conditional Access policies do not inadvertently weaken their security baseline.

Details

Thomas Naunheim profile picture

Thomas Naunheim

Released: June 11, 2025

Tables

AuditLogsMaester_CL

Keywords

ConditionalAccessPolicyAuditLogsMaesterAccountIPAddress

Operators

letwherehas=~extendtostringsplitproject-reordersummarizearg_minarg_maxjoinkind=inner!=parse_jsonextract_allprojectmv-expandproject-renamebag_pack_columns

Actions