# User Profile Migrator

A single-file Windows GUI application (`UserAccountMigrator.exe`) that does the two jobs a
technician hits on the same machine visit:

1. **Profiles & programs** — copies or merges the contents of one Windows user profile into another
   and catalogues the programs that were installed so they can be downloaded or reinstalled, with a
   preview-first workflow, full logging and a working undo.
2. **Drivers & unknown devices** — identifies the yellow triangles in Device Manager from their raw
   hardware IDs, and asks Windows Update which newer drivers are on offer for the PC.

The two halves are top-level tabs of one window, so there is one executable to copy to the machine.

**Built artifact:** `dist/win-x64/UserAccountMigrator.exe` (self-contained, ~62 MB, no .NET install
required on the target PC, Windows 10/11 x64).

The in-application **About** window checks the running file with the same signature API Windows
uses and reports whether it is signed, invalid or unsigned, together with its publisher metadata
and SHA-256. The public download page states the trust status of the exact published artifact.
**Help** carries the manual and release notes embedded, so it works with no internet connection.

Privacy policy: <https://insta.host/privacy>. The application has no telemetry and opens no network
connection of its own.

---

## Why it is safe

| Guarantee | How |
|---|---|
| The source profile is never changed | The engine only ever *reads* the source. There is no move or delete code path for it. |
| Nothing runs without a preview | "Start migration" stays disabled until a dry run has completed **for the exact settings you are about to use**. Change any option and it locks again. |
| Nothing is overwritten by default | The default placement copies everything into one new sub-folder of the destination profile, so collisions are impossible. |
| Overwrites are reversible | When merging in place, any file about to be replaced is copied to an `OverwriteBackup` folder first. |
| Every run is auditable | Each run gets its own folder under `C:\ProgramData\UserAccountMigrator\Logs` with a full text log and a CSV manifest of every single action. |
| Runs can be undone | "Undo a previous migration" replays the manifest backwards: files the tool created are deleted, files it replaced are restored. Anything it did not touch is left alone. |
| Dangerous configurations are refused | Same account as source and destination, identical profile paths, or one profile nested inside the other are all blocked in the engine *and* in pre-flight. |
| Copies are verified | Every file is size-checked after copying; optional SHA-256 deep verification. |
| The program catalogue only reads | It reads uninstall registry keys and lists installer files. It never installs, uninstalls or changes a program, on either machine. |
| Explicit confirmation | Live runs require typing `MIGRATE`; registry imports require `IMPORT`; undo requires `UNDO`. |
| Scanning for drivers changes nothing | Enumerating devices, decoding hardware IDs, exporting reports and checking for updates are all read-only. Anything that installs a driver asks first and names what it will do. |

---

## Using it

Copy the `.exe` to the PC and run it. It requests administrator rights automatically (UAC prompt);
if you decline, it opens read-only and pre-flight will block the migration.

The window has two top-level tabs — **Profiles & programs** and **Drivers & unknown devices**. Each
has its own numbered steps and its own status bar; nothing in one half affects the other.

