Private/Audit/Invoke-AdminManagementChecks.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Invoke-AdminManagementChecks {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$AuditData,

        [string]$OrgUnitPath = '/'
    )

    $checkDefs = Get-AuditCategoryDefinitions -Category 'AdminManagementChecks'
    $findings = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($check in $checkDefs.checks) {
        $funcName = "Test-Fortification$($check.id -replace '-', '')"
        if (Get-Command $funcName -ErrorAction SilentlyContinue) {
            try {
                $finding = & $funcName -AuditData $AuditData -CheckDefinition $check -OrgUnitPath $OrgUnitPath
                if ($finding) { $findings.Add($finding) }
            } catch {
                $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'ERROR' `
                    -CurrentValue "Check failed: $_" -OrgUnitPath $OrgUnitPath))
            }
        } else {
            $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'SKIP' `
                -CurrentValue 'Check not yet implemented' -OrgUnitPath $OrgUnitPath))
        }
    }

    return @($findings)
}

# ── ADMIN-001: Super Admin Account Inventory ─────────────────────────────
function Test-FortificationADMIN001 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $na = Get-NotAssessedFinding -CheckDefinition $CheckDefinition -ErrorMap $AuditData.Errors `
        -SourceKey 'Users' -Subject 'user inventory'
    if ($na) { return $na }

    if (-not $AuditData.Users) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User data not available' -OrgUnitPath $OrgUnitPath
    }

    $superAdmins = @($AuditData.Users | Where-Object { $_.isAdmin -eq $true })
    $activeSuperAdmins = @($superAdmins | Where-Object { -not $_.suspended })
    $suspendedSuperAdmins = @($superAdmins | Where-Object { $_.suspended -eq $true })

    $adminEmails = @($activeSuperAdmins | ForEach-Object { $_.primaryEmail })

    $status = if ($activeSuperAdmins.Count -eq 0) { 'FAIL' } else { 'PASS' }
    $currentValue = "$($activeSuperAdmins.Count) active super admin(s), $($suspendedSuperAdmins.Count) suspended"

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath `
        -Details @{
            ActiveSuperAdmins    = $adminEmails
            TotalSuperAdmins     = $superAdmins.Count
            ActiveCount          = $activeSuperAdmins.Count
            SuspendedCount       = $suspendedSuperAdmins.Count
        }
}

# ── ADMIN-002: Admin Role Assignments Audit ──────────────────────────────
function Test-FortificationADMIN002 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $na = Get-NotAssessedFinding -CheckDefinition $CheckDefinition -ErrorMap $AuditData.Errors `
        -SourceKey @('Roles', 'RoleAssignments') -Subject 'admin roles and assignments'
    if ($na) { return $na }

    if (-not $AuditData.Roles -or -not $AuditData.RoleAssignments) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'Role and role assignment data not available. Verify in Admin Console > Account > Admin roles' `
            -OrgUnitPath $OrgUnitPath
    }

    $roles = @($AuditData.Roles)
    $assignments = @($AuditData.RoleAssignments)

    # Identify built-in vs custom roles
    $builtInRoles = @($roles | Where-Object { $_.isSystemRole -eq $true -or $_.isSuperAdminRole -eq $true })
    $customRoles = @($roles | Where-Object { $_.isSystemRole -ne $true -and $_.isSuperAdminRole -ne $true })

    $status = if ($assignments.Count -gt 0 -and $customRoles.Count -eq 0) { 'WARN' }
              else { 'PASS' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($assignments.Count) role assignment(s) across $($roles.Count) roles ($($builtInRoles.Count) built-in, $($customRoles.Count) custom)" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{
            TotalRoles       = $roles.Count
            BuiltInRoles     = $builtInRoles.Count
            CustomRoles      = $customRoles.Count
            TotalAssignments = $assignments.Count
        }
}

