modules/Devolutions.CIEM.Graph/Public/Sync-CIEMAttackPathRuleCatalog.ps1

function Sync-CIEMAttackPathRuleCatalog {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param()

    $ErrorActionPreference = 'Stop'

    $patternDir = Join-Path $script:GraphRoot 'Data' 'attack_paths'
    if (-not (Test-Path -Path $patternDir -PathType Container)) {
        throw "Attack path rule catalog directory not found: $patternDir"
    }

    $patternFiles = @(Get-ChildItem -Path $patternDir -Filter '*.json' -File | Sort-Object Name)
    if ($patternFiles.Count -eq 0) {
        throw "Attack path rule catalog directory contains no JSON rules: $patternDir"
    }

    $now = (Get-Date).ToString('o')
    $ruleIds = [System.Collections.Generic.List[string]]::new()

    foreach ($file in $patternFiles) {
        $rule = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json -ErrorAction Stop

        foreach ($field in @('id', 'name', 'severity', 'category', 'description', 'remediation', 'remediation_script')) {
            if (-not $rule.PSObject.Properties[$field] -or [string]::IsNullOrWhiteSpace([string]$rule.$field)) {
                throw "Attack path rule file '$($file.Name)' is missing required field '$field'."
            }
        }

        if (-not $rule.PSObject.Properties['steps'] -or @($rule.steps).Count -eq 0) {
            throw "Attack path rule file '$($file.Name)' is missing required field 'steps'."
        }

        $relativeScriptPath = [string]$rule.remediation_script
        $absoluteScriptPath = Join-Path $script:ModuleRoot $relativeScriptPath
        if (-not (Test-Path -Path $absoluteScriptPath -PathType Leaf)) {
            throw "Attack path rule '$($rule.id)' references missing remediation script '$relativeScriptPath'."
        }

        $psuScriptName = ConvertToCIEMAttackPathPsuScriptName -RemediationScriptPath $relativeScriptPath
        $stepsJson = $rule.steps | ConvertTo-Json -Depth 20 -Compress
        if ([string]::IsNullOrWhiteSpace($stepsJson)) {
            throw "Attack path rule '$($rule.id)' produced empty steps JSON."
        }

        $ruleIds.Add([string]$rule.id)

        Invoke-CIEMQuery -Query @"
INSERT OR REPLACE INTO attack_path_rules (
    id, name, severity, category, description, remediation,
    remediation_script_path, psu_script_name, steps_json, disabled, updated_at
) VALUES (
    @id, @name, @severity, @category, @description, @remediation,
    @remediation_script_path, @psu_script_name, @steps_json, 0, @updated_at
)
"@
 -Parameters @{
            id                      = [string]$rule.id
            name                    = [string]$rule.name
            severity                = [string]$rule.severity
            category                = [string]$rule.category
            description             = [string]$rule.description
            remediation             = [string]$rule.remediation
            remediation_script_path = $relativeScriptPath
            psu_script_name         = $psuScriptName
            steps_json              = $stepsJson
            updated_at              = $now
        } -AsNonQuery | Out-Null
    }

    [pscustomobject]@{
        RuleCount  = $ruleIds.Count
        CatalogDir = $patternDir
        Status     = 'Synced'
    }
}