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.
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.
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.
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.
Microsoft.Graph module v2.x installed (Install-Module Microsoft.Graph -Scope CurrentUser)Application.Read.All, Policy.ReadWrite.ApplicationConfiguration, SecurityEvents.ReadWrite.AllApp 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.
# 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 -AutoSizeAlert on Bulk Data Access by OAuth AppCredential 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.
Detect Credential Additions Outside Change Windows// 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 descMulti-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.
// 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 descWhen 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.
// 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 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.
# 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"| Resource | Description |
|---|---|
| App Governance Overview | Official overview of App Governance threat detection capabilities and policy types |
| App Governance Policies | Create and manage custom and built-in App Governance detection policies |
| CloudAppEvents Table Reference | Schema reference for CloudAppEvents in Advanced Hunting including all available columns |
| Service Principal Sign-In Logs | Monitor workload identity authentication with AADServicePrincipalSignInLogs |
| Microsoft Graph Security API | Programmatically manage security alerts, policies, and automated investigation |
| Automated Investigation & Response | Configure Defender XDR automated investigation for app-related threat alerts |