Private/Audit/Invoke-AuthenticationChecks.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-AuthenticationChecks {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$AuditData,

        [string]$OrgUnitPath = '/'
    )

    $checkDefs = Get-AuditCategoryDefinitions -Category 'AuthenticationChecks'
    $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)
}

# ── AUTH-001: 2SV Enforcement ───────────────────────────────────────────────
function Test-FortificationAUTH001 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $users = @($AuditData.Users | Where-Object { -not $_.suspended })
    if ($users.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No active users found' -OrgUnitPath $OrgUnitPath
    }

    $enforced = @($users | Where-Object { $_.isEnforcedIn2Sv -eq $true })
    $enforcedRate = [Math]::Round(($enforced.Count / $users.Count) * 100, 1)

    $status = if ($enforcedRate -ge 95) { 'PASS' }
              elseif ($enforcedRate -ge 50) { 'WARN' }
              else { 'FAIL' }

    $currentValue = "$enforcedRate% ($($enforced.Count) of $($users.Count) active users) have 2SV enforced"

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath `
        -Details @{ EnforcedCount = $enforced.Count; TotalActive = $users.Count; Rate = $enforcedRate }
}

# ── AUTH-002: 2SV Enrollment Rate ───────────────────────────────────────────
function Test-FortificationAUTH002 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $users = @($AuditData.Users | Where-Object { -not $_.suspended })
    if ($users.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No active users found' -OrgUnitPath $OrgUnitPath
    }

    $enrolled = @($users | Where-Object { $_.isEnrolledIn2Sv -eq $true })
    $rate = [Math]::Round(($enrolled.Count / $users.Count) * 100, 1)

    $status = if ($rate -ge 95) { 'PASS' }
              elseif ($rate -ge 80) { 'WARN' }
              else { 'FAIL' }

    $currentValue = "$rate% ($($enrolled.Count) of $($users.Count) active users) enrolled in 2SV"

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath `
        -Details @{ EnrolledCount = $enrolled.Count; TotalActive = $users.Count; Rate = $rate }
}

# ── AUTH-003: 2SV Method Strength ───────────────────────────────────────────
function Test-FortificationAUTH003 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # This check evaluates the OU policy for allowed 2SV methods
    # Without direct API access to the 2SV policy settings, we infer from user data
    $users = @($AuditData.Users | Where-Object { -not $_.suspended -and $_.isEnrolledIn2Sv -eq $true })
    if ($users.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No 2SV-enrolled users found' -OrgUnitPath $OrgUnitPath
    }

    # The Admin SDK doesn't expose per-user 2SV method in the users.list response.
    # This check is reported as INFO with guidance to verify in Admin Console.
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Verify in Admin Console that security keys are the required 2SV method' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'API does not expose per-user 2SV method type. Manual verification recommended.' }
}

# ── AUTH-004: Password Minimum Length ───────────────────────────────────────
function Test-FortificationAUTH004 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # Password policy details are not fully exposed via the Directory API
    # Check OU policies if available
    $policy = $AuditData.OrgUnitPolicies[$OrgUnitPath]
    if ($policy -and $policy.passwordMinLength) {
        $minLen = [int]$policy.passwordMinLength
        $status = if ($minLen -ge 12) { 'PASS' }
                  elseif ($minLen -ge 8) { 'WARN' }
                  else { 'FAIL' }
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue "Minimum password length: $minLen characters" -OrgUnitPath $OrgUnitPath
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Password policy details not available via API. Verify minimum length of 12+ characters in Admin Console' `
        -OrgUnitPath $OrgUnitPath
}

# ── AUTH-005: Password Reuse Restriction ────────────────────────────────────
function Test-FortificationAUTH005 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $policy = $AuditData.OrgUnitPolicies[$OrgUnitPath]
    if ($policy -and $null -ne $policy.passwordReuseRestriction) {
        $status = if ($policy.passwordReuseRestriction -eq $true) { 'PASS' } else { 'FAIL' }
        $currentValue = if ($policy.passwordReuseRestriction) { 'Password reuse restricted' } else { 'Password reuse allowed' }
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Password reuse policy not available via API. Verify in Admin Console' `
        -OrgUnitPath $OrgUnitPath
}

# ── AUTH-006: Session Duration ──────────────────────────────────────────────
function Test-FortificationAUTH006 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $policy = $AuditData.OrgUnitPolicies[$OrgUnitPath]
    if ($policy -and $policy.sessionDurationHours) {
        $hours = [int]$policy.sessionDurationHours
        $status = if ($hours -le 12) { 'PASS' }
                  elseif ($hours -le 24) { 'WARN' }
                  else { 'FAIL' }
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue "Session duration: $hours hours" -OrgUnitPath $OrgUnitPath
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Session duration policy not available via API. Verify in Admin Console that sessions are limited to 12 hours or less' `
        -OrgUnitPath $OrgUnitPath
}

