Advanced ⏱ 150 min 📋 10 Steps

Investigate App-Based Attacks & Supply Chain Threats

Investigate OAuth supply chain attacks, detect compromised app indicators, contain and remediate app-based breaches, build detection rules for supply chain threats, harden tenant defences, and create incident response playbooks.

📋 Overview

About This Lab

OAuth-based supply chain attacks exploit the trusted relationship between applications and your tenant. Attackers compromise a legitimate vendor''s app or create convincing lookalike apps to gain persistent access to enterprise data. This lab covers investigating app-based attacks, detecting supply chain compromises, building incident response procedures for app-based threats, and hardening your tenant against future attacks.

🏢 Enterprise Use Case

A security alert reveals that a widely-used project management app experienced a supply chain compromise. The vendor''s OAuth client secret was leaked, and an attacker used it to access data in 500+ tenants. Your organisation is one of the affected tenants. The SOC must determine: what data was accessed, which users are affected, whether the attacker established persistence, and how to remediate without disrupting 200 employees who depend on the app.

🎯 What You Will Learn

  1. Understand OAuth supply chain attack patterns
  2. Detect compromised app indicators (IOCs)
  3. Investigate the scope of an app-based breach
  4. Contain compromised applications
  5. Determine data exposure and impact
  6. Remediate and recover from app-based attacks
  7. Build detection rules for supply chain threats
  8. Harden tenant against future app-based attacks
  9. Create an app incident response playbook
  10. Conduct post-incident review and lessons learned

🔑 Why This Matters

Supply chain attacks through OAuth apps are among the most sophisticated threats to enterprise security. The SolarWinds and Midnight Blizzard attacks demonstrated how compromised applications can bypass all traditional security controls. Your defence must include the ability to detect, investigate, and respond to app-based threats rapidly.

⚙️ Prerequisites

  • Licensing: Microsoft 365 E5, or Microsoft Defender for Cloud Apps + App Governance add-on; Entra Workload Identities Premium recommended
  • Roles: Security Administrator or Incident Manager role in Microsoft Entra ID; Security Operator for containment actions
  • Prior Labs: Completion of Lab 01, Lab 02 (threat detection), and Lab 03 (compliance)
  • Portal Access: Microsoft Defender XDR portal with Advanced Hunting enabled
  • Tooling: PowerShell 7+ with Microsoft.Graph module v2.x; Azure CLI for Logic App management
  • Permissions: Graph scopes - Application.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All, AppRoleAssignment.ReadWrite.All, User.RevokeSessions.All, SecurityEvents.ReadWrite.All
  • Scenario: This lab simulates a supply chain compromise investigation - no live production apps will be impacted if you follow the simulation steps

Step 1 · Understand Supply Chain Attack Patterns

  • Compromised vendor: Attacker gains control of a legitimate app's credentials
  • Consent phishing: Attacker creates a lookalike app and tricks users into consenting
  • Credential stuffing: Leaked client secrets used to impersonate legitimate apps
  • Token theft: Attacker steals access tokens from compromised infrastructure
  • Redirect URI manipulation: Attacker modifies app redirect URIs to intercept tokens

Step 2 · Detect Compromised App Indicators

Consent phishing is the most common vector for OAuth-based supply chain attacks. Attackers register apps with names that mimic trusted services, then send phishing emails containing OAuth consent links. When a user clicks and approves, the attacker’s app gains persistent access to their mailbox, files, and profile without needing their password. The KQL query below detects these malicious consent patterns by correlating suspicious app consent events with known phishing indicators.

  1. Monitor for sudden changes in app data access patterns
  2. Detect new credential additions to established apps
  3. Alert on redirect URI changes for registered apps
  4. Monitor for apps authenticating from new IP ranges
  5. Track permission scope changes without corresponding admin consent
