Public/Get-CIEMPSUScriptDefinition.ps1

function Get-CIEMPSUScriptDefinition {
    <#
    .SYNOPSIS
        Returns CIEM-owned PSU script definitions from the packaged manifest.

    .DESCRIPTION
        Builds the script definitions used by PSU module resources and by
        explicit script registration. The definitions are derived from
        data/psu-scripts.json and the attack path remediation templates bundled
        with the module.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param()

    $ErrorActionPreference = 'Stop'

    $manifestPath = Join-Path -Path $script:ModuleRoot -ChildPath 'data/psu-scripts.json'
    if (-not (Test-Path -Path $manifestPath -PathType Leaf)) {
        throw "CIEM PSU script manifest not found: $manifestPath"
    }

    $manifest = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json -Depth 10

    $normalizeScriptName = {
        param(
            [Parameter(Mandatory)]
            [AllowEmptyString()]
            [string]$Name
        )
        $Name.Replace('\', '/').TrimStart('/')
    }

    $expectedScriptNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

    foreach ($scriptDef in @($manifest.scripts)) {
        $scriptName = [string]$scriptDef.name
        if ([string]::IsNullOrWhiteSpace($scriptName)) {
            throw 'CIEM script manifest contains an entry with an empty name.'
        }

        $normalizedScriptName = & $normalizeScriptName -Name $scriptName
        if ($normalizedScriptName -match '^Checks/AttackPathRemediation-') {
            throw "CIEM script manifest script name '$scriptName' is reserved for template scripts and must not be registered in PSU automation."
        }

        if (-not $expectedScriptNames.Add($normalizedScriptName)) {
            throw "CIEM script manifest contains a duplicate script name: $normalizedScriptName"
        }

        $path = [string]$scriptDef.path
        if ([string]::IsNullOrWhiteSpace($path)) {
            throw "CIEM script manifest entry '$normalizedScriptName' is missing path."
        }

        if ([System.IO.Path]::IsPathRooted($path)) {
            throw "CIEM script manifest entry '$normalizedScriptName' must use a relative path: $path"
        }

        if ($path -match '(^|[\\/])\.\.([\\/]|$)') {
            throw "CIEM script manifest entry '$normalizedScriptName' contains invalid parent path traversal: $path"
        }

        $absolutePath = Join-Path -Path $script:ModuleRoot -ChildPath $path
        if (-not (Test-Path -Path $absolutePath -PathType Leaf)) {
            throw "CIEM script not found for registration '$normalizedScriptName': $absolutePath"
        }

        $content = Get-Content -Path $absolutePath -Raw
        if ([string]::IsNullOrWhiteSpace($content)) {
            throw "CIEM script content is empty for registration '$normalizedScriptName': $absolutePath"
        }

        [pscustomobject]@{
            Name                    = $normalizedScriptName
            Content                 = $content
            Description             = [string]$scriptDef.description
            Status                  = [string]$scriptDef.status
            Timeout                 = [double]$scriptDef.timeout
            DisableManualInvocation = [bool]$scriptDef.disableManualInvocation
            Type                    = 'Core'
        }
    }

    $remediationTemplates = $manifest.remediationTemplates
    if ($null -eq $remediationTemplates) {
        throw 'CIEM script manifest is missing remediationTemplates.'
    }

    $templateRootPath = [string]$remediationTemplates.path
    if ([string]::IsNullOrWhiteSpace($templateRootPath)) {
        throw 'CIEM script manifest remediationTemplates is missing path.'
    }

    if ([System.IO.Path]::IsPathRooted($templateRootPath)) {
        throw "CIEM script manifest remediationTemplates path must be relative: $templateRootPath"
    }

    if ($templateRootPath -match '(^|[\\/])\.\.([\\/]|$)') {
        throw "CIEM script manifest remediationTemplates path contains invalid parent path traversal: $templateRootPath"
    }

    $templatePath = [string]$remediationTemplates.templatePath
    if ([string]::IsNullOrWhiteSpace($templatePath)) {
        throw 'CIEM script manifest remediationTemplates is missing templatePath.'
    }

    if ([System.IO.Path]::IsPathRooted($templatePath)) {
        throw "CIEM script manifest remediationTemplates templatePath must be relative: $templatePath"
    }

    if ($templatePath -match '(^|[\\/])\.\.([\\/]|$)') {
        throw "CIEM script manifest remediationTemplates templatePath contains invalid parent path traversal: $templatePath"
    }

    $absoluteTemplatePath = Join-Path -Path $script:ModuleRoot -ChildPath $templatePath
    if (-not (Test-Path -Path $absoluteTemplatePath -PathType Leaf)) {
        throw "CIEM attack path remediation script template not found: $absoluteTemplatePath"
    }

    $attackPathScriptTemplate = Get-Content -Path $absoluteTemplatePath -Raw
    if ([string]::IsNullOrWhiteSpace($attackPathScriptTemplate)) {
        throw "CIEM attack path remediation script template is empty: $absoluteTemplatePath"
    }

    $templateRoot = Join-Path -Path $script:ModuleRoot -ChildPath $templateRootPath
    if (-not (Test-Path -Path $templateRoot -PathType Container)) {
        throw "CIEM attack path remediation template folder not found: $templateRoot"
    }

    $templateNamePrefixProperty = $remediationTemplates.PSObject.Properties['namePrefix']
    if (-not $templateNamePrefixProperty) {
        throw 'CIEM script manifest remediationTemplates is missing namePrefix.'
    }

    $normalizedTemplateNamePrefix = & $normalizeScriptName -Name ([string]$templateNamePrefixProperty.Value)
    if ($normalizedTemplateNamePrefix -ne '') {
        throw "CIEM script manifest remediationTemplates namePrefix must be empty so PSU attack path script names use the template file basename: $($templateNamePrefixProperty.Value)"
    }

    foreach ($templateFile in @(Get-ChildItem -Path $templateRoot -Filter '*.ps1' -File | Sort-Object Name)) {
        $normalizedScriptName = [System.IO.Path]::GetFileNameWithoutExtension($templateFile.Name)
        if (-not $expectedScriptNames.Add($normalizedScriptName)) {
            throw "CIEM script manifest contains a duplicate script name: $normalizedScriptName"
        }

        $content = MergeCIEMAttackPathRemediationScriptTemplate `
            -TemplateContent $attackPathScriptTemplate `
            -ScriptBodyContent (Get-Content -Path $templateFile.FullName -Raw) `
            -ScriptName $normalizedScriptName

        if ([string]::IsNullOrWhiteSpace($content)) {
            throw "CIEM script content is empty for registration '$normalizedScriptName': $($templateFile.FullName)"
        }

        [pscustomobject]@{
            Name                    = $normalizedScriptName
            Content                 = $content
            Description             = [string]$remediationTemplates.description
            Status                  = [string]$remediationTemplates.status
            Timeout                 = [double]$remediationTemplates.timeout
            DisableManualInvocation = [bool]$remediationTemplates.disableManualInvocation
            Type                    = 'AttackPath'
        }
    }
}