# ── ADMIN-003: Delegated Admin Permissions Review ────────────────────────
function Test-FortificationADMIN003 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $na = Get-NotAssessedFinding -CheckDefinition $CheckDefinition -ErrorMap $AuditData.Errors `
        -SourceKey 'Roles' -Subject 'admin roles'
    if ($na) { return $na }

    if (-not $AuditData.Roles) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'Role data not available. Verify custom admin roles in Admin Console > Account > Admin roles' `
            -OrgUnitPath $OrgUnitPath
    }

    $customRoles = @($AuditData.Roles | Where-Object { $_.isSystemRole -ne $true -and $_.isSuperAdminRole -ne $true })

    if ($customRoles.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue 'No custom admin roles configured. Only built-in roles are in use' `
            -OrgUnitPath $OrgUnitPath
    }

    $roleNames = @($customRoles | ForEach-Object { $_.roleName ?? $_.name ?? 'Unknown' })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue "$($customRoles.Count) custom admin role(s) should be reviewed for appropriate permission scoping" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ CustomRoles = $roleNames; Count = $customRoles.Count }
}

# ── ADMIN-004: Inactive/Suspended Admin Accounts ────────────────────────
function Test-FortificationADMIN004 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $na = Get-NotAssessedFinding -CheckDefinition $CheckDefinition -ErrorMap $AuditData.Errors `
        -SourceKey 'Users' -Subject 'user inventory'
    if ($na) { return $na }

    if (-not $AuditData.Users) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User data not available' -OrgUnitPath $OrgUnitPath
    }

    # Find suspended users who still have admin roles
    $suspendedAdmins = @($AuditData.Users | Where-Object {
        $_.suspended -eq $true -and ($_.isAdmin -eq $true -or $_.isDelegatedAdmin -eq $true)
    })

    if ($suspendedAdmins.Count -gt 0) {
        $adminEmails = @($suspendedAdmins | ForEach-Object { $_.primaryEmail })
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue "$($suspendedAdmins.Count) suspended user(s) still have admin role assignments" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ SuspendedAdmins = $adminEmails }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue 'No suspended users with active admin role assignments' `
        -OrgUnitPath $OrgUnitPath
}

# ── ADMIN-005: User Account Inventory ────────────────────────────────────
function Test-FortificationADMIN005 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $na = Get-NotAssessedFinding -CheckDefinition $CheckDefinition -ErrorMap $AuditData.Errors `
        -SourceKey 'Users' -Subject 'user inventory'
    if ($na) { return $na }

    if (-not $AuditData.Users) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User data not available' -OrgUnitPath $OrgUnitPath
    }

    $allUsers = @($AuditData.Users)
    $active = @($allUsers | Where-Object { -not $_.suspended -and -not $_.archived })
    $suspended = @($allUsers | Where-Object { $_.suspended -eq $true })
    $archived = @($allUsers | Where-Object { $_.archived -eq $true })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "Total: $($allUsers.Count) users ($($active.Count) active, $($suspended.Count) suspended, $($archived.Count) archived)" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{
            TotalUsers    = $allUsers.Count
            ActiveCount   = $active.Count
            SuspendedCount = $suspended.Count
            ArchivedCount  = $archived.Count
        }
}

# ── ADMIN-006: Stale User Accounts ───────────────────────────────────────
function Test-FortificationADMIN006 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $na = Get-NotAssessedFinding -CheckDefinition $CheckDefinition -ErrorMap $AuditData.Errors `
        -SourceKey 'Users' -Subject 'user inventory'
    if ($na) { return $na }

    if (-not $AuditData.Users) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User data not available' -OrgUnitPath $OrgUnitPath
    }

    $staleDays = 90
    $now = [datetime]::UtcNow
    $activeUsers = @($AuditData.Users | Where-Object { -not $_.suspended })
    $staleUsers = [System.Collections.Generic.List[string]]::new()

    foreach ($user in $activeUsers) {
        $lastLogin = $null
        if ($user.lastLoginTime) {
            try { $lastLogin = [datetime]::Parse($user.lastLoginTime) } catch { }
        }
        if (-not $lastLogin -or ($now - $lastLogin).TotalDays -gt $staleDays) {
            $staleUsers.Add($user.primaryEmail)
        }
    }

    if ($staleUsers.Count -gt 0) {
        $staleRate = [Math]::Round(($staleUsers.Count / $activeUsers.Count) * 100, 1)
        $status = if ($staleRate -gt 20) { 'FAIL' }
                  elseif ($staleRate -gt 10) { 'WARN' }
                  else { 'PASS' }
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue "$($staleUsers.Count) of $($activeUsers.Count) active users ($staleRate%) inactive for $staleDays+ days" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ StaleUsers = @($staleUsers | Select-Object -First 50); StaleCount = $staleUsers.Count; ThresholdDays = $staleDays }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "All $($activeUsers.Count) active users have logged in within the last $staleDays days" `
        -OrgUnitPath $OrgUnitPath
}

