Intermediate ⏱ 90 min πŸ“‹ 10 Steps

Configure App Governance Threat Detection

Enable and configure App Governance threat detection policies for anomalous OAuth app behaviour, credential changes, privilege escalation, and cross-tenant activity. Investigate alerts and integrate with automated response workflows.

πŸ“‹ Overview

About This Lab

App Governance threat detection policies use machine learning and rule-based engine to detect anomalous OAuth app behaviour in near real time. This includes unusual data access volumes, suspicious credential changes, privilege escalation via consent manipulation, and cross-tenant app activity. This lab covers building layered threat detection policies that work in concert with manual investigation to catch sophisticated OAuth-based attacks.

🏒 Enterprise Use Case

After deploying App Governance monitoring (Lab 01), the security team identifies baseline behaviour for 340 OAuth apps. Now they need automated detection for: apps that suddenly increase data access 10x, apps that add new credentials outside of change windows, apps accessing executive mailboxes for the first time, and apps from publishers who recently changed their domain registration.

🎯 What You Will Learn

  1. Review built-in App Governance threat detections
  2. Create custom data access anomaly policies
  3. Configure credential change detection
  4. Set up privilege escalation monitoring
  5. Create policies for cross-tenant app activity
  6. Configure alert severity and notification routing
  7. Investigate App Governance threat alerts
  8. Take remediation actions on detected threats
  9. Build an app threat investigation playbook
  10. Integrate with Defender XDR automated investigation

πŸ”‘ Why This Matters

OAuth apps operate in the background with persistent API access. Without threat detection, a compromised app can exfiltrate data for months without triggering any alert. App Governance threat detection closes this visibility gap by continuously monitoring app behaviour against learned baselines and known attack patterns.

βš™οΈ Prerequisites

  • Licensing: Microsoft 365 E5, or Microsoft Defender for Cloud Apps + App Governance add-on
  • Roles: Security Administrator or Global Administrator in Microsoft Entra ID
  • Prior Lab: Completion of Lab 01 - Deploy & Configure App Governance Monitoring
  • Portal Access: Microsoft Defender XDR portal with App Governance blade enabled
  • Tooling: PowerShell 7+ with Microsoft.Graph module v2.x installed (Install-Module Microsoft.Graph -Scope CurrentUser)
  • Data: At least 7 days of App Governance telemetry for baseline anomaly detection to function
  • Permissions: Microsoft Graph scopes - Application.Read.All, Policy.ReadWrite.ApplicationConfiguration, SecurityEvents.ReadWrite.All

Step 1 Β· Review Built-in Threat Detections

App Governance ships with built-in anomaly detection policies powered by Microsoft's ML models. Before creating custom policies you should review and enable these defaults - they provide zero-configuration detection for the most common OAuth app threat patterns including bulk data exfiltration, credential manipulation, and publisher impersonation. The script below connects to Microsoft Graph and programmatically enables every built-in policy so nothing is missed.

  1. Navigate to Cloud Apps > App Governance > Policies
  2. Review the built-in anomaly detection policies
  3. Enable: Unusual increase in data usage by an app
  4. Enable: Unusual addition of credentials to an app
  5. Enable: App with suspicious OAuth properties
  6. Enable: Misleading OAuth app name
# WHAT: Enumerate and enable all built-in App Governance threat detection policies
# WHY:  Ensures no built-in ML-based detections are left disabled - these catch
#       anomalous data access, credential stuffing, and consent phishing out of the box
# REQUIRES: Microsoft.Graph PowerShell module v2.x
#           Scopes: Policy.ReadWrite.ApplicationConfiguration, Application.Read.All

