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
}