# ── ADMIN-007: OU Structure Review ───────────────────────────────────────
function Test-FortificationADMIN007 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $na = Get-NotAssessedFinding -CheckDefinition $CheckDefinition -ErrorMap $AuditData.Errors `
        -SourceKey 'OrgUnits' -Subject 'organizational units'
    if ($na) { return $na }

    if (-not $AuditData.Tenant -or -not $AuditData.Tenant.OrgUnits) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'Organizational unit data not available. Verify OU structure in Admin Console > Directory > Organizational units' `
            -OrgUnitPath $OrgUnitPath
    }

    $orgUnits = @($AuditData.Tenant.OrgUnits)
    $ouPaths = @($orgUnits | ForEach-Object { $_.orgUnitPath ?? $_.OrgUnitPath ?? '/' })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "$($orgUnits.Count) organizational unit(s) configured" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ OUCount = $orgUnits.Count; OUPaths = $ouPaths }
}

# ── ADMIN-008: Directory Sharing Settings ────────────────────────────────
function Test-FortificationADMIN008 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # GWS-1: directory.workspace_resource_type_visibility { domainSharedContacts=bool }.
    # When true, domain shared contacts are exposed across the global directory — a directory-
    # exposure surface worth review (especially for K-12 / student OUs). This is intentionally
    # WARN-on-exposure ("review this"), not FAIL — appropriateness depends on the audience.
    # Grade the WEAKEST OU: if ANY targeted policy exposes shared contacts the tenant WARNs;
    # PASS only when every targeted policy keeps them hidden.
    $pol = $AuditData.CloudIdentityPolicies
    if (-not $pol) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Cloud Identity Policy API not available (cloud-identity.policies.readonly not delegated, or API disabled)' `
            -OrgUnitPath $OrgUnitPath
    }
    $vals = @(Resolve-GooglePolicyValue -Policies $pol -Type 'directory.workspace_resource_type_visibility' -Field 'domainSharedContacts')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No directory.workspace_resource_type_visibility policy returned for this tenant' -OrgUnitPath $OrgUnitPath
    }

    $visible = @($vals | Where-Object { $_ -eq $true })
    $note = "domainSharedContacts: $((@($vals | ForEach-Object { "$_" }) | Select-Object -Unique) -join ', ') ($($visible.Count) of $($vals.Count) targeted policies)"
    if ($visible.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "Domain shared contacts visible in the directory — review whether this exposure is appropriate ($($visible.Count) of $($vals.Count) targeted policies)" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'Domain shared contacts visible in the global directory expose contact information; review whether this is appropriate for the audience (e.g. K-12 / student OUs)'; ExposingPolicies = $visible.Count; TargetedPolicies = $vals.Count; DomainSharedContacts = $note }
    }
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "Domain shared contacts not exposed in the directory in all $($vals.Count) targeted policies" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ DomainSharedContacts = $note }
}

# ── ADMIN-009: User Profile Visibility ───────────────────────────────────
function Test-FortificationADMIN009 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # GWS-1: directory.workspace_resource_type_visibility { googleGroups=bool }. This is the
    # closest directory-visibility signal this policy type exposes — when true, groups are
    # visible in the global directory. Intentionally WARN-on-exposure ("review this"), not FAIL.
    # Grade the WEAKEST OU: if ANY targeted policy makes groups visible the tenant WARNs; PASS
    # only when every targeted policy keeps them hidden.
    $pol = $AuditData.CloudIdentityPolicies
    if (-not $pol) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Cloud Identity Policy API not available (cloud-identity.policies.readonly not delegated, or API disabled)' `
            -OrgUnitPath $OrgUnitPath
    }
    $vals = @(Resolve-GooglePolicyValue -Policies $pol -Type 'directory.workspace_resource_type_visibility' -Field 'googleGroups')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No directory.workspace_resource_type_visibility policy returned for this tenant' -OrgUnitPath $OrgUnitPath
    }

    $visible = @($vals | Where-Object { $_ -eq $true })
    $note = "googleGroups: $((@($vals | ForEach-Object { "$_" }) | Select-Object -Unique) -join ', ') ($($visible.Count) of $($vals.Count) targeted policies)"
    $detail = 'This policy type (directory.workspace_resource_type_visibility) exposes googleGroups + domainSharedContacts only; profile-level visibility may also warrant manual review in Admin Console > Directory > Directory settings > Profile sharing'
    if ($visible.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "Groups visible in the global directory — review whether this exposure is appropriate ($($visible.Count) of $($vals.Count) targeted policies)" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = $detail; ExposingPolicies = $visible.Count; TargetedPolicies = $vals.Count; GoogleGroups = $note }
    }
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "Groups not exposed in the global directory in all $($vals.Count) targeted policies" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = $detail; GoogleGroups = $note }
}

