Public/Invoke-Fortification.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-Fortification { [CmdletBinding()] param( [string]$ServiceAccountKeyPath, [string]$AdminEmail, [ValidateSet('All', 'Authentication', 'EmailSecurity', 'DriveSecurity', 'OAuthSecurity', 'AdminManagement', 'Collaboration', 'DeviceManagement', 'LoggingAlerting')] [string[]]$Categories = @('All'), [switch]$IncludeChildOUs, [string]$TargetOU = '/', [int]$UserSampleSize = 500, [string]$OutputDirectory, [switch]$NoReports, [switch]$NoDelta, [switch]$Quiet, [Alias('RuntimeConfig')] [string]$ConfigPath, [Alias('MissionConfig')] [string]$ConfigFile ) # --- Resolve mission config (guerrilla-config.json) --- if ($ConfigFile) { $missionCfg = Read-MissionConfig -Path $ConfigFile $vaultName = $missionCfg.VaultName # Resolve GWS credentials from vault $gwsRef = $missionCfg.Config.credentials.references.googleWorkspace if ($gwsRef) { if (-not $PSBoundParameters.ContainsKey('ServiceAccountKeyPath')) { try { $saJson = Get-GuerrillaCredential -VaultKey $gwsRef.vaultKey -VaultName $vaultName $tempSaPath = Join-Path ([System.IO.Path]::GetTempPath()) "guerrilla-sa-$([guid]::NewGuid().ToString('N').Substring(0,8)).json" $saJson | Set-Content -Path $tempSaPath -Encoding UTF8 $ServiceAccountKeyPath = $tempSaPath } catch { Write-Warning "Failed to resolve GWS service account from vault: $_" } } if (-not $PSBoundParameters.ContainsKey('AdminEmail')) { try { $AdminEmail = Get-GuerrillaCredential -VaultKey "$($gwsRef.vaultKey)_ADMIN_EMAIL" -VaultName $vaultName } catch { Write-Verbose "AdminEmail not found in vault — will fall back to config.json or parameter." } } } # Apply targetOU from mission config if (-not $PSBoundParameters.ContainsKey('TargetOU')) { $gwsAudit = $missionCfg.EnabledEnvironments['googleWorkspace'] if ($gwsAudit -and $gwsAudit.audit -and $gwsAudit.audit.targetOU) { $TargetOU = $gwsAudit.audit.targetOU } } # Apply categories from mission config if (-not $PSBoundParameters.ContainsKey('Categories')) { $gwsEnv = $missionCfg.EnabledEnvironments['googleWorkspace'] if ($gwsEnv -and $gwsEnv.audit -and $gwsEnv.audit.categories) { $missionCats = @($gwsEnv.audit.categories.GetEnumerator() | Where-Object { $_.Value } | ForEach-Object { $_.Key }) if ($missionCats.Count -gt 0) { $Categories = $missionCats } } } } $scanId = [guid]::NewGuid().ToString() $scanStart = [datetime]::UtcNow # --- Load config --- $cfgPath = if ($ConfigPath) { $ConfigPath } else { $script:ConfigPath } $config = $null if ($cfgPath -and (Test-Path $cfgPath)) { $config = Get-Content -Path $cfgPath -Raw | ConvertFrom-Json -AsHashtable } # Merge parameters over config over defaults $keyPath = if ($ServiceAccountKeyPath) { $ServiceAccountKeyPath } elseif ($config) { $config.google.serviceAccountKeyPath } else { $null } $admin = if ($AdminEmail) { $AdminEmail } elseif ($config) { $config.google.adminEmail } else { $null } $outDir = if ($OutputDirectory) { $OutputDirectory } elseif ($config -and $config.output.directory) { $config.output.directory } else { Join-Path (Get-PSGuerrillaDataRoot) 'Reports' } # Validate required parameters if (-not $keyPath) { throw 'ServiceAccountKeyPath is required. Provide it as a parameter or set it in config.' } if (-not $admin) { throw 'AdminEmail is required. Provide it as a parameter or set it in config.' } # --- Operation header --- if (-not $Quiet) { Write-OperationHeader -Operation 'FORTIFICATION AUDIT' -Mode 'Config' -Target $admin -DaysBack 0 } # --- Determine scopes needed --- $scopes = Get-FortificationScopes -Categories $Categories # --- Authenticate --- if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Authenticating to Google Workspace' } $accessToken = Get-GoogleAccessToken -ServiceAccountKeyPath $keyPath -AdminEmail $admin -Scopes $scopes # --- Collect data --- if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Beginning data collection' } $auditData = Get-FortificationData ` -AccessToken $accessToken ` -ServiceAccountKeyPath $keyPath ` -AdminEmail $admin ` -Categories $Categories ` -UserSampleSize $UserSampleSize ` -TargetOU $TargetOU ` -Quiet:$Quiet # Report collection errors if ($auditData.Errors.Count -gt 0 -and -not $Quiet) { Write-ProgressLine -Phase INFO -Message "Data collection had $($auditData.Errors.Count) error(s)" foreach ($errKey in $auditData.Errors.Keys) { Write-ProgressLine -Phase INFO -Message " $errKey" -Detail $auditData.Errors[$errKey] } } # --- Run checks --- if (-not $Quiet) { Write-ProgressLine -Phase FORTIFYING -Message 'Evaluating configuration checks' } $allFindings = [System.Collections.Generic.List[PSCustomObject]]::new() $categoryMap = @{ Authentication = 'Invoke-AuthenticationChecks' EmailSecurity = 'Invoke-EmailSecurityChecks' DriveSecurity = 'Invoke-DriveSecurityChecks' OAuthSecurity = 'Invoke-OAuthSecurityChecks' AdminManagement = 'Invoke-AdminManagementChecks' Collaboration = 'Invoke-CollaborationChecks' DeviceManagement = 'Invoke-DeviceManagementChecks' LoggingAlerting = 'Invoke-LoggingAlertingChecks' } $categoriesToRun = if ($Categories -contains 'All') { $categoryMap.Keys } else { $Categories } foreach ($cat in $categoriesToRun) { $funcName = $categoryMap[$cat] if (-not $funcName) { continue } if (Get-Command $funcName -ErrorAction SilentlyContinue) { if (-not $Quiet) { Write-ProgressLine -Phase FORTIFYING -Message $cat } try { $catFindings = & $funcName -AuditData $auditData -OrgUnitPath $TargetOU foreach ($f in @($catFindings)) { $allFindings.Add($f) } $passed = @($catFindings | Where-Object Status -eq 'PASS').Count $failed = @($catFindings | Where-Object Status -eq 'FAIL').Count if (-not $Quiet) { Write-ProgressLine -Phase FORTIFYING -Message $cat -Detail "P:$passed F:$failed / $($catFindings.Count)" } } catch { Write-Warning "Category $cat failed: $_" } } } # --- Score --- if (-not $Quiet) { Write-ProgressLine -Phase FORTIFYING -Message 'Calculating posture score' } $scoreResult = Get-AuditPostureScore -Findings @($allFindings) $overallScore = $scoreResult.OverallScore $scoreLabel = Get-FortificationScoreLabel -Score $overallScore # --- Delta comparison --- $delta = $null if (-not $NoDelta) { $stateDir = Split-Path $cfgPath -Parent $statePath = Join-Path $stateDir 'fortification-state.json' if (Test-Path $statePath) { if (-not $Quiet) { Write-ProgressLine -Phase FORTIFYING -Message 'Comparing against previous scan' } try { $previousState = Get-Content -Path $statePath -Raw | ConvertFrom-Json -AsHashtable $delta = Compare-FortificationState -CurrentFindings @($allFindings) -PreviousState $previousState } catch { Write-Verbose "Delta comparison failed: $_" } } } # --- Severity counts --- $failFindings = @($allFindings | Where-Object Status -eq 'FAIL') $critCount = @($failFindings | Where-Object Severity -eq 'Critical').Count $highCount = @($failFindings | Where-Object Severity -eq 'High').Count $medCount = @($failFindings | Where-Object Severity -eq 'Medium').Count $lowCount = @($failFindings | Where-Object Severity -eq 'Low').Count $passCount = @($allFindings | Where-Object Status -eq 'PASS').Count $failCount = $failFindings.Count $warnCount = @($allFindings | Where-Object Status -eq 'WARN').Count $skipCount = @($allFindings | Where-Object Status -in @('SKIP', 'ERROR')).Count # --- Console report --- if (-not $Quiet) { Write-FortificationReport ` -OverallScore $overallScore ` -ScoreLabel $scoreLabel ` -CategoryScores $scoreResult.CategoryScores ` -TotalChecks $allFindings.Count ` -PassCount $passCount ` -FailCount $failCount ` -WarnCount $warnCount ` -SkipCount $skipCount ` -CriticalCount $critCount ` -HighCount $highCount ` -MediumCount $medCount ` -LowCount $lowCount ` -TopFindings @($allFindings) } # --- Generate reports --- $csvPath = $null; $htmlPath = $null; $jsonPath = $null if (-not $NoReports) { if (-not (Test-Path $outDir)) { New-Item -Path $outDir -ItemType Directory -Force | Out-Null } $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $genCsv = if ($config -and $null -ne $config.output.generateCsv) { $config.output.generateCsv } else { $true } $genHtml = if ($config -and $null -ne $config.output.generateHtml) { $config.output.generateHtml } else { $true } $genJson = if ($config -and $null -ne $config.output.generateJson) { $config.output.generateJson } else { $true } if ($genCsv) { $csvPath = Join-Path $outDir "fortification_report_$timestamp.csv" Export-FortificationReportCsv -Findings @($allFindings) -FilePath $csvPath if (-not $Quiet) { Write-ProgressLine -Phase REPORTING -Message 'CSV report' -Detail $csvPath } } if ($genHtml) { $htmlPath = Join-Path $outDir "fortification_report_$timestamp.html" Export-FortificationReportHtml ` -Findings @($allFindings) ` -OverallScore $overallScore ` -ScoreLabel $scoreLabel ` -CategoryScores $scoreResult.CategoryScores ` -TenantDomain ($auditData.Tenant.Domain ?? $admin.Split('@')[-1]) ` -Delta $delta ` -FilePath $htmlPath if (-not $Quiet) { Write-ProgressLine -Phase REPORTING -Message 'HTML report' -Detail $htmlPath } } if ($genJson) { $jsonPath = Join-Path $outDir "fortification_report_$timestamp.json" Export-FortificationReportJson ` -Findings @($allFindings) ` -OverallScore $overallScore ` -ScoreLabel $scoreLabel ` -CategoryScores $scoreResult.CategoryScores ` -TenantDomain ($auditData.Tenant.Domain ?? $admin.Split('@')[-1]) ` -ScanId $scanId ` -Delta $delta ` -FilePath $jsonPath if (-not $Quiet) { Write-ProgressLine -Phase REPORTING -Message 'JSON report' -Detail $jsonPath } } } # --- Save state for future delta --- if (-not $NoDelta) { $stateDir = Split-Path $cfgPath -Parent if (-not (Test-Path $stateDir)) { New-Item -Path $stateDir -ItemType Directory -Force | Out-Null } $statePath = Join-Path $stateDir 'fortification-state.json' $newState = @{ schemaVersion = 1 lastScanTimestamp = [datetime]::UtcNow.ToString('o') lastScanId = $scanId overallScore = $overallScore findings = @($allFindings | ForEach-Object { @{ checkId = $_.CheckId status = $_.Status currentValue = $_.CurrentValue orgUnitPath = $_.OrgUnitPath severity = $_.Severity category = $_.Category } }) categoryScores = $scoreResult.CategoryScores } $newState | ConvertTo-Json -Depth 5 | Set-Content -Path $statePath -Encoding UTF8 } # --- Emit result object --- $result = [PSCustomObject]@{ PSTypeName = 'PSGuerrilla.AuditResult' ScanId = $scanId Timestamp = $scanStart TenantDomain = $auditData.Tenant.Domain ?? $admin.Split('@')[-1] OverallScore = $overallScore ScoreLabel = $scoreLabel CategoryScores = $scoreResult.CategoryScores TotalChecks = $allFindings.Count PassCount = $passCount FailCount = $failCount WarnCount = $warnCount SkipCount = $skipCount CriticalCount = $critCount HighCount = $highCount MediumCount = $medCount LowCount = $lowCount Findings = @($allFindings) Delta = $delta HtmlReportPath = $htmlPath CsvReportPath = $csvPath JsonReportPath = $jsonPath } return $result } function Get-FortificationScopes { [CmdletBinding()] param( [string[]]$Categories = @('All') ) $scopeMap = @{ Authentication = @( 'https://www.googleapis.com/auth/admin.directory.user.readonly' 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly' 'https://www.googleapis.com/auth/admin.directory.customer.readonly' ) EmailSecurity = @( 'https://www.googleapis.com/auth/admin.directory.user.readonly' 'https://www.googleapis.com/auth/admin.directory.domain.readonly' 'https://www.googleapis.com/auth/admin.directory.customer.readonly' 'https://www.googleapis.com/auth/gmail.settings.basic' ) DriveSecurity = @( 'https://www.googleapis.com/auth/admin.directory.user.readonly' 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly' 'https://www.googleapis.com/auth/admin.directory.customer.readonly' ) OAuthSecurity = @( 'https://www.googleapis.com/auth/admin.directory.user.readonly' 'https://www.googleapis.com/auth/admin.directory.customer.readonly' 'https://www.googleapis.com/auth/admin.reports.audit.readonly' 'https://www.googleapis.com/auth/admin.directory.domain.readonly' ) AdminManagement = @( 'https://www.googleapis.com/auth/admin.directory.user.readonly' 'https://www.googleapis.com/auth/admin.directory.group.readonly' 'https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly' 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly' 'https://www.googleapis.com/auth/admin.directory.customer.readonly' ) Collaboration = @( 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly' 'https://www.googleapis.com/auth/admin.directory.group.readonly' 'https://www.googleapis.com/auth/admin.directory.customer.readonly' ) DeviceManagement = @( 'https://www.googleapis.com/auth/admin.directory.device.mobile.readonly' 'https://www.googleapis.com/auth/admin.directory.device.chromeos.readonly' 'https://www.googleapis.com/auth/admin.directory.customer.readonly' 'https://www.googleapis.com/auth/chrome.management.policy.readonly' ) LoggingAlerting = @( 'https://www.googleapis.com/auth/admin.directory.customer.readonly' 'https://www.googleapis.com/auth/apps.alerts' ) } $allScopes = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $categoriesToInclude = if ($Categories -contains 'All') { $scopeMap.Keys } else { $Categories } foreach ($cat in $categoriesToInclude) { if ($scopeMap.ContainsKey($cat)) { foreach ($scope in $scopeMap[$cat]) { [void]$allScopes.Add($scope) } } } return @($allScopes) } |