1- # MaskingHelpers — secret-redaction utilities for the round-trip integration
2- # test scripts. Imported via:
3- #
4- # Import-Module (Join-Path $PSScriptRoot 'MaskingHelpers.psm1') -Force
5- #
6-
7- # Module-scoped flag. Flip to $false locally if you need to see raw values
8- # while debugging — never commit that change.
1+ # MaskingHelpers — secret-redaction utilities for the round-trip integration test scripts.
2+
93$script :EnableMasking = $true
104
11- # Built-in regex redactions applied by Protect-LogLine on every line, in
12- # addition to caller-supplied exact-match Replacements. These cover sensitive
13- # values that the caller cannot enumerate up front (ARM async-operation
14- # signing material, request IDs, etc.).
15- #
16- # IMPORTANT: keep these patterns conservative. We deliberately do NOT mask
17- # every GUID, because well-known role-definition IDs and template hashes are
18- # legitimately public and useful in debugging. Anchor patterns to the path
19- # segment, header name, or query-parameter context that makes the value
20- # sensitive.
21- #
22- # References:
23- # - ARM async operations contract (see `Azure-AsyncOperation` /
24- # `operationStatuses` URL shape with t/c/s/h query parameters):
25- # https://learn.microsoft.com/azure/azure-resource-manager/management/async-operations
26- # - Azure correlation / request IDs (`x-ms-correlation-request-id`,
27- # `x-ms-request-id`, `x-ms-client-request-id`):
28- # https://learn.microsoft.com/azure/azure-resource-manager/management/request-limits-and-throttling
295$script :BuiltinRedactions = @ (
30- # ARM async-operation signing material: t= (timestamp), c= (cert),
31- # s= (signature), h= (hash) on operationStatuses / operationResults URLs.
32- # These query parameters are effectively a bearer credential for polling
33- # the long-running operation result and MUST NOT appear in logs.
346 @ { Pattern = ' ([?&])(t|c|s|h)=[^&'' "\s]+'
357 Replacement = ' $1$2=<REDACTED:arm-async>' }
368
37- # Azure subscription IDs embedded in ARM resource paths.
389 @ { Pattern = ' /subscriptions/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
39- Replacement = ' /subscriptions/<SUBSCRIPTION_ID >' }
10+ Replacement = ' /subscriptions/<REDACTED:subscription-id >' }
4011
41- # ARM long-running operation IDs (numeric or alphanumeric, 10+ chars)
42- # following /operationStatuses/ or /operationResults/.
4312 @ { Pattern = ' /(operationStatuses|operationResults)/[A-Za-z0-9._-]{10,}'
44- Replacement = ' /$1/<OPERATION_ID >' }
13+ Replacement = ' /$1/<REDACTED:operation-id >' }
4514
46- # x-ms-correlation-request-id, x-ms-request-id, x-ms-client-request-id
47- # header values. Match Python-repr style (`'x-ms-request-id': 'guid'`),
48- # HTTP-header style (`x-ms-request-id: guid`), and JSON style.
4915 @ { Pattern = " (?i)(['"" ]?x-ms-(?:correlation-)?(?:request|client-request)-id['"" ]?\s*[:=]\s*['"" ]?)[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
5016 Replacement = ' $1<REDACTED:request-id>' }
5117
52- # x-ms-routing-request-id: '<REGION>:<TIMESTAMP>:<guid>'. Mask the whole
53- # value because all three components (region, timestamp, GUID) leak
54- # tenant geography / activity timing.
5518 @ { Pattern = " (?i)(['"" ]?x-ms-routing-request-id['"" ]?\s*[:=]\s*['"" ]?)[A-Z0-9]+:\d{8}T\d{6}Z:[0-9a-fA-F-]{36}"
5619 Replacement = ' $1<REDACTED:routing-request-id>' }
5720
58- # Authorization: Bearer <token> — defense in depth. We do not expect
59- # bearer tokens to appear because `az --debug` redacts them by default,
60- # but if Azure CLI ever changes that, this catches them.
6121 @ { Pattern = " (?i)(authorization[:\s=]+bearer\s+)[A-Za-z0-9._\-+/=]+"
6222 Replacement = ' $1<REDACTED:bearer>' }
6323
64- # JWT-shaped tokens (eyJ<base64url>.<base64url>.<base64url>).
6524 @ { Pattern = ' \beyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}'
6625 Replacement = ' <REDACTED:jwt>' }
26+
27+ @ { Pattern = ' [A-Za-z0-9](?:[A-Za-z0-9._%+\-]*[A-Za-z0-9])?@[A-Za-z0-9](?:[A-Za-z0-9.\-]*[A-Za-z0-9])?\.[A-Za-z]{2,}'
28+ Replacement = ' <REDACTED:email>' }
6729)
6830
6931function Protect-Identifier {
@@ -78,29 +40,32 @@ function Protect-Identifier {
7840 }
7941
8042 if ([string ]::IsNullOrWhiteSpace($Value )) {
81- return ' <empty>'
43+ return ' <REDACTED: empty>'
8244 }
8345
8446 if ($Value.Length -le ($Prefix + $Suffix )) {
85- return ( ' * ' * $Value .Length )
47+ return ' <REDACTED> '
8648 }
8749
8850 return " {0}...{1}" -f $Value.Substring (0 , $Prefix ), $Value.Substring ($Value.Length - $Suffix )
8951}
9052
9153function Protect-SubscriptionId {
9254 param ([string ]$Value )
93- return Protect-Identifier - Value $Value - Prefix 3 - Suffix 0
55+ if (-not $script :EnableMasking ) { return $Value }
56+ return ' <REDACTED:subscription-id>'
9457}
9558
96- # Resource group names follow the convention `bvt-<timestamp>-<rand>-(src|tgt)-rg`.
97- # Reveal the last 7 characters so the role suffix (`-src-rg` / `-tgt-rg`) is
98- # visible in logs while the unique id stays masked.
9959function Protect-ResourceGroupName {
10060 param ([string ]$Value )
10161 return Protect-Identifier - Value $Value - Prefix 3 - Suffix 7
10262}
10363
64+ function Protect-ApimName {
65+ param ([string ]$Value )
66+ return Protect-Identifier - Value $Value - Prefix 3 - Suffix 8
67+ }
68+
10469function Protect-LogLine {
10570 param (
10671 [string ]$Line ,
@@ -113,7 +78,6 @@ function Protect-LogLine {
11378
11479 $protectedLine = $Line
11580
116- # Pass 1: caller-supplied exact-string replacements (sub id, RG name, ...).
11781 if ($Replacements ) {
11882 foreach ($entry in $Replacements.GetEnumerator ()) {
11983 if ([string ]::IsNullOrEmpty($entry.Key ) -or [string ]::IsNullOrEmpty($entry.Value )) {
@@ -124,9 +88,6 @@ function Protect-LogLine {
12488 }
12589 }
12690
127- # Pass 2: built-in regex redactions for well-known secret shapes that
128- # callers cannot enumerate up front (ARM signing material, request IDs,
129- # operation IDs, bearer tokens). Always applied.
13091 foreach ($rule in $script :BuiltinRedactions ) {
13192 $protectedLine = [System.Text.RegularExpressions.Regex ]::Replace(
13293 $protectedLine ,
@@ -137,11 +98,6 @@ function Protect-LogLine {
13798 return $protectedLine
13899}
139100
140- # Native-process invocation with masked output. Uses System.Diagnostics.Process
141- # with redirected stdout/stderr so the child's raw bytes bypass PowerShell's
142- # ErrorRecord promotion (which would otherwise leak unmasked to Start-Transcript).
143-
144- # Windows npm shims (e.g. npx.cmd) need cmd.exe; *nix shebang scripts launch directly.
145101function Resolve-NativeExecutable {
146102 param ([string ]$Name )
147103
@@ -159,8 +115,6 @@ function Resolve-NativeExecutable {
159115 return [pscustomobject ]@ { FilePath = $exePath ; Prefix = $prefix }
160116}
161117
162- # Stderr is always masked + streamed. Stdout is either captured (and returned)
163- # or masked + streamed. Sets $global:LASTEXITCODE.
164118function Invoke-MaskedProcess {
165119 [CmdletBinding ()]
166120 param (
@@ -180,7 +134,6 @@ function Invoke-MaskedProcess {
180134 $psi.RedirectStandardInput = $false
181135 $psi.UseShellExecute = $false
182136 $psi.CreateNoWindow = $true
183- # Default OEM encoding on Windows mangles `az --debug` output.
184137 $psi.StandardOutputEncoding = [System.Text.Encoding ]::UTF8
185138 $psi.StandardErrorEncoding = [System.Text.Encoding ]::UTF8
186139
@@ -194,15 +147,11 @@ function Invoke-MaskedProcess {
194147 $proc = [System.Diagnostics.Process ]::new()
195148 $proc.StartInfo = $psi
196149
197- # Allocate explicitly — `$x = if (...) { [List[T]]::new() }` returns $null
198- # because PowerShell enumerates the empty list.
199150 $stdoutBuffer = $null
200151 if ($CaptureStdout ) {
201152 $stdoutBuffer = [System.Collections.Generic.List [string ]]::new()
202153 }
203154
204- # Use ThreadJob readers rather than Register-ObjectEvent: Action handlers
205- # don't drain reliably while the main runspace is in Start-Sleep.
206155 $readerScript = {
207156 param ([System.IO.StreamReader ]$Reader ,
208157 [System.Collections.Concurrent.ConcurrentQueue [string ]]$Queue )
@@ -241,7 +190,6 @@ function Invoke-MaskedProcess {
241190 }
242191 }
243192
244- # Readers exit on EOF once the child closes its pipes.
245193 Wait-Job - Job $outJob , $errJob | Out-Null
246194
247195 while ($stderrQueue.TryDequeue ([ref ]$line )) {
@@ -268,7 +216,6 @@ function Invoke-MaskedProcess {
268216 }
269217}
270218
271- # Run `npx apiops <args>` masking ALL output streams. Returns the exit code.
272219function Invoke-MaskedApiopsCommand {
273220 [CmdletBinding ()]
274221 param (
@@ -283,9 +230,6 @@ function Invoke-MaskedApiopsCommand {
283230 return $LASTEXITCODE
284231}
285232
286- # Run `az <args>` capturing stdout (typically JSON) for the caller while
287- # masking and streaming stderr (verbose/debug output). Returns stdout as a
288- # single string. Sets/preserves $LASTEXITCODE for the caller.
289233function Invoke-MaskedAzCommand {
290234 [CmdletBinding ()]
291235 param (
@@ -303,6 +247,7 @@ Export-ModuleMember -Function `
303247 Protect-Identifier , `
304248 Protect-SubscriptionId , `
305249 Protect-ResourceGroupName , `
250+ Protect-ApimName , `
306251 Protect-LogLine , `
307252 Resolve-NativeExecutable , `
308253 Invoke-MaskedProcess , `
0 commit comments