# ── ADMIN-010: Groups Settings and External Membership ───────────────────
function Test-FortificationADMIN010 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # GWS-1: groups_for_business.groups_sharing { ownersCanAllowExternalMembers=bool }.
    # When true, group owners may add members outside the organization — an external data
    # exposure surface. Grade the WEAKEST OU: if ANY targeted policy allows external members
    # the tenant FAILs; PASS only when every targeted policy disallows it.
    $pol = $AuditData.CloudIdentityPolicies
    if (-not $pol) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Cloud Identity Policy API not available (cloud-identity.policies.readonly not delegated, or API disabled)' `
            -OrgUnitPath $OrgUnitPath
    }
    $vals = @(Resolve-GooglePolicyValue -Policies $pol -Type 'groups_for_business.groups_sharing' -Field 'ownersCanAllowExternalMembers')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No groups_for_business.groups_sharing policy returned for this tenant' -OrgUnitPath $OrgUnitPath
    }

    $allowed = @($vals | Where-Object { $_ -eq $true })
    if ($allowed.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue "Group owners can allow external members in $($allowed.Count) of $($vals.Count) targeted policies — external membership exposes organizational data" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'Restrict in Admin Console > Apps > Groups for Business > Sharing settings'; AllowingPolicies = $allowed.Count; TargetedPolicies = $vals.Count }
    }
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "External members disallowed in all $($vals.Count) targeted policies" `
        -OrgUnitPath $OrgUnitPath
}

# ── ADMIN-011: Group Creation Restrictions ───────────────────────────────
function Test-FortificationADMIN011 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # GWS-1: groups_for_business.groups_sharing { createGroupsAccessLevel=enum }.
    # Open creation (anyone / any user in domain) is a weaker posture than admin-restricted
    # creation, because it lets unmanaged sharing channels proliferate. The Cloud Identity
    # Policy API documents a small enum set, but the exact spelling has varied, so we match
    # known OPEN and known ADMIN-RESTRICTED values case-insensitively and treat anything we
    # don't recognise as WARN (never PASS on an unknown enum). Grade the WEAKEST (most-open)
    # OU: if any targeted policy is open the tenant FAILs; an unrecognised value WARNs.
    $pol = $AuditData.CloudIdentityPolicies
    if (-not $pol) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Cloud Identity Policy API not available (cloud-identity.policies.readonly not delegated, or API disabled)' `
            -OrgUnitPath $OrgUnitPath
    }
    $vals = @(Resolve-GooglePolicyValue -Policies $pol -Type 'groups_for_business.groups_sharing' -Field 'createGroupsAccessLevel')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No groups_for_business.groups_sharing policy returned for this tenant' -OrgUnitPath $OrgUnitPath
    }

    $levels  = @($vals | ForEach-Object { "$_" })
    $note    = "Create-group access level: $((@($levels) | Select-Object -Unique) -join ', ') (across $($levels.Count) targeted policy/policies)"
    # Known OPEN spellings (any user / anyone in domain) — weakest posture.
    $openRe  = '(?i)^(ANYONE_CAN_CREATE|ALL|ANYONE|EVERYONE|USERS_IN_DOMAIN|DOMAIN_USERS)$'
    # Known ADMIN-RESTRICTED spellings — secure posture.
    $adminRe = '(?i)^(ADMIN_ONLY|ADMINS_ONLY|ADMIN)$'
    $open    = @($levels | Where-Object { $_ -match $openRe })
    $admin   = @($levels | Where-Object { $_ -match $adminRe })
    $unknown = @($levels | Where-Object { $_ -notmatch $openRe -and $_ -notmatch $adminRe })

    if ($unknown.Count -gt 0) {
        # Never PASS/FAIL on an enum value we don't recognise — surface it for manual review.
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "Unrecognized group-creation access level — verify manually whether creation is admin-restricted — $note" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'Enum spelling not recognized; the known OPEN/ADMIN values are best-effort guesses (ANYONE_CAN_CREATE/ALL/ANYONE/EVERYONE/USERS_IN_DOMAIN/DOMAIN_USERS vs ADMIN_ONLY/ADMINS_ONLY/ADMIN)' }
    }
    if ($open.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue "Group creation is open (non-admin) in $($open.Count) of $($levels.Count) targeted policy/policies — restrict to admins — $note" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'Unrestricted group creation can lead to unmanaged data sharing channels; OPEN enum spellings (ANYONE_CAN_CREATE/ALL/ANYONE/EVERYONE/USERS_IN_DOMAIN/DOMAIN_USERS) are best-effort guesses' }
    }
    # All targeted OUs restrict creation to admins.
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "Group creation restricted to admins in all $($admin.Count) targeted policy/policies — $note" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'ADMIN-restricted enum spellings (ADMIN_ONLY/ADMINS_ONLY/ADMIN) are best-effort guesses' }
}

