Complete #147, #152, #153 - the partial fixes from 0.74.0 - #155
Merged
Conversation
#147 - the remainder, WITHOUT requiring admin: - Set-MsixToolVerification -Enabled $false is the new escape hatch for air-gapped agents: session-scoped and in-memory, so NO admin rights are needed. The module must never require elevation - that is why offreg.dll is used instead of reg.exe load in the first place. - A process/user-scoped MSIX_SKIP_TOOL_VERIFICATION is now IGNORED (with a loud Warning pointing at the cmdlet). That is the actual attack: a non-admin can persist an env var in HKCU\Environment and every FUTURE session silently trusts an arbitrary tools root. A machine-scoped value is still honoured for fleet configuration; note that READING it needs no admin, only writing does. - _MsixTestTrustedExecutable: Resolve-MsixProcMonPath and Resolve-MsixDebugViewPath now Authenticode-verify every candidate before returning it. ProcMon needs its kernel driver so it runs ELEVATED, and the old resolver trusted both a user-settable override and a fixed 'C:\PSF\ProcessMonitor\Procmon.exe' fallback - a directory any standard user can create, since the root of C: grants Authenticated Users CreateDirectories. That was a local privilege-escalation path. Untrusted candidates are skipped rather than fatal, so a poisoned override cannot deny service either. #153 - the remainder: - _MsixOfflineSaveHive returns $false on failure and does not throw; the return was discarded, so a failed save shipped a SIGNED modification package with none of the requested registry keys, exit code 0, and a log line claiming success. Now throws (and no longer leaks a [bool] into the output stream). - Idempotency guards added for Add-MsixFileTypeAssociation, Add-MsixShellVerbExtension, Add-MsixFileExplorerContextMenu and the Rule element of Add-MsixFirewallRule. All four appended unconditionally, so a re-run - or Invoke-MsixPlaybook replaying a step list verbatim - produced duplicates; a conflicting FTA makes Add-AppxPackage reject the package. - The rest of the @($null) cluster: 15 more sites across 15 files. No <Applications> makes @(...) an array holding one $null whose .Count reports 1, so count-based guards pass and the element then throws on any method call - or, in Compare-MsixPackage, silently produced a phantom app entry. - Invoke-MsixAutoFixLoop -CaptureTrace called Invoke-MsixProcMonCapture with -PackagePath, which is not a parameter, inside a catch that logged and continued - so the documented feature had never once worked. Now derives the -PackageFamilyName / -AppId the cmdlet actually takes. - Test-MsixSignature: NotTrusted added to NeedsSelfSign; such a package will not install in a clean sandbox, which is exactly what -AutoSign is for. - Add-MsixVcRuntimeBundle no longer coerces an undetectable (or arm64) architecture to x86 while logging "auto-detected", and no longer packs a PARTIAL bundle as success - both shipped a package that fails at launch. - Write-MsixLog writes UTF-8 with -LiteralPath: the ANSI default on 5.1 turned the arrows and box drawing this module emits into literal '?', and -Path treated [ ] in a log path as wildcards and silently dropped the line. - Test isolation: the update-check tests wrote to the REAL user cache at %LOCALAPPDATA%\MSIX\update-check.json, poisoning it with a mocked version - which produced a false "1.0.0 is available" notice on this machine. They now redirect the cache path. Tests: +14 in MSIX.CorrectnessGuards.Tests.ps1 and MSIX.ToolVerification.Tests.ps1. Full suite 765/0/1, PSSA clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Never-invoked exported functions: 73 -> 26, and all 26 that remain are genuinely environment-bound (network installers, Hyper-V/VHDX, the ProcMon kernel driver, Windows Sandbox, or a live installed package), each listed with its reason. - The ratchet now guards the WHOLE exported surface, not just Add/Remove/Set/ Update mutators. That narrow scope is exactly how 73 functions came to be uncovered with nothing reporting it. Three orchestrators (Invoke-MsixAccelerator, Invoke-MsixAutoFixLoop, Invoke-MsixRemediationPlan) are recorded as real debt with a ceiling so the list can only shrink. - Authenticode REJECTION is tested for the first time. Both existing tests mocked Get-AuthenticodeSignature to return Valid, so the throw branch - the control that stops a planted toolchain binary from being executed - had never executed. Every rejection status is now driven, plus a valid-signature-but- untrusted-publisher case and a positive control so the suite cannot pass by simply throwing at everything. - Read-only scanner matrix: all 15 scanners against a well-formed package, against the degenerate manifest shape the module GENERATES itself (no <Applications>/<Extensions>/<Capabilities> - the shape behind the null-deref class), and against a missing package to prove they fail loudly instead of returning an empty result that reads as "clean". Explicit named invocations back the matrix, because `& $Name` dispatch is invisible to AST coverage detection and teaching the ratchet to trust strings again is the exact weakness that was just removed. - Local-surface tests for the version reporters, path resolvers, generators, accelerator import, live-store queries and Invoke-MsixSelfSign (verified unelevated). - Code coverage is now measured in CI and reported in the job summary; there was no measurement at all before. - Build-MsixTestFixture gains -ManifestXml so a test can build a deliberately degenerate package. - Test isolation: the update-check tests wrote to the REAL user cache at %LOCALAPPDATA%\MSIX\update-check.json, poisoning it with a mocked version and producing a false "1.0.0 is available" notice on the developer's machine. Full suite 848/0/1 (was 765), PSSA clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
0.74.0 closed #147, #152 and #153 only partially. This finishes all three.
#147 — security, without requiring admin
The bypass hardening in 0.74.0 leaned on a machine-scoped environment variable, which would have left a non-admin on an air-gapped agent stuck. The module must never require elevation — that is the whole reason it parses hives through
offreg.dllinstead ofreg.exe load, which demandsSeBackupPrivilege/SeRestorePrivilege.Set-MsixToolVerification -Enabled $false— session-scoped, in-memory, no admin rights. This is the supported escape hatch for offline agents.MSIX_SKIP_TOOL_VERIFICATIONis now ignored (with a Warning pointing at the cmdlet). That is the actual attack: a non-admin persists it inHKCU\Environmentand every future session silently trusts an arbitrary tools root. Machine scope is still honoured for fleet config — reading it needs no admin, only writing does.Resolve-MsixProcMonPath/Resolve-MsixDebugViewPathnow Authenticode-verify every candidate before returning it. ProcMon loads a kernel driver so it runs elevated, and the resolver trusted a user-settable override plus a fixedC:\PSF\ProcessMonitor\Procmon.exefallback — a directory any standard user can create, since the root ofC:grants Authenticated UsersCreateDirectories. Local privilege escalation. Untrusted candidates are skipped, not fatal, so a poisoned override cannot deny service either.Verified unelevated: session opt-out works, a planted unsigned ProcMon is refused, a genuinely signed binary is accepted, and the only
reg.exe/privilege references left in the codebase are the comments explaining why offreg is used.#153 — correctness
_MsixOfflineSaveHivereturn no longer discarded — a failed save shipped a signed modification package with none of the requested registry keys, exit code 0, and a log line claiming success.Add-MsixFileTypeAssociation,Add-MsixShellVerbExtension,Add-MsixFileExplorerContextMenu, and theRuleelement ofAdd-MsixFirewallRule.@($null)cluster — 15 sites across 15 files.-CaptureTracenow works: it calledInvoke-MsixProcMonCapture -PackagePath, not a parameter of that cmdlet, inside a catch that logged and continued — so the feature had never produced a.pml.Test-MsixSignaturenow includesNotTrustedinNeedsSelfSign.Add-MsixVcRuntimeBundleno longer coerces an undetectable/arm64 architecture to x86 while logging "auto-detected", and no longer packs a partial bundle as success.Write-MsixLogwrites UTF-8 with-LiteralPath(ANSI on 5.1 turned→into a literal?;-Pathtreated[ ]as wildcards and dropped the line).#152 — coverage
Add/Remove/Set/Update— the narrow scope is exactly how 73 functions went uncovered with nothing reporting it. Three orchestrators are recorded as real debt with a ceiling so the list can only shrink.Get-AuthenticodeSignatureto returnValid, so thethrowbranch — the control that stops a planted toolchain binary from executing — had never run. Every rejection reason is now driven, plus a valid-signature-but-untrusted-publisher case and a positive control.Validation
🤖 Generated with Claude Code