Private/Transforms.ps1
|
# Catalog transforms and the helpers that place a value at a property path. # # idOf:<type> a name as that type's id, through POST /deployments/resources/read # idsOf:<type> names as ids # relationOf:<kind> names as [{id, type: <kind>}], resolved through VirtualMetric/<kind>s # secure a SecureString, kept as one until the body is serialized # merge a hashtable deep-merged into the object at the target # status enable/disable: properties.status active or passive function Resolve-VMetricResourceId { # The ids of resources of one type, by name, in the order given. A name nothing answers to is an error. [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory)] [string] $Type, [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $Name, [AllowNull()] [System.Collections.IDictionary] $Parent ) if ($Name.Count -eq 0) { return , ([string[]]@()) } $resources = foreach ($item in $Name) { $lookup = [ordered]@{ type = $Type; name = $item } if ($Parent) { $lookup['parent'] = $Parent } $lookup } $answer = Invoke-VMetricRequest -Method POST -Path '/deployments/resources/read' -Body ([ordered]@{ resources = @($resources) }) $found = ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $answer -Name 'resources') $ids = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $Name.Count; $i++) { $match = $null foreach ($candidate in $found) { if ([string](Get-VMetricMember -InputObject $candidate -Name 'name') -ceq $Name[$i]) { $match = $candidate break } } if (-not $match -and $i -lt $found.Count) { $match = $found[$i] } if (-not $match -or -not (Get-VMetricMember -InputObject $match -Name 'exists')) { throw (New-VMetricException -Message "No $Type named '$(ConvertTo-VMetricSafeText -Text $Name[$i])' was found." -Code 'ResourceNotFound' -Category ObjectNotFound) } $ids.Add([string](Get-VMetricMember -InputObject $match -Name 'id')) } return , $ids.ToArray() } function Resolve-VMetricParamValue { # A bound value as the catalog param's transform makes it. Switches become booleans. [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Param, [AllowNull()] [object] $Value ) if ($Value -is [System.Management.Automation.SwitchParameter]) { $Value = $Value.IsPresent } $transform = [string](Get-VMetricMember -InputObject $Param -Name 'transform') # field and fieldValue resolve against the resource a set reads (Resolve-VMetricField), not here. if (-not $transform -or $transform -eq 'secure' -or $transform -eq 'merge' -or $transform -like 'field:*' -or $transform -eq 'fieldValue') { if ($Value -is [array]) { return , $Value } return $Value } if ($transform -eq 'status') { if ($Value -is [bool]) { return $(if ($Value) { 'active' } else { 'passive' }) } return [string]$Value } $separator = $transform.IndexOf(':') $name = if ($separator -gt 0) { $transform.Substring(0, $separator) } else { $transform } $argument = if ($separator -gt 0) { $transform.Substring($separator + 1) } else { '' } if ($name -eq 'idOf') { if ($null -eq $Value -or [string]$Value -eq '') { return '' } return (Resolve-VMetricResourceId -Type $argument -Name @([string]$Value))[0] } if ($name -eq 'idsOf' -or $name -eq 'relationOf') { $names = [System.Collections.Generic.List[string]]::new() foreach ($item in (ConvertTo-VMetricArray -InputObject $Value)) { if ([string]$item -ne '') { $names.Add([string]$item) } } if ($name -eq 'idsOf') { return , (Resolve-VMetricResourceId -Type $argument -Name $names.ToArray()) } $relations = [System.Collections.Generic.List[object]]::new() foreach ($id in (Resolve-VMetricResourceId -Type "VirtualMetric/$($argument)s" -Name $names.ToArray())) { $relations.Add([ordered]@{ id = $id; type = $argument }) } return , $relations.ToArray() } throw (New-VMetricException -Message "The cmdlet catalog uses a transform this module does not know ($transform). Update the VirtualMetric module." ` -Code 'UnknownTransform' -Category NotImplemented -Terminating) } function Get-VMetricEntryList { # The name/value pairs of a dictionary or a PSCustomObject. [CmdletBinding()] [OutputType([object[]])] param( [AllowNull()] [object] $InputObject ) $entries = [System.Collections.Generic.List[object]]::new() if ($InputObject -is [System.Collections.IDictionary]) { foreach ($key in $InputObject.Keys) { $entries.Add([pscustomobject]@{ Name = [string]$key; Value = $InputObject[$key] }) } } elseif (Test-VMetricObjectLike -InputObject $InputObject) { foreach ($property in $InputObject.psobject.Properties) { if ($property.MemberType -eq 'NoteProperty') { $entries.Add([pscustomobject]@{ Name = $property.Name; Value = $property.Value }) } } } return , $entries.ToArray() } function Get-VMetricChildDictionary { # The dictionary under a key, created (or converted from a PSCustomObject) when needed. The existing key's # spelling is kept: the engine reads property names case-sensitively. [CmdletBinding()] [OutputType([System.Collections.IDictionary])] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Parent, [Parameter(Mandatory)] [string] $Name ) $key = Get-VMetricDictionaryKey -Dictionary $Parent -Name $Name if ($null -eq $key) { $key = $Name $Parent[$key] = [ordered]@{} } elseif ($Parent[$key] -isnot [System.Collections.IDictionary]) { if (Test-VMetricObjectLike -InputObject $Parent[$key]) { $Parent[$key] = Copy-VMetricData -InputObject $Parent[$key] } else { $Parent[$key] = [ordered]@{} } } return $Parent[$key] } function Set-VMetricPathValue { # Sets the value at a dotted path (properties.properties.port), creating the objects on the way. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Changes an in-memory object only.')] [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [string] $Path, [AllowNull()] [object] $Value ) $segments = $Path.Split('.') $node = $Target for ($i = 0; $i -lt $segments.Length - 1; $i++) { $node = Get-VMetricChildDictionary -Parent $node -Name $segments[$i] } $last = $segments[$segments.Length - 1] $key = Get-VMetricDictionaryKey -Dictionary $node -Name $last if ($null -eq $key) { $key = $last } $node[$key] = $Value } function Get-VMetricPathValue { # The value at a dotted path, matching keys ignoring case; $null when a step is missing. [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [string] $Path ) $node = $Target foreach ($segment in $Path.Split('.')) { $node = Get-VMetricMember -InputObject $node -Name $segment if ($null -eq $node) { return $null } } if ($node -is [array]) { return , $node } return $node } # -Field and -Value (transforms field and fieldValue) change one field of a set's resource by the name # Format-List * shows it under. The resource is its own schema: a name resolves against the properties the # engine read, a property first and then a key of one of the output type's flatten objects (a device's own # settings), the way the object copies those keys onto itself; and the value takes the type of the value it # replaces. Cloud Shell's runtime/fields.ts does the same, message for message. # How many names an unknown field's message lists before it stops. $script:VMetricFieldNamesShown = 20 # A number -Value may be written as: digits, an optional fraction and exponent. No hex, no thousands # separators, whatever the culture. $script:VMetricFieldNumberPattern = '^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$' function Test-VMetricFieldLeaf { # A value -Field can set: one value, not an object or a list. [CmdletBinding()] [OutputType([bool])] param( [AllowNull()] [object] $Value ) return ($null -eq $Value -or $Value -is [string] -or $Value -is [System.ValueType]) } function Test-VMetricFieldNumber { [CmdletBinding()] [OutputType([bool])] param( [AllowNull()] [object] $Value ) return ($Value -is [long] -or $Value -is [int] -or $Value -is [short] -or $Value -is [byte] -or $Value -is [sbyte] -or $Value -is [ulong] -or $Value -is [uint] -or $Value -is [ushort] -or $Value -is [double] -or $Value -is [single] -or $Value -is [decimal]) } function Get-VMetricFieldContainer { # The flatten objects of a resource's properties, spelled as the read spells them: where a field that is no # property of its own is looked for. [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Properties, [AllowEmptyCollection()] [string[]] $Flatten = @() ) $containers = [System.Collections.Generic.List[string]]::new() foreach ($name in $Flatten) { $key = Get-VMetricDictionaryKey -Dictionary $Properties -Name $name if ($null -ne $key -and $Properties[$key] -is [System.Collections.IDictionary] -and -not $containers.Contains($key)) { $containers.Add($key) } } return , $containers.ToArray() } function Get-VMetricFieldName { # The names -Field takes on a resource, as Format-List * shows them: each property that holds one value, # then each key of the flatten objects that no property shadows, and the fields inside an object as dotted # paths (tls.status). A list is left out: -Field sets one value. [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Properties, [AllowEmptyCollection()] [string[]] $Flatten = @() ) $names = [System.Collections.Generic.List[string]]::new() $walk = { param([string] $Prefix, $Value, [int] $Depth) if (Test-VMetricFieldLeaf -Value $Value) { $names.Add($Prefix) return } if ($Value -is [System.Collections.IDictionary] -and $Depth -lt 5) { foreach ($key in $Value.Keys) { & $walk "$Prefix.$key" $Value[$key] ($Depth + 1) } } } $containers = Get-VMetricFieldContainer -Properties $Properties -Flatten $Flatten $own = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($key in $Properties.Keys) { $name = [string]$key if ($containers -contains $name) { continue } $null = $own.Add($name) & $walk $name $Properties[$key] 1 } foreach ($container in $containers) { $settings = $Properties[$container] foreach ($key in $settings.Keys) { if (-not $own.Contains([string]$key)) { & $walk ([string]$key) $settings[$key] 1 } } } return , $names.ToArray() } function Format-VMetricFieldNameList { # The names an unknown field's message offers, the first few of them. [CmdletBinding()] [OutputType([string])] param( [AllowEmptyCollection()] [string[]] $Name = @() ) if ($Name.Count -eq 0) { return 'none' } $shown = @($Name | Select-Object -First $script:VMetricFieldNamesShown | ForEach-Object { ConvertTo-VMetricSafeText -Text $_ }) $text = $shown -join ', ' if ($Name.Count -gt $script:VMetricFieldNamesShown) { $text += ', ...' } return $text } function Resolve-VMetricField { # -Field in a resource's current properties: the dotted path under properties it names, matched ignoring # case and spelled as the read spells it, and the value there now. [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Properties, [Parameter(Mandatory)] [AllowEmptyString()] [string] $Field, [AllowEmptyCollection()] [string[]] $Flatten = @(), [Parameter(Mandatory)] [string] $Label ) $shown = ConvertTo-VMetricSafeText -Text $Field $notFound = { $list = Format-VMetricFieldNameList -Name (Get-VMetricFieldName -Properties $Properties -Flatten $Flatten) New-VMetricException -Message "'$shown' is not a field of $Label that -Field can change. Its fields: $list." -Code 'FieldNotFound' -Category InvalidArgument } $segments = $Field.Split('.') foreach ($segment in $segments) { if ($segment.Trim() -eq '') { throw (& $notFound) } } $containers = Get-VMetricFieldContainer -Properties $Properties -Flatten $Flatten $path = [System.Collections.Generic.List[string]]::new() $node = $null $first = Get-VMetricDictionaryKey -Dictionary $Properties -Name $segments[0] # A flatten object named alone is no one field (the resolution below says so); followed by a key, it is # the path it spells. if ($null -ne $first -and ($containers -notcontains $first -or $segments.Length -gt 1)) { $path.Add($first) $node = $Properties[$first] } else { foreach ($container in $containers) { $key = Get-VMetricDictionaryKey -Dictionary $Properties[$container] -Name $segments[0] if ($null -ne $key) { $path.Add($container) $path.Add($key) $node = $Properties[$container][$key] break } } if ($path.Count -eq 0) { if ($null -eq $first) { throw (& $notFound) } $path.Add($first) $node = $Properties[$first] } } for ($i = 1; $i -lt $segments.Length; $i++) { $key = $null if ($node -is [System.Collections.IDictionary]) { $key = Get-VMetricDictionaryKey -Dictionary $node -Name $segments[$i] } if ($null -eq $key) { throw (& $notFound) } $path.Add($key) $node = $node[$key] } if (-not (Test-VMetricFieldLeaf -Value $node)) { throw (New-VMetricException -Message "'$shown' of $Label holds more than one value, and -Field sets one: name one of its fields, or use -Settings or -Property." ` -Code 'FieldNotSingle' -Category InvalidArgument) } [pscustomobject]@{ Path = 'properties.' + ($path -join '.') Current = $node } } function ConvertTo-VMetricFieldValue { # -Value as the type of the value it replaces: true or false for a boolean (from $true or the text true), a # number for a number (a whole one for a whole one), text for text, and as given where the field is empty. [CmdletBinding()] param( [AllowNull()] [object] $Current, [AllowNull()] [object] $Value, [Parameter(Mandatory)] [string] $Field, [Parameter(Mandatory)] [string] $Label ) if ($Value -is [System.Management.Automation.PSObject]) { $Value = $Value.psobject.BaseObject } $shown = ConvertTo-VMetricSafeText -Text $Field # A SecureString stays one: the request sends its text, and a preview masks it wherever it appears. if ($Value -is [System.Security.SecureString] -and ($null -eq $Current -or $Current -is [string])) { return $Value } if (-not (Test-VMetricFieldLeaf -Value $Value) -and $Value -isnot [System.Security.SecureString]) { throw (New-VMetricException -Message "-Value for '$shown' is one value: text, a number, or true or false." -Code 'FieldValueInvalid' -Category InvalidArgument) } $valueText = ConvertTo-VMetricSafeText -Text ([string]$Value) if ($Value -is [bool]) { $valueText = if ($Value) { 'true' } else { 'false' } } $text = if ($Value -is [string]) { $Value.Trim() } else { $null } if ($Current -is [bool]) { if ($Value -is [bool]) { return $Value } if ($text -eq 'true') { return $true } if ($text -eq 'false') { return $false } throw (New-VMetricException -Message "'$shown' of $Label is true or false, and -Value '$valueText' is neither." -Code 'FieldValueInvalid' -Category InvalidArgument) } if (Test-VMetricFieldNumber -Value $Current) { $whole = [math]::Truncate([double]$Current) -eq [double]$Current $invariant = [System.Globalization.CultureInfo]::InvariantCulture if ($whole) { # Exactly, not through a double: an id is larger than a double holds to the unit. $exact = 0L if ($Value -is [long] -or $Value -is [int] -or $Value -is [short] -or $Value -is [byte] -or $Value -is [sbyte] -or $Value -is [uint] -or $Value -is [ushort]) { return [long]$Value } if ($null -ne $text -and [long]::TryParse($text, [System.Globalization.NumberStyles]::AllowLeadingSign, $invariant, [ref] $exact)) { return $exact } } $number = $null if (Test-VMetricFieldNumber -Value $Value) { $number = [double]$Value } elseif ($null -ne $text -and $text -match $script:VMetricFieldNumberPattern) { $number = [double]::Parse($text, [System.Globalization.NumberStyles]::Float, $invariant) } if ($null -eq $number -or [double]::IsNaN($number) -or [double]::IsInfinity($number)) { throw (New-VMetricException -Message "'$shown' of $Label is a number, and -Value '$valueText' is not." -Code 'FieldValueInvalid' -Category InvalidArgument) } if (-not $whole) { return $number } if ([math]::Truncate($number) -ne $number -or [math]::Abs($number) -gt 9007199254740991) { throw (New-VMetricException -Message "'$shown' of $Label is a whole number, and -Value '$valueText' is not." -Code 'FieldValueInvalid' -Category InvalidArgument) } return [long]$number } if ($Current -is [string]) { if ($Value -is [bool] -or $Value -is [string]) { return $(if ($Value -is [bool]) { $valueText } else { $Value }) } return [System.Convert]::ToString($Value, [System.Globalization.CultureInfo]::InvariantCulture) } # An empty field: nothing to take a type from, so the value as given. return $Value } function Get-VMetricFieldCompletion { # What Tab offers after -Field: the fields of the resource the command line names, read through the engine. # A completion never fails and never signs in: without a session, a name or a resource, it offers nothing. [CmdletBinding()] [OutputType([System.Management.Automation.CompletionResult])] param( [Parameter(Mandatory)] [string] $CommandName, [AllowNull()] [System.Collections.IDictionary] $Bound, [AllowNull()] [AllowEmptyString()] [string] $WordToComplete ) if ($null -eq $script:VMetricSession -or $null -eq $Bound) { return } try { $entry = Get-VMetricCatalogCmdlet -Name $CommandName $resourceType = [string](Get-VMetricMember -InputObject $entry -Name 'resourceType') $lookup = [ordered]@{ type = $resourceType; name = $null } foreach ($param in (Get-VMetricCatalogParameter -Entry $entry)) { $target = [string](Get-VMetricMember -InputObject $param -Name 'target') $value = $Bound[[string]$param.name] if ($value -isnot [string] -or $value -eq '') { continue } if ($target -eq 'name') { $lookup['name'] = $value } elseif ($target -eq 'parent.name') { $lookup['parent'] = [ordered]@{ type = [string](Get-VMetricMember -InputObject $entry -Name 'parentType'); name = $value } } } if (-not $resourceType -or -not $lookup['name']) { return } $read = Invoke-VMetricRequest -Method POST -Path '/deployments/resources/read' -Body ([ordered]@{ resources = @($lookup) }) -AsHashtable $current = (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $read -Name 'resources')) | Select-Object -First 1 $properties = Get-VMetricMember -InputObject $current -Name 'properties' if (-not [bool](Get-VMetricMember -InputObject $current -Name 'exists') -or $properties -isnot [System.Collections.IDictionary]) { return } $flatten = [System.Collections.Generic.List[string]]::new() foreach ($field in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject (Get-VMetricCatalogType -Name ([string]$entry.output)) -Name 'flatten'))) { $flatten.Add([string]$field) } $prefix = ([string]$WordToComplete).Trim([char[]]"'`"") foreach ($name in (Get-VMetricFieldName -Properties $properties -Flatten $flatten.ToArray())) { if ($name.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { $text = ConvertTo-VMetricSafeText -Text $name [System.Management.Automation.CompletionResult]::new($text, $text, [System.Management.Automation.CompletionResultType]::ParameterValue, $text) } } } catch { return } } function Register-VMetricFieldCompleter { # Every generated set that takes -Field (transform field) completes it with Get-VMetricFieldCompletion. [CmdletBinding()] param() $completer = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $null = $parameterName, $commandAst Get-VMetricFieldCompletion -CommandName $commandName -Bound $fakeBoundParameters -WordToComplete $wordToComplete } try { $entries = @((Get-VMetricCatalog).Cmdlets.Values) } catch { # No catalog, no completion: the cmdlets say why when they are called. return } foreach ($entry in $entries) { foreach ($param in (Get-VMetricCatalogParameter -Entry $entry)) { if ([string](Get-VMetricMember -InputObject $param -Name 'transform') -like 'field:*') { Register-ArgumentCompleter -CommandName ([string]$entry.name) -ParameterName ([string]$param.name) -ScriptBlock $completer } } } } function Set-VMetricRelationValue { # relationOf: the relations of its own kind (director or cluster) are replaced and the others kept, so # -Director and -Cluster combine, and Set -Director keeps a device's clusters. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Changes an in-memory object only.')] [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $Kind, [AllowNull()] [object] $Value ) $relations = [System.Collections.Generic.List[object]]::new() foreach ($relation in (ConvertTo-VMetricArray -InputObject (Get-VMetricPathValue -Target $Target -Path $Path))) { if ([string](Get-VMetricMember -InputObject $relation -Name 'type') -ne $Kind) { $relations.Add($relation) } } foreach ($relation in (ConvertTo-VMetricArray -InputObject $Value)) { $relations.Add($relation) } Set-VMetricPathValue -Target $Target -Path $Path -Value $relations.ToArray() } function Merge-VMetricDictionary { # Deep-merges Source into Target: objects merge key by key, anything else (a list included) replaces. [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [object] $Source ) foreach ($entry in (Get-VMetricEntryList -InputObject $Source)) { $key = Get-VMetricDictionaryKey -Dictionary $Target -Name $entry.Name if ($null -ne $key -and (Test-VMetricObjectLike -InputObject $entry.Value) -and (Test-VMetricObjectLike -InputObject $Target[$key])) { $child = Get-VMetricChildDictionary -Parent $Target -Name $key Merge-VMetricDictionary -Target $child -Source $entry.Value continue } if ($null -eq $key) { $key = $entry.Name } $Target[$key] = Copy-VMetricData -InputObject $entry.Value } } function Merge-VMetricPathValue { # The merge transform: deep-merges a hashtable into the object at a dotted path. [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [string] $Path, [AllowNull()] [object] $Value, [string] $ParameterName = 'Property' ) if ($null -eq $Value) { return } if (-not (Test-VMetricObjectLike -InputObject $Value)) { throw (New-VMetricException -Message "-$ParameterName takes a hashtable of the properties to change, such as @{ name = 'value' }." ` -Code 'InvalidMergeValue' -Category InvalidArgument) } $node = $Target foreach ($segment in $Path.Split('.')) { $node = Get-VMetricChildDictionary -Parent $node -Name $segment } Merge-VMetricDictionary -Target $node -Source $Value } function Get-VMetricSecretText { # The text of every SecureString in a value, whatever its length: what a preview must never print, whatever # the server echoes. [CmdletBinding()] [OutputType([string[]])] param( [AllowNull()] [object] $InputObject ) $found = [System.Collections.Generic.List[string]]::new() $walk = { param($Value) if ($null -eq $Value -or $Value -is [string] -or $Value -is [System.ValueType]) { return } if ($Value -is [System.Security.SecureString]) { $text = [System.Net.NetworkCredential]::new('', $Value).Password if ($text.Length -gt 0) { $found.Add($text) } return } foreach ($entry in (Get-VMetricEntryList -InputObject $Value)) { & $walk $entry.Value } if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [System.Collections.IDictionary]) { foreach ($item in $Value) { & $walk $item } } } & $walk $InputObject return , $found.ToArray() } function Hide-VMetricSecretInObject { # Masks, in place, every string of an emitted object (a server's diagnostic, a what-if delta) that carries # the text of a secret the request held. [CmdletBinding()] param( [AllowNull()] [object] $InputObject, [AllowNull()] [string[]] $Secret ) if (-not $Secret -or $null -eq $InputObject) { return } $walk = { param($Value) if ($Value -is [System.Collections.IList]) { for ($i = 0; $i -lt $Value.Count; $i++) { if ($Value[$i] -is [string]) { $Value[$i] = Hide-VMetricSecretText -Text $Value[$i] -Secret $Secret } else { & $walk $Value[$i] } } return } if (-not (Test-VMetricObjectLike -InputObject $Value)) { return } if ($Value -is [System.Collections.IDictionary]) { foreach ($key in @($Value.Keys)) { if ($Value[$key] -is [string]) { $Value[$key] = Hide-VMetricSecretText -Text $Value[$key] -Secret $Secret } else { & $walk $Value[$key] } } return } foreach ($property in $Value.psobject.Properties) { if ($property.MemberType -ne 'NoteProperty') { continue } if ($property.Value -is [string]) { $property.Value = Hide-VMetricSecretText -Text $property.Value -Secret $Secret } else { & $walk $property.Value } } } & $walk $InputObject } function ConvertTo-VMetricJsonStringText { # A string as it reads between the quotes of a JSON string: as ConvertTo-Json writes it, or with -Html as # Go's encoding/json does (<, > and & escaped too), which is how the API echoes one inside JSON text. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [AllowEmptyString()] [string] $Text, [switch] $Html ) $escape = '\' + 'u' $builder = [System.Text.StringBuilder]::new() foreach ($character in $Text.ToCharArray()) { $code = [int]$character switch ($code) { 0x22 { [void]$builder.Append('\"') } 0x5C { [void]$builder.Append('\\') } 0x0A { [void]$builder.Append('\n') } 0x0D { [void]$builder.Append('\r') } 0x09 { [void]$builder.Append('\t') } 0x08 { [void]$builder.Append('\b') } 0x0C { [void]$builder.Append('\f') } default { if ($code -lt 0x20 -or ($Html -and ($code -in @(0x3C, 0x3E, 0x26, 0x2028, 0x2029)))) { [void]$builder.Append($escape).Append($code.ToString('x4')) } else { [void]$builder.Append($character) } } } } $builder.ToString() } function Get-VMetricSecretSpelling { # Every spelling of the secrets a message may carry: as they are, and as they read inside JSON text (a # secret holding a quote or a backslash is echoed as \" or \\), longest first. [CmdletBinding()] [OutputType([string[]])] param( [AllowNull()] [string[]] $Secret ) $spellings = [System.Collections.Generic.List[string]]::new() foreach ($value in $Secret) { if (-not $value) { continue } foreach ($spelling in @($value, (ConvertTo-VMetricJsonStringText -Text $value), (ConvertTo-VMetricJsonStringText -Text $value -Html))) { if (-not $spellings.Contains($spelling)) { $spellings.Add($spelling) } } } return , @($spellings | Sort-Object -Property Length -Descending) } function Hide-VMetricSecretText { # Masks the secrets in a message: a text that is a secret, of any length, becomes ***; a secret of three # characters or more is replaced wherever the text holds it, as it is or as JSON escapes it. (A shorter one # would mask every word it happens to spell; a value that holds one is masked where it sits, by # Hide-VMetricSecretValue.) [CmdletBinding()] [OutputType([string])] param( [AllowNull()] [AllowEmptyString()] [string] $Text, [AllowNull()] [string[]] $Secret ) if ([string]::IsNullOrEmpty($Text) -or -not $Secret) { return $Text } $spellings = Get-VMetricSecretSpelling -Secret $Secret foreach ($spelling in $spellings) { if ([string]::Equals($Text, $spelling, [System.StringComparison]::Ordinal)) { return '***' } } foreach ($spelling in $spellings) { if ($spelling.Length -ge 3) { $Text = $Text.Replace($spelling, '***') } } return $Text } function Get-VMetricSecretPath { # Where a value holds a SecureString, as the engine writes a what-if delta's path: properties.value, # properties.headers[0].value. A resource's secret paths are masked in its preview whatever the server # answers there. [CmdletBinding()] [OutputType([string[]])] param( [AllowNull()] [object] $InputObject ) $paths = [System.Collections.Generic.List[string]]::new() $walk = { param($Value, [string] $Path) if ($null -eq $Value -or $Value -is [string] -or $Value -is [System.ValueType]) { return } if ($Value -is [System.Security.SecureString]) { $paths.Add($Path) return } if ($Value -is [System.Collections.IDictionary] -or (Test-VMetricObjectLike -InputObject $Value)) { foreach ($entry in (Get-VMetricEntryList -InputObject $Value)) { & $walk $entry.Value $(if ($Path) { "$Path.$($entry.Name)" } else { $entry.Name }) } return } if ($Value -is [System.Collections.IEnumerable]) { $index = 0 foreach ($item in $Value) { & $walk $item "$Path[$index]" $index++ } } } & $walk $InputObject '' return , $paths.ToArray() } function Hide-VMetricSecretValue { # A copy of a what-if value for display, masked before it is encoded: a value at one of SecretPath (the # request's secrets' own paths) is *** whatever it holds, and every other string goes through # Hide-VMetricSecretText. Masking the encoded text instead would miss a secret JSON escapes, or a short one. [CmdletBinding()] param( [AllowNull()] [object] $InputObject, [AllowNull()] [string[]] $Secret, # The value's own path (properties.value). [AllowNull()] [AllowEmptyString()] [string] $Path, [AllowNull()] [string[]] $SecretPath ) $secrets = @($Secret) $secretPaths = @($SecretPath) $walk = { param($Value, [string] $Here) if ($null -eq $Value) { return $null } $atSecret = $false foreach ($candidate in $secretPaths) { if ($candidate -and [string]::Equals($candidate, $Here, [System.StringComparison]::OrdinalIgnoreCase)) { $atSecret = $true } } if ($Value -is [System.Security.SecureString]) { return '***' } if ($Value -is [string]) { if ($atSecret -and $Value -ne '') { return '***' } return (Hide-VMetricSecretText -Text $Value -Secret $secrets) } if ($Value -is [System.ValueType]) { if ($atSecret) { return '***' } return $Value } if ($Value -is [System.Collections.IDictionary] -or (Test-VMetricObjectLike -InputObject $Value)) { $map = [ordered]@{} foreach ($entry in (Get-VMetricEntryList -InputObject $Value)) { $map[$entry.Name] = & $walk $entry.Value $(if ($Here) { "$Here.$($entry.Name)" } else { $entry.Name }) } return $map } if ($Value -is [System.Collections.IEnumerable]) { $list = [System.Collections.Generic.List[object]]::new() $index = 0 foreach ($item in $Value) { $list.Add((& $walk $item "$Here[$index]")) $index++ } return , $list.ToArray() } return $Value } $masked = & $walk $InputObject ([string]$Path) if ($masked -is [array]) { return , $masked } return $masked } |