# ── ADMIN-012: Groups for Business Settings ──────────────────────────────
function Test-FortificationADMIN012 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # GWS-1: groups_for_business.service_status { serviceState=enum(ENABLED/DISABLED) }.
    # The Cloud Identity Policy API exposes whether Groups for Business is turned on, but NOT
    # the granular external-posting / member-visibility sub-settings. So we can only conclude
    # at the service level: DISABLED removes the Groups-for-Business sharing surface entirely
    # (secure -> PASS). When ENABLED we cannot see the granular sharing config, so we WARN and
    # point to manual review rather than inventing a PASS. Grade the WEAKEST (most-enabled) OU.
    $pol = $AuditData.CloudIdentityPolicies
    if (-not $pol) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Cloud Identity Policy API not available (cloud-identity.policies.readonly not delegated, or API disabled)' `
            -OrgUnitPath $OrgUnitPath
    }
    $vals = @(Resolve-GooglePolicyValue -Policies $pol -Type 'groups_for_business.service_status' -Field 'serviceState')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No groups_for_business.service_status policy returned for this tenant' -OrgUnitPath $OrgUnitPath
    }

    $states   = @($vals | ForEach-Object { "$_" })
    $note     = "Service state: $((@($states) | Select-Object -Unique) -join ', ') (across $($states.Count) targeted policy/policies)"
    $enabled  = @($states | Where-Object { $_ -match '(?i)^ENABLED$' })
    $disabled = @($states | Where-Object { $_ -match '(?i)^DISABLED$' })
    $unknown  = @($states | Where-Object { $_ -notmatch '(?i)^(ENABLED|DISABLED)$' })

    if ($unknown.Count -gt 0) {
        # Never PASS on an enum value we don't recognise.
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "Unrecognized Groups for Business service state — verify granular sharing settings manually — $note" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'Granular external-posting/visibility settings are not exposed via the Cloud Identity Policy API' }
    }
    if ($enabled.Count -gt 0) {
        # Service is on somewhere; granular posting/sharing config is not in the policy API.
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "Groups for Business enabled in $($enabled.Count) of $($states.Count) targeted policy/policies — verify external posting/sharing in Admin Console > Apps > Groups for Business > Sharing settings — $note" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'Granular external-posting/visibility settings are not exposed via the Cloud Identity Policy API' }
    }
    # All targeted OUs have the service disabled -> no Groups-for-Business sharing surface.
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "Groups for Business disabled in all $($disabled.Count) targeted policy/policies — no group sharing surface — $note" `
        -OrgUnitPath $OrgUnitPath
}

# ── ADMIN-013: Super Admin Count ─────────────────────────────────────────
function Test-FortificationADMIN013 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $na = Get-NotAssessedFinding -CheckDefinition $CheckDefinition -ErrorMap $AuditData.Errors `
        -SourceKey 'Users' -Subject 'user inventory'
    if ($na) { return $na }

    if (-not $AuditData.Users) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'User data not available' -OrgUnitPath $OrgUnitPath
    }

    $superAdmins = @($AuditData.Users | Where-Object { $_.isAdmin -eq $true -and -not $_.suspended })
    $count = $superAdmins.Count

    $status = if ($count -ge 2 -and $count -le 4) { 'PASS' }
              elseif ($count -eq 1) { 'FAIL' }
              elseif ($count -eq 0) { 'FAIL' }
              else { 'WARN' }

    $currentValue = switch ($true) {
        ($count -eq 0) { 'No active super admin accounts found - critical governance gap' }
        ($count -eq 1) { "Only 1 super admin - single point of failure. Recommended: 2-4" }
        ($count -ge 2 -and $count -le 4) { "$count super admin(s) - within recommended range of 2-4" }
        default { "$count super admin(s) - exceeds recommended maximum of 4. Review and reduce" }
    }

    $adminEmails = @($superAdmins | ForEach-Object { $_.primaryEmail })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath `
        -Details @{ SuperAdminCount = $count; SuperAdmins = $adminEmails; RecommendedRange = '2-4' }
}

# ── Assured Controls helper: read a field that the Policy API may return in camelCase
# (live JSON) or snake_case (as the docs spell it). Returns @() when absent so the
# caller SKIPs honestly rather than inventing a result. ────────────────────────────
function Get-AssuredControlsFieldValue {
    [CmdletBinding()]
    param(
        $Policies,
        [Parameter(Mandatory)][string]$Type,
        [Parameter(Mandatory)][string]$CamelField,
        [Parameter(Mandatory)][string]$SnakeField
    )
    $objs = @(Resolve-GooglePolicyValue -Policies $Policies -Type $Type)
    if ($objs.Count -eq 0) { return @() }
    $out = foreach ($o in $objs) {
        if ($null -eq $o) { continue }
        $names = $o.PSObject.Properties.Name
        if ($names -contains $CamelField)      { $o.$CamelField }
        elseif ($names -contains $SnakeField)  { $o.$SnakeField }
    }
    return @($out)
}

# ── ADMIN-014: Assured Controls - Access Approvals Enabled (GWS.ASSUREDCONTROLS.1.1v1)
function Test-FortificationADMIN014 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # Policy API: access_approval.axa_user_scoping { requiresCustomerApproval=bool }.
    # Secure = approval required everywhere. Grade WEAKEST OU: WARN (SHOULD) if any targeted
    # policy does not require approval. SKIP when the type/field is absent (Assured Controls
    # not licensed/configured, or API doesn't surface it) — never PASS on uncollectable data.
    $pol = $AuditData.CloudIdentityPolicies
    if (-not $pol) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Cloud Identity Policy API not available (cloud-identity.policies.readonly not delegated, or API disabled)' `
            -OrgUnitPath $OrgUnitPath
    }
    $vals = @(Get-AssuredControlsFieldValue -Policies $pol -Type 'access_approval.axa_user_scoping' `
        -CamelField 'requiresCustomerApproval' -SnakeField 'requires_customer_approval')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Access Approvals setting not exposed for this tenant (Assured Controls may not be licensed/configured). Not Assessed — verify in Admin Console > Data > Compliance > Access Management / Access Approvals' `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'GWS.ASSUREDCONTROLS.1.1 — access_approval.axa_user_scoping not returned by the Policy API for this tenant' }
    }
    $notRequired = @($vals | Where-Object { $_ -ne $true })
    if ($notRequired.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "Access Approvals not required in $($notRequired.Count) of $($vals.Count) targeted policy/policies — SCuBA recommends requiring approval before Google staff access data" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'GWS.ASSUREDCONTROLS.1.1 recommends enabling Access Approvals' }
    }
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "Access Approvals required in all $($vals.Count) targeted policy/policies" `
        -OrgUnitPath $OrgUnitPath
}

# ── ADMIN-015: Assured Controls - Support Access Restricted to U.S. Staff
# (GWS.ASSUREDCONTROLS.1.2v1) ──────────────────────────────────────────────────────
function Test-FortificationADMIN015 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # Policy API: access_management.user_scoping { allowedAudience=enum }. Documented enums:
    # US_GOOGLE_STAFF / CJIS_IRS_1075_GOOGLE_STAFF (US-restricted, secure), EU_GOOGLE_STAFF
    # (non-US -> WARN), PREFERENCE_UNSPECIFIED (not configured -> WARN). Grade WEAKEST OU.
    # Unknown enum -> WARN; absent -> SKIP (never PASS on uncollectable data).
    $pol = $AuditData.CloudIdentityPolicies
    if (-not $pol) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Cloud Identity Policy API not available (cloud-identity.policies.readonly not delegated, or API disabled)' `
            -OrgUnitPath $OrgUnitPath
    }
    $vals = @(Get-AssuredControlsFieldValue -Policies $pol -Type 'access_management.user_scoping' `
        -CamelField 'allowedAudience' -SnakeField 'allowed_audience')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Support-access audience setting not exposed for this tenant (Assured Controls may not be licensed/configured). Not Assessed — verify in Admin Console > Data > Compliance > Access Management' `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'GWS.ASSUREDCONTROLS.1.2 — access_management.user_scoping not returned by the Policy API for this tenant' }
    }
    $auds   = @($vals | ForEach-Object { "$_" })
    $note   = "Allowed support audience: $((@($auds) | Select-Object -Unique) -join ', ') (across $($auds.Count) targeted policy/policies)"
    $usOnly = @($auds | Where-Object { $_ -match '(?i)^(US_GOOGLE_STAFF|CJIS_IRS_1075_GOOGLE_STAFF)$' })
    $nonUs  = @($auds | Where-Object { $_ -match '(?i)^EU_GOOGLE_STAFF$' })
    $unset  = @($auds | Where-Object { $_ -match '(?i)^(PREFERENCE_UNSPECIFIED)?$' })
    $known  = @($auds | Where-Object { $_ -match '(?i)^(US_GOOGLE_STAFF|CJIS_IRS_1075_GOOGLE_STAFF|EU_GOOGLE_STAFF|PREFERENCE_UNSPECIFIED)$' })

    if ($known.Count -ne $auds.Count) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "Unrecognized support-audience value — verify manually that support is restricted to U.S. staff — $note" `
            -OrgUnitPath $OrgUnitPath
    }
    if ($nonUs.Count -gt 0 -or $unset.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "Support access is not restricted to U.S. Google staff in $((@($nonUs) + @($unset)).Count) of $($auds.Count) targeted policy/policies — $note" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'GWS.ASSUREDCONTROLS.1.2 recommends restricting support access to U.S. Google staff' }
    }
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "Support access restricted to U.S. Google staff in all $($usOnly.Count) targeted policy/policies — $note" `
        -OrgUnitPath $OrgUnitPath
}