// WHAT: Detect malicious OAuth consent phishing - apps consented via phishing campaigns
// WHY:  Consent phishing bypasses MFA and credential protections entirely. The attacker
//       doesn't steal a password - they trick the user into granting API access directly.
//       This query identifies consent events with high-risk characteristics.
// TABLES: AuditLogs - captures consent grant operations
//         AADSignInEventsBeta - provides sign-in context around the consent event
// KEY FIELDS:
//   OperationName    - "Consent to application"
//   TargetResources  - the app that received consent
//   InitiatedBy      - the user who clicked the consent prompt
// OUTPUT: Consent events matching phishing indicators: unverified publishers, high-risk
//         scopes, apps less than 30 days old, multiple users consenting within hours

// Detect suspicious consent patterns in the last 30 days
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName == "Consent to application"
| extend AppName = tostring(TargetResources[0].displayName),
         AppId = tostring(TargetResources[0].id),
         ConsentUser = tostring(InitiatedBy.user.userPrincipalName),
         ConsentIP = tostring(InitiatedBy.user.ipAddress),
         Scopes = tostring(parse_json(
             tostring(TargetResources[0].modifiedProperties))[0].newValue)
| project TimeGenerated, AppName, AppId, ConsentUser, ConsentIP, Scopes
// Flag 1: Multiple users consenting to the same app within 4 hours (campaign indicator)
| join kind=inner (
    AuditLogs
    | where TimeGenerated > ago(30d)
    | where OperationName == "Consent to application"
    | extend AppId = tostring(TargetResources[0].id),
             ConsentUser = tostring(InitiatedBy.user.userPrincipalName)
    | summarize ConsentCount = dcount(ConsentUser),
                FirstConsent = min(TimeGenerated),
                LastConsent = max(TimeGenerated)
        by AppId
    | where ConsentCount >= 3  // 3+ users = likely phishing campaign
    | where (LastConsent - FirstConsent) < 4h  // compressed timeframe
) on AppId
// Flag 2: Filter to high-risk permission scopes
| where Scopes has_any ("Mail.Read", "Mail.ReadWrite", "Files.ReadWrite",
                        "User.Read.All", "MailboxSettings.ReadWrite")
| project TimeGenerated, AppName, AppId, ConsentUser, ConsentIP,
          Scopes, ConsentCount, FirstConsent, LastConsent
| order by ConsentCount desc, TimeGenerated asc
💡 Pro Tip: Set up a near-real-time custom detection rule using the KQL above with a 1-hour frequency. When 3+ users consent to the same unverified app within 4 hours, automatically generate a Critical incident and trigger your consent phishing response playbook - speed is essential as attackers begin data exfiltration immediately after consent.

Step 3 · Investigate Breach Scope

  1. Identify the time window of compromise from App Governance activity logs
  2. Determine which users' data was accessed by the compromised app
  3. Quantify data accessed: mailbox contents, files, user profiles
  4. Check for persistence: did the attacker create new apps or modify existing ones?
  5. Review if the app was used to create inbox rules, send emails, or modify files
// WHAT: Build an activity timeline for a suspected compromised OAuth application
// WHY: Maps the full scope of a supply chain compromise - shows exactly what data
//      the attacker accessed, when, and how much, enabling accurate breach assessment
// TABLE: CloudAppEvents - captures API calls made by OAuth apps to Microsoft 365 services
// KEY FIELDS:
//   Application - the display name of the OAuth app making the API calls
//   ActionType  - the Graph API operation performed (e.g., "FileDownloaded",
//     "MailItemsAccessed", "FileUploaded", "FolderCreated", "SearchQueryPerformed")
//   RawEventData.Size - data volume in bytes per operation (when available)
// OUTPUT: Hourly breakdown of app activities and data volume over the past 90 days,
//         used to identify the start/end of anomalous activity (the compromise window)
CloudAppEvents
| where Timestamp > ago(90d)
| where Application == "CompromisedAppName"
| summarize Activities = count(), DataVolumeBytes = sum(RawEventData.Size)
    by ActionType, bin(Timestamp, 1h)
| order by Timestamp asc

Step 4 · Contain the Compromised Application