# Connect to Microsoft Graph with the required permission scopes
Connect-MgGraph -Scopes "Policy.ReadWrite.ApplicationConfiguration",`
    "Application.Read.All","SecurityEvents.ReadWrite.All"

# Retrieve the current list of App Governance anomaly detection policies
$policies = Invoke-MgGraphRequest -Method GET `
    -Uri "https://graph.microsoft.com/beta/security/appGovernance/policies" `
    | Select-Object -ExpandProperty value

# Display current policy status
$policies | Select-Object displayName, isEnabled, severity |
    Format-Table -AutoSize

# Define the built-in policies that should always be enabled
$requiredPolicies = @(
    "Unusual increase in data usage",
    "Unusual addition of credentials",
    "App with suspicious OAuth properties",
    "Misleading OAuth app name",
    "Suspicious OAuth app file download activities",
    "Unusual increase in Graph API calls"
)

# Enable each policy if it is currently disabled
foreach ($policyName in $requiredPolicies) {
    $match = $policies | Where-Object { $_.displayName -like "*$policyName*" }
    if ($match -and -not $match.isEnabled) {
        Invoke-MgGraphRequest -Method PATCH `
            -Uri "https://graph.microsoft.com/beta/security/appGovernance/policies/$($match.id)" `
            -Body (@{ isEnabled = $true } | ConvertTo-Json)
        Write-Host "[ENABLED]  $($match.displayName)" -ForegroundColor Green
    } elseif ($match) {
        Write-Host "[ACTIVE]   $($match.displayName)" -ForegroundColor Cyan
    } else {
        Write-Host "[MISSING]  $policyName - may require licence upgrade" -ForegroundColor Yellow
    }
}

# Final verification: list all now-active policies
Write-Host "`n=== Active Threat Detection Policies ===" -ForegroundColor White
Invoke-MgGraphRequest -Method GET `
    -Uri "https://graph.microsoft.com/beta/security/appGovernance/policies" `
    | Select-Object -ExpandProperty value `
    | Where-Object { $_.isEnabled } `
    | Select-Object displayName, severity, lastModifiedDateTime `
    | Format-Table -AutoSize
💡 Pro Tip: Built-in policies use adaptive baselines that take 7–14 days to calibrate. Enable them immediately but expect false positives during the learning period - tune severity thresholds after the first review cycle rather than disabling noisy policies outright.

Step 2 Β· Create Data Access Anomaly Policies

  1. Create a policy: Alert on Bulk Data Access by OAuth App
  2. Condition: Data access volume exceeds 5 GB in 24 hours
  3. Scope: All apps with Mail.Read or Files.Read permissions
  4. Severity: High
  5. Action: Alert + auto-disable app (for critical thresholds)

Step 3 Β· Configure Credential Change Detection

Credential manipulation is the first sign of an OAuth supply chain compromise. Attackers who gain access to an app registration add their own certificates or client secrets, giving themselves persistent API access that survives password resets. By correlating Entra ID audit logs with CloudAppEvents data access patterns, you can detect the tell-tale sequence: credential addition followed by anomalous data access - often within minutes.

  1. Create a policy: Detect Credential Additions Outside Change Windows
  2. Monitor for: new client secrets, new certificates, credential rotation
  3. Alert when credentials are added outside of approved maintenance windows
  4. Correlate with Entra ID audit logs for credential change context
// WHAT: Detect credential additions to OAuth apps followed by anomalous data access
// WHY:  Attackers who compromise an app registration first add credentials, then
//       immediately begin bulk data exfiltration - this query catches that sequence
// TABLE: AuditLogs - captures Entra ID operations including app credential changes
//        CloudAppEvents - captures subsequent API calls made by the app
// KEY FIELDS:
//   OperationName       - the Entra ID operation (e.g. "Add service principal credentials")
//   TargetResources     - identifies which app registration was modified
//   InitiatedBy         - who or what added the credentials
// OUTPUT: Apps that received new credentials AND showed data access within 60 minutes,
//         sorted by data volume - a strong indicator of compromise

// Step 1: Find all credential additions in the last 30 days
let CredentialEvents = AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName in (
    "Add service principal credentials",
    "Update application \u2013 Certificates and secrets management",
    "Add service principal certificate"
)
| extend AppId = tostring(TargetResources[0].id),
         AppName = tostring(TargetResources[0].displayName),
         Actor = tostring(InitiatedBy.user.userPrincipalName),
         ActorIP = tostring(InitiatedBy.user.ipAddress)
| project CredentialTime = TimeGenerated, AppId, AppName, Actor, ActorIP, OperationName;
// Step 2: Correlate with data access events from the same apps within 60 minutes
CredentialEvents
| join kind=inner (
    CloudAppEvents
    | where Timestamp > ago(30d)
    | where ActionType in ("MailItemsAccessed","FileDownloaded","FileUploaded")
    | project AccessTime = Timestamp, Application, ActionType,
              DataBytes = tolong(RawEventData.Size)
) on $left.AppName == $right.Application
| where AccessTime between (CredentialTime .. (CredentialTime + 60m))
| summarize DataAccessedBytes = sum(DataBytes),
            Actions = make_set(ActionType),
            AccessCount = count()
    by AppName, Actor, ActorIP, CredentialTime, OperationName
| where AccessCount > 10
| order by DataAccessedBytes desc
💡 Pro Tip: Define approved maintenance windows (e.g., Tuesdays 02:00–06:00 UTC) in your detection logic. Any credential addition outside these windows should automatically trigger a high-severity alert, even if the actor is a known admin - compromised admin accounts are a common attack vector.

Step 4 Β· Set Up Privilege Escalation Monitoring

  1. Monitor for apps requesting additional permissions via incremental consent
  2. Alert when an app's permission level escalates from Low to High
  3. Detect admin consent grants for apps that previously only had user-level consent
  4. Cross-reference permission escalations with app publisher and community usage

Step 5 Β· Monitor Cross-Tenant App Activity

Multi-tenant applications authenticate from their home tenant and access resources in yours. When a vendor is compromised, the attacker often authenticates from novel infrastructure - different IP ranges, different geographies, or entirely new tenants. By establishing an IP and location baseline for each multi-tenant app, you can detect deviations that signal a supply chain compromise before significant data exfiltration occurs.

  1. Identify multi-tenant apps accessing your data from unexpected tenants
  2. Alert on apps authenticating from IP addresses outside known datacenter ranges
  3. Monitor for changes in app redirect URIs or reply URLs
// WHAT: Detect multi-tenant apps authenticating from IPs never seen in their baseline
// WHY:  Supply-chain compromised apps begin operating from attacker infrastructure while
//       using the same legitimate AppId - detecting IP deviations catches this pattern
// TABLE: AADServicePrincipalSignInLogs - records every service principal authentication
// KEY FIELDS:
//   AppId              - unique application (client) identifier
//   IPAddress          - source IP used during authentication
//   Location           - geo-location derived from the IP address
//   HomeTenantId       - tenant where the app registration resides
//   ResourceTenantId   - your tenant (the resource being accessed)
// OUTPUT: Multi-tenant apps with new (previously unseen) authentication source IPs
//         in the last 7 days compared to a 30-day baseline

let BaselineDays = 30d;
let RecentDays   = 7d;
// Build a baseline of known IPs per app over the baseline window
let BaselineIPs = AADServicePrincipalSignInLogs
| where TimeGenerated between (ago(BaselineDays + RecentDays) .. ago(RecentDays))
| where ResultType == 0  // successful sign-ins only
| where HomeTenantId != ResourceTenantId  // multi-tenant apps only
| summarize KnownIPs = make_set(IPAddress) by AppId, AppDisplayName;
// Compare recent sign-in IPs against the established baseline
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(RecentDays)
| where ResultType == 0
| where HomeTenantId != ResourceTenantId
| join kind=inner BaselineIPs on AppId
| where not(KnownIPs has IPAddress)
| summarize NewIPCount    = dcount(IPAddress),
            NewIPs        = make_set(IPAddress),
            NewLocations  = make_set(Location),
            SignInCount   = count()
    by AppId, AppDisplayName, HomeTenantId
| order by NewIPCount desc
💡 Pro Tip: Create an allowlist of known vendor datacenter IP ranges (available from most SaaS vendors' trust pages) and flag any multi-tenant app authenticating outside those ranges. This provides a higher-fidelity signal than baseline comparison alone, especially for apps with variable infrastructure.

Step 6 Β· Configure Alert Routing

  1. Set alert severity levels: Low (informational), Medium (investigate), High (respond), Critical (auto-remediate)
  2. Route high-severity alerts to the SOC via email and Teams
  3. Route critical alerts to an automated response workflow
  4. Configure daily digest for low/medium alerts

Step 7 Β· Investigate Threat Alerts

When an App Governance alert fires, you need a structured investigation workflow that answers four questions quickly: What did the app do? When did it deviate from normal? Whose data was touched? Is the behaviour legitimate? The KQL query below builds a unified investigation timeline that combines the app's API activity with credential and configuration changes, giving you a single-pane view for rapid triage.

  1. Navigate to App Governance alerts in the Defender XDR alerts queue
  2. Click on a threat alert to view the investigation page
  3. Review: app identity, permission scopes, data access volume, credential history
  4. Check the app’s activity timeline for unusual patterns
  5. Determine if the behaviour is legitimate (e.g., backup job) or malicious
// WHAT: Build a unified investigation timeline for a suspected compromised OAuth app
// WHY:  During incident response you need a minute-by-minute view of everything the
//       app did - which users' data it accessed, what operations it performed, and
//       whether it attempted any persistence or lateral movement actions
// TABLES: CloudAppEvents - app API activity
//         AuditLogs      - credential and config changes from Entra ID
// OUTPUT: Chronological timeline combining API calls and config changes, grouped by
//         hour with activity counts and action types for rapid triage

let InvestigationApp = "";
let InvestigationWindow = 14d;

// Phase 1: App API activity timeline
let AppActivity = CloudAppEvents
| where Timestamp > ago(InvestigationWindow)
| where Application == InvestigationApp
| project Timestamp, ActionType,
          AccountDisplayName, IPAddress,
          DataBytes = tolong(RawEventData.Size),
          Source = "API-Activity";

// Phase 2: Credential and configuration changes from Entra ID
let ConfigChanges = AuditLogs
| where TimeGenerated > ago(InvestigationWindow)
| where TargetResources has InvestigationApp
| where OperationName in (
    "Add service principal credentials",
    "Update application \u2013 Certificates and secrets management",
    "Add delegated permission grant",
    "Add app role assignment",
    "Consent to application"
)
| project Timestamp = TimeGenerated,
          ActionType = OperationName,
          AccountDisplayName = tostring(InitiatedBy.user.userPrincipalName),
          IPAddress = tostring(InitiatedBy.user.ipAddress),
          DataBytes = long(0),
          Source = "Config-Change";

// Phase 3: Combine into a single chronological timeline
AppActivity
| union ConfigChanges
| order by Timestamp asc
| summarize EventCount = count(),
            TotalDataBytes = sum(DataBytes),
            Actions = make_set(ActionType),
            Users = make_set(AccountDisplayName)
    by bin(Timestamp, 1h), Source
| order by Timestamp asc
💡 Pro Tip: Export the investigation timeline to a CSV and overlay it with the app’s historical baseline (from Lab 01 monitoring data). This visual comparison makes it immediately obvious when the app deviated from normal - the β€œknee” in the graph is typically the moment of compromise.

Step 8 Β· Take Remediation Actions

  1. For confirmed threats: Disable the app immediately
  2. Rotate any credentials that may have been compromised
  3. Review all data accessed by the app during the anomalous period
  4. Report the app to Microsoft if it's a supply-chain compromise
  5. Document the incident for compliance and legal teams

Step 9 Β· Build App Threat Investigation Playbook

A documented playbook turns ad-hoc incident response into a repeatable, measurable process. The PowerShell script below integrates App Governance alerts with Azure Logic Apps to create an automated response workflow - when a critical-severity app threat is detected, the playbook automatically disables the app, revokes its tokens, notifies the SOC, and creates a ticket, all within seconds of detection.

  1. Define standard investigation steps for each alert type
  2. Create decision trees: when to disable vs. investigate further
  3. Document escalation procedures: SOC > App owner > Security management
  4. Include evidence collection requirements for legal and compliance
# WHAT: Automated threat response playbook for App Governance critical alerts
# WHY:  Manual response to OAuth app threats takes 30–60 minutes on average; this
#       script reduces containment time to under 60 seconds by automating the
#       disable-revoke-notify-document workflow
# REQUIRES: Microsoft.Graph module, Az module for Logic App deployment

# Connect to required services
Connect-MgGraph -Scopes "Application.ReadWrite.All",`
    "SecurityEvents.ReadWrite.All","DelegatedPermissionGrant.ReadWrite.All"
