Files
ImpTune/.planning/research/PITFALLS.md
T
2026-04-15 17:57:12 +02:00

26 KiB

Pitfalls Research

Domain: Printer deployment webapp — PowerShell script generation, .intunewin packaging, driver management Researched: 2026-04-10 Confidence: HIGH (most findings verified against official Microsoft docs and reputable community sources)


Critical Pitfalls

Pitfall 1: IntuneWinAppUtil.exe Is Windows-Only — Cannot Run in a Linux Docker Container

What goes wrong: The project plans to bundle IntuneWinAppUtil.exe inside the Docker container to produce .intunewin files. If the Docker image is Linux-based (the default and most common choice), the Windows-only .exe cannot execute. The .intunewin file simply cannot be generated, making the core export feature non-functional.

Why it happens: Development starts on a local Windows machine where bundling the .exe seems natural. The Linux container incompatibility only surfaces at deployment time or when the container image is built on a CI system. The Microsoft tool has no Linux build and cannot run under Wine reliably in production.

How to avoid: Use the Svrooij.ContentPrep C# library (NuGet) or its SvRooij.ContentPrep.Cmdlet PowerShell module as a cross-platform reimplementation of the .intunewin format. It targets .NET Standard 2.0, runs on Linux, is open-source, and is faster than the official tool. Integrate it directly into the webapp's backend rather than shelling out to an .exe. The format has been reverse-engineered and documented: it is a ZIP archive with AES-256 encrypted content and a metadata XML wrapper.

Warning signs:

  • Docker base image is ubuntu, debian, alpine, or any non-Windows image
  • Any reference to Process.Start("IntuneWinAppUtil.exe", ...) in the codebase
  • .intunewin generation works locally on dev machine but fails in container

Phase to address: Foundation / architecture phase — the packaging approach must be decided before any export feature is built. Choosing the wrong approach here causes a full rewrite of the export module.


Pitfall 2: Two-Step Driver Installation Skipped — pnputil Before Add-PrinterDriver

What goes wrong: The generated PowerShell script calls Add-PrinterDriver directly with an INF file path. This always fails with "The specified driver does not exist in the driver store." Add-PrinterDriver cannot install a driver from a raw INF — it can only reference drivers already staged in the Windows Driver Store.

Why it happens: The PowerShell documentation for Add-PrinterDriver does not make this prerequisite obvious. Developers assume it works like a driver setup wizard. The error message ("driver does not exist") is also misleading — the driver file is present, but it has not been staged.

How to avoid: The generated install script must always follow this two-step sequence:

  1. pnputil.exe /add-driver ".\drivers\*.inf" /subdirs /install — stages the driver into the Driver Store
  2. Add-PrinterDriver -Name "Exact Driver Name from INF" — registers the staged driver

The driver name passed to step 2 must exactly match the DriverDesc value inside the INF file — not the filename, not a display label from the UI.

Warning signs:

  • Script uses Add-PrinterDriver without a preceding pnputil call
  • Driver name is derived from user-entered text rather than parsed from the INF file
  • Test machines work (driver previously staged) but fresh endpoints fail

Phase to address: Script generation phase. The INF parser must extract the correct driver name, and the script template must enforce the two-step sequence unconditionally.


Pitfall 3: 32-bit PowerShell Execution Context in Intune — WOW64 Redirect

What goes wrong: Intune's Management Extension (IME) launches Win32 app install scripts in a 32-bit PowerShell process by default. In 32-bit context, C:\Windows\System32 is silently redirected to C:\Windows\SysWOW64, and registry writes to HKLM:\SOFTWARE go to HKLM:\SOFTWARE\WOW6432Node instead. Printer driver staging via pnputil called from System32 breaks. Registry detection rules written for 64-bit paths miss the actual keys written by a 32-bit installer.

Why it happens: The IME host process IntuneManagementExtension.exe is a 32-bit process. Unless explicitly forced otherwise, all child processes inherit this context. This is a well-documented but frequently overlooked Intune behavior.

How to avoid: The generated install script must include a self-relaunch guard at the top that detects 32-bit execution on a 64-bit OS and relaunches itself in 64-bit PowerShell:

