Public/Assert-IdleNoScriptBlock.ps1

function Assert-IdleNoScriptBlock {
    <#
    .SYNOPSIS
    Asserts that the provided object does not contain any ScriptBlock objects.

    .DESCRIPTION
    This is a security-critical helper that validates data-only constraints.
    It recursively walks hashtables, enumerables, and PSCustomObjects to ensure
    no ScriptBlock objects are present.

    This helper enforces IdLE's security boundary: workflow configuration and step inputs
    must not contain executable code.

    Step implementations should use this helper to validate their inputs rather than
    implementing custom ScriptBlock checks.

    .PARAMETER InputObject
    The object to validate. Can be null, a scalar value, or a complex nested structure.

    .PARAMETER Path
    The logical path describing the current position in the data structure.
    Used in error messages to pinpoint where a ScriptBlock was found.

    .OUTPUTS
    None. Throws an ArgumentException if a ScriptBlock is found.

    .EXAMPLE
    # Validate a hashtable
    $config = @{
        Mode = 'Enabled'
        Message = 'Out of office'
    }
    Assert-IdleNoScriptBlock -InputObject $config -Path 'With.Config'

    .EXAMPLE
    # Detect ScriptBlock in nested structure
    $data = @{
        Setting = { Write-Host "bad" }
    }
    Assert-IdleNoScriptBlock -InputObject $data -Path 'Input'
    # Throws: ScriptBlocks are not allowed in request data. Found at: Input.Setting
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowNull()]
        [object] $InputObject,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Path
    )

    if ($null -eq $InputObject) { return }

    if ($InputObject -is [scriptblock]) {
        throw [System.ArgumentException]::new(
            "ScriptBlocks are not allowed in request data. Found at: $Path",
            $Path
        )
    }

    # Hashtable / Dictionary
    if ($InputObject -is [System.Collections.IDictionary]) {
        foreach ($key in $InputObject.Keys) {
            Assert-IdleNoScriptBlock -InputObject $InputObject[$key] -Path "$Path.$key"
        }
        return
    }

    # Enumerable (but not string)
    if (($InputObject -is [System.Collections.IEnumerable]) -and ($InputObject -isnot [string])) {
        $i = 0
        foreach ($item in $InputObject) {
            Assert-IdleNoScriptBlock -InputObject $item -Path "$Path[$i]"
            $i++
        }
        return
    }

    # PSCustomObject (walk note properties)
    if ($InputObject -is [pscustomobject]) {
        foreach ($p in $InputObject.PSObject.Properties) {
            if ($p.MemberType -eq 'NoteProperty') {
                # PSPropertyInfo does not expose "InputObject" here; the value is in .Value.
                Assert-IdleNoScriptBlock -InputObject $p.Value -Path "$Path.$($p.Name)"
            }
        }
    }
}