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 version string recorded in metadata for schema compatibility. .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 = '' ) # 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 } # Write metadata $metadata = [PSCustomObject]@{ label = $Label tenant = $TenantId timestamp = (Get-Date -Format 'o') version = $Version sections = $Sections } $metaPath = Join-Path -Path $baselineDir -ChildPath '_baseline-metadata.json' $metadata | ConvertTo-Json -Depth 5 | Set-Content -Path $metaPath -Encoding UTF8 # 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 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 $saved++ } catch { Write-Warning "Baseline: skipped '$($csvFile.Name)': $_" } } Write-Verbose "Baseline '$Label' saved to '$baselineDir' ($saved collector files)" return $baselineDir } |