Private/Export/ConvertTo-ExportProperty.ps1
|
<#
Copyright (c) 2025-2026 Robert Faddes SPDX-License-Identifier: MPL-2.0 #> <# .SYNOPSIS Converts export transform entries into Select-Object-compatible property definitions. .DESCRIPTION Takes an array of transform entries from ExportTransforms.psd1 and converts them into an array suitable for Select-Object -Property. Simple string entries are passed through as direct property names. Hashtable entries with a Name key support three resolution modes: Path — Dot-notation for nested property access (e.g. 'DevicesStatus.NumberOfDevices'). Also resolves ETS ScriptProperties added via Types.ps1xml. Validated against ^[a-zA-Z_][a-zA-Z0-9_.]*$ to prevent injection. Method — A parameterless method name (e.g. 'GetSummary'). Resolves class methods and ETS ScriptMethods added via Types.ps1xml. Validated against ^[a-zA-Z_][a-zA-Z0-9_]*$ to prevent injection. Expression — A string expression evaluated as a scriptblock (e.g. '$_.GetSummary()'). Supports member access on $_, string operations, conditionals, and comparisons. Validated by Test-ExportExpression using layered security: 1. Length cap (500 characters) 2. ASCII character whitelist 3. PowerShell AST parsing with whitelisted node types only 4. Variable restriction ($_ $null $true $false only) Cmdlet calls, .NET type access, arbitrary variables, assignments, redirections, and scriptblock literals are blocked. If a hashtable contains more than one of Path, Method, or Expression, the entry is skipped with a warning. #> function ConvertTo-ExportProperty { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [array] $TransformEntries ) $Properties = [System.Collections.Generic.List[object]]::new() foreach ($Entry in $TransformEntries) { if ($Entry -is [string]) { $Properties.Add($Entry) } elseif ($Entry -is [hashtable] -and $Entry.ContainsKey('Name')) { $PropertyName = $Entry.Name # Count how many resolution keys are present $ResolutionKeys = @('Path', 'Method', 'Expression') | Where-Object {$Entry.ContainsKey($_)} if ($ResolutionKeys.Count -ne 1) { Write-Warning "Skipping transform property '$PropertyName': must contain exactly one of Path, Method, or Expression (found: $($ResolutionKeys -join ', '))" continue } $Calculated = $null if ($Entry.ContainsKey('Path')) { $PropertyPath = $Entry.Path # Validate path contains only safe property-access characters if ($PropertyPath -notmatch '^[a-zA-Z_][a-zA-Z0-9_.]*$') { Write-Warning "Skipping transform property '$PropertyName': invalid path '$PropertyPath'" continue } $Calculated = @{ Name = $PropertyName Expression = [scriptblock]::Create("`$_.$PropertyPath") } } elseif ($Entry.ContainsKey('Method')) { $MethodName = $Entry.Method # Validate method name contains only safe identifier characters if ($MethodName -notmatch '^[a-zA-Z_][a-zA-Z0-9_]*$') { Write-Warning "Skipping transform property '$PropertyName': invalid method name '$MethodName'" continue } $Calculated = @{ Name = $PropertyName Expression = [scriptblock]::Create("`$_.$MethodName()") } } elseif ($Entry.ContainsKey('Expression')) { $ExpressionString = $Entry.Expression # Layered validation: length, characters, AST nodes, variable restriction if (-not (Test-ExportExpression -Expression $ExpressionString -PropertyName $PropertyName)) { continue } $Calculated = @{ Name = $PropertyName Expression = [scriptblock]::Create($ExpressionString) } } if ($Calculated) { $Properties.Add($Calculated) } } else { Write-Warning "Skipping unrecognised transform entry: $($Entry | ConvertTo-Json -Compress -Depth 1)" } } return $Properties.ToArray() } # SIG # Begin signature block # MIIF+wYJKoZIhvcNAQcCoIIF7DCCBegCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAJU25ookLbBfwe # dWMbb9cCmizhZ6FIYi7x52ZSv0js3aCCA04wggNKMIICMqADAgECAhB464iXHfI6 # gksEkDDTyrNsMA0GCSqGSIb3DQEBCwUAMD0xFjAUBgNVBAoMDVJvYmVydCBGYWRk # ZXMxIzAhBgNVBAMMGkRhdHRvUk1NLkNvcmUgQ29kZSBTaWduaW5nMB4XDTI2MDMz # MTAwMTMzMFoXDTI4MDMzMTAwMjMzMFowPTEWMBQGA1UECgwNUm9iZXJ0IEZhZGRl # czEjMCEGA1UEAwwaRGF0dG9STU0uQ29yZSBDb2RlIFNpZ25pbmcwggEiMA0GCSqG # SIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn1EpMYQgl1RgWzQj2+wp2mvdfb3UsaBS # nxEVGoQ0gj96tJ2MHAF7zsITdUjwaflKS1vE6wAlOg5EI1V79tJCMxzM0bFpOdR1 # L5F2HE/ovIAKNkHxFUF5qWU8vVeAsOViFQ4yhHpzLen0WLF6vhmc9eH23dLQy5fy # tELZQEc2WbQFa4HMAitP/P9kHAu6CUx5s4woLIOyyR06jkr3l9vk0sxcbCxx7+dF # RrsSLyPYPH+bUAB8+a0hs+6qCeteBuUfLvGzpMhpzKAsY82WZ3Rd9X38i32dYj+y # dYx+nx+UEMDLjDJrZgnVa8as4RojqVLcEns5yb/XTjLxDc58VatdAgMBAAGjRjBE # MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU # H+B0vf97dYXqdUX1YMcWhFsY6fcwDQYJKoZIhvcNAQELBQADggEBAJmD4EEGNmcD # 1JtFoRGxuLJaTHxDwBsjqcRQRE1VPZNGaiwIm8oSQdHVjQg0oIyK7SEb02cs6n6Y # NZbwf7B7WZJ4aKYbcoLug1k1x9SoqwBmfElECeJTKXf6dkRRNmrAodpGCixR4wMH # KXqwqP5F+5j7bdnQPiIVXuMesxc4tktz362ysph1bqKjDQSCBpwi0glEIH7bv5Ms # Ey9Gl3fe+vYC5W06d2LYVebEfm9+7766hsOgpdDVgdtnN+e6uwIJjG/6PTG6TMDP # y+pr5K6LyUVYJYcWWUTZRBqqwBHiLGekPbxrjEVfxUY32Pq4QfLzUH5hhUCAk4HN # XpF9pOzFLMUxggIDMIIB/wIBATBRMD0xFjAUBgNVBAoMDVJvYmVydCBGYWRkZXMx # IzAhBgNVBAMMGkRhdHRvUk1NLkNvcmUgQ29kZSBTaWduaW5nAhB464iXHfI6gksE # kDDTyrNsMA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKEC # gAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwG # CisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIIdlH7Sj1PRsB0lFmn6Y0wck0FBD # CF4YPwuIHPIDnB3vMA0GCSqGSIb3DQEBAQUABIIBAJiFx0N3UJw2TD2TAITONQGB # bz/IZF1aFg0WEu9CWUWvQrk9pbXMgmsJabCii/jIaQLUL2nKijWNuE1NjbpB8Kt0 # qDh1CZ1cVoMnRctS90zctj/XkQ0CjTbIm65D6TogE8P/nE1bzP1O+fUFVI9/O/pq # zAWXkDxzw/3FRbJ5x9wbM1FMy/EJnJAXkeB/bGj2/ngw8lsWo6tQNxuCQTVj0kjv # ZsQ+wVMTn5wrbxkmCmGPP9zrRQFcY1ur6avrhoswFGzRY2aFwFztTw6DkbPlUz/B # NTr0C82zOP2BhAoTphGAuqVgSoTQVFdUrpaswL1lGUkHL+6Zk0pYILg1ctLxZHs= # SIG # End signature block |