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 = '/')

    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 = '/')

    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 = '/')

    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 = '/')

    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 = '/')

    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 = '/')

    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 = '/')

    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 = '/')

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Directory sharing settings not available via API. Verify in Admin Console > Directory > Directory settings > Sharing settings that contact sharing is restricted' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'External directory sharing exposes organizational structure and contact information to external parties' }
}

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

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'User profile visibility settings not available via API. Verify in Admin Console > Directory > Directory settings > Profile sharing that visibility is appropriately restricted' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'Profile information can aid reconnaissance; restrict visibility to internal users' }
}

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

    # Groups data may be available if collected
    if ($AuditData.Groups) {
        $groups = @($AuditData.Groups)
        $externalGroups = @($groups | Where-Object {
            $_.allowExternalMembers -eq $true -or $_.whoCanJoin -eq 'ANYONE_CAN_JOIN'
        })

        if ($externalGroups.Count -gt 0) {
            $groupEmails = @($externalGroups | ForEach-Object { $_.email } | Select-Object -First 20)
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
                -CurrentValue "$($externalGroups.Count) of $($groups.Count) group(s) allow external members" `
                -OrgUnitPath $OrgUnitPath `
                -Details @{ ExternalGroups = $groupEmails; TotalGroups = $groups.Count }
        }

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue "None of the $($groups.Count) group(s) allow external members" `
            -OrgUnitPath $OrgUnitPath
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Groups data not available. Verify in Admin Console > Apps > Groups for Business > Sharing settings that external membership is restricted' `
        -OrgUnitPath $OrgUnitPath
}

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

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Group creation restrictions not available via API. Verify in Admin Console > Apps > Groups for Business > Sharing settings that group creation is restricted to admins' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'Unrestricted group creation can lead to unmanaged data sharing channels' }
}

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

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Groups for Business settings not available via API. Verify in Admin Console > Apps > Groups for Business > Sharing settings that external posting and access are restricted' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'Review settings for external posting, member visibility, and group content sharing' }
}

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

    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' }
}