SetupPilot 0.1.0 Build and check Windows unattended answer files (unattend.xml) ================================================================ WHAT IT IS ---------- A Windows unattended answer file is the single point of failure in an imaging run. One misspelled configuration pass, one over-long ComputerName, one component with the wrong processorArchitecture, and Windows Setup does not complain -- it silently ignores the section. Two hundred machines then image "successfully" and every one of them is wrong. SetupPilot does two things: * generates a valid answer file from plain command-line options, and * audits an existing answer file for exactly those silent faults, reporting each finding with the line number it lives on. Answer files are just XML, so all of this runs on Windows, macOS and Linux. You do not need a Windows machine, a mounted image, or the Windows ADK to check a file before you deploy it. WHAT IT DOES NOT DO -- PLEASE READ THIS --------------------------------------- SetupPilot validates STRUCTURE and KNOWN-VALUE CORRECTNESS. It is NOT a full Microsoft schema validator. Concretely, SetupPilot does not: * validate against the Microsoft unattend XSD. It does not know the complete list of legal components, nor which child elements each component accepts, nor the legal value range of most of them. A typo in an element name inside a component is NOT caught. * know which settings are valid in which configuration pass. Putting a specialize-only setting in oobeSystem will pass check and fail in the field. * know anything about your Windows edition, build, language packs, drivers, product keys, or hardware. * verify that a ProductKey is real, that a TimeZone string is a time zone Windows recognises, or that a locale tag is installed in the image. * check anything outside the XML: WIM indexes, partition sizes against the actual disk, network reachability of driver paths, domain-join credentials, or whether the target machine boots UEFI or legacy. AN ANSWER FILE THAT PASSES `setuppilot check` CAN STILL FAIL ON REAL HARDWARE, for reasons that live entirely outside the XML. Passing check means "this file has none of the structural faults SetupPilot knows how to find". It does not mean "this deployment will work". Always pilot on one machine before you image the fleet. INSTALL ------- Prebuilt binaries are in dist/: dist/setuppilot-windows-amd64.exe dist/setuppilot-darwin-arm64 dist/setuppilot-darwin-amd64 dist/setuppilot-linux-amd64 Copy the one for your platform somewhere on your PATH and rename it to setuppilot (or setuppilot.exe). On macOS and Linux you may need: chmod +x setuppilot To build from source (Go 1.24 or newer, no third-party dependencies): go build -o setuppilot . COMMANDS -------- setuppilot new --out [options] [--apply] setuppilot check [--json] setuppilot show [--json] setuppilot redact --out [--apply] setuppilot -h | --help | help new -- generate an answer file ------------------------------ setuppilot new --out unattend.xml \ --computer-name DEPLOY-01 \ --locale en-GB \ --timezone "GMT Standard Time" \ --admin-user deployadmin \ --skip-oobe \ --disk-layout uefi \ --apply Options: --out Destination path. Required. --computer-name Value for . Default "*", which tells Windows to generate a random name. --locale Used for InputLocale, SystemLocale, UILanguage and UserLocale, in both windowsPE and oobeSystem. Default en-US. --timezone Windows time zone id, e.g. "GMT Standard Time". Default UTC. --admin-user Create this account and put it in the local Administrators group. --skip-oobe Suppress the out-of-box-experience screens (EULA, OEM registration, online account, wireless setup) and set NetworkLocation to Work. --disk-layout uefi|bios Emit a wipe-and-partition DiskConfiguration and a matching ImageInstall target. Omit it to leave disk handling out of the file entirely. --apply Actually write the file. WITHOUT --apply THIS IS A DRY RUN. The generated document is printed to stdout and nothing is written to disk; the notice explaining that goes to stderr, so you can redirect stdout somewhere useful if you want to. SetupPilot never writes a password into a generated answer file. If you need an account password, add it yourself and treat the file as a secret from that moment on -- or, better, leave it out and set the password through your management tooling after first boot. `new` refuses to overwrite an existing file. Delete it or pick another --out. check -- audit an answer file ----------------------------- setuppilot check unattend.xml setuppilot check unattend.xml --json Every finding is one of: ERROR This will break, or silently not do what you meant. WARN Legal, but probably not what you intended, or a security concern. INFO Context. Findings carry the line number of the element they are about, so you can jump straight to it in your editor. What check looks for: * well-formed XML (and, if not, where it broke) * root element is * root namespace is urn:schemas-microsoft-com:unattend * every names a real configuration pass: windowsPE, offlineServicing, generalize, specialize, auditSystem, auditUser, oobeSystem. These are CASE-SENSITIVE. "oobesystem" is not "oobeSystem"; Windows will ignore the entire section without a word, which is the single most expensive typo in deployment. When the name differs only by case, check tells you which pass you meant. * no duplicate configuration passes * no duplicate components within one pass * every component has name, processorArchitecture and publicKeyToken * processorArchitecture is one of amd64, x86, arm64, wow64 * components do not mix architectures (amd64 with x86 is an ERROR; wow64 alongside amd64 is only a WARN, because that combination is legitimate in some images) * publicKeyToken is the in-box Windows one (WARN if not) * is 15 characters or fewer * contains only A-Z, a-z, 0-9 and hyphen (the literal value "*" is accepted, meaning "generate a random name") * does not start or end with a hyphen, and is not purely numeric (both WARN) * plaintext passwords and product keys (always WARN) Exit status: 0 no ERROR findings 1 bad invocation, or the file could not be read 2 the file has at least one ERROR finding So `setuppilot check unattend.xml` drops straight into a build pipeline. Repeated findings of the same kind are capped at 25, with a note saying how many more were withheld. A file with 400 duplicate components has one problem, not 400. show -- read an answer file --------------------------- setuppilot show unattend.xml setuppilot show unattend.xml --json Prints a derived summary (computer name, locale, time zone, local accounts, whether OOBE is skipped, disk layout, how many secrets are in the file), then every configuration pass, the components in each, and the settings that were actually set, each with its line number. Password and product-key values are printed as . show never prints a secret. redact -- make a file safe to share ----------------------------------- setuppilot redact unattend.xml --out unattend-safe.xml --apply Writes a copy with every password and product-key value replaced by [REDACTED-BY-SETUPPILOT]. Everything else -- indentation, comments, attribute order, byte for byte -- is preserved, so the redacted copy still diffs cleanly against the original and the line numbers still line up. WITHOUT --apply THIS IS A DRY RUN. It lists what would be redacted and writes nothing. The original file is never modified. redact refuses if --out resolves to the same file as the input, and refuses to overwrite an existing --out. Use this before attaching an answer file to a ticket, a chat message, or a bug report. SECURITY NOTES -------------- SetupPilot never prints the contents of a password or product key. Not in check output, not in --json, not in show, not in error messages. It reports the element and the line number and stops there. SetupPilot does not expand XML entities. Go's encoding/xml does not resolve entities declared in a DTD's internal subset, and does not fetch external entities over the network or from the filesystem. A "billion laughs" expansion bomb is rejected as an invalid character entity in a few milliseconds and a few megabytes, and an external-entity (XXE) reference to file:///etc/passwd is rejected the same way -- the file is never opened and its contents never appear anywhere in the output. Input is bounded: files over 512 MiB are refused, nesting deeper than 128 levels is refused, and the parser is streaming, so a very large file costs bounded memory rather than a tree the size of the document. SetupPilot only ever writes to the path you pass as --out, only when you pass --apply, only if that path does not already exist, and it writes via a temporary file and an atomic rename so a failed write cannot leave a half-written answer file behind. A WORKED EXAMPLE ---------------- $ setuppilot new --out unattend.xml --computer-name DEPLOY-01 \ --locale en-GB --timezone "GMT Standard Time" \ --admin-user deployadmin --skip-oobe --disk-layout uefi --apply Wrote unattend.xml (4.4 KiB) Next: setuppilot check unattend.xml $ setuppilot check unattend.xml Checking unattend.xml INFO line - file unattend.xml (4.4 KiB) INFO line - 3 configuration pass(es), 5 component(s), 0 secret value(s) 0 error(s), 0 warning(s), 2 note(s) RESULT: PASS -- no structural errors found Now break it by hand -- change pass="oobeSystem" to pass="oobesystem": $ setuppilot check unattend.xml ERROR line 62 pass "oobesystem" is not a real configuration pass (case-sensitive) -- did you mean "oobeSystem"? Windows silently ignores the whole section RESULT: FAIL -- do not deploy this answer file $ echo $? 2 ROADMAP ------- * Full XSD validation against the Microsoft unattend schema, so that misspelled element names inside a component, illegal child elements, and out-of-range values are caught too -- the biggest single gap in this version. * Driver and package injection: declaring driver paths and .cab/.msu packages, and checking that the referenced files exist and match the image architecture. * A catalogue of per-Windows-version settings, so check can say "this setting does not exist on Windows 11 24H2" and "this setting belongs in specialize, not oobeSystem" -- the pass-placement rules are where most remaining silent failures live. LICENCE / STATUS ---------------- Version 0.1.0. Go standard library only, no third-party dependencies. Single-file source (main.go).