Once a compromised app is identified, you need to investigate whether it was used for lateral movement before containment. Attackers often use a compromised app’s API access to create inbox rules, forward emails to external addresses, add new permission grants, or create additional app registrations for persistence. The KQL query below maps the full lateral movement chain - every action the app took that could establish additional footholds in your tenant.

  1. Immediately disable the compromised service principal
  2. Revoke all OAuth permission grants for the app
  3. Block the app’s client ID in App Governance
  4. Revoke all sessions and tokens for affected users
  5. Contact the app vendor to coordinate response
// WHAT: Investigate lateral movement by a compromised OAuth application
// WHY:  Before containing the app, you must understand what persistence mechanisms
//       the attacker established - inbox rules, mail forwarding, new app registrations,
//       or additional consent grants - so containment covers ALL attacker footholds
// TABLES: CloudAppEvents - app API activity against Microsoft 365
//         AuditLogs - Entra ID changes made by or involving the app
// OUTPUT: All high-risk actions by the compromised app that could indicate lateral
//         movement, persistence, or data staging - grouped by attack technique

let CompromisedApp = "";
let InvestigationWindow = 30d;

// Phase 1: Detect inbox rule creation and mail forwarding (persistence + exfil)
let InboxRules = CloudAppEvents
| where Timestamp > ago(InvestigationWindow)
| where Application == CompromisedApp
| where ActionType in (
    "New-InboxRule", "Set-InboxRule",
    "Set-Mailbox",  // forwarding configuration
    "New-TransportRule"
)
| project Timestamp, ActionType,
          TargetUser = AccountDisplayName,
          Details = RawEventData,
          Technique = "T1114.003 - Email Forwarding Rule";

// Phase 2: Detect new app registrations or consent grants (persistence)
let PersistenceActions = AuditLogs
| where TimeGenerated > ago(InvestigationWindow)
| where InitiatedBy has CompromisedApp
    or TargetResources has CompromisedApp
| where OperationName in (
    "Add application",
    "Add service principal",
    "Consent to application",
    "Add delegated permission grant",
    "Add app role assignment to service principal",
    "Add service principal credentials"
)
| project Timestamp = TimeGenerated, ActionType = OperationName,
          TargetUser = tostring(TargetResources[0].displayName),
          Details = TargetResources,
          Technique = "T1098.003 - Additional Cloud Credentials";

// Phase 3: Detect data staging and bulk access (exfiltration preparation)
let DataStaging = CloudAppEvents
| where Timestamp > ago(InvestigationWindow)
| where Application == CompromisedApp
| where ActionType in (
    "FileDownloaded", "MailItemsAccessed",
    "SearchQueryPerformed", "FileUploaded"
)
| summarize ActionCount = count(),
            DataBytes = sum(tolong(RawEventData.Size)),
            UniqueUsers = dcount(AccountDisplayName)
    by ActionType, bin(Timestamp, 1h)
| where ActionCount > 50  // bulk activity threshold
| extend Technique = "T1530 - Data from Cloud Storage";

// Combine all lateral movement indicators
InboxRules
| union PersistenceActions
| project Timestamp, Technique, ActionType, TargetUser, Details
| order by Timestamp asc
💡 Pro Tip: Map the query results to the MITRE ATT&CK framework (technique IDs are included in the output). This not only helps your SOC understand the attacker’s playbook but also creates professional incident documentation that maps directly to the MITRE-based reports expected by CISOs and regulators.

Step 5 · Assess Data Impact

  1. Generate a list of all data objects accessed by the app during the compromise window
  2. Classify the data by sensitivity: public, internal, confidential, highly confidential
  3. Determine if any regulatory data was exposed (PII, PHI, financial data)
  4. Assess whether the data exposure triggers breach notification requirements
  5. Document findings for legal and privacy teams

Step 6 · Remediate and Recover

Containment must be swift and complete. The PowerShell script below performs a full containment sequence for a compromised OAuth application: disabling the service principal to stop all API access, revoking every delegated and application permission grant, removing all credentials and certificates, and revoking sessions for every user who consented to the app. Each action is logged with timestamps for forensic evidence preservation.

  1. Remove all credentials and secrets for the compromised app registration
  2. If the vendor is legitimate: wait for their all-clear, then re-register with new credentials
  3. Re-consent with minimal required permissions (least-privilege)
  4. Monitor the re-connected app closely for 30 days
  5. Update affected users about the incident and any required actions
# WHAT: Full containment and remediation of a compromised OAuth application
# WHY:  Every second the app remains active, the attacker can exfiltrate data. This
#       script executes a complete containment sequence: disable, revoke permissions,
#       remove credentials, revoke user sessions - with forensic evidence logging
# REQUIRES: Microsoft.Graph module v2.x
#           Scopes: Application.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All,
#                   AppRoleAssignment.ReadWrite.All, User.RevokeSessions.All

Connect-MgGraph -Scopes "Application.ReadWrite.All",`
    "DelegatedPermissionGrant.ReadWrite.All",`
    "AppRoleAssignment.ReadWrite.All",`
    "User.RevokeSessions.All"

function Invoke-AppContainment {
    param(
        [Parameter(Mandatory)][string]$ServicePrincipalId,
        [Parameter(Mandatory)][string]$IncidentId
    )

    $log = @()
    $ts  = { (Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC) }

    Write-Host "=== CONTAINMENT INITIATED: $IncidentId ===" -ForegroundColor Red

    # 1. DISABLE the service principal (stops all future API calls)
    try {
        Update-MgServicePrincipal -ServicePrincipalId $ServicePrincipalId `
            -AccountEnabled:$false
        $log += "$(& $ts) | DISABLED service principal $ServicePrincipalId"
        Write-Host "[1/5] Service principal DISABLED" -ForegroundColor Green
    } catch { Write-Host "[ERROR] Disable failed: $_" -ForegroundColor Red }

    # 2. REVOKE all delegated permission grants (OAuth2 user consent)
    $grants = Get-MgServicePrincipalOauth2PermissionGrant `
        -ServicePrincipalId $ServicePrincipalId -All
    foreach ($g in $grants) {
        Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $g.Id
        $log += "$(& $ts) | REVOKED delegated grant: $($g.Scope)"
    }
    Write-Host "[2/5] Revoked $($grants.Count) delegated permission grants" -ForegroundColor Yellow

    # 3. REMOVE all app role assignments (application-level permissions)
    $roles = Get-MgServicePrincipalAppRoleAssignment `
        -ServicePrincipalId $ServicePrincipalId -All
    foreach ($r in $roles) {
        Remove-MgServicePrincipalAppRoleAssignment `
            -ServicePrincipalId $ServicePrincipalId `
            -AppRoleAssignmentId $r.Id
        $log += "$(& $ts) | REMOVED app role: $($r.AppRoleId)"
    }
    Write-Host "[3/5] Removed $($roles.Count) app role assignments" -ForegroundColor Yellow

    # 4. REMOVE all credentials from the app registration
    $sp = Get-MgServicePrincipal -ServicePrincipalId $ServicePrincipalId
    $appReg = Get-MgApplication -Filter "appId eq '$($sp.AppId)'" -ErrorAction SilentlyContinue
    if ($appReg) {
        foreach ($secret in $appReg.PasswordCredentials) {
            Remove-MgApplicationPassword -ApplicationId $appReg.Id `
                -KeyId $secret.KeyId
            $log += "$(& $ts) | REMOVED client secret: $($secret.KeyId)"
        }
        foreach ($cert in $appReg.KeyCredentials) {
            Remove-MgApplicationKey -ApplicationId $appReg.Id `
                -KeyId $cert.KeyId
            $log += "$(& $ts) | REMOVED certificate: $($cert.KeyId)"
        }
        Write-Host "[4/5] Removed all credentials from app registration" -ForegroundColor Yellow
    }

    # 5. REVOKE sessions for all users who consented to this app
    $consentedUsers = $grants | Select-Object -ExpandProperty PrincipalId -Unique
    foreach ($userId in $consentedUsers) {
        Revoke-MgUserSignInSession -UserId $userId -ErrorAction SilentlyContinue
        $log += "$(& $ts) | REVOKED sessions for user: $userId"
    }
    Write-Host "[5/5] Revoked sessions for $($consentedUsers.Count) affected users" -ForegroundColor Yellow

    # Save forensic evidence log
    $logPath = "Containment-$IncidentId-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
    $log | Out-File $logPath -Encoding UTF8
    Write-Host "`n=== CONTAINMENT COMPLETE ==="  -ForegroundColor Green
    Write-Host "Evidence log: $logPath" -ForegroundColor Cyan
    Write-Host "Actions taken: $($log.Count)" -ForegroundColor Cyan
}

