Private/Resolve-BsiMapping.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    BSI mapping management — load, query, validate, update.
.DESCRIPTION
    Loads the bsi-azure-mapping.json, resolves controls against catalog,
    detects orphans, and provides lookup functions.
#>


function Get-BsiMapping {
    param([Parameter(Mandatory)][string]$Path)

    if (-not (Test-Path -LiteralPath $Path)) {
        throw "Mapping file not found: $Path"
    }
    return Get-Content -Raw -Path $Path | ConvertFrom-Json
}

function Get-BsiMappingControls {
    param([Parameter(Mandatory)]$Mapping)
    return $Mapping.mappings
}

function Get-BsiMappingControlById {
    param(
        [Parameter(Mandatory)]$Mapping,
        [Parameter(Mandatory)][string]$ControlId
    )
    return $Mapping.mappings | Where-Object { $_.controlId -eq $ControlId } | Select-Object -First 1
}

function Resolve-BsiOrphans {
    <#
    .SYNOPSIS
        Finds mapping entries whose control IDs are not in the catalog.
    .OUTPUTS
        Array of orphaned control IDs.
    #>

    param(
        [Parameter(Mandatory)]$Mapping,
        [Parameter(Mandatory)]$CatalogControls
    )

    $catalogIds = $CatalogControls | ForEach-Object { $_.id }
    return @($Mapping.mappings | Where-Object {
        $_.controlId -notlike "AZURE.*" -and $catalogIds -notcontains $_.controlId
    } | ForEach-Object { $_.controlId })
}

function Resolve-BsiUncoveredControls {
    <#
    .SYNOPSIS
        Finds catalog controls not covered by the mapping.
    .OUTPUTS
        Array of uncovered control IDs.
    #>

    param(
        [Parameter(Mandatory)]$Mapping,
        [Parameter(Mandatory)]$CatalogControls
    )

    $mappedIds = $Mapping.mappings | ForEach-Object { $_.controlId }
    $catalogIds = $CatalogControls | ForEach-Object { $_.id }
    return @($catalogIds | Where-Object { $mappedIds -notcontains $_ })
}

function Update-BsiMappingTimestamp {
    param([Parameter(Mandatory)][string]$Path)

    $mapping = Get-BsiMapping -Path $Path
    $mapping.lastSynced = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
    $mapping | ConvertTo-Json -Depth 10 | Set-Content -Path $Path -Encoding UTF8
}

function Update-BsiSkillReadme {
    param(
        [Parameter(Mandatory)]$Mapping,
        [Parameter(Mandatory)][string]$OutputPath
    )

    $lines = [System.Collections.ArrayList]::new()
    $null = $lines.Add("---")
    $null = $lines.Add("name: 'bsi-oscal-azure-compliance'")
    $null = $lines.Add("description: 'BSI IT-Grundschutz++ OSCAL compliance validator for Azure 3-tier deployments.'")
    $null = $lines.Add("metadata:")
    $null = $lines.Add(" generated_at: $(Get-Date -Format 'yyyy-MM-dd')")
    $null = $lines.Add(" generator: 'BSI.AzCompliance'")
    $null = $lines.Add(" source_repo: '$($Mapping.repository)'")
    $null = $lines.Add(" lastSynced: '$($Mapping.lastSynced)'")
    $null = $lines.Add("---")
    $null = $lines.Add("")
    $null = $lines.Add("# BSI OSCAL Azure Compliance Skill")
    $null = $lines.Add("")
    $null = $lines.Add("## Mapped Controls")
    $null = $lines.Add("")

    foreach ($entry in $Mapping.mappings) {
        $null = $lines.Add("- **$($entry.controlId)**: $($entry.title) [$($entry.category)]")
    }

    $null = $lines.Add("")
    $null = $lines.Add("## Usage")
    $null = $lines.Add('```powershell')
    $null = $lines.Add('Import-Module ./modules/BSI.AzCompliance')
    $null = $lines.Add('Invoke-BsiCompliance -Local -ScriptPath .\create_3-tier-webapp_VM_v2.ps1')
    $null = $lines.Add('Invoke-BsiCompliance -Remote -ConfigPath .\config.ps1')
    $null = $lines.Add('```')
    $lines | Set-Content -Path $OutputPath -Encoding UTF8
    Write-Verbose "[Mapping] Updated skill readme: $OutputPath"
}