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

    # GWS-1: cloud_sharing_options.cloud_data_sharing { sharingOptions=enum(DISABLED…) }.
    # This governs whether organizational data may be shared outside the tenant — the
    # closest policy-backed signal for "users can bulk-export org data". DISABLED is secure.
    # (Caveat: this is the cloud-data-sharing control, not the dedicated Google Takeout
    # service toggle, which the Cloud Identity Policy API does not expose separately.)
    $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 'cloud_sharing_options.cloud_data_sharing' -Field 'sharingOptions')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No cloud-data-sharing policy returned for this tenant' -OrgUnitPath $OrgUnitPath
    }

    $note = "Cloud data sharing option(s): $((@($vals) | Select-Object -Unique) -join ', ') (across $($vals.Count) targeted policy/policies)"
    $enabled = @($vals | Where-Object { "$_" -match '(?i)ENABLED' })
    $disabled = @($vals | Where-Object { "$_" -match '(?i)DISABLED' })

    if ($enabled.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue "Cloud data sharing outside the organization is enabled — $note" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ Note = 'Allows organizational data to be shared/exported outside the domain' }
    }
    if ($disabled.Count -eq $vals.Count) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue "Cloud data sharing outside the organization is disabled — $note" `
            -OrgUnitPath $OrgUnitPath
    }
    # Unrecognized enum value(s) — never PASS on unknown.
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue "Cloud data sharing option not recognized as secure — $note" `
        -OrgUnitPath $OrgUnitPath
}

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

    # GWS-1: rule.system_defined_alerts { displayName=str; action={alertCenterAction}; state=enum(ACTIVE…) }.
    # System-defined alert rules are what surface critical admin/security events to Alert Center
    # (and to email recipients). Count the ACTIVE ones. PASS when ≥1 is active.
    $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 'rule.system_defined_alerts')
    if ($vals.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No system-defined alert policy returned for this tenant' -OrgUnitPath $OrgUnitPath
    }

    $active = @($vals | Where-Object { "$($_.state)".Trim() -match '(?i)^ACTIVE$' })
    if ($active.Count -gt 0) {
        $names = @($active | ForEach-Object { $_.displayName } | Where-Object { $_ })
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue "$($active.Count) of $($vals.Count) system-defined alert rule(s) active" `
            -OrgUnitPath $OrgUnitPath `
            -Details @{ ActiveCount = $active.Count; TotalRules = $vals.Count; ActiveRules = $names }
    }

    # No active alert rules. Severity is Medium -> WARN (would FAIL only if Critical).
    $status = if ("$($CheckDefinition.severity)" -match '(?i)critical') { 'FAIL' } else { 'WARN' }
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "No system-defined alert rules are active ($($vals.Count) defined, none ACTIVE)" `
        -OrgUnitPath $OrgUnitPath `
        -Details @{ Note = 'Enable system-defined alert rules so critical admin/security events generate Alert Center notifications' }
}

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