# Usage:
# Invoke-AppContainment `
#     -ServicePrincipalId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
#     -IncidentId "INC-2026-0310-001"
💡 Pro Tip: Always run the lateral movement investigation query (Step 4) BEFORE executing containment. If the attacker created additional app registrations or inbox rules, disabling only the original compromised app leaves their persistence mechanisms active. Containment must cover every foothold identified in the investigation phase.

Step 7 · Build Supply Chain Detection Rules

  1. Create a custom detection rule: new credentials added to multi-tenant app
  2. Create a rule: app data access increases 5x from baseline within 24 hours
  3. Create a rule: app authenticates from IP not in its historical IP range
  4. Test detection rules with controlled simulations

Step 8 · Harden Tenant Against App Attacks

After containing and remediating a supply chain compromise, you need to assess the full blast radius. How many apps from the same publisher exist in your tenant? Did any other vendor apps exhibit similar anomalous behaviour during the same timeframe? The KQL query below performs a supply chain impact assessment - identifying all apps sharing characteristics with the compromised app (same publisher, same IP ranges, similar permission profiles) that may also require investigation.

  1. Restrict user consent to verified publishers with low-risk permissions only
  2. Enable admin consent workflow for all other app requests
  3. Configure workload identity protection for service principals
  4. Deploy Conditional Access for workload identities
  5. Enable continuous access evaluation for app tokens
// WHAT: Supply chain impact assessment - find all apps related to the compromised vendor
// WHY:  Supply chain attacks often affect multiple apps from the same publisher or
//       infrastructure. If one vendor app was compromised, other apps sharing the same
//       publisher, IP ranges, or permission patterns may also be affected.
// TABLES: AADServicePrincipalSignInLogs - authentication patterns for all service principals
//         AuditLogs - consent and configuration history
// OUTPUT: All apps matching the compromised app's publisher or authentication patterns,
//         with risk indicators for each - used to scope the investigation radius

let CompromisedAppId = "";
let InvestigationWindow = 30d;

// Step 1: Get the compromised app's characteristics
let CompromisedProfile = AADServicePrincipalSignInLogs
| where TimeGenerated > ago(InvestigationWindow)
| where AppId == CompromisedAppId
| summarize CompromisedIPs = make_set(IPAddress),
            CompromisedLocations = make_set(Location),
            Publisher = any(AppDisplayName)
    by HomeTenantId;

// Step 2: Find all OTHER apps from the same home tenant (same vendor)
let RelatedApps = AADServicePrincipalSignInLogs
| where TimeGenerated > ago(InvestigationWindow)
| where ResultType == 0
| where AppId != CompromisedAppId
| join kind=inner CompromisedProfile on HomeTenantId
| summarize AppIPs = make_set(IPAddress),
            SignInCount = count(),
            LastActive = max(TimeGenerated)
    by AppId, AppDisplayName, HomeTenantId;

// Step 3: Check which related apps share IP infrastructure with compromised app
RelatedApps
| join kind=inner CompromisedProfile on HomeTenantId
| mv-expand AppIP = AppIPs to typeof(string)
| extend SharesIP = CompromisedIPs has AppIP
| summarize SharedIPCount = countif(SharesIP),
            TotalIPs = dcount(AppIP),
            SignInCount = any(SignInCount),
            LastActive = any(LastActive)
    by AppId, AppDisplayName, HomeTenantId
