Private/Checkpoint.ps1
|
# Per-subscription and tenant checkpoint save/load # Enables resume of interrupted audits $script:TENANT_CHECKPOINT_FILE = "_tenant.json" function Save-TenantCheckpoint { <# .SYNOPSIS Persist tenant-level (Section 5) results to disk. #> param([object[]]$Results) $dir = $script:CHECKPOINT_DIR if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $path = Join-Path $dir $script:TENANT_CHECKPOINT_FILE $tmpPath = "$path.tmp" $data = [ordered]@{ tool_version = $script:CIS_VERSION benchmark_version = $script:BENCHMARK_VER timestamp = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") status = "completed" results = @($Results | ForEach-Object { [ordered]@{ control_id = $_.ControlId title = $_.Title level = $_.Level section = $_.Section status = $_.Status details = $_.Details remediation = $_.Remediation subscription_id = $_.SubscriptionId subscription_name = $_.SubscriptionName resource = $_.Resource } }) } $json = $data | ConvertTo-Json -Depth 10 Set-Content -Path $tmpPath -Value $json -Encoding UTF8 Move-Item -Path $tmpPath -Destination $path -Force Write-AuditLog "Tenant checkpoint saved: $path ($($Results.Count) results)" -Level DEBUG } function Get-TenantCheckpoint { <# .SYNOPSIS Load tenant-level results from checkpoint. Returns $null if no valid checkpoint exists (caller should re-run live). #> $dir = $script:CHECKPOINT_DIR $path = Join-Path $dir $script:TENANT_CHECKPOINT_FILE if (-not (Test-Path $path)) { return $null } try { $data = Get-Content $path -Encoding UTF8 -Raw | ConvertFrom-Json -Depth 20 } catch { Write-AuditLog "Could not read tenant checkpoint: $_" -Level WARNING return $null } if ($data.tool_version -ne $script:CIS_VERSION) { Write-AuditLog "Tenant checkpoint was written by v$($data.tool_version) (current: v$($script:CIS_VERSION)) — re-running." -Level WARNING return $null } $results = @($data.results | ForEach-Object { New-CISResult ` -ControlId ([string]$_.control_id) ` -Title ([string]$_.title) ` -Level ([int]$_.level) ` -Section ([string]$_.section) ` -Status ([string]$_.status) ` -Details ([string]$_.details) ` -Remediation ([string]$_.remediation) ` -SubscriptionId ([string]$_.subscription_id) ` -SubscriptionName ([string]$_.subscription_name) ` -Resource ([string]$_.resource) }) return $results } function Save-AuditCheckpoint { <# .SYNOPSIS Atomically persist per-subscription results to disk. #> param( [string]$SubscriptionId, [string]$SubscriptionName, [object[]]$Results, [string]$Status = "completed" ) $dir = $script:CHECKPOINT_DIR if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $safeName = $SubscriptionId -replace '[^a-zA-Z0-9\-]', '_' $path = Join-Path $dir "$safeName.json" $tmpPath = "$path.tmp" $data = [ordered]@{ tool_version = $script:CIS_VERSION benchmark_version = $script:BENCHMARK_VER timestamp = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") subscription_id = $SubscriptionId subscription_name = $SubscriptionName status = $Status results = @($Results | ForEach-Object { [ordered]@{ control_id = $_.ControlId title = $_.Title level = $_.Level section = $_.Section status = $_.Status details = $_.Details remediation = $_.Remediation subscription_id = $_.SubscriptionId subscription_name = $_.SubscriptionName resource = $_.Resource } }) } $json = $data | ConvertTo-Json -Depth 10 Set-Content -Path $tmpPath -Value $json -Encoding UTF8 Move-Item -Path $tmpPath -Destination $path -Force Write-AuditLog "Checkpoint: $path ($($Results.Count) results)" -Level DEBUG } function Get-AuditCheckpoints { <# .SYNOPSIS Load all completed checkpoints from disk. Returns hashtable: subscription_id -> @{ Results, SubscriptionName, Timestamp } #> $dir = $script:CHECKPOINT_DIR if (-not (Test-Path $dir)) { return @{} } $checkpoints = @{} foreach ($file in Get-ChildItem -Path $dir -Filter "*.json") { if ($file.Name -eq $script:TENANT_CHECKPOINT_FILE) { continue } try { $data = Get-Content $file.FullName -Encoding UTF8 -Raw | ConvertFrom-Json -Depth 20 if ($data.status -ne "completed") { continue } # Discard checkpoints written by a different version of the tool. # Control titles, remediation text, and detail strings change between # versions. Loading stale data produces misleading report output. if ([string]$data.tool_version -ne $script:CIS_VERSION) { Write-AuditLog "Checkpoint $($file.Name) was written by v$($data.tool_version) (current: v$($script:CIS_VERSION)) — re-running." -Level WARNING continue } $results = @($data.results | ForEach-Object { New-CISResult ` -ControlId ([string]$_.control_id) ` -Title ([string]$_.title) ` -Level ([int]$_.level) ` -Section ([string]$_.section) ` -Status ([string]$_.status) ` -Details ([string]$_.details) ` -Remediation ([string]$_.remediation) ` -SubscriptionId ([string]$_.subscription_id) ` -SubscriptionName ([string]$_.subscription_name) ` -Resource ([string]$_.resource) }) $checkpoints[$data.subscription_id] = @{ Results = $results SubscriptionName = [string]$data.subscription_name Timestamp = [string]$data.timestamp } } catch { Write-AuditLog "Skipping corrupt checkpoint $($file.Name): $_" -Level WARNING } } return $checkpoints } function Remove-AuditCheckpoints { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Internal audit helper; confirmation prompts would break non-interactive CI runs.')] param([string[]]$SubscriptionIds = @()) $dir = $script:CHECKPOINT_DIR if (-not (Test-Path $dir)) { return } if ($SubscriptionIds.Count -eq 0) { Remove-Item -Path (Join-Path $dir "*.json") -Force -ErrorAction SilentlyContinue } else { foreach ($sid in $SubscriptionIds) { $safeName = $sid -replace '[^a-zA-Z0-9\-]', '_' $path = Join-Path $dir "$safeName.json" if (Test-Path $path) { Remove-Item $path -Force } } } } |