# ── AUTH-007: SSO Configuration ─────────────────────────────────────────────
function Test-FortificationAUTH007 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # SSO configuration is not directly exposed in the Directory API user listing
    # but we can check if the tenant has SSO-related indicators
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'SSO configuration requires manual verification. Check Admin Console > Security > SSO' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'SSO profile settings are not fully exposed via the Admin SDK Directory API' }
}

# ── AUTH-008: Less Secure Apps Access ───────────────────────────────────────
function Test-FortificationAUTH008 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # Google deprecated LSA access for most accounts in 2024
    # Check if any users still have it enabled based on OU policy
    $policy = $AuditData.OrgUnitPolicies[$OrgUnitPath]
    if ($policy -and $null -ne $policy.lessSecureApps) {
        $status = if ($policy.lessSecureApps -eq $false) { 'PASS' } else { 'FAIL' }
        $currentValue = if ($policy.lessSecureApps) { 'Less secure apps allowed' } else { 'Less secure apps blocked' }
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath
    }

    # Google removed LSA for most Workspace editions by late 2024
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue 'Less secure apps access is deprecated and disabled by Google for most Workspace editions' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'Google deprecated LSA access in 2024. Verify if legacy edition.' }
}

# ── AUTH-009: App Passwords Policy ──────────────────────────────────────────
function Test-FortificationAUTH009 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'App password policy requires manual verification. Check Admin Console > Security > 2-Step Verification settings' `
        -OrgUnitPath $OrgUnitPath
}

# ── AUTH-010: Recovery Options Configuration ────────────────────────────────
function Test-FortificationAUTH010 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $superAdmins = @($AuditData.Users | Where-Object { $_.isAdmin -eq $true -and -not $_.suspended })
    if ($superAdmins.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No super admins found' -OrgUnitPath $OrgUnitPath
    }

    $adminsWithRecovery = @($superAdmins | Where-Object {
        $_.recoveryEmail -or $_.recoveryPhone
    })

    if ($adminsWithRecovery.Count -gt 0) {
        $adminEmails = @($adminsWithRecovery | ForEach-Object { $_.primaryEmail })
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue "$($adminsWithRecovery.Count) super admin(s) have personal recovery options configured" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ SuperAdminsWithRecovery = $adminEmails }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue 'No super admins have personal recovery options configured' `
        -OrgUnitPath $OrgUnitPath
}

# ── AUTH-011: Login Challenge Settings ──────────────────────────────────────
function Test-FortificationAUTH011 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Login challenge settings require manual verification. Check Admin Console > Security > Login challenges' `
        -OrgUnitPath $OrgUnitPath
}

# ── AUTH-012: Super Admin 2SV Enrollment ────────────────────────────────────
function Test-FortificationAUTH012 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $superAdmins = @($AuditData.Users | Where-Object { $_.isAdmin -eq $true -and -not $_.suspended })
    if ($superAdmins.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No super admins found' -OrgUnitPath $OrgUnitPath
    }

    $enrolled = @($superAdmins | Where-Object { $_.isEnrolledIn2Sv -eq $true })
    $notEnrolled = @($superAdmins | Where-Object { $_.isEnrolledIn2Sv -ne $true })

    if ($notEnrolled.Count -gt 0) {
        $adminEmails = @($notEnrolled | ForEach-Object { $_.primaryEmail })
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue "$($notEnrolled.Count) of $($superAdmins.Count) super admin(s) not enrolled in 2SV" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ NotEnrolled = $adminEmails; TotalSuperAdmins = $superAdmins.Count }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "All $($superAdmins.Count) super admins enrolled in 2SV" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ TotalSuperAdmins = $superAdmins.Count }
}

# ── AUTH-013: Stale Super Admin Accounts ────────────────────────────────────
function Test-FortificationAUTH013 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    $superAdmins = @($AuditData.Users | Where-Object { $_.isAdmin -eq $true -and -not $_.suspended })
    if ($superAdmins.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No super admins found' -OrgUnitPath $OrgUnitPath
    }

    $staleDays = 90
    $now = [datetime]::UtcNow
    $staleAdmins = [System.Collections.Generic.List[string]]::new()

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

    if ($staleAdmins.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue "$($staleAdmins.Count) super admin(s) inactive for more than $staleDays days" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ StaleAdmins = @($staleAdmins); ThresholdDays = $staleDays }
    }

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