Skip to content

Fix all findings of the October 2026 audit - #11

Merged
26zl merged 6 commits into
mainfrom
fix/audit-findings
Oct 17, 2026
Merged

26zl merged 6 commits into
mainfrom
fix/audit-findings

Conversation

@26zl

@26zl 26zl commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Fixes all 30 findings of the full-project audit of 2026-10-16 (commit 798d1c6): 4 High, 11 Medium, 15 Low. Everything that can be changed in the repository is changed here; the GitHub settings and the first release tag are listed under Maintainer follow-ups because a PR cannot change them.

How it was verified

All runs were sandboxed (LOCALAPPDATA, TEMP and $PROFILE redirected, machine-changing commands stubbed, a failing winget.cmd first on PATH), on Windows PowerShell 5.1 and PowerShell 7.6:

  • tests/unit.ps1 (new): 65/65 on both editions. The 31 cases added or tightened in the review pass below were also run against a copy of the code with those fixes reverse-applied: all 31 fail there, the other 34 pass.
  • tests/test.ps1: 28/28 steps; 110 commands executed, 0 failures.
  • tests/ci-functional.ps1 (mocked endpoints): 5/5, 106 of 166 exports executed (64%).
  • tests/smoke.ps1: median profile load 175 ms (PowerShell 7) and 139 ms (5.1).
  • PSScriptAnalyzer 1.25.0 with the new settings file: 0 findings. Both editions parse every .ps1 without errors; no BOM or non-ASCII characters.
  • The pin check (scheduled.yml logic) against upstream: every pinned hash matches, and the pins equal the latest upstream releases (Nerd Fonts v3.5.1, Oh My Posh v31.8.0).

Findings and fixes