| extend RiskLevel = case(
    SharedIPCount > 0, "HIGH - Shares infrastructure with compromised app",
    SignInCount > 100, "MEDIUM - Same vendor, high activity",
    "LOW - Same vendor, normal activity"
)
| order by case(RiskLevel has "HIGH", 1, RiskLevel has "MEDIUM", 2, 3) asc
💡 Pro Tip: After a supply chain incident, immediately check threat intelligence feeds (Microsoft Threat Intelligence, CISA alerts) for the compromised vendor’s app IDs. Other tenants may have already reported indicators of compromise - compare their IOCs against your audit logs to determine whether the attacker used the same techniques in your environment.

Step 9 · Create App Incident Response Playbook

  1. Define roles: incident commander, app investigator, communication lead
  2. Document containment procedures: disable, revoke, block
  3. Create communication templates: vendor notification, user notification, executive briefing
  4. Define evidence preservation requirements for legal
  5. Include regulatory notification checklists (GDPR 72-hour notification)

Step 10 · Conduct Post-Incident Review

After an OAuth supply chain incident, you need to verify that remediation was complete - no attacker persistence remains, all affected user sessions have been invalidated, and the app estate is clean. The PowerShell script below performs a comprehensive post-incident verification: confirming the compromised app is fully disabled and stripped of permissions, checking that no new suspicious apps appeared during the incident window, and validating that detection rules are in place to catch similar attacks in the future.

  1. Document the complete incident timeline: detection to remediation
  2. Identify gaps in detection (how could we have detected sooner?)
  3. Review response effectiveness (what went well, what needs improvement?)
  4. Update detection rules and policies based on lessons learned
  5. Present findings to leadership with improvement recommendations
# WHAT: Post-incident recovery verification for OAuth supply chain compromise
# WHY:  "Trust but verify" - after containment and remediation, you must confirm that
#       all attacker footholds have been removed, no persistence remains, and your
#       detection capabilities are updated to catch similar attacks in the future
# REQUIRES: Microsoft.Graph module v2.x
#           Scopes: Application.Read.All, AuditLog.Read.All, SecurityEvents.Read.All

