Common/Import-ControlRegistry.ps1

<#
.SYNOPSIS
    Loads the control registry and builds lookup tables for the report layer.
.DESCRIPTION
    Loads check data from the local controls/registry.json file (synced from
    CheckID via CI). Returns a hashtable keyed by CheckId with framework
    mappings and risk severity.

    Supports both CheckID schema versions:
    - v1.x: licensing.requiredServicePlans (array of plan IDs)
    - v2.0.0: licensing.minimum ("E3" or "E5") normalized via licensing-overlay.json

    Also builds a reverse lookup from CIS control IDs to CheckIds (stored
    under the special key '__cisReverseLookup') for backward compatibility
    with CSVs that still use the CisControl column.
.PARAMETER ControlsPath
    Path to the controls/ directory containing registry.json,
    risk-severity.json, and licensing-overlay.json.
.PARAMETER CisFrameworkId
    Framework ID for the active CIS benchmark version, used for the reverse
    lookup. Defaults to 'cis-m365-v6'.
.OUTPUTS
    [hashtable] - Keys are CheckIds, values are registry entry objects.
    Special key '__cisReverseLookup' maps CIS control IDs to CheckIds.
#>

function Import-ControlRegistry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ControlsPath,

        [Parameter()]
        [string]$CisFrameworkId = 'cis-m365-v6'
    )

    $registryPath = Join-Path -Path $ControlsPath -ChildPath 'registry.json'
    if (-not (Test-Path -Path $registryPath)) {
        Write-Warning "Control registry not found: $registryPath"
        return @{}
    }

    $raw = Get-Content -Path $registryPath -Raw | ConvertFrom-Json
    $checks = @($raw.checks)
    $schemaVersion = if ($raw.PSObject.Properties.Name -contains 'schemaVersion') { $raw.schemaVersion } else { '1.x' }
    Write-Verbose "Loaded $($checks.Count) checks from registry.json (schema $schemaVersion, data $($raw.dataVersion))"

    # Load licensing overlay (M365-Assess-specific service plan gating)
    $licensingOverlay = @{}
    $overlayPath = Join-Path -Path $ControlsPath -ChildPath 'licensing-overlay.json'
    if (Test-Path -Path $overlayPath) {
        $overlayData = Get-Content -Path $overlayPath -Raw | ConvertFrom-Json
        foreach ($prop in $overlayData.checks.PSObject.Properties) {
            $licensingOverlay[$prop.Name] = @($prop.Value)
        }
        Write-Verbose "Loaded $($licensingOverlay.Count) licensing overrides from licensing-overlay.json"
    }

    # Build hashtable keyed by CheckId
    $lookup = @{}
    $cisReverse = @{}

    foreach ($check in $checks) {
        # Normalize licensing across schema versions:
        # v1.x: { requiredServicePlans: [...] } — pass through
        # v2.0.0: { minimum: "E3"|"E5" } — resolved via licensing-overlay.json
        # Initialize to empty array; only populated if overlay or v1.x data matches.
        # Note: $requiredPlans must be declared with @() before conditional mutation —
        # assigning @() via an if/else expression returns $null in PowerShell because
        # an empty array emits nothing to the pipeline in that context.
        $requiredPlans = @()
        if ($licensingOverlay.ContainsKey($check.checkId)) {
            $requiredPlans = $licensingOverlay[$check.checkId]
        } elseif ($check.licensing -and $check.licensing.PSObject.Properties.Name -contains 'requiredServicePlans') {
            $requiredPlans = @($check.licensing.requiredServicePlans)
        }

        $entry = @{
            checkId           = $check.checkId
            name              = $check.name
            category          = $check.category
            collector         = $check.collector
            hasAutomatedCheck = $check.hasAutomatedCheck
            licensing         = @{ requiredServicePlans = $requiredPlans }
            frameworks        = @{}
            scf               = $check.scf           # PSCustomObject from CheckID v2.0.0; $null for local extensions
            impactRating      = $check.impactRating   # PSCustomObject from CheckID v2.0.0; $null for local extensions
            remediation       = if ($check.remediation) { [string]$check.remediation } else { '' }  # empty string not $null
        }

        # Convert framework PSCustomObject properties to hashtable
        foreach ($prop in $check.frameworks.PSObject.Properties) {
            $entry.frameworks[$prop.Name] = $prop.Value
        }

        $entry.riskSeverity = 'Medium'  # default, overridden from risk-severity.json below
        $lookup[$check.checkId] = $entry

        # Build CIS reverse lookup (parameterized for version upgrades)
        $cisMapping = $check.frameworks.$CisFrameworkId
        if ($cisMapping -and $cisMapping.controlId) {
            $cisReverse[$cisMapping.controlId] = $check.checkId
        }
    }

    $lookup['__cisReverseLookup'] = $cisReverse

    # Load risk severity overlay (local to M365-Assess, not in CheckID)
    $severityPath = Join-Path -Path $ControlsPath -ChildPath 'risk-severity.json'
    if (Test-Path -Path $severityPath) {
        $severityData = Get-Content -Path $severityPath -Raw | ConvertFrom-Json
        foreach ($prop in $severityData.checks.PSObject.Properties) {
            if ($lookup.ContainsKey($prop.Name)) {
                $lookup[$prop.Name].riskSeverity = $prop.Value
            }
        }
    }

    return $lookup
}