Private/Audit/Invoke-OAuthSecurityChecks.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-OAuthSecurityChecks { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$AuditData, [string]$OrgUnitPath = '/' ) $checkDefs = Get-AuditCategoryDefinitions -Category 'OAuthSecurityChecks' $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) } # ── OAUTH-001: OAuth App Whitelist/Blocklist ───────────────────────────── function Test-FortificationOAUTH001 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') # OAuth app allowlist/blocklist is managed in Admin Console and not fully exposed via API return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'OAuth app allowlist/blocklist configuration not available via API. Verify in Admin Console > Security > API controls > App access control' ` -OrgUnitPath $OrgUnitPath ` -Details @{ Note = 'Ensure an allowlist is configured with only approved applications and a blocklist exists for known-bad apps' } } # ── OAUTH-002: Installed OAuth Apps Inventory ──────────────────────────── function Test-FortificationOAUTH002 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') if (-not $AuditData.OAuthApps) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'OAuth token events not available. Collect token activity data from Reports API to enumerate installed apps' ` -OrgUnitPath $OrgUnitPath } # Enumerate unique apps from token events $uniqueApps = @{} foreach ($event in $AuditData.OAuthApps) { $appName = $event.Params.app_name if ($appName -and -not $uniqueApps.ContainsKey($appName)) { $scope = $event.Params.scope $uniqueApps[$appName] = @{ AppName = $appName Scopes = if ($scope) { @($scope -split '\s+') } else { @() } } } } $appCount = $uniqueApps.Count $status = if ($appCount -eq 0) { 'PASS' } elseif ($appCount -le 20) { 'WARN' } else { 'FAIL' } $appNames = @($uniqueApps.Keys | Sort-Object) return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status ` -CurrentValue "$appCount unique OAuth app(s) detected with token grants" ` -OrgUnitPath $OrgUnitPath ` -Details @{ AppCount = $appCount; AppNames = $appNames } } # ── OAUTH-003: OAuth Scope Analysis ────────────────────────────────────── function Test-FortificationOAUTH003 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') if (-not $AuditData.OAuthApps) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'OAuth token events not available. Collect token activity data from Reports API to analyze scopes' ` -OrgUnitPath $OrgUnitPath } $highRiskPatterns = @('gmail', 'mail.google', 'drive', 'admin', 'calendar', 'contacts', 'directory') $highRiskApps = [System.Collections.Generic.List[hashtable]]::new() $seenApps = @{} foreach ($event in $AuditData.OAuthApps) { $appName = $event.Params.app_name $scope = $event.Params.scope if (-not $appName -or $seenApps.ContainsKey($appName)) { continue } if ($scope) { $scopeLower = $scope.ToLower() $matchedScopes = @($highRiskPatterns | Where-Object { $scopeLower -match $_ }) if ($matchedScopes.Count -gt 0) { $seenApps[$appName] = $true $highRiskApps.Add(@{ AppName = $appName Scopes = $scope MatchedRisks = $matchedScopes }) } } } if ($highRiskApps.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No applications found with high-risk OAuth scopes' ` -OrgUnitPath $OrgUnitPath } $status = if ($highRiskApps.Count -gt 5) { 'FAIL' } else { 'WARN' } $appSummary = @($highRiskApps | ForEach-Object { "$($_.AppName) [$($_.MatchedRisks -join ', ')]" }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status ` -CurrentValue "$($highRiskApps.Count) app(s) with high-risk scopes (gmail, drive, admin, calendar)" ` -OrgUnitPath $OrgUnitPath ` -Details @{ HighRiskApps = $appSummary; TotalHighRisk = $highRiskApps.Count } } # ── OAUTH-004: OAuth App Risk Scoring ──────────────────────────────────── function Test-FortificationOAUTH004 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') if (-not $AuditData.OAuthApps) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'OAuth token events not available. Collect token activity data from Reports API to perform risk scoring' ` -OrgUnitPath $OrgUnitPath } # Score apps based on the breadth and sensitivity of granted scopes $scopeWeights = @{ 'admin' = 10 'gmail' = 8 'mail' = 8 'drive' = 7 'calendar' = 5 'contacts' = 4 'directory' = 6 'groups' = 5 'user' = 3 } $appScores = @{} foreach ($event in $AuditData.OAuthApps) { $appName = $event.Params.app_name $scope = $event.Params.scope if (-not $appName -or $appScores.ContainsKey($appName)) { continue } $score = 0 if ($scope) { $scopeLower = $scope.ToLower() foreach ($key in $scopeWeights.Keys) { if ($scopeLower -match $key) { $score += $scopeWeights[$key] } } } $appScores[$appName] = $score } $highRisk = @($appScores.GetEnumerator() | Where-Object { $_.Value -ge 10 } | Sort-Object Value -Descending) $mediumRisk = @($appScores.GetEnumerator() | Where-Object { $_.Value -ge 5 -and $_.Value -lt 10 }) $status = if ($highRisk.Count -gt 3) { 'FAIL' } elseif ($highRisk.Count -gt 0 -or $mediumRisk.Count -gt 5) { 'WARN' } else { 'PASS' } $currentValue = "$($highRisk.Count) high-risk and $($mediumRisk.Count) medium-risk app(s) based on scope analysis" $topApps = @($highRisk | Select-Object -First 10 | ForEach-Object { "$($_.Key) (score: $($_.Value))" }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status ` -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath ` -Details @{ HighRiskCount = $highRisk.Count; MediumRiskCount = $mediumRisk.Count; TopRiskApps = $topApps } } # ── OAUTH-005: Unverified App Access Policy ────────────────────────────── function Test-FortificationOAUTH005 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'Unverified app access policy not available via API. Verify in Admin Console > Security > API controls > App access control > Settings that unverified apps are blocked' ` -OrgUnitPath $OrgUnitPath ` -Details @{ Note = 'Unverified apps have not passed Google OAuth verification and may pose security risks' } } # ── OAUTH-006: API Access Control ──────────────────────────────────────── function Test-FortificationOAUTH006 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'API access control settings not available via API. Verify in Admin Console > Security > API controls > Manage Google Services that API access is restricted' ` -OrgUnitPath $OrgUnitPath ` -Details @{ Note = 'API access should be restricted to trusted applications only' } } # ── OAUTH-007: Marketplace App Installation Restrictions ───────────────── function Test-FortificationOAUTH007 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'Marketplace installation restrictions not available via API. Verify in Admin Console > Apps > Marketplace apps > Settings that installation is restricted to approved apps' ` -OrgUnitPath $OrgUnitPath ` -Details @{ Note = 'Unrestricted Marketplace app installation allows users to grant third-party apps access to organizational data' } } # ── OAUTH-008: Domain-Wide Delegation Grants Audit ─────────────────────── function Test-FortificationOAUTH008 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') if (-not $AuditData.DomainWideDelegation) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'Domain-wide delegation data not available. Verify in Admin Console > Security > API controls > Domain-wide delegation' ` -OrgUnitPath $OrgUnitPath } $grants = @($AuditData.DomainWideDelegation) if ($grants.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No domain-wide delegation grants configured' ` -OrgUnitPath $OrgUnitPath } # Analyze scope breadth of each grant $broadGrants = [System.Collections.Generic.List[string]]::new() $sensitiveScopes = @('gmail', 'drive', 'admin', 'calendar', 'directory') foreach ($grant in $grants) { $clientId = $grant.clientId ?? $grant.ClientId ?? 'Unknown' $scopes = $grant.scopes ?? $grant.Scopes ?? @() $scopeStr = ($scopes -join ' ').ToLower() foreach ($sensitive in $sensitiveScopes) { if ($scopeStr -match $sensitive) { $broadGrants.Add("$clientId (scopes include: $sensitive)") break } } } $status = if ($broadGrants.Count -gt 0) { 'FAIL' } elseif ($grants.Count -gt 5) { 'WARN' } else { 'PASS' } return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status ` -CurrentValue "$($grants.Count) domain-wide delegation grant(s) found; $($broadGrants.Count) with sensitive scopes" ` -OrgUnitPath $OrgUnitPath ` -Details @{ TotalGrants = $grants.Count; SensitiveGrants = @($broadGrants); } } # ── OAUTH-009: Service Account Key Enumeration ─────────────────────────── function Test-FortificationOAUTH009 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') # Service account keys are managed in Google Cloud Console, not directly in Workspace Admin SDK return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'Service account key inventory requires Google Cloud Console access. Verify in Cloud Console > IAM > Service accounts that all keys are rotated and unused keys removed' ` -OrgUnitPath $OrgUnitPath ` -Details @{ Note = 'Service account keys are managed in Google Cloud Console and should be rotated within 90 days' } } # ── OAUTH-010: Connected Apps With Sensitive Scopes ────────────────────── function Test-FortificationOAUTH010 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition, [string]$OrgUnitPath = '/') if (-not $AuditData.OAuthApps) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'OAuth token events not available. Collect token activity data from Reports API to identify apps with sensitive scopes' ` -OrgUnitPath $OrgUnitPath } $sensitiveScopes = @{ 'Drive' = @('drive', 'drive.file', 'drive.readonly') 'Gmail' = @('gmail', 'mail.google') 'Calendar' = @('calendar') } $appsByService = @{ Drive = [System.Collections.Generic.List[string]]::new(); Gmail = [System.Collections.Generic.List[string]]::new(); Calendar = [System.Collections.Generic.List[string]]::new() } $seenApps = @{} foreach ($event in $AuditData.OAuthApps) { $appName = $event.Params.app_name $scope = $event.Params.scope if (-not $appName -or -not $scope -or $seenApps.ContainsKey($appName)) { continue } $seenApps[$appName] = $true $scopeLower = $scope.ToLower() foreach ($service in $sensitiveScopes.Keys) { foreach ($pattern in $sensitiveScopes[$service]) { if ($scopeLower -match $pattern) { $appsByService[$service].Add($appName) break } } } } $totalSensitive = ($appsByService.Values | ForEach-Object { $_.Count } | Measure-Object -Sum).Sum $status = if ($totalSensitive -gt 10) { 'FAIL' } elseif ($totalSensitive -gt 5) { 'WARN' } else { 'PASS' } $breakdown = @($appsByService.GetEnumerator() | Where-Object { $_.Value.Count -gt 0 } | ForEach-Object { "$($_.Key): $($_.Value.Count) app(s)" }) return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status ` -CurrentValue "$totalSensitive app(s) with Drive, Gmail, or Calendar access ($($breakdown -join '; '))" ` -OrgUnitPath $OrgUnitPath ` -Details @{ DriveApps = @($appsByService.Drive) GmailApps = @($appsByService.Gmail) CalendarApps = @($appsByService.Calendar) TotalCount = $totalSensitive } } |