Private/Hide-IDBridgeSecureString.ps1

<#
.SYNOPSIS
Replace every SecureString value in an object graph with $null, in place.

.DESCRIPTION
Internal helper for the PostRun plugin payload. Walks the RunResult object graph and nulls
out any value that is a [SecureString] — no named-field list, so new credential fields are
covered automatically (ADKey/GoogleKey, PassphraseAPI Nonce/AuthToken, create-splat
passwords, and anything added later).

Recurses into hashtables/IDictionary values, IList/array elements, and PSCustomObject
properties only. Other typed objects (ADUser, Google API responses' typed members, etc.)
are not walked — they are large graphs, hold no SecureStrings, and their properties are
often read-only. A reference-equality visited set makes the walk cycle-safe (source records
reference shared PassphraseAPI hashtables and appear in multiple change lists).

Mutates the input in place; called only at end of run, after all writes, when nothing
downstream needs the SecureStrings anymore.

.PARAMETER InputObject
The root of the object graph to scrub.

.OUTPUTS
None. The input object graph is modified in place.

.EXAMPLE
Hide-IDBridgeSecureString -InputObject $runResult

.NOTES
   Created by: Sam Cattanach
   Modified: 2026-07-12
#>

function Hide-IDBridgeSecureString {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [AllowNull()]
        $InputObject
    )

    $visited = [System.Collections.Generic.HashSet[object]]::new(
        [System.Collections.Generic.ReferenceEqualityComparer]::Instance
    )

    # Local recursive walker. Only containers we can both inspect and assign into are
    # traversed; everything else is a leaf.
    function Invoke-Scrub {
        param ($Node)

        if ($null -eq $Node -or $Node -is [string] -or $Node.GetType().IsValueType) { return }
        if (-not $visited.Add($Node)) { return }

        if ($Node -is [System.Collections.IDictionary]) {
            # Copy the key list first - we assign into the dictionary while iterating.
            foreach ($key in @($Node.Keys)) {
                if ($Node[$key] -is [System.Security.SecureString]) {
                    $Node[$key] = $null
                } else {
                    Invoke-Scrub -Node $Node[$key]
                }
            }
            return
        }

        if ($Node -is [System.Collections.IList]) {
            for ($i = 0; $i -lt $Node.Count; $i++) {
                if ($Node[$i] -is [System.Security.SecureString]) {
                    try { $Node[$i] = $null } catch { } # read-only/fixed collections: leave as-is
                } else {
                    Invoke-Scrub -Node $Node[$i]
                }
            }
            return
        }

        if ($Node -is [System.Management.Automation.PSCustomObject]) {
            foreach ($property in $Node.PSObject.Properties) {
                if ($property.Value -is [System.Security.SecureString]) {
                    if ($property.IsSettable) { $property.Value = $null }
                } else {
                    Invoke-Scrub -Node $property.Value
                }
            }
        }
    }

    Invoke-Scrub -Node $InputObject
}