Common/Export-AssessmentBridgeJson.ps1

function Export-AssessmentBridgeJson {
    <#
    .SYNOPSIS
        Writes a structured JSON export of assessment findings for M365-Remediate integration.
    .DESCRIPTION
        Produces _Assessment-<Tenant>.json alongside the HTML and XLSX in the assessment output
        folder. The file contains per-finding structured data, tenant metadata, and domain summary
        counts in a format M365-Remediate can parse to pre-populate its remediation queue.
    .PARAMETER AllFindings
        Array of enriched check rows from Build-SectionHtml (CheckId, Status, RiskSeverity,
        Frameworks, Remediation, CurrentValue, etc.).
    .PARAMETER RegistryData
        Control registry hashtable from Import-ControlRegistry. Used to look up effort ratings.
    .PARAMETER TenantId
        Tenant GUID or domain written to the metadata block.
    .PARAMETER TenantName
        Display name of the tenant.
    .PARAMETER AssessedAt
        ISO 8601 timestamp for when the assessment ran. Defaults to current UTC time.
    .PARAMETER AssessmentVersion
        Semantic version string of M365-Assess that produced this data.
    .PARAMETER RegistryVersion
        Version/date label from registry.json (e.g. '2026-04-20').
    .PARAMETER OutputPath
        Full path for the output JSON file.
    .PARAMETER SensitiveCheckIds
        CheckId patterns (wildcards accepted) whose currentValue should be replaced with
        '[REDACTED]'. Defaults to empty (no redaction).
    .OUTPUTS
        [string] Path of the JSON file written.
    .EXAMPLE
        Export-AssessmentBridgeJson -AllFindings $allCisFindings -RegistryData $controlRegistry `
            -TenantId 'contoso.com' -TenantName 'Contoso' -AssessmentVersion '2.3.0' `
            -RegistryVersion '2026-04-20' -OutputPath 'C:\output\_Assessment-contoso.json'
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [PSCustomObject[]]$AllFindings,

        [Parameter()]
        [hashtable]$RegistryData = @{},

        [Parameter(Mandatory)]
        [string]$TenantId,

        [Parameter()]
        [string]$TenantName = '',

        [Parameter()]
        [string]$AssessedAt = '',

        [Parameter()]
        [string]$AssessmentVersion = '',

        [Parameter()]
        [string]$RegistryVersion = '',

        [Parameter(Mandatory)]
        [string]$OutputPath,

        [Parameter()]
        [string[]]$SensitiveCheckIds = @()
    )

    $findings = foreach ($f in $AllFindings) {
        $baseCheckId = $f.CheckId -replace '\.\d+$', ''
        $regEntry    = if ($RegistryData.ContainsKey($baseCheckId)) { $RegistryData[$baseCheckId] } else { $null }

        $severity = if ($f.RiskSeverity) { $f.RiskSeverity.ToLower() } else { 'medium' }

        $effort = if ($regEntry) {
            $e = if ($regEntry -is [hashtable]) { $regEntry['effort'] } else { $regEntry.effort }
            if ($e) { $e } else { 'medium' }
        } else { 'medium' }

        $fwSource  = if ($f.PSObject.Properties['Frameworks'] -and $f.Frameworks) { $f.Frameworks } else { $null }
        $frameworks = if ($fwSource -is [hashtable])  { [string[]]($fwSource.Keys) }
                      elseif ($fwSource)               { [string[]]($fwSource.PSObject.Properties.Name) }
                      else                             { [string[]]@() }

        $isSensitive = $SensitiveCheckIds.Count -gt 0 -and
            ($SensitiveCheckIds | Where-Object { $f.CheckId -like $_ }).Count -gt 0

        [PSCustomObject]@{
            checkId      = $f.CheckId
            status       = $f.Status
            severity     = $severity
            effort       = $effort
            frameworks   = $frameworks
            currentValue = if ($isSensitive) { '[REDACTED]' } else { $f.CurrentValue }
            remediation  = $f.Remediation
        }
    }

    $domainSummary = [ordered]@{}
    foreach ($f in $AllFindings) {
        $baseId = $f.CheckId -replace '\.\d+$', ''
        $d = switch -Wildcard ($baseId) {
            'CA-*'           { 'Conditional Access';    break }
            'ENTRA-ENTAPP-*' { 'Enterprise Apps';       break }
            'ENTRA-*'        { 'Entra ID';              break }
            'EXO-*'          { 'Exchange Online';       break }
            'DNS-*'          { 'Exchange Online';       break }
            'INTUNE-*'       { 'Intune';                break }
            'DEFENDER-*'     { 'Defender';              break }
            'SPO-*'          { 'SharePoint & OneDrive'; break }
            'TEAMS-*'        { 'Teams';                 break }
            'PURVIEW-*'      { 'Purview / Compliance';  break }
            'DLP-*'          { 'Purview / Compliance';  break }
            'COMPLIANCE-*'   { 'Purview / Compliance';  break }
            default          { 'Other' }
        }
        if (-not $domainSummary.Contains($d)) {
            $domainSummary[$d] = @{ pass = 0; warn = 0; fail = 0; review = 0; total = 0 }
        }
        $bucket = $domainSummary[$d]
        $bucket.total++
        switch ($f.Status) {
            'Pass'    { $bucket.pass++ }
            'Warning' { $bucket.warn++ }
            'Fail'    { $bucket.fail++ }
            'Review'  { $bucket.review++ }
        }
    }

    $bridge = [ordered]@{
        schemaVersion     = '1.0'
        assessedAt        = if ($AssessedAt) { $AssessedAt } else { [datetime]::UtcNow.ToString('o') }
        tenantId          = $TenantId
        tenantName        = $TenantName
        assessmentVersion = $AssessmentVersion
        registryVersion   = $RegistryVersion
        findings          = @($findings)
        domainSummary     = $domainSummary
    }

    $json = $bridge | ConvertTo-Json -Depth 6
    $json = $json -replace '"frameworks":\s*null', '"frameworks": []'
    Set-Content -Path $OutputPath -Value $json -Encoding UTF8
    return $OutputPath
}