if ($env:PROCESSOR_ARCHITEW6432 -eq "AMD64") {
    $scriptPath = $PSCOMMANDPATH
    & "$env:WINDIR\SysNative\WindowsPowerShell\v1.0\powershell.exe" `
        -NoProfile -ExecutionPolicy Bypass -File $scriptPath @args
    exit $LASTEXITCODE
}

All generated scripts must include this block unconditionally. The Intune package install command must also reference %WinDir%\SysNative\WindowsPowerShell\v1.0\PowerShell.exe rather than the default powershell.exe.

Warning signs:

  • Script works when tested interactively but fails in Intune deployment
  • pnputil path errors on 64-bit machines
  • Registry detection rules find nothing despite successful local testing
  • $env:PROCESSOR_ARCHITEW6432 is not checked anywhere in the generated script

Phase to address: Script generation phase — the 64-bit relaunch guard must be in the base script template from day one.


Pitfall 4: Elevation Detection Conflates "Administrator" with "SYSTEM"

What goes wrong: The generated script checks [Security.Principal.WindowsPrincipal]::IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) to decide whether to self-elevate. This returns $true for both an elevated user AND for the SYSTEM account. The script then either skips self-elevation when running as SYSTEM (correct), or incorrectly triggers a UAC prompt when running in an elevated non-SYSTEM context, breaking silent Intune deployment.

A second common mistake: the script self-elevates by launching Start-Process powershell -Verb RunAs, which triggers a UAC dialog. When Intune runs in SYSTEM context, UAC prompts never appear — the elevated child process launches silently but the parent exits with code 0, causing Intune to mark the deployment as successful while the real installation runs in a detached process with no error reporting.

Why it happens: SYSTEM and "elevated administrator" are conflated. SYSTEM always passes the IsInRole(Administrator) check, so the check alone cannot distinguish "running under Intune/RMM as SYSTEM" from "running as an elevated local admin user."

How to avoid: Use the identity name to explicitly detect SYSTEM:

$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$isSystem  = $identity -eq "NT AUTHORITY\SYSTEM"
$isAdmin   = ([Security.Principal.WindowsPrincipal]$identity).IsInRole(
                 [Security.Principal.WindowsBuiltInRole]::Administrator)

if ($isSystem) {
    # Running under Intune/NinjaRMM — no elevation needed, proceed directly
} elseif (-not $isAdmin) {
    # Running as unprivileged user — self-elevate via Start-Process -Verb RunAs
    # Then exit immediately so Intune does not track two instances
}

When self-elevating, the parent process must exit immediately after launching the elevated child, and the child must perform all real work and exit with the correct code.

Warning signs:

  • Elevation check only uses IsInRole without checking for SYSTEM identity
  • Self-elevation uses Start-Process -Verb RunAs without immediate exit of the parent
  • Intune reports "Success" but printer is not installed

Phase to address: Script generation phase — the elevation logic is part of the base script template. Write and test this logic against a SYSTEM-context simulator (e.g., PsExec -s) before shipping.


Pitfall 5: Driver Name Mismatch Between UI Label and INF DriverDesc

What goes wrong: The webapp lets users type a "driver name" free-form, or shows them the filename of the uploaded INF. The generated script passes this string to Add-PrinterDriver -Name. If the string does not exactly match the DriverDesc value inside the INF file (including spaces, capitalization, and special characters), the command fails silently or with a cryptic error.

Why it happens: The relationship between the INF filename, the device model name, and the DriverDesc field is non-obvious. Driver packages for multi-model lines (HP, Ricoh) contain dozens of DriverDesc entries in the same INF. The correct one is not always the first, and it is not the filename.

How to avoid: The webapp must parse uploaded INF files at upload time and extract all DriverDesc values from [Version] and [Manufacturer] sections. Present these as a dropdown — never trust free-form user entry for the driver name. Store the parsed name in the driver record, and use it verbatim in script generation. INF parsing is straightforward text processing: find lines matching DriverDesc\s*=\s*(.+) in the INF file.

Warning signs:

  • Driver name field is a free-text input rather than a parsed value
  • Driver upload stores only the filename, not the parsed metadata
  • No INF parsing step in the driver upload workflow

Phase to address: Driver management phase — INF parsing must happen at upload time, not at export time.


Pitfall 6: Unsigned or Test-Signed Drivers Block Installation on Windows 11

What goes wrong: Older or third-party printer drivers may lack a valid .cat (catalog) digital signature file or may be test-signed. Windows 10 (21H2+) and Windows 11 enforce driver signature requirements by default. pnputil /add-driver fails with "The third-party INF does not contain digital signature information." The error appears in the deployment log but not always surfaced to the technician.

Why it happens: MSPs dealing with legacy hardware (especially older Brother, Ricoh, or Canon models) frequently encounter legacy drivers that were signed with expired certificates or not at all. The issue is invisible during testing on machines where the driver was previously installed via a setup wizard that bypassed enforcement.

How to avoid: At driver upload time, the webapp should inspect the uploaded ZIP/INF package for the presence of a .cat file alongside the INF. If absent, warn the technician immediately with a clear message: "This driver package has no catalog file. Installation may fail on Windows 10 21H2+ and Windows 11 unless driver signature enforcement is disabled — which is not recommended for managed endpoints." The generated script should log pnputil output to a temp file so failures are captured. Recommend technicians obtain WHQL-certified drivers from manufacturer download portals.

Warning signs:

  • Uploaded driver ZIP contains .inf but no .cat file
  • pnputil output is not captured or logged in the generated script
  • Deployment works on older test machines but fails on Windows 11 endpoints

Phase to address: Driver management phase (upload validation) and script generation phase (pnputil output logging).


Pitfall 7: Generated Scripts Are Not Idempotent — Re-runs Cause Errors

What goes wrong: Intune will re-run the install script if the detection rule fails to match (e.g., after a Windows update, a reimaging, or a detection script bug). If the script does not check for existing state before acting, it throws errors: Add-PrinterPort fails with "Port already exists," Add-Printer fails with "Printer already exists," and the script exits with a non-zero code, triggering endless Intune retry loops.

Why it happens: Script authors test the happy path (fresh machine), not the re-run path. Idempotency is easy to overlook in a one-shot deployment context.

How to avoid: Every generated script must guard each step:

# Port
if (-not (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue)) {
    Add-PrinterPort -Name $portName -PrinterHostAddress $ipAddress
}
# Printer
if (-not (Get-Printer -Name $printerName -ErrorAction SilentlyContinue)) {
    Add-Printer -Name $printerName -DriverName $driverName -PortName $portName
}

The detection rule (separate from the install script) must check for the printer queue by exact name using Get-Printer. The generated detection script must be included in the package and documented.

Warning signs:

  • Script template uses Add-PrinterPort / Add-Printer without Get- guards
  • No detection script template included in the export package
  • Technicians report "deployment loops" or repeated installs

Phase to address: Script generation phase — idempotency guards belong in the base template, not as an afterthought.


Pitfall 8: Script Injection via Unsanitized User Input in Generated PowerShell

What goes wrong: The webapp takes user input (printer name, IP address, port name, driver name) and interpolates it into a generated PowerShell script string. A malicious or careless input like MyPrinter"; Remove-Item C:\Windows -Recurse -Force; # breaks out of the string literal and injects arbitrary PowerShell commands into the generated script. The generated script is then downloaded and executed with SYSTEM privileges on endpoints.

Why it happens: Script generation via string templates is the obvious implementation approach. Developers working on an internal tool often deprioritize injection risks, especially when no authentication is required and the tool is network-isolated.

How to avoid: Never interpolate raw user input into script string literals. Use PowerShell's own string quoting rules — single-quoted strings ('...') do not interpolate. For values that must appear inside double-quoted strings, escape all embedded quotes and special characters, or use parameter passing at the call site rather than embedding values inline. Validate all inputs before storage: IP addresses via regex ^\d{1,3}(\.\d{1,3}){3}$, printer names against a character allowlist [A-Za-z0-9 \-_()], port names similarly. Reject inputs that fail validation at the form level. Treat this as non-negotiable even for an internal tool — a compromised technician machine or a mistake can still cause damage.

Warning signs:

  • Script template uses "...$printerName..." without escaping
  • No input validation on printer name, IP, or port name fields
  • Generated script contains verbatim user-provided strings

Phase to address: Script generation phase. Input validation must be defined in the data model before the template engine is built.


Technical Debt Patterns

Shortcut Immediate Benefit Long-term Cost When Acceptable
Hard-code IntuneWinAppUtil.exe as the packaging backend Fastest path to a working demo on Windows Breaks entirely in Linux Docker; requires full rewrite of export module Never — decide on cross-platform library from day one
Free-text driver name input instead of INF parsing Faster UI to build Constant technician error; driver not found failures; support burden Never for the name field; free-text acceptable for display name only
Single-step driver install (no pnputil) Simpler script template 100% failure rate on clean endpoints Never
No idempotency guards in install script Simpler code Endless Intune retry loops when detection logic has edge cases Never in generated scripts
Skip 64-bit relaunch guard Smaller script Silent failures on 64-bit Intune-managed machines (the majority of fleet) Never
Store uploaded driver ZIPs with original filenames Zero-effort storage Path traversal risk; filename collisions across clients; no sanitization Never — normalize to UUID-based filenames at upload
No logging in generated scripts Shorter scripts Zero diagnostic information when deployment fails; blind troubleshooting MVP: acceptable if a $logPath variable placeholder is in the template from the start

Integration Gotchas

Integration Common Mistake Correct Approach
Intune Win32 app upload Packaging with IntuneWinAppUtil.exe from Docker/Linux Use Svrooij.ContentPrep C# library — cross-platform, no .exe dependency
Intune deployment context Assuming scripts run as 64-bit Add PROCESSOR_ARCHITEW6432 relaunch guard; reference SysNative path in install command
Intune detection rules Using file-based detection (driver INF path) Use PowerShell detection script: Get-Printer -Name "ExactName" then exit 0 or exit 1
NinjaRMM script execution Relying on default execution policy Wrap all generated scripts with -ExecutionPolicy Bypass in the call instruction documented in the ZIP README
pnputil Calling with relative paths Use $PSScriptRoot to build absolute paths to INF files; pnputil does not resolve relative paths reliably under SYSTEM
Add-PrinterDriver Passing display name or filename Parse and store DriverDesc from INF at upload time; pass exact value at script generation time

Performance Traps

Trap Symptoms Prevention When It Breaks
Synchronous .intunewin generation on request Web request hangs for 5-30 seconds during packaging of large driver sets Run packaging as a background job; return a job ID; poll or use a progress endpoint Any driver bundle over ~50 MB
Storing driver ZIPs in-memory during upload Memory spikes; container OOM on large driver packages (HP PCL6 drivers can be 200 MB+) Stream upload directly to disk; set a hard upload size limit with feedback Files over ~50 MB on containers with 512 MB RAM
Re-packaging every export request Slow UX for technicians exporting the same printer repeatedly Cache generated packages keyed by (printer config hash + driver hash); invalidate on config change High-frequency re-export scenario (multi-client MSP)

Security Mistakes

Mistake Risk Prevention
Injecting unsanitized user input into script string templates Generated scripts contain arbitrary PowerShell executed as SYSTEM on endpoints — full system compromise Whitelist-validate all inputs; use single-quoted PS strings or explicit escaping; never use Invoke-Expression in templates
Storing uploaded driver files with user-provided filenames Path traversal: a filename like ../../etc/passwd or ../../app/main.py overwrites application files Rename all uploaded files to {uuid}{ext} immediately on receipt; validate extension against allowlist (.zip, .inf, .cab)
No file type validation on driver uploads A technician (or attacker on the internal network) uploads an executable disguised as a driver Check MIME type AND file magic bytes, not just extension; reject anything not ZIP/INF/CAB
Executing arbitrary uploaded content on the server side If the server processes INF files by shelling out to Windows tools, a crafted INF could exploit the parser Parse INF files with a safe text parser (regex/line scan); never execute uploaded files server-side
No-auth endpoint serving driver downloads Internal tool assumption breaks if the container is accidentally exposed; driver packages can be exfiltrated Document network isolation requirement explicitly; add a health-check-only public endpoint; bind to localhost or internal interface only

UX Pitfalls

Pitfall User Impact Better Approach
No validation feedback on driver upload Technician uploads a driver, generates a package, deploys it, and only discovers the driver was unsigned or malformed after endpoint failure Parse and validate the INF at upload time; show driver name, version, architecture, and signature status before the upload is confirmed
Driver name is free-text Technician guesses the driver name; script fails silently on endpoints Parse DriverDesc from INF; present as a read-only confirmed value or a dropdown if multiple models exist
No copy-to-clipboard for detection script Technicians manually retype the detection rule into Intune, introducing errors Include a "copy detection script" button alongside every generated package
No package preview before download Technician downloads, uploads to Intune, and only then realizes IP was wrong Show a collapsible "what's in this package" summary: script preview, driver files list, icon, detection script
Export fails silently with generic error Technician has no actionable information Show specific error: "Driver INF not found in ZIP," "Driver name not matched," "Packaging failed: [detail]"

"Looks Done But Isn't" Checklist

  • Driver staging: Script calls pnputil /add-driver before Add-PrinterDriver — verify with a clean VM, not a dev machine where the driver is pre-staged
  • 64-bit guard: $env:PROCESSOR_ARCHITEW6432 check is the first meaningful block in every generated install script
  • SYSTEM detection: Script distinguishes SYSTEM identity from elevated-administrator identity before any self-elevation logic
  • Idempotency: Re-running the install script on a machine where the printer is already installed exits with code 0 and makes no changes
  • Detection script: Every exported package includes a separate detection script, not just the install script
  • Driver name source: Driver name in generated script comes from parsed INF DriverDesc, not from user text field
  • INF signature check: Upload flow warns when no .cat file is present in the driver package
  • .intunewin format: Package can be created from the Docker container without IntuneWinAppUtil.exe
  • NinjaRMM ZIP: ZIP contains a README.txt or INSTALL.txt explaining how to run the script (-ExecutionPolicy Bypass)
  • Port guard: Add-PrinterPort is preceded by Get-PrinterPort check
  • Input validation: Printer name, IP address, and port name fields reject invalid characters at form submission

Recovery Strategies

Pitfall Recovery Cost Recovery Steps
IntuneWinAppUtil.exe bundled in Linux container HIGH Rewrite export module to use Svrooij.ContentPrep C# library; test .intunewin format compatibility with Intune upload
Driver name free-text in DB with wrong values MEDIUM Add INF parser; run migration to re-parse all stored driver packages and update name fields; regenerate cached packages
No 64-bit guard in script template LOW Add guard to base template; regenerate all cached packages; inform technicians to re-download existing packages
Unsigned driver deployed to fleet HIGH Remove printer and driver from all endpoints via remediation script; source signed driver; redeploy
Script injection in generated output HIGH Audit all stored printer configs for malicious inputs; add validation to data model; re-generate all scripts

Pitfall-to-Phase Mapping

Pitfall Prevention Phase Verification
IntuneWinAppUtil.exe Linux incompatibility Foundation / Architecture Build and run the container on a Linux host; generate a .intunewin file; validate it uploads to Intune
pnputil two-step requirement Script generation Deploy generated script to a clean Windows 11 VM via Intune; confirm printer appears
32-bit WOW64 execution context Script generation Deploy via Intune (not interactive); check $env:PROCESSOR_ARCHITEW6432 in script log
SYSTEM vs. elevated-admin confusion Script generation Run script via PsExec -s on a test machine; confirm no UAC prompt; confirm correct behavior
Driver name mismatch Driver management (upload) Upload a multi-model HP or Ricoh INF; verify dropdown shows correct DriverDesc values
Unsigned driver blocking Driver management (upload) Upload a driver ZIP with no .cat file; confirm warning is shown before upload completes
Non-idempotent scripts Script generation Run install script twice on the same machine; confirm exit code 0, no errors, no duplicate printers
Script injection Script generation + input validation Submit "; malicious code # as printer name; verify it is rejected at form level and absent from generated script
Driver filename path traversal Driver management (upload) Upload a file named ../../test.txt; verify it is stored as a UUID-named file

Sources


Pitfalls research for: ImpTune — printer deployment webapp Researched: 2026-04-10