Connect-AzAccount

# Function: Contain a compromised OAuth application
function Invoke-AppGovernanceThreatResponse {
    param(
        [Parameter(Mandatory)][string]$ServicePrincipalId,
        [Parameter(Mandatory)][string]$AlertId,
        [string]$Severity = "Critical"
    )

    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC
    $evidence = @{ AlertId = $AlertId; Timestamp = $timestamp; Actions = @() }

    # Step 1: Disable the service principal immediately
    try {
        Update-MgServicePrincipal -ServicePrincipalId $ServicePrincipalId `
            -AccountEnabled:$false
        $evidence.Actions += "ServicePrincipal disabled"
        Write-Host "[CONTAINED] Service principal disabled" -ForegroundColor Green
    } catch {
        Write-Host "[ERROR] Failed to disable SP: $_" -ForegroundColor Red
    }

    # Step 2: Remove all delegated permission grants (OAuth2 consent)
    $grants = Get-MgServicePrincipalOauth2PermissionGrant `
        -ServicePrincipalId $ServicePrincipalId
    foreach ($grant in $grants) {
        Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id
        $evidence.Actions += "Revoked grant: $($grant.Scope)"
    }
    Write-Host "[REVOKED] $($grants.Count) permission grants removed" -ForegroundColor Yellow

    # Step 3: Remove all app role assignments
    $roles = Get-MgServicePrincipalAppRoleAssignment `
        -ServicePrincipalId $ServicePrincipalId
    foreach ($role in $roles) {
        Remove-MgServicePrincipalAppRoleAssignment `
            -ServicePrincipalId $ServicePrincipalId `
            -AppRoleAssignmentId $role.Id
        $evidence.Actions += "Removed role: $($role.AppRoleId)"
    }
    Write-Host "[REVOKED] $($roles.Count) app role assignments removed" -ForegroundColor Yellow

    # Step 4: Export evidence for legal and compliance
    $evidencePath = "AppGov-Incident-$AlertId-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
    $evidence | ConvertTo-Json -Depth 5 | Out-File $evidencePath
    Write-Host "[DOCUMENTED] Evidence saved to $evidencePath" -ForegroundColor Cyan

    return $evidence
}

# Usage: respond to a specific alert
# Invoke-AppGovernanceThreatResponse `
#     -ServicePrincipalId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
#     -AlertId "AG-2026-03-001"
💡 Pro Tip: Always preserve evidence before remediation. The script above logs every action taken with timestamps - this audit trail is essential for regulatory compliance (GDPR Article 33 requires documenting breach response within 72 hours) and for post-incident forensics to prove containment was complete.

Step 10 Β· Integrate with Automated Response

  1. Configure Defender XDR automated investigation to include App Governance alerts
  2. Create Logic App playbooks that auto-disable apps meeting critical threat criteria
  3. Set up auto-notification workflows to app owners when their apps trigger alerts
  4. Track automated vs. manual investigations for efficiency metrics

πŸ“š Documentation Resources

ResourceDescription
App Governance OverviewOfficial overview of App Governance threat detection capabilities and policy types
App Governance PoliciesCreate and manage custom and built-in App Governance detection policies
CloudAppEvents Table ReferenceSchema reference for CloudAppEvents in Advanced Hunting including all available columns
Service Principal Sign-In LogsMonitor workload identity authentication with AADServicePrincipalSignInLogs
Microsoft Graph Security APIProgrammatically manage security alerts, policies, and automated investigation
Automated Investigation & ResponseConfigure Defender XDR automated investigation for app-related threat alerts

Summary

What You Accomplished

  • Enabled and configured App Governance threat detection policies
  • Created custom policies for data access anomalies, credential changes, and privilege escalation
  • Investigated and remediated app threat alerts
  • Built investigation playbooks and integrated with automated response

Next Steps

← Previous Lab Next Lab β†’