Security/Get-StrykerIncidentReadiness.ps1

<#
.SYNOPSIS
    Collects Stryker Incident Readiness checks for the M365 Security Assessment.
.DESCRIPTION
    Evaluates 9 security controls inspired by the Stryker Corporation cyberattack
    (March 2026). These checks cover attack vectors not assessed by other M365 Assess
    collectors: stale admin detection, on-prem synced admins, CA policy exclusion
    analysis, privileged group assignments, overprivileged apps, multi-admin approval,
    RBAC scope tags, break-glass account detection, and device wipe audit.
 
    Requires: Directory.Read.All, AuditLog.Read.All, Policy.Read.All,
    DeviceManagementConfiguration.Read.All, DeviceManagementRBAC.Read.All,
    RoleManagement.Read.Directory
.PARAMETER OutputPath
    Path to export the CSV results file.
.NOTES
    Author: Daren9m
    Ported from: StrykerScan (https://github.com/Galvnyz/StrykerScan)
#>


[CmdletBinding()]
param(
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$OutputPath
)

# Continue on errors: individual checks query different Graph endpoints and some
# may fail due to missing licenses or permissions without invalidating the rest.
$ErrorActionPreference = 'Continue'

# ── Verify Graph connection ──────────────────────────────────────────
if (-not (Assert-GraphConnection)) { return }

Import-Module -Name Microsoft.Graph.Identity.DirectoryManagement -ErrorAction SilentlyContinue
Import-Module -Name Microsoft.Graph.Identity.SignIns -ErrorAction SilentlyContinue

# ── Output collection ────────────────────────────────────────────────
$settings = [System.Collections.Generic.List[PSCustomObject]]::new()
$checkIdCounter = @{}

function Add-Setting {
    param(
        [string]$Category,
        [string]$Setting,
        [string]$CurrentValue,
        [string]$RecommendedValue,
        [string]$Status,
        [string]$CheckId = '',
        [string]$Remediation = ''
    )
    $subCheckId = $CheckId
    if ($CheckId) {
        if (-not $checkIdCounter.ContainsKey($CheckId)) { $checkIdCounter[$CheckId] = 0 }
        $checkIdCounter[$CheckId]++
        $subCheckId = "$CheckId.$($checkIdCounter[$CheckId])"
    }
    $settings.Add([PSCustomObject]@{
        Category         = $Category
        Setting          = $Setting
        CurrentValue     = $CurrentValue
        RecommendedValue = $RecommendedValue
        Status           = $Status
        CheckId          = $subCheckId
        Remediation      = $Remediation
    })
    if ($CheckId -and (Get-Command -Name Update-CheckProgress -ErrorAction SilentlyContinue)) {
        Update-CheckProgress -CheckId $subCheckId -Setting $Setting -Status $Status
    }
}

# ── Shared: privileged role template IDs ─────────────────────────────
$coreRoleTemplateIds = @(
    '62e90394-69f5-4237-9190-012177145e10'  # Global Administrator
    '3a2c62db-5318-420d-8d74-23affee5d9d5'  # Intune Administrator
    '194ae4cb-b126-40b2-bd5b-6091b380977d'  # Security Administrator
)
$extendedRoleTemplateIds = $coreRoleTemplateIds + @(
    'f2ef992c-3afb-46b9-b7cf-a126ee74c451'  # Global Reader
)
$groupCheckRoleTemplateIds = $extendedRoleTemplateIds + @(
    '29232cdf-9323-42fd-ade2-1d097af3e4de'  # Exchange Administrator
    'fe930be7-5e62-47db-91af-98c3a49a38b1'  # User Administrator
)

$breakGlassPatterns = @('break.?glass', 'emergency.?access', 'breakglass', 'bg.?admin')

# ── Helper: get unique admin map for a set of role template IDs ──────
function Get-AdminMap {
    param([string[]]$RoleTemplateIds)
    $map = @{}
    foreach ($roleTemplateId in $RoleTemplateIds) {
        $role = Get-MgDirectoryRole -Filter "roleTemplateId eq '$roleTemplateId'" -ErrorAction SilentlyContinue
        if (-not $role) { continue }
        $members = Get-MgDirectoryRoleMemberAsUser -DirectoryRoleId $role.Id -All -ErrorAction SilentlyContinue
        if (-not $members) { continue }
        foreach ($member in $members) {
            if (-not $map.ContainsKey($member.Id)) {
                $map[$member.Id] = $member
            }
        }
    }
    return $map
}

# =====================================================================
# CHECK 1: ENTRA-STALEADMIN-001 — Stale Admin Accounts (>90 days)
# =====================================================================
try {
    $staleThresholdDays = 90
    $cutoffDate = (Get-Date).AddDays(-$staleThresholdDays)
    $adminMap = Get-AdminMap -RoleTemplateIds $extendedRoleTemplateIds
    $adminsChecked = $adminMap.Count

    if ($adminsChecked -eq 0) {
        Add-Setting -Category 'Stale Admin Detection' -Setting 'Admin accounts inactive >90 days' `
            -CurrentValue 'Unable to enumerate admin accounts' `
            -RecommendedValue 'All admins active within 90 days' `
            -Status 'Review' -CheckId 'ENTRA-STALEADMIN-001' `
            -Remediation 'Ensure Directory.Read.All permission is granted.'
    }
    else {
        # Batch-fetch sign-in activity
        $adminIds = @($adminMap.Keys)
        $chunkSize = 15
        $signInData = @{}

        for ($i = 0; $i -lt $adminIds.Count; $i += $chunkSize) {
            $chunk = $adminIds[$i..[math]::Min($i + $chunkSize - 1, $adminIds.Count - 1)]
            $idFilter = ($chunk | ForEach-Object { "'$_'" }) -join ','
            try {
                $uri = "https://graph.microsoft.com/v1.0/users?`$filter=id in ($idFilter)&`$select=id,displayName,userPrincipalName,signInActivity"
                $response = Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop
                foreach ($user in $response.value) {
                    $signInData[$user.id] = $user.signInActivity
                }
            }
            catch {
                foreach ($uid in $chunk) { $signInData[$uid] = $null }
            }
        }

        $allNull = ($signInData.Values | Where-Object { $null -ne $_ }).Count -eq 0
        if ($allNull -and $adminsChecked -gt 0) {
            Add-Setting -Category 'Stale Admin Detection' -Setting 'Admin accounts inactive >90 days' `
                -CurrentValue 'Sign-in activity data unavailable (AuditLog.Read.All may not be consented)' `
                -RecommendedValue 'All admins active within 90 days' `
                -Status 'Review' -CheckId 'ENTRA-STALEADMIN-001' `
                -Remediation 'Grant AuditLog.Read.All permission and reconnect to Microsoft Graph.'
        }
        else {
            $staleAdmins = @()
            foreach ($adminId in $adminIds) {
                $admin = $adminMap[$adminId]
                $activity = $signInData[$adminId]
                $lastSignIn = if ($activity) { $activity.lastSignInDateTime } else { $null }

                if (-not $lastSignIn) {
                    $isBreakGlass = $false
                    foreach ($p in $breakGlassPatterns) {
                        if ($admin.DisplayName -match $p) { $isBreakGlass = $true; break }
                    }
                    if (-not $isBreakGlass) {
                        $staleAdmins += "$($admin.DisplayName) ($($admin.UserPrincipalName)) - Never signed in"
                    }
                }
                elseif ([datetime]$lastSignIn -lt $cutoffDate) {
                    $daysSince = [math]::Round(((Get-Date) - [datetime]$lastSignIn).TotalDays)
                    $staleAdmins += "$($admin.DisplayName) ($($admin.UserPrincipalName)) - Last sign-in: $([datetime]$lastSignIn | Get-Date -Format 'yyyy-MM-dd') ($daysSince days ago)"
                }
            }

            if ($staleAdmins.Count -eq 0) {
                Add-Setting -Category 'Stale Admin Detection' -Setting 'Admin accounts inactive >90 days' `
                    -CurrentValue "All $adminsChecked admin(s) active within $staleThresholdDays days" `
                    -RecommendedValue 'All admins active within 90 days' `
                    -Status 'Pass' -CheckId 'ENTRA-STALEADMIN-001' `
                    -Remediation 'No action needed.'
            }
            else {
                Add-Setting -Category 'Stale Admin Detection' -Setting 'Admin accounts inactive >90 days' `
                    -CurrentValue "$($staleAdmins.Count) stale admin(s): $($staleAdmins -join '; ')" `
                    -RecommendedValue 'All admins active within 90 days' `
                    -Status 'Fail' -CheckId 'ENTRA-STALEADMIN-001' `
                    -Remediation 'Review each stale account. Remove admin role if no longer needed, or require password reset and MFA re-registration. Enable PIM to auto-expire admin assignments.'
            }
        }
    }
}
catch {
    Add-Setting -Category 'Stale Admin Detection' -Setting 'Admin accounts inactive >90 days' `
        -CurrentValue "Error: $($_.Exception.Message)" `
        -RecommendedValue 'All admins active within 90 days' `
        -Status 'Review' -CheckId 'ENTRA-STALEADMIN-001' `
        -Remediation 'Ensure Directory.Read.All and AuditLog.Read.All permissions are granted.'
}

# =====================================================================
# CHECK 2: ENTRA-SYNCADMIN-001 — On-Prem Synced Admin Accounts
# =====================================================================
try {
    $adminMap = Get-AdminMap -RoleTemplateIds $coreRoleTemplateIds
    $adminsChecked = $adminMap.Count

    if ($adminsChecked -eq 0) {
        Add-Setting -Category 'Cloud Admin Isolation' -Setting 'On-prem synced admin accounts' `
            -CurrentValue 'Unable to enumerate admin accounts' `
            -RecommendedValue 'All admin accounts cloud-only' `
            -Status 'Review' -CheckId 'ENTRA-SYNCADMIN-001' `
            -Remediation 'Ensure Directory.Read.All permission is granted.'
    }
    else {
        $syncedAdmins = @()
        foreach ($admin in $adminMap.Values) {
            if ($admin.OnPremisesSyncEnabled -eq $true) {
                $syncedAdmins += "$($admin.DisplayName) ($($admin.UserPrincipalName))"
            }
        }

        if ($syncedAdmins.Count -eq 0) {
            Add-Setting -Category 'Cloud Admin Isolation' -Setting 'On-prem synced admin accounts' `
                -CurrentValue "All $adminsChecked admin(s) are cloud-only" `
                -RecommendedValue 'All admin accounts cloud-only' `
                -Status 'Pass' -CheckId 'ENTRA-SYNCADMIN-001' `
                -Remediation 'No action needed.'
        }
        else {
            Add-Setting -Category 'Cloud Admin Isolation' -Setting 'On-prem synced admin accounts' `
                -CurrentValue "$($syncedAdmins.Count) synced admin(s): $($syncedAdmins -join '; ')" `
                -RecommendedValue 'All admin accounts cloud-only' `
                -Status 'Fail' -CheckId 'ENTRA-SYNCADMIN-001' `
                -Remediation 'Create dedicated cloud-only admin accounts on the .onmicrosoft.com domain. Remove admin roles from on-prem synced accounts. On-prem compromise leads to cloud admin compromise.'
        }
    }
}
catch {
    Add-Setting -Category 'Cloud Admin Isolation' -Setting 'On-prem synced admin accounts' `
        -CurrentValue "Error: $($_.Exception.Message)" `
        -RecommendedValue 'All admin accounts cloud-only' `
        -Status 'Review' -CheckId 'ENTRA-SYNCADMIN-001' `
        -Remediation 'Ensure Directory.Read.All permission is granted.'
}

# =====================================================================
# CHECK 3: CA-EXCLUSION-001 — Privileged Admins Excluded from CA
# =====================================================================
try {
    $policies = Get-MgIdentityConditionalAccessPolicy -All -ErrorAction Stop
    $enabledPolicies = @($policies | Where-Object { $_.State -eq 'enabled' })

    if ($enabledPolicies.Count -eq 0) {
        Add-Setting -Category 'CA Policy Exclusions' -Setting 'Admins excluded from CA policies' `
            -CurrentValue 'No enabled Conditional Access policies found' `
            -RecommendedValue 'No privileged admins excluded from CA policies' `
            -Status 'Warning' -CheckId 'CA-EXCLUSION-001' `
            -Remediation 'Create Conditional Access policies to protect admin access.'
    }
    else {
        $adminMap = Get-AdminMap -RoleTemplateIds $coreRoleTemplateIds
        if ($adminMap.Count -eq 0) {
            Add-Setting -Category 'CA Policy Exclusions' -Setting 'Admins excluded from CA policies' `
                -CurrentValue 'Unable to enumerate admin accounts' `
                -RecommendedValue 'No privileged admins excluded from CA policies' `
                -Status 'Review' -CheckId 'CA-EXCLUSION-001' `
                -Remediation 'Ensure Directory.Read.All permission is granted.'
        }
        else {
            $riskyExclusions = @()
            foreach ($policy in $enabledPolicies) {
                $excludedUserIds = @($policy.Conditions.Users.ExcludeUsers | Where-Object { $_ })
                if ($excludedUserIds.Count -eq 0) { continue }

                foreach ($excludedId in $excludedUserIds) {
                    if ($adminMap.ContainsKey($excludedId)) {
                        $admin = $adminMap[$excludedId]
                        $adminName = "$($admin.DisplayName) ($($admin.UserPrincipalName))"
                        $isBreakGlass = $false
                        foreach ($p in $breakGlassPatterns) {
                            if ($adminName -match $p) { $isBreakGlass = $true; break }
                        }
                        if (-not $isBreakGlass) {
                            $riskyExclusions += "$adminName excluded from '$($policy.DisplayName)'"
                        }
                    }
                }
            }

            $uniqueExclusions = @($riskyExclusions | Select-Object -Unique)

            if ($uniqueExclusions.Count -eq 0) {
                Add-Setting -Category 'CA Policy Exclusions' -Setting 'Admins excluded from CA policies' `
                    -CurrentValue 'No privileged admins excluded (break-glass accounts filtered)' `
                    -RecommendedValue 'No privileged admins excluded from CA policies' `
                    -Status 'Pass' -CheckId 'CA-EXCLUSION-001' `
                    -Remediation 'No action needed. Continue to review CA exclusions periodically.'
            }
            else {
                Add-Setting -Category 'CA Policy Exclusions' -Setting 'Admins excluded from CA policies' `
                    -CurrentValue "$($uniqueExclusions.Count) exclusion(s): $($uniqueExclusions -join '; ')" `
                    -RecommendedValue 'No privileged admins excluded from CA policies' `
                    -Status 'Fail' -CheckId 'CA-EXCLUSION-001' `
                    -Remediation 'Remove admin accounts from CA policy exclusion lists. Only break-glass emergency access accounts should be excluded. Use Entra ID Access Reviews to audit CA exclusions regularly.'
            }
        }
    }
}
catch {
    Add-Setting -Category 'CA Policy Exclusions' -Setting 'Admins excluded from CA policies' `
        -CurrentValue "Error: $($_.Exception.Message)" `
        -RecommendedValue 'No privileged admins excluded from CA policies' `
        -Status 'Review' -CheckId 'CA-EXCLUSION-001' `
        -Remediation 'Ensure Policy.Read.All and Directory.Read.All permissions are granted.'
}

# =====================================================================
# CHECK 4: ENTRA-ROLEGROUP-001 — Unprotected Privileged Group Assignments
# =====================================================================
try {
    $unprotectedGroups = @()
    $groupsChecked = 0

    foreach ($roleTemplateId in $groupCheckRoleTemplateIds) {
        $role = Get-MgDirectoryRole -Filter "roleTemplateId eq '$roleTemplateId'" -ErrorAction SilentlyContinue
        if (-not $role) { continue }

        $members = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/directoryRoles/$($role.Id)/members" -Method GET -ErrorAction SilentlyContinue
        if (-not $members -or -not $members.value) { continue }

        foreach ($member in $members.value) {
            if ($member.'@odata.type' -ne '#microsoft.graph.group') { continue }
            $groupsChecked++

            if ($member.isAssignableToRole -ne $true) {
                $memberCount = 'unknown'
                try {
                    $countUri = "https://graph.microsoft.com/v1.0/groups/$($member.id)/members/`$count"
                    $countResponse = Invoke-MgGraphRequest -Uri $countUri -Method GET -Headers @{ 'ConsistencyLevel' = 'eventual' } -ErrorAction Stop
                    $memberCount = $countResponse
                }
                catch { Write-Verbose "Could not get member count for group $($member.displayName)" }
                $unprotectedGroups += "$($member.displayName) — Role: $($role.DisplayName), Members: $memberCount"
            }
        }
    }

    if ($groupsChecked -eq 0) {
        Add-Setting -Category 'Privileged Group Protection' -Setting 'Groups in privileged role assignments' `
            -CurrentValue 'No groups assigned to privileged roles (individual users only)' `
            -RecommendedValue 'All role-assigned groups have isAssignableToRole enabled' `
            -Status 'Pass' -CheckId 'ENTRA-ROLEGROUP-001' `
            -Remediation 'No action needed.'
    }
    elseif ($unprotectedGroups.Count -eq 0) {
        Add-Setting -Category 'Privileged Group Protection' -Setting 'Groups in privileged role assignments' `
            -CurrentValue "All $groupsChecked group(s) have isAssignableToRole enabled" `
            -RecommendedValue 'All role-assigned groups have isAssignableToRole enabled' `
            -Status 'Pass' -CheckId 'ENTRA-ROLEGROUP-001' `
            -Remediation 'No action needed.'
    }
    else {
        Add-Setting -Category 'Privileged Group Protection' -Setting 'Groups in privileged role assignments' `
            -CurrentValue "$($unprotectedGroups.Count) unprotected group(s): $($unprotectedGroups -join '; ')" `
            -RecommendedValue 'All role-assigned groups have isAssignableToRole enabled' `
            -Status 'Fail' -CheckId 'ENTRA-ROLEGROUP-001' `
            -Remediation 'Recreate each group with isAssignableToRole = true (cannot be changed post-creation). Copy membership, reassign the role, then delete the old group. Role-assignable groups restrict membership management to Global Admins and Privileged Role Admins.'
    }
}
catch {
    Add-Setting -Category 'Privileged Group Protection' -Setting 'Groups in privileged role assignments' `
        -CurrentValue "Error: $($_.Exception.Message)" `
        -RecommendedValue 'All role-assigned groups have isAssignableToRole enabled' `
        -Status 'Review' -CheckId 'ENTRA-ROLEGROUP-001' `
        -Remediation 'Ensure Directory.Read.All permission is granted.'
}

# =====================================================================
# CHECK 5: ENTRA-APPS-002 — Overprivileged App Registrations
# =====================================================================
try {
    $dangerousPermissions = @{
        'DeviceManagementConfiguration.ReadWrite.All'              = 'Modify Intune device config'
        'DeviceManagementManagedDevices.ReadWrite.All'             = 'Wipe/retire/manage all devices'
        'DeviceManagementManagedDevices.PrivilegedOperations.All'  = 'Privileged device operations'
        'DeviceManagementRBAC.ReadWrite.All'                       = 'Modify Intune RBAC roles'
        'DeviceManagementApps.ReadWrite.All'                       = 'Deploy/modify apps on devices'
        'DeviceManagementServiceConfig.ReadWrite.All'              = 'Modify Intune service config'
        'RoleManagement.ReadWrite.Directory'                       = 'Modify Entra ID role assignments'
        'AppRoleAssignment.ReadWrite.All'                          = 'Grant itself additional permissions'
        'Directory.ReadWrite.All'                                  = 'Modify all directory objects'
    }
    $graphAppId = '00000003-0000-0000-c000-000000000000'

    $uri = "https://graph.microsoft.com/v1.0/applications?`$select=id,appId,displayName,requiredResourceAccess&`$top=999"
    $response = Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop
    $apps = $response.value

    $graphSp = Get-MgServicePrincipal -Filter "appId eq '$graphAppId'" -ErrorAction Stop
    $graphAppRoles = @{}
    foreach ($role in $graphSp.AppRoles) {
        $graphAppRoles[$role.Id] = $role.Value
    }

    $riskyApps = @()
    foreach ($app in $apps) {
        $graphAccess = $app.requiredResourceAccess | Where-Object { $_.resourceAppId -eq $graphAppId }
        if (-not $graphAccess) { continue }

        $dangerousFound = @()
        foreach ($access in $graphAccess.resourceAccess) {
            if ($access.type -ne 'Role') { continue }
            $permName = $graphAppRoles[$access.id]
            if ($permName -and $dangerousPermissions.ContainsKey($permName)) {
                $dangerousFound += $permName
            }
        }

        if ($dangerousFound.Count -gt 0) {
            $riskyApps += "$($app.displayName) (AppId: $($app.appId)) — $($dangerousFound -join ', ')"
        }
    }

    if ($riskyApps.Count -eq 0) {
        Add-Setting -Category 'Application Permissions' -Setting 'Apps with dangerous Intune write permissions' `
            -CurrentValue "No risky apps found ($(@($apps).Count) evaluated)" `
            -RecommendedValue 'No apps with dangerous device management write permissions' `
            -Status 'Pass' -CheckId 'ENTRA-APPS-002' `
            -Remediation 'No action needed. Continue to review app permissions during app registration reviews.'
    }
    else {
        Add-Setting -Category 'Application Permissions' -Setting 'Apps with dangerous Intune write permissions' `
            -CurrentValue "$($riskyApps.Count) risky app(s): $($riskyApps -join '; ')" `
            -RecommendedValue 'No apps with dangerous device management write permissions' `
            -Status 'Fail' -CheckId 'ENTRA-APPS-002' `
            -Remediation 'Review each app in Entra > Applications > App registrations > API permissions. Remove unnecessary write permissions and replace with read-only equivalents. Use Managed Identities where possible.'
    }
}
catch {
    Add-Setting -Category 'Application Permissions' -Setting 'Apps with dangerous Intune write permissions' `
        -CurrentValue "Error: $($_.Exception.Message)" `
        -RecommendedValue 'No apps with dangerous device management write permissions' `
        -Status 'Review' -CheckId 'ENTRA-APPS-002' `
        -Remediation 'Ensure Directory.Read.All permission is granted.'
}

# =====================================================================
# CHECK 6: INTUNE-MAA-001 — Multi-Admin Approval
# =====================================================================
try {
    $uri = "https://graph.microsoft.com/beta/deviceManagement/operationApprovalPolicies"
    $response = Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop
    $policies = $response.value

    if ($policies -and $policies.Count -gt 0) {
        $policyDescriptions = @($policies | ForEach-Object {
            $type = if ($_.policyType) { $_.policyType } else { 'Custom' }
            "$type ($($_.approverGroupIds.Count) approver group(s))"
        })
        Add-Setting -Category 'Multi-Admin Approval' -Setting 'Intune Multi-Admin Approval enabled' `
            -CurrentValue "$($policies.Count) policy(ies) configured: $($policyDescriptions -join '; ')" `
            -RecommendedValue 'Multi-Admin Approval enabled for destructive operations' `
            -Status 'Pass' -CheckId 'INTUNE-MAA-001' `
            -Remediation 'No action needed. Review policies periodically to ensure coverage of critical operations.'
    }
    else {
        Add-Setting -Category 'Multi-Admin Approval' -Setting 'Intune Multi-Admin Approval enabled' `
            -CurrentValue 'No Multi-Admin Approval policies configured' `
            -RecommendedValue 'Multi-Admin Approval enabled for destructive operations' `
            -Status 'Fail' -CheckId 'INTUNE-MAA-001' `
            -Remediation 'Enable Multi-Admin Approval in Intune admin center > Tenant administration > Multi-admin approval. Create policies for device wipe/retire, script deployment, app deployment, and role assignment changes.'
    }
}
catch {
    $errorMessage = $_.Exception.Message
    if ($errorMessage -match '404|NotFound') {
        Add-Setting -Category 'Multi-Admin Approval' -Setting 'Intune Multi-Admin Approval enabled' `
            -CurrentValue 'Feature not available (requires Intune Plan 2 or Intune Suite)' `
            -RecommendedValue 'Multi-Admin Approval enabled for destructive operations' `
            -Status 'Review' -CheckId 'INTUNE-MAA-001' `
            -Remediation 'Verify your Intune license includes Multi-Admin Approval (Intune Plan 2 or Intune Suite).'
    }
    else {
        Add-Setting -Category 'Multi-Admin Approval' -Setting 'Intune Multi-Admin Approval enabled' `
            -CurrentValue "Error (Beta API): $errorMessage" `
            -RecommendedValue 'Multi-Admin Approval enabled for destructive operations' `
            -Status 'Review' -CheckId 'INTUNE-MAA-001' `
            -Remediation 'Ensure DeviceManagementConfiguration.Read.All permission is granted. This check uses the Microsoft Graph Beta API.'
    }
}

# =====================================================================
# CHECK 7: INTUNE-RBAC-001 — RBAC Role Assignments Without Scope Tags
# =====================================================================
try {
    $defsUri = "https://graph.microsoft.com/v1.0/deviceManagement/roleDefinitions"
    $defsResponse = Invoke-MgGraphRequest -Uri $defsUri -Method GET -ErrorAction Stop

    $assignments = @()
    foreach ($roleDef in $defsResponse.value) {
        if ($roleDef.isBuiltIn -eq $true -and $roleDef.displayName -eq 'Intune Role Administrator') { continue }
        $assignUri = "https://graph.microsoft.com/v1.0/deviceManagement/roleDefinitions/$($roleDef.id)/roleAssignments"
        try {
            $assignResponse = Invoke-MgGraphRequest -Uri $assignUri -Method GET -ErrorAction Stop
            foreach ($a in $assignResponse.value) {
                $a['_roleName'] = $roleDef.displayName
                $assignments += $a
            }
        }
        catch { Write-Verbose "Could not fetch role assignments for $($roleDef.displayName)" }
    }

    if (-not $assignments -or $assignments.Count -eq 0) {
        Add-Setting -Category 'RBAC Scope Tags' -Setting 'RBAC role assignments without scope tags' `
            -CurrentValue 'No custom RBAC role assignments found' `
            -RecommendedValue 'All role assignments use scope tags' `
            -Status 'Pass' -CheckId 'INTUNE-RBAC-001' `
            -Remediation 'No action needed.'
    }
    else {
        $broadAssignments = @()
        foreach ($assignment in $assignments) {
            $scopeMembers = $assignment.resourceScopes
            $isBroad = (-not $scopeMembers) -or ($scopeMembers.Count -eq 0) -or ($scopeMembers.Count -eq 1 -and $scopeMembers[0] -eq '0')
            if ($isBroad) {
                $roleName = if ($assignment['_roleName']) { $assignment['_roleName'] } else { 'Unknown' }
                $broadAssignments += "$($assignment.displayName) [Role: $roleName]"
            }
        }

        if ($broadAssignments.Count -eq 0) {
            Add-Setting -Category 'RBAC Scope Tags' -Setting 'RBAC role assignments without scope tags' `
                -CurrentValue "All $($assignments.Count) assignment(s) use scope tags" `
                -RecommendedValue 'All role assignments use scope tags' `
                -Status 'Pass' -CheckId 'INTUNE-RBAC-001' `
                -Remediation 'No action needed.'
        }
        else {
            Add-Setting -Category 'RBAC Scope Tags' -Setting 'RBAC role assignments without scope tags' `
                -CurrentValue "$($broadAssignments.Count) of $($assignments.Count) assignment(s) have full tenant scope: $($broadAssignments -join '; ')" `
                -RecommendedValue 'All role assignments use scope tags' `
                -Status 'Fail' -CheckId 'INTUNE-RBAC-001' `
                -Remediation 'Create scope tags in Intune > Tenant administration > Scope (Tags). Assign scope tags to device groups, then edit role assignments to restrict to specific tags instead of Default (full tenant).'
        }
    }
}
catch {
    Add-Setting -Category 'RBAC Scope Tags' -Setting 'RBAC role assignments without scope tags' `
        -CurrentValue "Error: $($_.Exception.Message)" `
        -RecommendedValue 'All role assignments use scope tags' `
        -Status 'Review' -CheckId 'INTUNE-RBAC-001' `
        -Remediation 'Ensure DeviceManagementRBAC.Read.All permission is granted.'
}

# =====================================================================
# CHECK 8: ENTRA-BREAKGLASS-001 — Break-Glass Emergency Access Account
# =====================================================================
try {
    $globalAdminRoleId = '62e90394-69f5-4237-9190-012177145e10'
    $role = Get-MgDirectoryRole -Filter "roleTemplateId eq '$globalAdminRoleId'" -ErrorAction Stop

    if (-not $role) {
        Add-Setting -Category 'Emergency Access' -Setting 'Break-glass emergency access account' `
            -CurrentValue 'Global Administrator role not activated' `
            -RecommendedValue 'At least 1 break-glass account with Global Admin role' `
            -Status 'Fail' -CheckId 'ENTRA-BREAKGLASS-001' `
            -Remediation 'Create break-glass emergency access accounts with the Global Administrator role.'
    }
    else {
        $members = Get-MgDirectoryRoleMemberAsUser -DirectoryRoleId $role.Id -All -ErrorAction Stop
        $detectedAccounts = @()
        $confidenceLevel = 'None'

        # Method 1: Name pattern matching (high confidence)
        foreach ($member in $members) {
            foreach ($pattern in $breakGlassPatterns) {
                if ($member.DisplayName -match $pattern -or $member.UserPrincipalName -match $pattern) {
                    $detectedAccounts += "$($member.DisplayName) ($($member.UserPrincipalName)) [name match]"
                    $confidenceLevel = 'High'
                    break
                }
            }
        }

        # Method 2: CA exclusion pattern (medium confidence fallback)
        if ($detectedAccounts.Count -eq 0) {
            $caPolicies = Get-MgIdentityConditionalAccessPolicy -All -ErrorAction SilentlyContinue
            $enabledCaPolicies = @($caPolicies | Where-Object { $_.State -eq 'enabled' })
            $excludedUserIds = @()
            foreach ($policy in $enabledCaPolicies) {
                if ($policy.Conditions.Users.ExcludeUsers) {
                    $excludedUserIds += $policy.Conditions.Users.ExcludeUsers
                }
            }
            $globalAdminIds = @($members | Select-Object -ExpandProperty Id)
            $excludedAdmins = @($excludedUserIds | Where-Object { $_ -in $globalAdminIds } | Select-Object -Unique)

            if ($excludedAdmins.Count -gt 0) {
                foreach ($adminId in $excludedAdmins) {
                    $admin = $members | Where-Object { $_.Id -eq $adminId }
                    if ($admin) {
                        $detectedAccounts += "$($admin.DisplayName) ($($admin.UserPrincipalName)) [CA exclusion pattern]"
                        $confidenceLevel = 'Medium'
                    }
                }
            }
        }

        if ($detectedAccounts.Count -gt 0) {
            $status = if ($confidenceLevel -eq 'High') { 'Pass' } else { 'Warning' }
            Add-Setting -Category 'Emergency Access' -Setting 'Break-glass emergency access account' `
                -CurrentValue "$($detectedAccounts.Count) account(s) detected (confidence: $confidenceLevel): $($detectedAccounts -join '; ')" `
                -RecommendedValue 'At least 1 break-glass account with Global Admin role' `
                -Status $status -CheckId 'ENTRA-BREAKGLASS-001' `
                -Remediation $(if ($status -eq 'Pass') { 'No action needed. Ensure break-glass accounts are excluded from all CA policies, monitored for sign-in activity, and tested quarterly.' } else { 'Verify detected account(s) are intentional break-glass accounts. Consider renaming to clearly identify them.' })
        }
        else {
            Add-Setting -Category 'Emergency Access' -Setting 'Break-glass emergency access account' `
                -CurrentValue 'No break-glass account detected among Global Admins' `
                -RecommendedValue 'At least 1 break-glass account with Global Admin role' `
                -Status 'Fail' -CheckId 'ENTRA-BREAKGLASS-001' `
                -Remediation 'Create 2 cloud-only break-glass accounts (e.g., BreakGlass-Admin-01@contoso.onmicrosoft.com) with Global Admin role, FIDO2 security keys, excluded from all CA policies, and monitored for sign-in activity.'
        }
    }
}
catch {
    Add-Setting -Category 'Emergency Access' -Setting 'Break-glass emergency access account' `
        -CurrentValue "Error: $($_.Exception.Message)" `
        -RecommendedValue 'At least 1 break-glass account with Global Admin role' `
        -Status 'Review' -CheckId 'ENTRA-BREAKGLASS-001' `
        -Remediation 'Ensure Directory.Read.All and Policy.Read.All permissions are granted.'
}

# =====================================================================
# CHECK 9: INTUNE-WIPEAUDIT-001 — Mass Device Wipe Activity
# =====================================================================
try {
    $failThreshold = 10
    $warnThreshold = 5
    $lookbackDays = 30
    $startDate = (Get-Date).ToUniversalTime().AddDays(-$lookbackDays).ToString('yyyy-MM-ddTHH:mm:ssZ')

    $uri = "https://graph.microsoft.com/v1.0/deviceManagement/auditEvents?`$filter=activityDateTime ge $startDate and (activityType eq 'Wipe' or activityType eq 'Retire' or activityType eq 'Delete')&`$orderby=activityDateTime desc&`$top=500"
    $response = Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop
    $wipeEvents = $response.value

    if (-not $wipeEvents -or $wipeEvents.Count -eq 0) {
        Add-Setting -Category 'Device Wipe Audit' -Setting 'Mass device wipe activity (30 days)' `
            -CurrentValue "No wipe/retire/delete actions in last $lookbackDays days" `
            -RecommendedValue 'No suspicious burst wipe patterns' `
            -Status 'Pass' -CheckId 'INTUNE-WIPEAUDIT-001' `
            -Remediation 'No action needed.'
    }
    else {
        # Analyze for burst patterns (sliding 24-hour window)
        $sortedEvents = @($wipeEvents | Sort-Object { $_.activityDateTime })
        $maxBurstCount = 0
        $burstWindow = $null

        for ($i = 0; $i -lt $sortedEvents.Count; $i++) {
            $windowStart = [datetime]$sortedEvents[$i].activityDateTime
            $windowEnd = $windowStart.AddHours(24)
            $windowCount = ($sortedEvents | Where-Object {
                [datetime]$_.activityDateTime -ge $windowStart -and [datetime]$_.activityDateTime -le $windowEnd
            } | Measure-Object).Count
            if ($windowCount -gt $maxBurstCount) {
                $maxBurstCount = $windowCount
                $burstWindow = $windowStart.ToString('yyyy-MM-dd')
            }
        }

        $summary = "Total: $($wipeEvents.Count) events, Peak 24h burst: $maxBurstCount"
        if ($burstWindow) { $summary += " (on $burstWindow)" }

        if ($maxBurstCount -ge $failThreshold) {
            Add-Setting -Category 'Device Wipe Audit' -Setting 'Mass device wipe activity (30 days)' `
                -CurrentValue "ALERT: $maxBurstCount wipe/retire/delete actions in 24h window on $burstWindow. $summary" `
                -RecommendedValue 'No suspicious burst wipe patterns' `
                -Status 'Fail' -CheckId 'INTUNE-WIPEAUDIT-001' `
                -Remediation 'IMMEDIATE: Identify the actor in Intune > Tenant admin > Audit logs. Disable the account and revoke sessions in Entra. Assess scope of affected devices. Check for persistence (scripts, config changes). Contact Microsoft Support (Severity A).'
        }
        elseif ($maxBurstCount -ge $warnThreshold) {
            Add-Setting -Category 'Device Wipe Audit' -Setting 'Mass device wipe activity (30 days)' `
                -CurrentValue "$maxBurstCount wipe/retire/delete actions in 24h window on $burstWindow (elevated but may be legitimate). $summary" `
                -RecommendedValue 'No suspicious burst wipe patterns' `
                -Status 'Warning' -CheckId 'INTUNE-WIPEAUDIT-001' `
                -Remediation 'Review audit logs to confirm wipes were part of documented processes (device refresh, offboarding). If unexplained, investigate as potential compromise.'
        }
        else {
            Add-Setting -Category 'Device Wipe Audit' -Setting 'Mass device wipe activity (30 days)' `
                -CurrentValue "$($wipeEvents.Count) action(s) found, no suspicious burst patterns (peak: $maxBurstCount in 24h). $summary" `
                -RecommendedValue 'No suspicious burst wipe patterns' `
                -Status 'Pass' -CheckId 'INTUNE-WIPEAUDIT-001' `
                -Remediation 'No action needed. Continue monitoring audit logs.'
        }
    }
}
catch {
    Add-Setting -Category 'Device Wipe Audit' -Setting 'Mass device wipe activity (30 days)' `
        -CurrentValue "Error: $($_.Exception.Message)" `
        -RecommendedValue 'No suspicious burst wipe patterns' `
        -Status 'Review' -CheckId 'INTUNE-WIPEAUDIT-001' `
        -Remediation 'Ensure DeviceManagementApps.Read.All permission is granted for the Intune audit events endpoint.'
}

# ── Export results ───────────────────────────────────────────────────
$report = @($settings)
Write-Verbose "Collected $($report.Count) Stryker Incident Readiness settings"

if ($OutputPath) {
    $report | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Output "Exported Stryker Incident Readiness ($($report.Count) settings) to $OutputPath"
}
else {
    Write-Output $report
}