ID Sev Fix
F-01 High Windows Terminal changes are recorded per value in wt-changes.json (what was there before, what was written) and reverted value by value in every installed variant; values changed after install are kept. The untouched pre-install file is kept as settings.json.psp-original. Installs without a record restore the oldest backup and keep the current file. Backups go only after a successful revert. setup.ps1, Update-Profile and Set-TerminalBackground all use the same recorder (Update-WindowsTerminalSettingsFile).
F-02 High .psprc.ps1 trust is bound to the file's SHA-256 (trustedRcHashes); a changed file is skipped with a warning until re-approved with Add-TrustedDirectory, like direnv allow. psp-doctor lists stale approvals. SECURITY.md corrected.
F-03 High `-Ref <tag
F-04 High Shared tests/lib/Sandbox.ps1: every suite redirects LOCALAPPDATA, TEMP and $PROFILE before loading the profile, installs fail-closed stubs (winget, Install-/Uninstall-Module, Set-ExecutionPolicy, Start-Process, registry writes, firewall), stubs the oh-my-posh cache clear, and points font removal at the sandbox ($script:PSP.TestFontScopes). Suites refuse to run elevated without -AllowElevated. rawhunt.ps1 needs -AllowSideEffects. Terminal-Icons is no longer removed.
F-05 Med Uninstall-Profile (including -HardResetWindowsTerminal) leaves settings.json alone when windowsTerminal.manage = false. Regression test added.
F-06 Med user-settings.json is parsed with comment/trailing-comma support on both editions; a file that still does not parse is never overwritten (wizard, trust and background writes); the previous file is kept as .bak.
F-07 Med Terminal-Icons removal only after an interactive prompt (default No), never in CI, documented.
F-08 Med Update-Profile installs nothing unless -InstallMissingTools (documented: accepts winget agreements, pinned PSFzf).
F-09 Med setup.ps1 needs no elevation (per-user font and telemetry scope; machine-wide when elevated); no restart from an elevated setup; the wizard runs setup in the current session; winutil hashes and runs in-memory bytes; elevated launches use resolved paths (Get-PspShellPath, wt.exe by Get-Command -CommandType Application).
F-10 Med killport/Stop-ListeningPort refuse core Windows processes; Remove-LockedItem lists lockers and confirms each (-Tree opt-in); Clear-Cache validates the temp path, refuses drive roots/home/system folders and asks first (ConfirmImpact High); dstop/dprune ask first; pkill refuses match-all patterns and asks for wildcards.
F-11 Med Oh My Posh themes pinned to v31.8.0 with SHA-256 (default and presets), wizard catalog read at that tag; Nerd Fonts pinned to v3.5.1 with every curated archive's SHA-256; PSFzf -RequiredVersion 2.7.12; npx http-server@14.1.1. Weekly pin check in scheduled.yml.
F-12 Med New tests/unit.ps1 regression suite (WT revert, manage=false, .psprc.ps1 re-approval, hash gates in Update-Profile and setup.ps1 with stubbed downloads, wizard input, encoding, guards, wizard resume). CI tests the shipped functions instead of copies. Coverage audits require at least 60% of exports to execute (local gate: 70% of covered); network soft-fail only on transport exception types. CONTRIBUTING: bug fix => regression test.
F-13 Med README, CONTRIBUTING, SECURITY and the issue template corrected item by item (uninstall semantics, -All leftovers, test/coverage claims, Update-Profile description, Clear-Cache scope, Find-FileLocker on folders, required checks wording, .psprc.ps1 trust, full data-flow table incl. GITHUB_TOKEN and VirusTotal sharing, Discussions link). New "What setup changes" and "Limitations" sections.
F-14 Med setup.ps1: Set-StrictMode, $ErrorActionPreference = 'Stop' with explicit handling, native calls isolated, backups stop the write on failure, atomic writes, transcript log in %LOCALAPPDATA%\PowerShellProfile\logs, [CmdletBinding(SupportsShouldProcess)] with -WhatIf honored end to end, profile_user.ps1 backed up and not rewritten when unchanged, documented exit codes, comment-based help, Windows build check.
F-15 Med SemVer Version in the profile, CHANGELOG.md, release.yml. Ruleset changes are maintainer follow-ups below.
F-16 Low Clear-ProfileCache keeps telemetry.owned, wt-changes.json, installed.json; the marker is deleted only after the variable is removed; scope (User/Machine) recorded in the marker.
F-17 Low oldprofile.*.ps1 copies are removed with -RemoveUserData/-All (or when profile.original.ps1 was restored) and otherwise listed under Preserved; empty profile folders are removed.
F-18 Low ConvertFrom-JsonC: string-aware tokenizer for //, /* */ and trailing commas, used for every settings read.
F-19 Low sed detects and keeps the encoding/BOM (UTF-8, UTF-16 LE/BE, UTF-32, ANSI); all settings reads use Get-Utf8FileText/Read-JsonFile; relative paths resolve against the PowerShell location.
F-20 Low wifipass parses netsh wlan export profile XML; Get-WslDistro uses --running --quiet plus position-based parsing; ports fallback matches the empty remote endpoint instead of the localized state.
F-21 Low Resumed wizard state is rebuilt from the curated lists and range checks; state file in the cache folder, removed only after a successful save; editor name quoted; local-folder auto-detection off when -Ref/-ExpectedSha256 is given, requires setprofile.ps1, never the temp root.
F-22 Low certcheck/tlscert capture SslPolicyErrors and print name-mismatch and chain problems.
F-23 Low env masks secret-looking names and values unless -ShowValues; pwnd prompts with -AsSecureString when called without an argument.
F-24 Low Copy-SshKey remote script adds a newline guard and skips present keys (tested under Git's sh); trash uses Microsoft.VisualBasic.FileIO with a cancel check and reports permanent deletes; extract refuses to overwrite without -Force.
F-25 Low Comment-based help for 156 public functions (.SYNOPSIS, .EXAMPLE for the main commands) and setup.ps1/setprofile.ps1; version shown; NO_COLOR honored through Write-PspHost; oh-my-posh render timeout (PromptTimeoutMs) with fallback prompt.
F-26 Low nscan (and portscan) print an authorization notice; Full/Stealth/Vuln need confirmation (ShouldContinue) or -Force.
F-27 Low One WT merge engine; setup.ps1 loads its helpers from the verified profile (five pre-bundle helpers are copies checked by CI); locallab -Install uses setup.ps1 -CiMode instead of its own merge; load-time state lives in $script:PSP and temporaries are removed after load.
F-28 Low PR CI mocks every third-party endpoint (no public paste upload, no ipwho.is/wttr.in/rdap.org/HIBP/Toptal calls); live probes run weekly in scheduled.yml without uploading. persist-credentials: false, job timeouts, pinned windows-2025 image, concurrency group. Harden-runner stays in audit mode: block mode is not available on Windows runners (comment corrected).
F-29 Low .editorconfig, CHANGELOG.md, CODE_OF_CONDUCT.md, .mailmap (both author names -> 26zl), tracked AGENTS.md. Old commit subjects are left as they are (rewriting main would need a force-push).
F-30 Low Load-time probes use -CommandType Application (no module discovery on a miss), kubeconfig re-read only when it changes, load-time budget in CI (tests/smoke.ps1: 3 s PS7, 4 s PS5.1).

Found while testing and fixed too: user keybindings from user-settings.json were merged as a nested array (shipped bindings dropped); the wizard's quick-start preset never selected its font; empty arrays the profile created in settings.json stayed behind after uninstall.

Review pass

Two independent reviews of this branch (profile; setup, tests and CI) found further problems, fixed here with a regression case each:

  • Windows Terminal: values that an earlier version wrote without a record are now recorded against settings.json.psp-original (or the oldest backup), so uninstall restores the pre-install value instead of keeping ours; single-element arrays are no longer unrolled; bare profiles lists and profiles missing from the baseline are handled; an action the user bound to more keys is kept; -HardResetWindowsTerminal drops the record of the deleted file; settings.json.psp-original survives uninstall unless -RemoveUserData; Undo-WindowsTerminalSettingsFile is ConfirmImpact High; an unreadable user-settings.json makes setup, Update-Profile and uninstall leave settings.json alone.
  • Trust and execution: the .psprc.ps1 that runs is the text that was hashed (no swap between check and run; $PSScriptRoot still set); trust changes merge into the file on disk, so another session's entries survive; winutil -ExpectedSha256/-Force need an elevated shell (unelevated, WinUtil relaunches itself from its latest, unchecked release); Invoke-ProfileWizard -Ref vX.Y.Z checks setup.ps1 against the release checksums and passes the tag on; trash refuses SSH/WinRM/service sessions, where Windows deletes permanently.
  • Profile copies: profile.original.ps1 is never a copy of this profile (installs made before Read config files as UTF-8 and keep durable profile backups #9 could end up with one); setup and setprofile.ps1 use the oldest backup that is the user's own, and uninstall never restores a copy of this profile, so it no longer deletes the user's real original.
  • Setup: script-block runs remove their variables from the caller's session (irm | iex documented as unsupported); no restart offer after a failed install; the quick start no longer installs VS Code when it is missing; [ in a wizard reply no longer aborts the wizard; -LocalRepo prints the local bundle hash (a clone's CRLF files never match a release hash).
  • Smaller fixes: Update-Profile -InstallMissingTools works on an up-to-date install; load-time temporaries are _psp* and only those are removed (a plugin's $cfg survives); oh-my-posh timeouts kill the whole process tree and the fallback no longer runs the prompt hooks twice; Set-PspInstallState drops stale values; internal writes never prompt or honor -WhatIf twice; Clear-Cache accepts Temp\2 and continues past files in use; Clear-ProfileCache keeps background images; extract accepts PSDrive paths; nscan names its target; env masks more secret names and connection-string values.
  • Tests and CI: the sandbox blocks winget also when it is looked up as an application; Update-Tools is really executed against a recording winget; test.ps1 no longer installs PSScriptAnalyzer or overwrites the clipboard, and skips the install check without an install; the scheduled PSFzf pin check can now fail; a unit case keeps $script:SharedFunctionNames complete.

Upgrade notes for existing users

  • Directories trusted for .psprc.ps1 with an earlier version have no stored hash: their .psprc.ps1 is skipped with a warning until Add-TrustedDirectory is run once more in that directory.
  • Update-Profile no longer installs missing tools by itself; use -InstallMissingTools.
  • Run setup.ps1 unelevated; elevation is no longer required. Run winutil -ExpectedSha256/-Force from an elevated shell.
  • Run setup as a script block or a file, as the README shows; irm | iex is not supported.

Maintainer follow-ups (not possible in a PR)

  1. Ruleset "Protect main" (Settings -> Rules): add functional to the required status checks; enable "Restrict deletions" and "Block force pushes"; change the Admin role bypass to "For pull requests only"; review the three app bypasses (IDs 29110, 946600, 1143301) and remove the ones that do not need to write to main. Current settings: gh api repos/26zl/PowerShellPerfect/rulesets/13089750.
  2. Enable release immutability (Settings -> General -> Releases) so tags and release files cannot be changed after publishing.
  3. Cut the first release after merging: git tag v1.0.0 <merge commit> and git push origin v1.0.0. release.yml checks the tag against Version, publishes the files with SHA256SUMS.txt and the attestation, and takes the notes from CHANGELOG.md (adjust the 1.0.0 date first if needed). Until then the README release command has no release to download; the main install path still works.
  4. Optional: gh repo edit 26zl/PowerShellPerfect --delete-branch-on-merge; enable Discussions if you want a Q&A space (the issue template now links to Troubleshooting instead).
  5. Not done on purpose: rewriting old commit messages (needs a force-push to main). Harden-runner block mode is unavailable on Windows runners; switch release.yml (Ubuntu) to block once its first run shows the endpoints.
  6. Manual checks CI cannot do: Windows 10 and 11 client VMs (PS 5.1 and 7) for the README install, wizard, Update-Profile -Ref, uninstall with WT Stable + Preview; wifipass/Get-WslDistro under a non-English display language; behavior under WDAC/Constrained Language Mode is documented as unsupported.

26zl added 6 commits October 17, 2026 02:31
- Record every Windows Terminal change in wt-changes.json and revert it value by
  value in all installed variants on uninstall, keeping later user edits; keep
  the pre-install file as settings.json.psp-original; honor
  windowsTerminal.manage = false in uninstall (F-01, F-05).
- Bind .psprc.ps1 trust to the approved file's SHA-256 (F-02).
- Add -Ref to Update-Profile and Invoke-ProfileWizard, resolve it to a commit,
  verify release tags against SHA256SUMS.txt, and install tools only with
  -InstallMissingTools (F-03, F-08).
- Parse JSONC safely, read files as UTF-8 on Windows PowerShell 5.1, keep the
  encoding in sed, and never overwrite an unparseable user-settings.json
  (F-06, F-18, F-19).
- Guard destructive commands (killport, Stop-ListeningPort, Remove-LockedItem,
  Clear-Cache, pkill, dstop, dprune, extract, trash) and harden winutil,
  certcheck, tlscert, env, pwnd, nscan, Copy-SshKey, wifipass and
  Get-WslDistro (F-09, F-10, F-20, F-22, F-23, F-24, F-26).
- Keep state files in Clear-ProfileCache and remove profile backups with
  -RemoveUserData (F-16, F-17).
- Pin the Oh My Posh theme, Nerd Font, PSFzf and http-server (F-11).
- Add a version, comment-based help, NO_COLOR support through Write-PspHost,
  a prompt render timeout, cheaper load-time probes and a kubeconfig cache;
  keep load-time state in $script:PSP (F-25, F-27, F-30).
- Record Windows Terminal values that an earlier version wrote against the
  pre-install copy, keep single-element arrays as arrays, handle bare
  profiles lists, keep an action the user bound to more keys, drop the record
  of a hard-reset file, and keep settings.json.psp-original unless
  -RemoveUserData.
- Run the .psprc.ps1 text that was hashed (with $PSScriptRoot set) and merge
  trust changes into user-settings.json as it is on disk.
- winutil -ExpectedSha256/-Force need an elevated shell; Invoke-ProfileWizard
  checks a release's setup.ps1 against SHA256SUMS.txt and passes the tag on;
  trash refuses sessions where Windows would delete permanently; an
  unreadable user-settings.json leaves settings.json alone; a copy of this
  profile is never restored as profile.original.ps1.
- Name load-time temporaries _psp* and remove only those after load.
- Resolve -Ref to a commit, download the bundle from it and verify it
  (release SHA256SUMS.txt, -ExpectedSha256 or explicit -SkipHashCheck) before
  any change; load shared helpers from the verified profile (F-03, F-27).
- Run without elevation: per-user font and telemetry scope, machine-wide only
  when elevated, no elevated restart (F-09).
- Strict mode, Stop on errors, backups that stop the write on failure, atomic
  writes, a transcript log, -WhatIf, exit codes and Get-Help text (F-14).
- Keep unparseable user-settings.json, re-validate resumed wizard state, quote
  the editor name, ask before removing Terminal-Icons (F-06, F-07, F-21).
- Pin fonts and themes with SHA-256 and PSFzf by version (F-11).
- setprofile.ps1: help, -WhatIf, profile.original.ps1, exit codes.
- Fix the wizard quick start, which never selected its font.
- Never keep an installed copy of the profile as profile.original.ps1 (setup
  and setprofile.ps1); leave Windows Terminal alone when user-settings.json
  does not parse; no restart offer after a failed install; the quick start
  uses an installed editor; wizard replies match names literally; a
  script-block run removes its variables from the caller's session; a local
  install prints its bundle hash.
- tests/lib/Sandbox.ps1: redirect LOCALAPPDATA, TEMP and $PROFILE, block
  machine-changing commands, keep font removal in the sandbox, refuse
  elevated runs without -AllowElevated (F-04).
- tests/unit.ps1: regression tests for the audit fixes on Windows PowerShell
  5.1 and PowerShell 7 (F-12).
- tests/test.ps1: run every step in a sandbox, test the shipped functions,
  soft-fail only on transport exceptions, require an executed share.
- tests/ci-functional.ps1: mock third-party endpoints (-LiveNetwork for the
  scheduled run), never upload to the public paste, require 60% executed.
- tests/rawhunt.ps1 needs -AllowSideEffects; locallab uses setup.ps1 instead
  of its own Windows Terminal merge; tests/smoke.ps1 adds a load-time budget;
  PSScriptAnalyzer 1.25.0 with PSScriptAnalyzerSettings.psd1 (F-28, F-30).
- Put a failing, logging winget.cmd first on PATH in every sandbox and run
  Update-Tools against a recording winget; test.ps1 no longer installs
  PSScriptAnalyzer or overwrites the clipboard outside CI, and skips the
  install check on a machine without an install.
- Regression cases for every fix of the follow-up review; each fails against
  the code before the fix.
- ci.yml: windows-2025 image, persist-credentials: false, timeouts,
  concurrency, PSScriptAnalyzer 1.25.0 with the settings file, sandboxed
  loads with a load-time budget, setup structure checks, unit tests on
  PowerShell 5.1 and 7, mocked functional run (F-12, F-28, F-30).
- release.yml: publish tagged releases with SHA256SUMS.txt and a build
  provenance attestation (F-03, F-15).
- scheduled.yml: weekly live-network functional run and pin check (F-11, F-28).
- scheduled.yml: the PSFzf pin check fails when the version is missing.
- README: verified release install, no elevation, what setup changes and how
  uninstall reverts it, accurate command and test descriptions, limitations.
- SECURITY.md: release verification, content-bound .psprc.ps1 trust, pinned
  components, complete data flows. CONTRIBUTING.md: tests, regression-test
  rule, required checks, releases (F-02, F-13).
- Add CHANGELOG.md, AGENTS.md, CODE_OF_CONDUCT.md, .editorconfig and
  .mailmap; point the issue template at Troubleshooting instead of the
  disabled Discussions; extend the PR checklist (F-15, F-29).
Run tests/lint.ps1 with -NoInstall and skip the step with the install command when the pinned analyzer is missing, as tests/test.ps1 does.
@26zl
26zl merged commit f0384d2 into main Oct 17, 2026
5 checks passed
@26zl
26zl deleted the fix/audit-findings branch October 17, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant