Skip to content

Prepare a USB stick with the post-install toolkit - #21

Open
DevSecNinja wants to merge 2 commits into
mainfrom
devsecninja-fluffy-garbanzo
Open

Prepare a USB stick with the post-install toolkit#21
DevSecNinja wants to merge 2 commits into
mainfrom
devsecninja-fluffy-garbanzo

Conversation

@DevSecNinja

Copy link
Copy Markdown
Owner

Why

I download stock Windows 11 ISOs from the Visual Studio portal, and building a custom ISO is a lot of effort for something I do occasionally. What I actually want most of the time is: flash the stock ISO to a USB stick, install Windows, and have the documented change catalog applied without fetching this repo on the new machine.

post-install.ps1 already covers "machine already exists". This adds the missing middle step for "new machine from my own media", without requiring the full offline build path.

What this adds

prepare-usb.ps1 -> New-PostInstallUsb. You flash a stock ISO to a stick as usual, then point this at it once:

./prepare-usb.ps1 -Path E: -Profile opinionated -WhatIf   # validate + show the plan
./prepare-usb.ps1 -Path E: -Profile opinionated

It validates before writing anything (removable volume root, real Setup media, architecture from the UEFI boot loader, catalog-id validity, free space), stages the toolkit into a folder at the stick root, and generates a bootstrap that self-elevates, copies itself to %ProgramData%\windows-iso-maker so it survives unplugging and reboots, and runs post-install.ps1 under a transcript.

In the default FirstLogon mode it also writes a minimal Autounattend.xml containing only an oobeSystem FirstLogonCommands block. Windows Setup stays completely stock: interactive edition, partitioning and OOBE, including Entra ID sign-in. No DiskConfiguration, no WillWipeDisk, no edition selection, and no disk is ever wiped by this tool. -Mode Toolkit skips the answer file entirely.

This is deliberately a different artifact from the full build answer file in templates/autounattend/autounattend.xml.template, which does repartition, so a new firstlogon.xml.template was added rather than reusing that one.

The important caveat, and why the second commit exists

A rubber-duck review pushed back on the first-logon model, and it was right. Microsoft documents that FirstLogonCommands run elevated only if the first user to sign in is a local administrator; a standard user gets a consent prompt, and nothing runs if it is declined or if UAC is disabled. Whether an Entra ID account becomes a local admin is decided by Entra/Intune, so the automatic run is not guaranteed in exactly the scenario this targets. The docs originally asserted Entra compatibility as fact; they now state the caveat, cite the source, and point at -Mode Toolkit.

Since a silent no-op on a brand-new machine is the worst possible failure, the second commit makes every failure mode visible in %ProgramData%\windows-iso-maker\logs\bootstrap.log: discovery logs before it searches and says so when it finds nothing, and the bootstrap reports elevation failures with guidance.

Worth a careful look: running the real generated command end to end caught a bug the tests did not. Start-Process -Verb RunAs -Wait succeeding only means the process started, so a child that died immediately was reported as "Elevated run finished". It now checks the child's exit code.

Notes for review

  • The bootstrap is generated as a string so settings are baked in, which means PSScriptAnalyzer cannot lint the output. Mitigated by asserting it parses via [scriptblock]::Create and by unit-testing the generators and the literal escaping. A static script plus a settings .psd1 would be lintable, and is a reasonable follow-up if you'd prefer it.
  • Removable detection no longer relies on Win32_LogicalDisk DriveType alone, since USB SSDs report as fixed. A volume on a USB bus (MSFT_Disk BusType 7) is now accepted rather than requiring -Force.
  • New .ps1 files are ASCII-only. Windows PowerShell 5.1 reads no-BOM files as ANSI, so an em dash inside a double-quoted string becomes a smart quote and breaks parsing. This bit during development and is why Autounattend.xml is now written BOM-less via UTF8Encoding($false) rather than Set-Content -Encoding UTF8.
  • No new per-feature switches or catalog entries; this reuses Resolve-CatalogSelection and the existing profiles unchanged.

Validation

  • PSScriptAnalyzer: 0 findings across src, tests, and all dispatchers.
  • Pester: 1337 passing, 0 failing (30 in the new suite).
  • Verified under real powershell.exe 5.1, not just the test host: module import, bootstrap execution, and the actual generated first-logon command driven from the rendered XML, covering both the "toolkit found" and "no toolkit found" paths (the latter via a subst drive root).
  • No ISO build or DISM servicing was run; those paths are untouched.

DevSecNinja and others added 2 commits July 30, 2026 14:59
…allUsb)

Adds a third, low-effort way to use the tool: flash a STOCK Windows 11 ISO
(e.g. from a Visual Studio subscription) to a USB stick, then point
prepare-usb.ps1 at that stick once.

New-PostInstallUsb validates the target (removable volume root, real Windows
Setup media, media architecture from the UEFI boot loader, catalog selection,
free space), stages the toolkit into a folder at the stick root, and generates a
self-contained bootstrap that self-elevates, copies itself to
%ProgramData%\windows-iso-maker so it survives unplugging and reboots, and runs
post-install.ps1 with the chosen profile under a transcript.

In the default FirstLogon mode it also writes a MINIMAL Autounattend.xml
containing only an oobeSystem FirstLogonCommands block, from the new
templates/autounattend/firstlogon.xml.template. Windows Setup therefore stays
completely stock (interactive edition, partitioning and OOBE, including an
Entra ID sign-in) and no disk is ever wiped. -Mode Toolkit stages the toolkit
without any answer file.

New files are ASCII-only: Windows PowerShell 5.1 reads no-BOM files as ANSI, so
an em dash inside a double-quoted string becomes a smart quote and breaks
parsing. Verified the module imports and the bootstrap runs end to end under
powershell.exe 5.1.

Docs: new docs/usb.md, a "Three ways to use this tool" section in the README,
and cross-links from post-install.md / autounattend.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c85d9b13-c960-464c-be72-29d9f33ce3b1
Follow-up to the review of New-PostInstallUsb. The staging half was sound; the
first-logon execution model was where the real risk sat.

Verified against Microsoft's documentation that FirstLogonCommands run elevated
only when the first user to sign in is a local administrator - a standard user
gets a consent prompt, and nothing runs if it is declined or if UAC is disabled.
Whether an Entra ID account becomes a local admin is decided by Entra/Intune, so
the automatic run is NOT guaranteed in the scenario this feature targets. The
docs previously asserted Entra compatibility as fact; they now state the caveat,
cite the source, and point at -Mode Toolkit as the hook-free alternative.

Because a silent no-op on a brand-new machine is the worst possible failure:

- The generated discovery command now writes a breadcrumb to
  %ProgramData%\windows-iso-maker\logs\bootstrap.log before it searches, and
  records explicitly when no toolkit is found instead of exiting 0 in silence.
- The bootstrap logs before elevation, reports ELEVATION FAILED with guidance
  when the relaunch throws, and - found by running the real generated command
  end to end - now checks the elevated child's exit code, since Start-Process
  succeeding only means the process started. A child that died instantly was
  previously reported as "Elevated run finished".
- Discovery moved into New-PostInstallDiscoveryCommand so it is unit tested,
  rejects quote characters in the folder name, and is asserted to contain no
  double quote (it is embedded in -Command "..." inside XML).

Also:
- Removable detection no longer relies on Win32_LogicalDisk DriveType alone.
  USB SSDs and USB-NVMe enclosures report as fixed, so a volume on a USB bus
  (MSFT_Disk BusType 7) is now accepted rather than requiring -Force.
- Autounattend.xml is written as UTF-8 WITHOUT a BOM. Set-Content -Encoding UTF8
  emits a BOM under PS 5.1, contradicting the repository's encoding rule.
- -WslServicing and -WslAutoReboot are forwarded, closing a capability gap
  against post-install.ps1.
- 11 new tests cover the CIM probing branch (previously uncovered, because a
  temp-dir fake USB is not a volume root), the discovery command, the bootstrap
  generator and the literal escaping.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c85d9b13-c960-464c-be72-29d9f33ce3b1
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