# ── ADMIN-016: Assured Controls - Multi-Region Data Processing Disabled
# (GWS.ASSUREDCONTROLS.2.1v1) ──────────────────────────────────────────────────────
function Test-FortificationADMIN016 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # Policy API: data_regions.data_processing_region { limitToStorageRegion=bool }. SCuBA:
    # processing should be limited to the storage region (multi-region disabled) -> true is
    # secure. Grade WEAKEST OU: WARN (SHOULD) if any targeted policy does not limit processing.
    # Absent -> SKIP (Data Regions / Assured Controls not licensed) — never PASS on missing data.
    $pol = $AuditData.CloudIdentityPolicies
    if (-not $pol) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Cloud Identity Policy API not available (cloud-identity.policies.readonly not delegated, or API disabled)' `
            -OrgUnitPath $OrgUnitPath
    }
    $vals = @(Get-AssuredControlsFieldValue -Policies $pol -Type 'data_regions.data_processing_region' `
        -CamelField 'limitToStorageRegion' -SnakeField 'limit_to_storage_region')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Data-processing-region setting not exposed for this tenant (Data Regions / Assured Controls may not be licensed/configured). Not Assessed — verify in Admin Console > Data > Compliance > Data regions' `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'GWS.ASSUREDCONTROLS.2.1 — data_regions.data_processing_region not returned by the Policy API for this tenant' }
    }
    $notLimited = @($vals | Where-Object { $_ -ne $true })
    if ($notLimited.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "Data processing is not limited to the storage region in $($notLimited.Count) of $($vals.Count) targeted policy/policies — multi-region processing remains possible" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'GWS.ASSUREDCONTROLS.2.1 recommends limiting data processing to the chosen storage region across all products' }
    }
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "Data processing limited to the storage region in all $($vals.Count) targeted policy/policies" `
        -OrgUnitPath $OrgUnitPath
}