Orchestrator/Export-AssessmentBaseline.ps1
|
function Export-AssessmentBaseline { <# .SYNOPSIS Saves a named baseline snapshot of all security-config collector results. .DESCRIPTION Reads all security-config CSVs (those containing CheckId and Status columns) from the current assessment folder and serialises them to JSON in a labelled baseline directory under <OutputFolder>/Baselines/<Label>_<TenantId>/. A metadata file records the label, tenant, version, sections run, and timestamp so that Compare-AssessmentBaseline can validate compatibility. .PARAMETER AssessmentFolder Path to the completed assessment output folder. .PARAMETER OutputFolder Root output folder (parent of Baselines/). Typically the -OutputFolder value passed to Invoke-M365Assessment. .PARAMETER Label Human-readable baseline label (e.g. 'Q1-2026'). Used as the folder name prefix and referenced with -CompareBaseline on future runs. .PARAMETER TenantId Tenant identifier for the baseline folder name suffix. .PARAMETER Sections Array of section names that were assessed (recorded in metadata). .PARAMETER Version Assessment module version string (e.g. '1.15.0') recorded in metadata. .PARAMETER RegistryVersion Registry data version string (from controls/registry.json dataVersion) recorded in metadata to enable version-aware drift comparison. .EXAMPLE Export-AssessmentBaseline -AssessmentFolder $assessmentFolder ` -OutputFolder '.\M365-Assessment' -Label 'Q1-2026' -TenantId 'contoso.com' #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$AssessmentFolder, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$OutputFolder, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Label, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$TenantId, [Parameter()] [string[]]$Sections = @(), [Parameter()] [string]$Version = '', [Parameter()] [string]$RegistryVersion = '' ) # Sanitise label for use as a folder name $safeLabel = $Label -replace '[^\w\-]', '_' $safeTenant = $TenantId -replace '[^\w\.\-]', '_' $baselineDir = Join-Path -Path $OutputFolder -ChildPath "Baselines\${safeLabel}_${safeTenant}" if (-not (Test-Path -Path $baselineDir -PathType Container)) { $null = New-Item -Path $baselineDir -ItemType Directory -Force } # Copy each security-config CSV as JSON (identified by having CheckId + Status columns) $csvFiles = Get-ChildItem -Path $AssessmentFolder -Filter '*.csv' -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '_*' } $saved = 0 $checkCount = 0 foreach ($csvFile in $csvFiles) { try { $rows = Import-Csv -Path $csvFile.FullName -ErrorAction Stop if (-not $rows) { continue } $firstRow = $rows | Select-Object -First 1 $props = $firstRow.PSObject.Properties.Name # Only baseline security-config tables (must have both CheckId and Status) if ('CheckId' -notin $props -or 'Status' -notin $props) { continue } $jsonName = [System.IO.Path]::GetFileNameWithoutExtension($csvFile.Name) + '.json' $jsonPath = Join-Path -Path $baselineDir -ChildPath $jsonName $rows | ConvertTo-Json -Depth 5 | Set-Content -Path $jsonPath -Encoding UTF8 $checkCount += @($rows).Count $saved++ } catch { Write-Warning "Baseline: skipped '$($csvFile.Name)': $_" } } # Write manifest after CSV scan (includes accurate CheckCount) $manifest = [PSCustomObject]@{ Label = $Label SavedAt = (Get-Date -Format 'o') TenantId = $TenantId AssessmentVersion = $Version RegistryVersion = $RegistryVersion CheckCount = $checkCount Sections = $Sections } $manifestPath = Join-Path -Path $baselineDir -ChildPath 'manifest.json' $manifest | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestPath -Encoding UTF8 Write-Verbose "Baseline '$Label' saved to '$baselineDir' ($saved collector files, $checkCount checks)" return $baselineDir } |