From f984bec0e34ccdfb4ba00db8ca776f93b7e5900b Mon Sep 17 00:00:00 2001
From: TheAbider <51920546+TheAbider@users.noreply.github.com>
Date: Wed, 1 Jul 2026 06:07:36 -0700
Subject: [PATCH 1/2] Harden credential & secret handling (v1.121.10)
Fixes from an adversarial audit of password generation, SecureString hygiene,
and secret-leak sinks.
22-Password.ps1:
- New-StrongPassword no longer returns the generated plaintext. The sole caller
used it as a bare statement, streaming the password to top-level Out-Default,
which re-echoed it to the console AND into the just-resumed on-disk transcript
-- defeating the Stop-Transcript display mitigation the function performs. It's
shown once via [Console]::WriteLine and never leaves the function.
- Keep the clipboard auto-clear Timer rooted (script scope). An unreferenced
System.Threading.Timer is GC-eligible immediately and its finalizer cancels the
pending callback, so the advertised 60s clipboard clear could silently never run,
leaving the plaintext in the clipboard / Win+V history.
56-OperationsMenu.ps1:
- Collect the FileServer CF-Access-Client-Secret with Read-Host -AsSecureString and
marshal it with a zero-freed BSTR, instead of a plain Read-Host that echoes the
secret to the console (and thus the transcript).
Audit also confirmed the generator already uses RNGCryptoServiceProvider, and that
the RADIUS/VPN/SIEM/Azure-Arc secrets required in plaintext by their native tools
(netsh, azcmagent, agent config) are handled as safely as those tools permit.
Tests: Section 197 (7 asserts); 5315/5315 passing. PSSA 0. Monolithic + version
stamps -> 1.121.10.
---
Changelog.md | 12 ++++++++++++
Header.ps1 | 2 +-
Modules/00-Initialization.ps1 | 2 +-
Modules/22-Password.ps1 | 20 +++++++++++++++----
Modules/56-OperationsMenu.ps1 | 18 ++++++++++++++---
README.md | 2 +-
RackStack.ps1 | 2 +-
RackStack.psd1 | 2 +-
Tests/Run-Tests.ps1 | 37 ++++++++++++++++++++++++++++++++++-
9 files changed, 84 insertions(+), 13 deletions(-)
diff --git a/Changelog.md b/Changelog.md
index e6c337b..cd303b0 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,5 +1,17 @@
# Changelog
+## v1.121.10
+
+Credential-handling hardening — fixes found by a deep audit of password generation, SecureString hygiene, and secret leakage.
+
+- **A generated password no longer ends up in the session transcript.** The strong-password generator already went out of its way to keep the password off-screen-log by pausing transcription while it displayed — but it then *returned* the password, which the menu echoed back to the console (and into the transcript on disk). It no longer returns the plaintext; it's shown once for you to record and nothing more.
+- **The clipboard auto-clear actually runs now.** The 60-second "auto-clears from clipboard" timer could be garbage-collected before it fired, silently leaving the password in the clipboard (and Windows clipboard history). The timer is now held so it reliably clears.
+- **The file-server access secret is entered hidden.** Setting the Cloudflare Access client secret in the in-tool settings used a normal prompt that echoed the secret to the screen (and transcript). It's now entered masked and handled without leaving a plaintext copy behind.
+
+(The audit also confirmed the password generator already uses a cryptographic RNG, and that the RADIUS / VPN / SIEM / Azure Arc secrets that must be passed to their native tools are handled as safely as those tools allow.)
+
+No module or CLI action changes (81 modules, 201 actions).
+
## v1.121.9
Installer download & agent-install hardening — fixes found by a deep audit of the download → verify → execute chain (FileServer, Agent Installer, ISO download).
diff --git a/Header.ps1 b/Header.ps1
index 1b8f0b3..4c0b0b8 100644
--- a/Header.ps1
+++ b/Header.ps1
@@ -30,7 +30,7 @@
7h3 4b1d3r
.VERSION
- 1.121.9
+ 1.121.10
.LAST UPDATED
07/01/2026
diff --git a/Modules/00-Initialization.ps1 b/Modules/00-Initialization.ps1
index 9dbaff3..6517ab3 100644
--- a/Modules/00-Initialization.ps1
+++ b/Modules/00-Initialization.ps1
@@ -233,7 +233,7 @@ if (-not $PSCommandPath -and $script:ScriptPath) {
if (-not $script:ModuleRoot -and $script:ScriptPath) {
$script:ModuleRoot = [System.IO.Path]::GetDirectoryName($script:ScriptPath)
}
-$script:ScriptVersion = "1.121.9"
+$script:ScriptVersion = "1.121.10"
$script:ScriptStartTime = Get-Date
# Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe.
diff --git a/Modules/22-Password.ps1 b/Modules/22-Password.ps1
index 6cf2e60..c2a5c6c 100644
--- a/Modules/22-Password.ps1
+++ b/Modules/22-Password.ps1
@@ -427,15 +427,27 @@ function New-StrongPassword {
}
} catch { }
}
- # Timer is fire-once (period = -1). Reference is intentionally not held;
- # GC will reclaim after the callback fires.
- $null = New-Object System.Threading.Timer($timerCallback, $expectedClip, ($clipTTL * 1000), [System.Threading.Timeout]::Infinite)
+ # Timer is fire-once (period = -1). It MUST be kept rooted: an unreferenced
+ # System.Threading.Timer is GC-eligible immediately and its finalizer CANCELS the
+ # pending callback, so a collection within the 60s window (ordinary gen0 GC in the menu
+ # loop, or the explicit [GC]::Collect() in Clear-SecureMemory) would silently skip the
+ # clipboard clear and leave the plaintext in the clipboard/Win+V history indefinitely.
+ # Hold it in a script-scope field; dispose the prior one so repeated generation doesn't
+ # leak timers.
+ if ($script:_clipClearTimer) { try { $script:_clipClearTimer.Dispose() } catch { } }
+ $script:_clipClearTimer = New-Object System.Threading.Timer($timerCallback, $expectedClip, ($clipTTL * 1000), [System.Threading.Timeout]::Infinite)
Write-OutputColor " Copied to clipboard (auto-clears in ${clipTTL}s while this window is open)." -color "Success"
}
catch {
Write-OutputColor " Tip: Select and copy the password above." -color "Info"
}
- return $password
+ # Deliberately DO NOT return the plaintext. The only caller (menu option 11) discards the
+ # value, and returning it emits the password on the success stream to top-level Out-Default,
+ # which both re-echoes it to the console AND writes it into the (just-resumed) on-disk
+ # transcript — defeating the Stop-Transcript dance above that kept it out of the log. The
+ # password is shown once via [Console]::WriteLine for the operator to record; it must not
+ # leave this function.
+ return
}
#endregion
\ No newline at end of file
diff --git a/Modules/56-OperationsMenu.ps1 b/Modules/56-OperationsMenu.ps1
index c2aaf32..b73f672 100644
--- a/Modules/56-OperationsMenu.ps1
+++ b/Modules/56-OperationsMenu.ps1
@@ -1627,9 +1627,21 @@ function Show-EditDefaults {
$val = Read-Host
if (-not [string]::IsNullOrWhiteSpace($val)) { $script:FileServer.ClientId = $val }
- Write-OutputColor " Enter CF-Access-Client-Secret:" -color "Info"
- $val = Read-Host
- if (-not [string]::IsNullOrWhiteSpace($val)) { $script:FileServer.ClientSecret = $val }
+ # Collect the CF-Access-Client-Secret hidden: a plain Read-Host echoes the typed
+ # secret to the console, which Start-Transcript then persists to the on-disk log.
+ # Marshal the SecureString to the plaintext string the FileServer needs as an HTTP
+ # header, zero-freeing the BSTR in a finally. (ClientId above is not a secret.)
+ Write-OutputColor " Enter CF-Access-Client-Secret (input hidden, leave empty to keep current):" -color "Info"
+ $secVal = Read-Host -AsSecureString
+ if ($secVal -and $secVal.Length -gt 0) {
+ $secBstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secVal)
+ try {
+ $script:FileServer.ClientSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($secBstr)
+ }
+ finally {
+ [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($secBstr)
+ }
+ }
Write-OutputColor " Enter ISOs subfolder name (default: ISOs):" -color "Info"
$val = Read-Host
diff --git a/README.md b/README.md
index caf45ec..220bb39 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
-
+
diff --git a/RackStack.ps1 b/RackStack.ps1
index 4b11057..fd4c407 100644
--- a/RackStack.ps1
+++ b/RackStack.ps1
@@ -13,7 +13,7 @@
Environment-specific settings are configured via defaults.json.
.VERSION
- 1.121.9
+ 1.121.10
.NOTES
- Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing)
- Must be run as Administrator
diff --git a/RackStack.psd1 b/RackStack.psd1
index b03361c..8f8cf9e 100644
--- a/RackStack.psd1
+++ b/RackStack.psd1
@@ -1,6 +1,6 @@
@{
RootModule = 'RackStack.psm1'
- ModuleVersion = '1.121.9'
+ ModuleVersion = '1.121.10'
GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91'
Author = 'TheAbider'
CompanyName = 'TheAbider'
diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1
index dcbb86c..63e3732 100644
--- a/Tests/Run-Tests.ps1
+++ b/Tests/Run-Tests.ps1
@@ -1,6 +1,6 @@
<#
.SYNOPSIS
- Automated Test Runner for RackStack v1.121.9
+ Automated Test Runner for RackStack v1.121.10
.DESCRIPTION
Comprehensive non-interactive test suite covering:
@@ -9914,6 +9914,41 @@ catch {
Write-TestResult "Download-Execute Hardening Tests" $false $_.Exception.Message
}
+# ============================================================================
+# SECTION 197: CREDENTIAL & SECRET HANDLING (v1.121.10)
+# ============================================================================
+# Guards three fixes from the credential/secret-handling audit:
+# H New-StrongPassword must not RETURN the generated plaintext — a bare-statement caller would
+# stream it to top-level Out-Default, re-echoing it to the console AND into the on-disk
+# transcript, defeating the module's own Stop-Transcript display mitigation.
+# L1 the clipboard auto-clear Timer must be kept rooted (script-scope), or GC cancels it and the
+# plaintext lingers in the clipboard / Win+V history.
+# L2 the FileServer CF-Access-Client-Secret must be collected hidden (-AsSecureString) and marshalled
+# with a zero-freed BSTR, so a plain Read-Host doesn't echo it to console/transcript.
+Write-SectionHeader "SECTION 197: CREDENTIAL & SECRET HANDLING"
+
+try {
+ $pwRaw = Get-Content "$modulesPath\22-Password.ps1" -Raw
+ $opsRaw = Get-Content "$modulesPath\56-OperationsMenu.ps1" -Raw
+
+ # H — no plaintext return of the generated password (case-sensitive: the generated var is
+ # lowercase $password; an unrelated entry-helper legitimately returns $Password1).
+ Write-TestResult "22-Password: New-StrongPassword does not return the plaintext (H)" (-not ($pwRaw -cmatch 'return \$password'))
+ Write-TestResult "22-Password: still shows the password via [Console]::WriteLine (H)" ($pwRaw -match '\[Console\]::WriteLine\([\s\S]{0,20}\$\(\$password')
+
+ # L1 — clipboard auto-clear timer rooted in script scope; old unrooted form gone.
+ Write-TestResult "22-Password: clipboard clear timer is kept rooted (L1)" ($pwRaw -match '\$script:_clipClearTimer = New-Object System\.Threading\.Timer')
+ Write-TestResult "22-Password: prior clipboard timer disposed on regen (L1)" ($pwRaw -match 'if \(\$script:_clipClearTimer\) \{ try \{ \$script:_clipClearTimer\.Dispose')
+ Write-TestResult "22-Password: no unrooted (`$null =) timer (L1)" (-not ($pwRaw -match '\$null = New-Object System\.Threading\.Timer'))
+
+ # L2 — CF client secret collected hidden and zero-freed.
+ Write-TestResult "56-Ops: CF client secret uses Read-Host -AsSecureString (L2)" ($opsRaw -match 'Enter CF-Access-Client-Secret[\s\S]{0,140}Read-Host -AsSecureString')
+ Write-TestResult "56-Ops: CF client secret BSTR is zero-freed (L2)" ($opsRaw -match 'ClientSecret = \[System\.Runtime\.InteropServices\.Marshal\]::PtrToStringBSTR[\s\S]{0,200}ZeroFreeBSTR')
+}
+catch {
+ Write-TestResult "Credential & Secret Handling Tests" $false $_.Exception.Message
+}
+
# ============================================================================
# SECTION 174: DOCUMENTATION FRESHNESS (counts must match the codebase)
# ============================================================================
From dd6c4fb33b0e81801c5b3c624dfb8712759d8ad8 Mon Sep 17 00:00:00 2001
From: TheAbider <51920546+TheAbider@users.noreply.github.com>
Date: Wed, 1 Jul 2026 06:24:28 -0700
Subject: [PATCH 2/2] Return generated password only under -PassThru (fix
Pester unit tests)
The initial fix removed New-StrongPassword's plaintext return outright, which broke
the Password.Tests.ps1 Pester tests that capture the value to verify length/complexity/
uniqueness. Gate the return behind an explicit -PassThru switch instead: the interactive
menu action calls it WITHOUT -PassThru (so the password still never streams to Out-Default /
the transcript), while tests and programmatic callers opt in and handle the value.
- 22-Password.ps1: add [switch]$PassThru; return the plaintext only when set.
- Password.Tests.ps1: pass -PassThru at the call sites that assert on the value.
- Run-Tests.ps1 Section 197: assert the return is -PassThru-guarded and the menu caller
does not opt in.
Structural 5317/5317; full Pester 312/312; monolithic re-synced.
---
Modules/22-Password.ps1 | 19 +++++++++++-------
README.md | 2 +-
Tests/Pester/Password.Tests.ps1 | 34 ++++++++++++++++-----------------
Tests/Run-Tests.ps1 | 13 +++++++++----
4 files changed, 39 insertions(+), 29 deletions(-)
diff --git a/Modules/22-Password.ps1 b/Modules/22-Password.ps1
index c2a5c6c..977839b 100644
--- a/Modules/22-Password.ps1
+++ b/Modules/22-Password.ps1
@@ -328,7 +328,11 @@ function Show-LocalAccountAudit {
# Function to generate a strong random password
function New-StrongPassword {
- param([int]$Length = 16)
+ # -PassThru returns the generated plaintext to the caller. It is OFF by default: the
+ # interactive menu action must NOT receive the value (a bare-statement call would stream it
+ # to top-level Out-Default, re-echoing it to the console and into the on-disk transcript).
+ # Only explicit programmatic/test callers that capture and handle the value pass -PassThru.
+ param([int]$Length = 16, [switch]$PassThru)
if ($Length -lt 12) { $Length = 12 }
if ($Length -gt 128) { $Length = 128 }
@@ -442,12 +446,13 @@ function New-StrongPassword {
Write-OutputColor " Tip: Select and copy the password above." -color "Info"
}
- # Deliberately DO NOT return the plaintext. The only caller (menu option 11) discards the
- # value, and returning it emits the password on the success stream to top-level Out-Default,
- # which both re-echoes it to the console AND writes it into the (just-resumed) on-disk
- # transcript — defeating the Stop-Transcript dance above that kept it out of the log. The
- # password is shown once via [Console]::WriteLine for the operator to record; it must not
- # leave this function.
+ # Return the plaintext ONLY when the caller explicitly opts in via -PassThru (tests /
+ # programmatic use, which capture and handle it). The interactive menu action (option 11)
+ # calls this WITHOUT -PassThru, so the password never emits on the success stream to
+ # top-level Out-Default — which would re-echo it to the console AND write it into the
+ # just-resumed on-disk transcript, defeating the Stop-Transcript dance above. On the default
+ # path it is shown once via [Console]::WriteLine for the operator to record and nothing more.
+ if ($PassThru) { return $password }
return
}
#endregion
\ No newline at end of file
diff --git a/README.md b/README.md
index 220bb39..b2df9dc 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
-
+
diff --git a/Tests/Pester/Password.Tests.ps1 b/Tests/Pester/Password.Tests.ps1
index a73519c..639d536 100644
--- a/Tests/Pester/Password.Tests.ps1
+++ b/Tests/Pester/Password.Tests.ps1
@@ -89,49 +89,49 @@ Describe 'New-StrongPassword' {
Context 'Length boundaries' {
It 'defaults to length 16' {
- $pw = New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null
$pw.Length | Should -Be 16
}
It 'respects an explicit length parameter (24)' {
- $pw = New-StrongPassword -Length 24 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru -Length 24 6>$null 4>$null 5>$null 3>$null 2>$null
$pw.Length | Should -Be 24
}
It 'floors length to 12 when caller asks for less' {
- $pw = New-StrongPassword -Length 4 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru -Length 4 6>$null 4>$null 5>$null 3>$null 2>$null
$pw.Length | Should -Be 12
}
It 'caps length at 128 when caller asks for more' {
- $pw = New-StrongPassword -Length 1000 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru -Length 1000 6>$null 4>$null 5>$null 3>$null 2>$null
$pw.Length | Should -Be 128
}
}
Context 'Generated complexity' {
It 'contains at least one uppercase letter' {
- $pw = New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null
$pw | Should -Match '[A-Z]'
}
It 'contains at least one lowercase letter' {
- $pw = New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null
$pw | Should -Match '[a-z]'
}
It 'contains at least one digit (excluding ambiguous 0/1)' {
- $pw = New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null
$pw | Should -Match '[2-9]'
}
It 'contains at least one special char from the safe set !@#%^&*-_=+' {
- $pw = New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null
$pw | Should -Match '[!@#%^&\*\-_=+]'
}
It 'excludes ambiguous characters 0, 1, I, O, i, l, o (case-sensitive check)' {
- $pw = New-StrongPassword -Length 64 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru -Length 64 6>$null 4>$null 5>$null 3>$null 2>$null
# PowerShell -match (and Pester Should -Match) is case-insensitive by default,
# which would falsely flag uppercase L. Use -cnotmatch for a literal case-sensitive check.
($pw -cmatch '[01IOilo]') | Should -BeFalse
@@ -144,7 +144,7 @@ Describe 'New-StrongPassword' {
# includes # and -, and the Fisher-Yates shuffle can land one of those
# first — that's an existing generator/validator coherence gap, not
# something this test should pin. Check the per-class rules directly.
- $pw = New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null
+ $pw = New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null
$pw.Length | Should -BeGreaterOrEqual 12
($pw -cmatch '[A-Z]') | Should -BeTrue
($pw -cmatch '[a-z]') | Should -BeTrue
@@ -156,7 +156,7 @@ Describe 'New-StrongPassword' {
Context 'Cryptographic randomness (statistical)' {
It 'produces distinct passwords across calls' {
$samples = 1..10 | ForEach-Object {
- New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null
+ New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null
}
($samples | Select-Object -Unique).Count | Should -Be 10
}
@@ -166,12 +166,12 @@ Describe 'New-StrongPassword' {
It 'exercises the clipboard-success branch when Set-Clipboard works' {
Mock Set-Clipboard { } # silent success
Mock Get-Clipboard { 'whatever' } # for the timer callback path
- { New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
+ { New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
}
It 'exercises the clipboard-failure branch when Set-Clipboard throws' {
Mock Set-Clipboard { throw 'clipboard locked' }
- { New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
+ { New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
}
}
@@ -187,7 +187,7 @@ Describe 'New-StrongPassword' {
Mock Stop-Transcript { 'fake-transcript-path' } # truthy = was running
Mock Start-Transcript { } # no-op success
Mock Set-Clipboard { }
- { New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
+ { New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
} finally {
$script:TranscriptPath = $oldTp
}
@@ -200,7 +200,7 @@ Describe 'New-StrongPassword' {
Mock Stop-Transcript { 'something-truthy' }
Mock Start-Transcript { } # the else branch path
Mock Set-Clipboard { }
- { New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
+ { New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
} finally {
$script:TranscriptPath = $oldTp
}
@@ -209,7 +209,7 @@ Describe 'New-StrongPassword' {
It 'exercises the no-transcript-running path (Stop-Transcript throws)' {
Mock Stop-Transcript { throw 'no transcript running' } # catch -> $transcriptWasRunning stays false
Mock Set-Clipboard { }
- { New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
+ { New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
}
It 'exercises the transcript-restart-fails path (Start-Transcript throws)' {
@@ -219,7 +219,7 @@ Describe 'New-StrongPassword' {
Mock Stop-Transcript { 'truthy' }
Mock Start-Transcript { throw 'restart failed' } # outer catch swallows it
Mock Set-Clipboard { }
- { New-StrongPassword 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
+ { New-StrongPassword -PassThru 6>$null 4>$null 5>$null 3>$null 2>$null } | Should -Not -Throw
} finally {
$script:TranscriptPath = $oldTp
}
diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1
index 63e3732..b1c8bb9 100644
--- a/Tests/Run-Tests.ps1
+++ b/Tests/Run-Tests.ps1
@@ -9930,10 +9930,15 @@ Write-SectionHeader "SECTION 197: CREDENTIAL & SECRET HANDLING"
try {
$pwRaw = Get-Content "$modulesPath\22-Password.ps1" -Raw
$opsRaw = Get-Content "$modulesPath\56-OperationsMenu.ps1" -Raw
-
- # H — no plaintext return of the generated password (case-sensitive: the generated var is
- # lowercase $password; an unrelated entry-helper legitimately returns $Password1).
- Write-TestResult "22-Password: New-StrongPassword does not return the plaintext (H)" (-not ($pwRaw -cmatch 'return \$password'))
+ $runnerRaw197 = Get-Content "$modulesPath\49-MenuRunner.ps1" -Raw
+
+ # H — the generated plaintext is returned ONLY under an explicit -PassThru opt-in (tests /
+ # programmatic callers that capture it). The interactive menu action calls New-StrongPassword
+ # WITHOUT -PassThru, so on the default path the password never streams to top-level
+ # Out-Default (which would re-echo it to the console and into the on-disk transcript).
+ Write-TestResult "22-Password: New-StrongPassword has a -PassThru opt-in switch (H)" ($pwRaw -match '\[switch\]\$PassThru')
+ Write-TestResult "22-Password: plaintext return is guarded by -PassThru (H)" ($pwRaw -match 'if \(\$PassThru\) \{ return \$password \}')
+ Write-TestResult "22-Password: interactive menu caller does NOT pass -PassThru (H)" ($runnerRaw197 -match '"11"\s*\{\s*New-StrongPassword;')
Write-TestResult "22-Password: still shows the password via [Console]::WriteLine (H)" ($pwRaw -match '\[Console\]::WriteLine\([\s\S]{0,20}\$\(\$password')
# L1 — clipboard auto-clear timer rooted in script scope; old unrooted form gone.