Connect-MgGraph -Scopes "Application.Read.All","AuditLog.Read.All",`
    "SecurityEvents.Read.All"

function Invoke-PostIncidentVerification {
    param(
        [Parameter(Mandatory)][string]$ServicePrincipalId,
        [Parameter(Mandatory)][string]$IncidentId,
        [Parameter(Mandatory)][datetime]$IncidentStartDate
    )

    $results = @()
    Write-Host "=== POST-INCIDENT VERIFICATION: $IncidentId ===" -ForegroundColor Cyan

    # CHECK 1: Verify service principal is disabled
    $sp = Get-MgServicePrincipal -ServicePrincipalId $ServicePrincipalId
    $check1 = [PSCustomObject]@{
        Check  = "Service Principal Disabled"
        Status = if (-not $sp.AccountEnabled) { "PASS" } else { "FAIL" }
        Detail = "AccountEnabled = $($sp.AccountEnabled)"
    }
    $results += $check1

    # CHECK 2: Verify no permission grants remain
    $grants = Get-MgServicePrincipalOauth2PermissionGrant `
        -ServicePrincipalId $ServicePrincipalId -All
    $roles = Get-MgServicePrincipalAppRoleAssignment `
        -ServicePrincipalId $ServicePrincipalId -All
    $check2 = [PSCustomObject]@{
        Check  = "Permissions Fully Revoked"
        Status = if ($grants.Count -eq 0 -and $roles.Count -eq 0) { "PASS" } else { "FAIL" }
        Detail = "Delegated grants: $($grants.Count), App roles: $($roles.Count)"
    }
    $results += $check2

    # CHECK 3: Verify no credentials remain on the app registration
    $appReg = Get-MgApplication -Filter "appId eq '$($sp.AppId)'" -ErrorAction SilentlyContinue
    $credCount = 0
    if ($appReg) {
        $credCount = $appReg.PasswordCredentials.Count + $appReg.KeyCredentials.Count
    }
    $check3 = [PSCustomObject]@{
        Check  = "Credentials Removed"
        Status = if ($credCount -eq 0) { "PASS" } else { "FAIL" }
        Detail = "Remaining credentials: $credCount"
    }
    $results += $check3

    # CHECK 4: Scan for new app registrations created during incident window
    $suspiciousApps = Get-MgApplication -All | Where-Object {
        $_.CreatedDateTime -ge $IncidentStartDate -and
        $_.CreatedDateTime -le (Get-Date)
    }
    $check4 = [PSCustomObject]@{
        Check  = "No Suspicious New Apps"
        Status = if ($suspiciousApps.Count -le 2) { "PASS" } else { "REVIEW" }
        Detail = "Apps created since incident: $($suspiciousApps.Count)"
    }
    $results += $check4

    # CHECK 5: Verify inbox rules created during incident have been removed
    # (This requires Exchange Online PowerShell - flag for manual review)
    $check5 = [PSCustomObject]@{
        Check  = "Inbox Rules Reviewed"
        Status = "MANUAL"
        Detail = "Verify no attacker inbox rules remain via Exchange Online PowerShell"
    }
    $results += $check5

    # Display verification results
    Write-Host "`n=== VERIFICATION RESULTS ===" -ForegroundColor White
    foreach ($r in $results) {
        $color = switch ($r.Status) {
            "PASS"   { "Green" }
            "FAIL"   { "Red" }
            "REVIEW" { "Yellow" }
            "MANUAL" { "Cyan" }
        }
        Write-Host "[$($r.Status)] $($r.Check): $($r.Detail)" -ForegroundColor $color
    }

    # Export verification report
    $reportPath = "PostIncident-Verification-$IncidentId.json"
    $results | ConvertTo-Json | Out-File $reportPath
    Write-Host "`nReport saved: $reportPath" -ForegroundColor Green

    $failCount = ($results | Where-Object Status -eq "FAIL").Count
    if ($failCount -gt 0) {
        Write-Host "`nWARNING: $failCount checks FAILED - remediation incomplete!" -ForegroundColor Red
    } else {
        Write-Host "`nAll automated checks passed. Complete manual review items." -ForegroundColor Green
    }
}

# Usage:
# Invoke-PostIncidentVerification `
#     -ServicePrincipalId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
#     -IncidentId "INC-2026-0310-001" `
#     -IncidentStartDate (Get-Date).AddDays(-7)
💡 Pro Tip: Run this verification script daily for 30 days after the incident. Sophisticated attackers anticipate rapid containment and may have planted time-delayed persistence (scheduled tasks, deferred consent grants). Continuous verification catches these delayed detonation techniques that a single post-incident check would miss.

📚 Documentation Resources

ResourceDescription
Investigate OAuth App AttacksMicrosoft guidance on investigating compromised and malicious OAuth applications
Consent Phishing InvestigationStep-by-step guide for investigating illicit consent grant attacks
Workload Identity ProtectionSecure service principals with Conditional Access and identity protection
MITRE ATT&CK - Cloud TechniquesReference for cloud-specific attack techniques used in supply chain compromises
Microsoft Graph - Revoke SessionsAPI reference for programmatically revoking user and app sessions
Midnight Blizzard Attack AnalysisMicrosoft’s analysis of the Midnight Blizzard OAuth app attack - lessons learned

Summary

What You Accomplished

  • Understood OAuth supply chain attack patterns and indicators
  • Investigated a simulated app-based breach end-to-end
  • Contained and remediated a compromised application
  • Built custom detection rules for supply chain threats
  • Hardened the tenant against future app-based attacks
  • Created incident response playbooks and conducted post-incident review

Next Steps

  • Deploy Entra Workload Identity Protection for advanced service principal security
  • Integrate App Governance with Microsoft Sentinel for automated incident response
  • Participate in Microsoft''s Incident Response tabletop exercises
← Previous Lab All Labs →