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

        [string]$OrgUnitPath = '/'
    )

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

# ── LOG-001: Audit Log Retention Settings ────────────────────────────────
function Test-FortificationLOG001 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # Audit log retention is determined by Workspace edition and BigQuery export configuration
    # Enterprise editions retain admin audit logs for 6 months, other logs vary
    # BigQuery export is recommended for long-term retention
    $edition = $AuditData.Tenant.edition ?? $AuditData.Tenant.Edition ?? $null

    if ($edition) {
        $status = switch -Wildcard ($edition) {
            '*enterprise*' { 'PASS' }
            '*business*'   { 'WARN' }
            default        { 'WARN' }
        }
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue "Workspace edition: $edition. Log retention varies by edition. Verify BigQuery export for long-term retention" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Edition = $edition; Note = 'Enterprise editions retain admin logs for 6 months. Configure BigQuery export for longer retention' }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Audit log retention settings not determinable via API. Verify in Admin Console > Reporting > Audit and configure BigQuery export for long-term retention' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'Log retention varies by Workspace edition. BigQuery export recommended for compliance' }
}

# ── LOG-002: Alert Center Rules Inventory ────────────────────────────────
function Test-FortificationLOG002 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    if (-not $AuditData.AlertRules) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'Alert rules data not available. Verify in Admin Console > Security > Alert center that alert rules are configured for security events' `
            -OrgUnitPath $OrgUnitPath
    }

    $rules = @($AuditData.AlertRules)
    if ($rules.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'No alert rules configured. Security events are not being monitored' `
            -OrgUnitPath $OrgUnitPath
    }

    $status = if ($rules.Count -ge 5) { 'PASS' }
              elseif ($rules.Count -ge 2) { 'WARN' }
              else { 'WARN' }

    $ruleNames = @($rules | ForEach-Object { $_.name ?? $_.Name ?? $_.displayName ?? 'Unnamed rule' })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($rules.Count) alert rule(s) configured in Alert Center" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ RuleCount = $rules.Count; RuleNames = $ruleNames }
}

# ── LOG-003: Activity Rules Coverage Analysis ────────────────────────────
function Test-FortificationLOG003 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    if (-not $AuditData.AlertRules) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'Alert rules data not available. Verify that activity rules cover login, Drive, Admin, email, and OAuth event categories' `
            -OrgUnitPath $OrgUnitPath
    }

    $rules = @($AuditData.AlertRules)

    # Analyze coverage across key security domains
    $expectedDomains = @('Login', 'Drive', 'Admin', 'Email', 'OAuth')
    $coveredDomains = [System.Collections.Generic.List[string]]::new()
    $uncoveredDomains = [System.Collections.Generic.List[string]]::new()

    foreach ($domain in $expectedDomains) {
        $domainLower = $domain.ToLower()
        $hasCoverage = $false
        foreach ($rule in $rules) {
            $ruleName = ($rule.name ?? $rule.Name ?? $rule.displayName ?? '').ToLower()
            $ruleSource = ($rule.source ?? $rule.Source ?? '').ToLower()
            if ($ruleName -match $domainLower -or $ruleSource -match $domainLower) {
                $hasCoverage = $true
                break
            }
        }
        if ($hasCoverage) { $coveredDomains.Add($domain) }
        else { $uncoveredDomains.Add($domain) }
    }

    $coverageRate = [Math]::Round(($coveredDomains.Count / $expectedDomains.Count) * 100, 0)

    $status = if ($uncoveredDomains.Count -eq 0) { 'PASS' }
              elseif ($uncoveredDomains.Count -le 2) { 'WARN' }
              else { 'FAIL' }

    $currentValue = if ($uncoveredDomains.Count -eq 0) {
        "All $($expectedDomains.Count) key security domains have alert coverage ($coverageRate%)"
    } else {
        "$coverageRate% coverage: Missing rules for $($uncoveredDomains -join ', ')"
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath `
        -Details @{
            CoveredDomains   = @($coveredDomains)
            UncoveredDomains = @($uncoveredDomains)
            CoverageRate     = $coverageRate
            TotalRules       = $rules.Count
        }
}

# ── LOG-004: Data Export Settings ────────────────────────────────────────
function Test-FortificationLOG004 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # Google Takeout settings are OU-level policies not directly available via the Admin SDK
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Google Takeout (data export) settings not available via API. Verify in Admin Console > Apps > Additional Google services > Google Takeout that data export is disabled or restricted' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'Unrestricted Takeout allows users to bulk-export organizational data including emails, Drive files, and contacts' }
}

# ── LOG-005: Admin Email Alerts Configuration ────────────────────────────
function Test-FortificationLOG005 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Admin email alert configuration not available via API. Verify in Admin Console > Security > Alert center that email notifications are configured for critical alert types' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'Email alerts should be configured for super admin changes, security setting modifications, and bulk user operations' }
}

# ── LOG-006: Reporting API Access ────────────────────────────────────────
function Test-FortificationLOG006 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/')

    # Check domain-wide delegation for Reports API scopes
    if ($AuditData.DomainWideDelegation) {
        $grants = @($AuditData.DomainWideDelegation)
        $reportsGrants = [System.Collections.Generic.List[string]]::new()

        foreach ($grant in $grants) {
            $clientId = $grant.clientId ?? $grant.ClientId ?? 'Unknown'
            $scopes = $grant.scopes ?? $grant.Scopes ?? @()
            $scopeStr = ($scopes -join ' ').ToLower()

            if ($scopeStr -match 'reports' -or $scopeStr -match 'audit') {
                $reportsGrants.Add($clientId)
            }
        }

        if ($reportsGrants.Count -gt 0) {
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
                -CurrentValue "$($reportsGrants.Count) domain-wide delegation grant(s) with Reports/Audit API access. Review for authorization" `
                -OrgUnitPath $OrgUnitPath `
                -Details @{ GrantsWithReportsAccess = @($reportsGrants) }
        }

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue 'No domain-wide delegation grants with Reports API access detected' `
            -OrgUnitPath $OrgUnitPath
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Reporting API access review requires domain-wide delegation data. Verify in Admin Console > Security > API controls > Domain-wide delegation' `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'Reports API access should be restricted to authorized service accounts only' }
}