modules/Devolutions.CIEM.Graph/Private/InvokeCIEMAttackPathEvaluation.ps1

function InvokeCIEMAttackPathEvaluation {
    [CmdletBinding()]
    [OutputType('CIEMAttackPath')]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject]$Pattern,

        [Parameter()]
        [string]$SeedNodeId
    )

    $ErrorActionPreference = 'Stop'

    $steps = $Pattern.steps
    if (-not $steps -or @($steps).Count -lt 2) { return @() }

    # Step 0: Find seed nodes matching the first step (always a node step)
    $firstStep = $steps[0]
    [string[]]$kinds = @($firstStep.kind)

    $kindPlaceholders = @()
    $parameters = @{}
    for ($k = 0; $k -lt $kinds.Count; $k++) {
        $kindPlaceholders += "@kind$k"
        $parameters["kind$k"] = $kinds[$k]
    }
    $seedSql = "SELECT id, kind, display_name, properties FROM graph_nodes WHERE kind IN ($($kindPlaceholders -join ', '))"
    if ($SeedNodeId) {
        $seedSql += " AND id = @seedNodeId"
        $parameters["seedNodeId"] = $SeedNodeId
    }
    $seedNodes = @(Invoke-CIEMQuery -Query $seedSql -Parameters $parameters)

    # Apply node_filter if present on first step
    if ($firstStep.node_filter) {
        $nodeFilter = [PSCustomObject]$firstStep.node_filter
        $seedNodes = @($seedNodes | Where-Object {
            if (-not $_.properties) { return $false }
            ResolveCIEMAttackPathFilter -PropertiesJson $_.properties -Filter $nodeFilter
        })
    }

    if ($seedNodes.Count -eq 0) { return @() }

    # Initialize paths: each path is an array of interleaved node/edge objects
    $currentPaths = [System.Collections.Generic.List[object]]::new()
    foreach ($node in $seedNodes) {
        $nodeObj = [PSCustomObject]@{ id = $node.id; kind = $node.kind; display_name = $node.display_name; properties = $node.properties; _type = 'node' }
        $currentPaths.Add(@(, $nodeObj))
    }

    # Process remaining steps in pairs: (edge step, optional node step)
    for ($i = 1; $i -lt @($steps).Count; $i += 2) {
        $edgeStep = $steps[$i]
        $nodeStep = if ($i + 1 -lt @($steps).Count) { $steps[$i + 1] } else { $null }

        $edgeKind = $edgeStep.edge
        $direction = if ($edgeStep.direction) { $edgeStep.direction } else { 'outbound' }

        $newPaths = [System.Collections.Generic.List[object]]::new()

        foreach ($path in $currentPaths) {
            $currentNode = $path[$path.Count - 1]
            $currentNodeId = $currentNode.id

            # Build edge query based on direction
            if ($direction -eq 'reverse') {
                $edgeSql = @"
SELECT e.id AS edge_id, e.source_id, e.target_id, e.kind AS edge_kind, e.properties AS edge_properties,
       n.id AS node_id, n.kind AS node_kind, n.display_name AS node_display_name, n.properties AS node_properties
FROM graph_edges e
JOIN graph_nodes n ON n.id = e.source_id
WHERE e.target_id = @nodeId AND e.kind = @edgeKind
"@

            } else {
                $edgeSql = @"
SELECT e.id AS edge_id, e.source_id, e.target_id, e.kind AS edge_kind, e.properties AS edge_properties,
       n.id AS node_id, n.kind AS node_kind, n.display_name AS node_display_name, n.properties AS node_properties
FROM graph_edges e
JOIN graph_nodes n ON n.id = e.target_id
WHERE e.source_id = @nodeId AND e.kind = @edgeKind
"@

            }
            $edgeParams = @{ nodeId = $currentNodeId; edgeKind = $edgeKind }
            $matches = @(Invoke-CIEMQuery -Query $edgeSql -Parameters $edgeParams)

            foreach ($match in $matches) {
                # Apply edge filter if present
                if ($edgeStep.filter) {
                    if (-not $match.edge_properties) { continue }
                    $edgeFilter = [PSCustomObject]$edgeStep.filter
                    $passes = ResolveCIEMAttackPathFilter -PropertiesJson $match.edge_properties -Filter $edgeFilter
                    if (-not $passes) { continue }
                }

                # Apply node kind filter if there is a subsequent node step
                if ($nodeStep) {
                    [string[]]$expectedKinds = @($nodeStep.kind)
                    if ($match.node_kind -notin $expectedKinds) { continue }

                    # Apply node_filter if present on the node step
                    if ($nodeStep.node_filter) {
                        if (-not $match.node_properties) { continue }
                        $nFilter = [PSCustomObject]$nodeStep.node_filter
                        $nodeFilterPasses = ResolveCIEMAttackPathFilter -PropertiesJson $match.node_properties -Filter $nFilter
                        if (-not $nodeFilterPasses) { continue }
                    }
                }

                # Build edge object
                $edgeObj = [PSCustomObject]@{
                    id        = $match.edge_id
                    source_id = $match.source_id
                    target_id = $match.target_id
                    kind      = $match.edge_kind
                    properties = $match.edge_properties
                    _type     = 'edge'
                }

                # Build next node object
                $nextNodeObj = [PSCustomObject]@{
                    id           = $match.node_id
                    kind         = $match.node_kind
                    display_name = $match.node_display_name
                    properties   = $match.node_properties
                    _type        = 'node'
                }

                # Extend path
                $newPath = [object[]]::new($path.Count + 2)
                for ($p = 0; $p -lt $path.Count; $p++) {
                    $newPath[$p] = $path[$p]
                }
                $newPath[$path.Count] = $edgeObj
                $newPath[$path.Count + 1] = $nextNodeObj

                $newPaths.Add($newPath)
            }
        }

        $currentPaths = $newPaths
        if ($currentPaths.Count -eq 0) { return @() }
    }

    # Convert paths to output format
    @(foreach ($path in $currentPaths) {
        $pathNodes = @($path | Where-Object { $_._type -eq 'node' })
        $pathEdges = @($path | Where-Object { $_._type -eq 'edge' })

        $attackPath = [CIEMAttackPath]::new()
        $attackPath.PatternId   = $Pattern.id
        $attackPath.PatternName = $Pattern.name
        $attackPath.Severity    = $Pattern.severity
        $attackPath.Category    = $Pattern.category
        $attackPath.Path        = $pathNodes
        $attackPath.Edges       = $pathEdges
        $attackPath
    })
}