A footer runs along the bottom of the window on both tabs, with a link to the author
([Patrick Hamid](https://linkedin.com/in/phamid)) and a **Buy me a coffee** link
(<https://coffee.insta.host>) for anyone who finds the tool useful. Both open in your default
browser; the addresses are fixed in the code and the tool never sends anything to them.

## Profiles & programs

### 1. Accounts
Lists every real user profile from `HKLM\...\ProfileList` with its account name, type, folder,
size (on demand), last modified date, sign-in state and SID.

* Select a row → **Set as SOURCE** (the profile to take data from).
* Select a row → **Set as DESTINATION** (the profile to put data into).
* Accounts that exist but have never signed in are listed separately — sign in once as that user so
  Windows creates the profile, then refresh.

Both users should be **signed out**. The tool warns loudly if they are not.

#### Profiles on other disks (offline profiles)
**Add from another drive…** opens a picker showing every ready volume with its label, bus type,
file system, free/total space and whether it looks like it holds Windows. Pick a disk — or
**Browse to a folder…** — and the tool scans it read-only for profiles.

It understands the layouts you actually meet in recovery work:

| You point it at | It finds |
|---|---|
| `D:\` (old system disk in a caddy) | `D:\Users\*` |
| `D:\` after a reinstall | `D:\Windows.old\Users\*` |
| `D:\Users` or `D:\Documents and Settings` | the profiles directly inside |
| `E:\Backups\OldPC\Users\john` | that one profile |

A folder counts as a profile if it holds `NTUSER.DAT`, or (for copied/partial backups) a plausible
combination of `Desktop`, `Documents`/`My Documents` and `AppData`. `Default`, `All Users`,
`Default User` and junction points are skipped; `Public` is included because shared data is often
worth recovering. The search is depth-limited and skips `Windows`, `Program Files`, `ProgramData`,
`$Recycle.Bin` and similar so it stays fast.

Found profiles join the main list as type **External** / state **Offline**, survive **Refresh
list**, and can be dropped again with **Remove**. Everything else works normally with them:
sizing, preview, the copy engine, and even the registry tab (their `NTUSER.DAT` is mounted
temporarily). Pre-flight re-checks that the disk is still attached before a run.

An offline profile can also be used as the **destination** — handy when restoring data to a disk
that is going back into another PC — after an explicit confirmation. In that case ownership and
permissions are deliberately skipped and logged, because the account does not exist on this
computer; re-apply them once the disk is back in its own machine.

### 2. What to copy
* Tick the folders to copy. **Recommended** ticks Desktop, Documents, Downloads, Pictures, Music,
  Videos, Favorites, Links, Contacts, Searches, Saved Games, 3D Objects.
* **Placement**
  * *Into one new sub-folder* (default) — everything lands in e.g.
    `C:\Users\jane\Migrated from john 2026-08-04\Desktop\...`. Nothing existing can be touched.
  * *Merge into matching folders* — Desktop into Desktop, Documents into Documents, etc.
* **Conflict policy** (only relevant when merging): skip existing (default), overwrite only when the
  source file is newer, keep both copies (`report (migrated).docx`), or always overwrite.
* **Exclusions** — semicolon separated, `*` and `?` wildcards. Patterns containing `\` match the
  path relative to the profile root; otherwise they match the file or folder name. Sensible
  defaults are pre-filled (registry hives, temp, caches, `AppData\Local\Packages`, LocalLow…).
* **Safety options** — skip junctions/symlinks/OneDrive placeholders, include hidden and system
  files, preserve timestamps, back up before overwriting, verify, SHA-256 deep verify, apply
  ownership + full control to the destination account, create a System Restore point.

### 3. Settings (advanced) — optional
Copies per-user registry settings (mapped drives, printers, Office, file associations, startup
items, application settings, custom `HKCU` keys).

1. **Export selected from SOURCE** — mounts the source `NTUSER.DAT` under `HKEY_USERS`, exports each
   key to a plain-text `.reg` file, then unmounts. Review the files.
2. **Import into DESTINATION** — mounts the destination hive, exports the destination's current
   version of each key to a `rollback\*.rollback.reg` file, rewrites the exported keys to point at
   the destination hive, then imports. If a key did not exist, the rollback file simply deletes it
   again.

The destination user must be signed out or Windows will overwrite the hive on sign-out.

### 4. Programs - catalogue what was installed
Copying files never brings the applications across. This tab reads the uninstall registry keys and
writes a **reinstall kit**: what was installed, which of it can be reinstalled automatically, and
what needs a person. It only reads - nothing is installed, changed or uninstalled from this tab.

It reads, for the profile selected as SOURCE:

| Source | Read from |
|---|---|
| Programs for all users, 64-bit and 32-bit | `HKLM\...\Uninstall` and `WOW6432Node\...\Uninstall` |
| Programs installed for that one user | that profile's `NTUSER.DAT`, mounted temporarily |
| Microsoft Store / MSIX apps | the Appx package store |
| An **offline** disk's Windows | its `Windows\System32\config\SOFTWARE` hive, mounted temporarily |

Windows updates, hotfixes, patches of a parent product and hidden system components are filtered out,
and the same product found in two hives is collapsed into one row.

Each program is then given a route back:

* **winget** - a package id is known, so it can be scripted. Ids come from the built-in map of
  common business applications, and - on a live machine - from `winget list`, which always wins.
  **Look up unknown apps in winget** searches winget by name for whatever is left (needs internet).
* **Microsoft Store** - reinstall from the Store, or with `winget --source msstore`.
* **Vendor download** - no package id, but the program's own registry entry gave a vendor URL.
  Double-click the row to open it.
* **Installer on disk** - an installer for it was found in Downloads, on the Desktop or in an
  `Installers`/`Software` folder on the old disk. **Copy found installers to…** copies them off.
* **Manual / licensed** - accounting suites, Adobe, Autodesk, managed antivirus, printer software.
  These carry a plain-English warning about serials, data files and activation limits, because they
  are the ones that ruin a rebuild if the old machine is wiped first.
* **Redistributable** - VC++ runtimes, .NET, chipset and GPU drivers. Listed, hidden by default and
  never installed by the script unless asked for; the new machine supplies its own.

**Write the reinstall kit…** produces, for the ticked rows:

```
20260805-181500_programs_john\
  installed-programs.html          readable report, grouped by route, with clickable links
  installed-programs.csv           every field, for the job notes or a handover sheet
  reinstall-programs.ps1           run on the NEW PC: -WhatIf, -IncludeRedistributables, -FromFolder
  download-installers.ps1          "winget download" everything into a folder for an offline rebuild
  winget-packages.json             winget import <file>
  manual-reinstall-checklist.md    tick-box list of what still needs a person, plus a pre-wipe check
  installers-found-on-disk.csv     installers already sitting on the old disk
  catalogue.log                    exactly what was read, and anything that could not be
```

`reinstall-programs.ps1` is safe to rehearse: `-WhatIf` prints the plan and installs nothing, and
`-FromFolder D:\Installers` prefers an installer you already downloaded or copied over the internet
copy, falling back to winget only when there is no local match.

### 5. Preview & run
* **Pre-flight checks** — elevation, both profiles selected/different/present, nesting, sign-in
  state, folder selection, free disk space vs. the previewed size, and a plain-English summary of
  the conflict behaviour. Any `FAIL` blocks the run.
* **1. Run preview** — walks everything and writes `preview.log` + `planned-actions.csv`. Nothing is
  written to the destination.
* **2. Start migration** — unlocked only by a matching preview. Live progress bar, per-file log,
  and **Cancel** at any time (already-copied files stay; use Undo to remove them).
* **Undo a previous migration…** — pick the `manifest.csv` of any past run.

### Output locations
```
C:\ProgramData\UserAccountMigrator\Logs\
  20260804-131500_john_to_jane_preview\   preview.log, planned-actions.csv
  20260804-132200_john_to_jane\           migration.log, manifest.csv,
                                          OverwriteBackup\..., undo-*.log
  20260804-133000_registry_export\        *.reg, import\rollback\*.rollback.reg
  20260804-134000_programs_john\          the reinstall kit (see tab 4)
```

---

## Drivers & unknown devices

The second top-level tab. Nothing here changes the PC until you ask it to.

### 1. Unknown devices
Enumerates every PnP device through SetupAPI/CfgMgr32 — the same source Device Manager uses — and
lists everything Windows filed under **"Other devices"** plus anything reporting a problem code
(Code 28 "drivers are not installed", Code 10, Code 43, Code 45…), each translated into plain
English with a recommended next step. Optionally includes ghost devices that are no longer plugged
in.

Each device's raw hardware ID is decoded — bus, vendor, device, subsystem, revision, PCI class — so
`PCI\VEN_8086&DEV_A0F0&SUBSYS_00748086&REV_20` reads as *Intel Corporation — Wi-Fi 6 AX201*.
`PCI`, `USB`, `HDAUDIO`, `ACPI`, `HID` and `SCSI` identifiers are all understood.

* **Identify online** opens the right lookup for that exact device: Device Hunt, PCI Lookup, and a
  **Microsoft Update Catalog** query trimmed to `VEN_xxxx&DEV_xxxx` (leaving the subsystem and
  revision on usually returns zero catalog results).
* **Retry driver match** re-runs driver matching against the driver store and then tells you whether
  the device actually picked something up.
* **Details…** shows every property, all hardware and compatible IDs, and the lookup buttons.

### 2. All devices
The full inventory with driver version, date and provider, filterable by text and by class.

### 3. Driver updates
Asks the Windows Update agent which driver packages are on offer — including the **optional driver
updates Windows never installs by itself**.

* tick **Include Microsoft Update** to get third-party driver packages as well;
* every offer is matched back to a device on this PC, so you can see which update fixes which
  yellow triangle;
* tick the ones you want and **Install ticked updates** downloads and installs them (EULAs accepted
  automatically) and reports whether a restart is needed;
* common failures are explained rather than shown as a hex code — a disabled `wuauserv`, a WSUS
  policy blocking driver offers, and so on.

### 4. Driver store & tools
* the third-party packages published in the driver store (`pnputil /enum-drivers`), with removal;
* **Add drivers** — point it at a folder of extracted vendor drivers and every matching INF is
  installed (`/add-driver *.inf /subdirs /install`); this is the fix for a freshly imaged machine
  with no network driver;
* **Back up all drivers** — exports the driver store to a folder for a rebuild;
* **Download ID database** — fetches the public `pci.ids` / `usb.ids` files to
  `C:\ProgramData\DriverFinder\ids` so exact device names resolve **offline** from then on (without
  it, a built-in table of ~200 common vendors is used);
* **Vendor support page** — a direct link to the manufacturer's drivers, using the BIOS serial where
  the vendor supports it (Dell service tag, Lenovo/HP serial);
* scan for hardware changes, open Device Manager.

### Reports
**Export report…** writes a self-contained HTML report (machine identity, devices needing attention
with recommended actions, available driver updates, full inventory) or CSV, defaulting to
`Documents\DriverFinder`. Driver-side logs go to `C:\ProgramData\DriverFinder\Logs`.

---

## Known limits (by design)

* Copies **profile data**, it does not re-point a profile or rename an account. To hand a machine to
  a new account, create/sign in as the new account, then migrate into it.
* Offline profiles are identified by folder name — an old disk's SIDs cannot be resolved to account
  names on this computer, so the account name shown is the profile folder name.
* Mounting an offline `NTUSER.DAT` (registry tab) needs the disk to be writable; `reg load` fails on
  a read-only or BitLocker-locked volume, and the error is logged.
* The same applies to an offline `SOFTWARE` hive on the programs tab. When it cannot be mounted the
  tool falls back to listing the `Program Files` folder names, so the technician still gets a list -
  without versions or publishers.
* The programs tab catalogues and *identifies* software; it does not license it. Serial numbers,
  activation states and cloud sign-ins cannot be read out of a profile, which is why licensed
  products are always listed as manual work with a warning to collect the keys before the old
  machine is wiped.
* winget package ids resolved by name search are a best guess - the report says so, and the row
  should be checked before the reinstall script is trusted on a customer machine.
* `AppData\Local` and `AppData\LocalLow` are unticked/excluded by default — machine-specific caches
  and UWP package state rarely survive a move and can break applications. Tick `AppData` and remove
  the exclusions only if you know the application tolerates it.
* Files locked by a signed-in user may fail to copy; they are logged as errors and the run
  continues. Sign both users out for a clean result.
* Credential Manager entries, DPAPI-protected secrets and EFS-encrypted files cannot be transferred
  between accounts — they are cryptographically bound to the original user.
* OneDrive/cloud placeholder files are skipped while "skip junctions, symlinks and OneDrive
  placeholders" is on, so the tool will not trigger multi-gigabyte downloads.
* The driver half only ever *offers* what Windows Update offers. If a WSUS/Intune policy suppresses
  driver updates, the list will be empty — the Update Catalog links on the unknown-devices tab still
  work, and the driver `.cab` can be downloaded on another PC and installed from the tools tab.
* "Retry driver match" only re-matches against INFs already in the driver store; it does not go
  online. Use the driver-updates tab or a vendor download for anything the store does not cover.
* No System Restore point is created before a driver install. Create one first on a production PC.

---

## Building from source

Requires the .NET 7 SDK. The build works from Windows, macOS or Linux.

```bash
./build.sh              # test + publish self-contained single-file exe -> dist/win-x64
./build.sh --framework  # small exe, needs the .NET 7 Desktop Runtime on the PC
./build.sh --test       # run the engine tests only
./build.sh --sign       # build, then Authenticode-sign the executable
./build.sh --release    # build, sign, and package the download with its checksums
```

Because the .NET SDK cannot customise the Windows apphost when publishing from macOS/Linux
(`NETSDK1074`), the build does two things automatically:

* `tools/patch_pe.py` flips the PE subsystem from console to Windows GUI so no console window
  appears;
* the application elevates itself at runtime via the UAC `runas` verb instead of relying on an
  embedded manifest (`app.manifest` is still in the project, so a Windows build embeds it and the
  runtime path simply becomes a no-op).

### Signing

`tools/sign.sh` signs the executable. It runs off-Windows, which is the point — the release is
cross-built on a Mac, so `signtool` is not available for the normal path and `osslsigncode` drives
the signature instead.

The PE header is rewritten by `patch_pe.py`, so signing always happens **after** that step.
`build.sh` sequences it correctly; signing first would invalidate the signature.

Configure exactly one credential, in the environment or in `tools/signing.env` (git-ignored — it
names key material):

| Credential | Variables |
|---|---|
| Azure Trusted Signing | `SIGN_TS_ENDPOINT`, `SIGN_TS_ACCOUNT`, `SIGN_TS_PROFILE` |
| Hardware token (YubiKey / SafeNet) | `SIGN_PKCS11_MODULE`, `SIGN_PKCS11_CERT`, `SIGN_PKCS11_PIN` |
| Certificate file | `SIGN_PFX`, and `SIGN_PFX_PASSWORD` or `SIGN_PFX_PASSWORD_FILE` |
| Windows build host | `SIGN_SUBJECT` |

With none of them set, `sign.sh` **fails** rather than quietly shipping an unsigned binary. Set
`SIGN_METHOD=skip` to build an explicitly unsigned executable.

#### Current status — not yet signed

**See [`tools/SIGNING-SETUP.md`](tools/SIGNING-SETUP.md) for the full procedure.** In short:

The chosen route is **Azure Artifact Signing** — a verified publisher name for ~US$10/month with
no hardware token. It is blocked on one prerequisite: Microsoft restricts *individual* identity
validation to the United States and Canada, so an Australian publisher needs an **`Organization`**
Azure billing account, and every existing account is `Individual`. Creating one is a manual
signup — there is no `az billing account create`.

Provisioning either side of that is automated:

```bash
tools/setup-artifact-signing.sh --subscription <id>                    # account
tools/setup-artifact-signing.sh --subscription <id> --profile <val-id> # profile + signing.env
```

The first form refuses to create the paid resource if the billing account is not `Organization`,
so you cannot start paying for something that will strand at validation.

The Partner Center registration this project has is a **Microsoft Store Consumer Developer**
account (publisher name `InstaHost`, Seller ID 95573030). That does **not** provide a
code-signing certificate for a self-hosted download — Store packages are signed by Microsoft at
ingestion, only for distribution through the Store.

Note also that a non-EV certificate does not remove the SmartScreen warning; reputation accrues
per certificate over time. What changes immediately is that the prompt names the publisher instead
of saying it is unknown.

The certificate subject is taken from the validated legal entity and **cannot be customised**, so
`<Company>` must be reconciled to it. It is set to `MyTechie` — the business name registered with
ASIC against the ABN behind the billing account since 2019, and the `soldTo.companyName` on that
account. Product branding remains *InstaHost*; `Company` is the legal publisher, not the brand.
`BuildInfo.Publisher` reads the value back out of the assembly, so the About window cannot
disagree with what the build stamped in.

Every signature is SHA-256 and RFC 3161 timestamped (`SIGN_TIMESTAMP_URL`, DigiCert by default).
The timestamp is what keeps copies already in the field verifying after the certificate expires.

Passwords and PINs are passed through a `600` temporary file, never on the command line, so they
do not appear in the process list.

```bash
tools/sign.sh dist/win-x64/UserAccountMigrator.exe   # sign, then verify
tools/sign.sh --verify dist/win-x64/UserAccountMigrator.exe
```

Chain validation off-Windows needs an explicit CA bundle, so `osslsigncode` will report the chain
as unverified even for a good signature; the digest and timestamp lines are the meaningful ones.
Confirm trust on Windows with `Get-AuthenticodeSignature`, or simply open **About** in the
application, which runs `WinVerifyTrust` against itself.

`<Company>` in the project file is what Windows shows as the publisher, so it has to match the
common name on the certificate.

### Layout
```
src/UserAccountMigrator/
  Program.cs           entry point + self-elevation + dark-mode init
  ShellForm.cs         the window: brand header, Help/About, both halves as top-level tabs
  Ui/Theme.cs          the insta.host palette, control theming, Windows dark-mode hooks
  Ui/Controls.cs       flat tab strip, gradient progress bar, brand header, card
  Ui/BuildInfo.cs      version, build date, SHA-256, Authenticode verification
  Ui/AboutDialog.cs    version, publisher, signature state, checksum
  Ui/HelpWindow.cs     embedded manual, safety notes, privacy summary, release notes
  RELEASE_NOTES.md     embedded in the executable and shown by Help
  MigrationPanel.cs    the five-tab profile/programs GUI
  ConfirmDialog.cs     type-the-word confirmation dialog
  Models.cs            options, results, manifest entries
  ProfileService.cs    profile discovery, SID resolution, sizing
  ExternalProfileScanner.cs  offline profile discovery on other disks / Windows.old
  DriveScanDialog.cs   drive picker for offline profiles
  MigrationEngine.cs   dry-run/copy engine, verification, manifest
  UndoService.cs       manifest-driven rollback
  RegistryMigrator.cs  NTUSER.DAT mount / export / guarded import
  ProgramInventory.cs  program catalogue model, noise filter, app map, winget parsing
  ProgramScanner.cs    uninstall-key reading (live + offline hives), installer harvesting
  ProgramCatalogExporter.cs  CSV / HTML / winget import / reinstall + download scripts / checklist
  SystemUtil.cs        long paths, exclusions, elevation, icacls, restore points
  Drivers/             the driver half, in namespace DriverFinder
    DriverPanel.cs         the four-tab driver GUI
    DeviceDetailsDialog.cs per-device detail sheet with lookup buttons
    DeviceScanner.cs       SetupAPI/CfgMgr32 enumeration (+ Get-PnpDevice fallback)
    HardwareId.cs          hardware ID decoding, built-in vendor table, lookup links
    IdsDatabase.cs         pci.ids / usb.ids parser, download + cache
    ProblemCodes.cs        CM_PROB_* codes in plain English, with advice
    WindowsUpdateService.cs late-bound Windows Update Agent COM: search, download, install
    UpdateQuery.cs         WUA criteria + HRESULT translation
    DriverTools.cs         pnputil: driver store, offline installs, backup, export
    MachineInfo.cs         PC identity from the registry, for vendor support links
    ReportService.cs       driver CSV + HTML reports
    TextUtil.cs, Models.cs, SystemUtil.cs
tests/EngineTests/     headless end-to-end tests of both engines (runs on any OS)
tools/patch_pe.py      PE subsystem fix-up
```

The two halves keep their own namespaces (`UserAccountMigrator` and `DriverFinder`), so each has its
own `SystemUtil`/`Models` without colliding. Both GUIs are `UserControl`s; `ShellForm` owns the
window and the "something is still running" guard on close.

### Tests
`./build.sh --test` exercises the real engine against synthetic profile folders: exclusion matching,
preview-writes-nothing, skip-existing merge, keep-both renaming, overwrite + backup + undo
round-trip, isolated placement, refusal of overlapping profiles, external-drive scanning (including
`Windows.old`, single-profile folders and skip rules) and an end-to-end migration from an offline
profile.

It also covers the program catalogue end to end: update/system-component filtering, install-date
parsing, the reinstall-route classifier (winget, Store, vendor link, installer on disk, licensed,
redistributable), cross-hive de-duplication, `winget list` table parsing including truncated names
and non-installable `ARP\...` ids, installer-to-program matching, and the generated kit itself -
CSV quoting, PowerShell quoting, HTML escaping, and the rule that unticked programs never reach an
artefact.

The same run then executes the driver-side assertions (170 of them): hardware-ID decoding for
`PCI`/`USB`/`HDAUDIO`/`ACPI`/`SCSI`, Update Catalog query trimming, the `pci.ids`/`usb.ids` parser
(including the class blocks that must not be mistaken for vendors), `pnputil /enum-drivers` parsing,
the Windows Update criteria builder, problem-code mapping, update-to-device matching, date
normalisation and both driver report writers. Any failure on either side fails the build.
