docs: complete project research
Adds STACK, FEATURES, ARCHITECTURE, PITFALLS, and SUMMARY research files covering the full ImpTune technology stack, feature set, architecture patterns, and critical pitfalls for the printer deployment package generator. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,324 @@
|
|||||||
|
# Architecture Research
|
||||||
|
|
||||||
|
**Domain:** Single-container deployment package generator webapp (Intune + NinjaRMM)
|
||||||
|
**Researched:** 2026-04-10
|
||||||
|
**Confidence:** HIGH (core patterns well-established; .intunewin internals MEDIUM — encryption layer may require IntuneWinAppUtil.exe rather than reimplementation)
|
||||||
|
|
||||||
|
## Standard Architecture
|
||||||
|
|
||||||
|
### System Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Browser (SPA or SSR) │
|
||||||
|
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
|
||||||
|
│ │ Printer │ │ Driver Library │ │ Package Export │ │
|
||||||
|
│ │ Config Form │ │ Browser │ │ (Download Trigger)│ │
|
||||||
|
│ └──────┬───────┘ └────────┬────────┘ └──────────┬─────────┘ │
|
||||||
|
└──────────┼───────────────────┼──────────────────────┼────────────┘
|
||||||
|
│ HTTP/REST │ HTTP/REST │ HTTP/REST
|
||||||
|
┌──────────┼───────────────────┼──────────────────────┼────────────┐
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ API Layer (HTTP server) │
|
||||||
|
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
|
||||||
|
│ │ /printers │ │ /drivers │ │ /packages │ │
|
||||||
|
│ │ CRUD routes │ │ upload routes │ │ generate routes │ │
|
||||||
|
│ └──────┬───────┘ └────────┬────────┘ └──────────┬─────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
├──────────┼───────────────────┼──────────────────────┼────────────┤
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ Service / Domain Layer │
|
||||||
|
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
|
||||||
|
│ │ Printer │ │ Driver │ │ Package Builder │ │
|
||||||
|
│ │ Service │ │ Service │ │ Service │ │
|
||||||
|
│ └──────┬───────┘ └────────┬────────┘ └──────────┬─────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌────────┘ ┌────────┘ │
|
||||||
|
│ │ ▼ ▼ │
|
||||||
|
│ │ ┌─────────────────┐ ┌─────────────────────────────┐ │
|
||||||
|
│ │ │ Driver Store │ │ Package Generators │ │
|
||||||
|
│ │ │ (volume mount) │ │ ┌─────────┐ ┌───────────┐ │ │
|
||||||
|
│ │ └─────────────────┘ │ │ PS Tmpl │ │ intunewin │ │ │
|
||||||
|
│ │ │ │ Engine │ │ Builder │ │ │
|
||||||
|
│ │ │ └─────────┘ └───────────┘ │ │
|
||||||
|
│ │ │ ┌─────────┐ │ │
|
||||||
|
│ │ │ │ ZIP │ │ │
|
||||||
|
│ │ │ │ Builder │ │ │
|
||||||
|
│ │ │ └─────────┘ │ │
|
||||||
|
│ │ └─────────────────────────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Data Layer │ │
|
||||||
|
│ │ ┌────────────────┐ ┌─────────────────┐ │ │
|
||||||
|
│ │ │ SQLite DB │ │ Filesystem │ │ │
|
||||||
|
│ │ │ (printers, │ │ (drivers/ vol, │ │ │
|
||||||
|
│ │ │ clients, │ │ tmp/ for │ │ │
|
||||||
|
│ │ │ driver refs) │ │ in-progress │ │ │
|
||||||
|
│ │ │ │ │ packages) │ │ │
|
||||||
|
│ │ └────────────────┘ └─────────────────┘ │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Responsibilities
|
||||||
|
|
||||||
|
| Component | Responsibility | Typical Implementation |
|
||||||
|
|-----------|----------------|------------------------|
|
||||||
|
| API Layer | HTTP routing, request validation, response formatting | FastAPI (Python) or Hono/Express (Node) |
|
||||||
|
| Printer Service | CRUD for printer configs, parameter validation | Plain service class over SQLite |
|
||||||
|
| Driver Service | File upload handling, driver metadata storage, deduplication | Service + volume-mounted filesystem |
|
||||||
|
| Package Builder Service | Orchestrates generation: invokes PS template engine, intunewin builder, ZIP builder | Coordinator service, calls sub-generators |
|
||||||
|
| PowerShell Template Engine | Renders printer-specific PS install script from a template | Jinja2 or string template with parameter substitution |
|
||||||
|
| IntuneWin Builder | Produces a valid .intunewin file from a source folder | Wraps bundled IntuneWinAppUtil.exe via subprocess call |
|
||||||
|
| ZIP Builder | Produces NinjaRMM-ready ZIP: PS script + driver files | Python zipfile / Node archiver in-process |
|
||||||
|
| SQLite DB | Stores printer configs, client groups, driver metadata references | SQLite via a lightweight ORM or raw queries |
|
||||||
|
| Driver Store (volume) | Holds uploaded driver ZIP/INF blobs, persists across container restarts | Docker volume, addressed by content-hash filenames |
|
||||||
|
| Temp workspace | Staging area for a build: assembled files before zipping | tmpfs or host-path temp dir, cleaned after download |
|
||||||
|
|
||||||
|
## Recommended Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
imptune/
|
||||||
|
├── api/ # HTTP route handlers
|
||||||
|
│ ├── printers.py # /printers CRUD
|
||||||
|
│ ├── drivers.py # /drivers upload + list
|
||||||
|
│ ├── packages.py # /packages/generate endpoints
|
||||||
|
│ └── clients.py # /clients group management
|
||||||
|
├── services/
|
||||||
|
│ ├── printer_service.py # Printer domain logic
|
||||||
|
│ ├── driver_service.py # Driver file management
|
||||||
|
│ └── package_builder.py # Orchestrates package generation
|
||||||
|
├── generators/
|
||||||
|
│ ├── ps_template.py # PowerShell script renderer
|
||||||
|
│ ├── intunewin_builder.py # .intunewin packaging (calls IntuneWinAppUtil)
|
||||||
|
│ └── zip_builder.py # NinjaRMM ZIP assembly
|
||||||
|
├── templates/
|
||||||
|
│ └── printer_install.ps1.j2 # Jinja2 PS script template
|
||||||
|
├── db/
|
||||||
|
│ ├── database.py # SQLite connection, migrations
|
||||||
|
│ └── models.py # Printer, Client, Driver models
|
||||||
|
├── storage/
|
||||||
|
│ └── driver_store.py # Read/write to volume-mounted driver path
|
||||||
|
├── config.py # Env-var driven configuration
|
||||||
|
├── main.py # App entrypoint, mounts routes
|
||||||
|
├── Dockerfile
|
||||||
|
└── docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Structure Rationale
|
||||||
|
|
||||||
|
- **api/:** Thin handlers only — validation and delegation to services. No business logic here.
|
||||||
|
- **services/:** All domain logic lives here, testable without HTTP context.
|
||||||
|
- **generators/:** Each output format is isolated. Adding a new format (SCCM, PDQ) means adding one file here.
|
||||||
|
- **templates/:** PS script is a template file, not a string in code. Easier to edit, diff, and review.
|
||||||
|
- **db/:** All persistence in one place. SQLite means no daemon dependency.
|
||||||
|
- **storage/:** Abstracts the filesystem from the rest of the app; makes future swap to S3 straightforward.
|
||||||
|
|
||||||
|
## Architectural Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Request-Generate-Stream (synchronous generation)
|
||||||
|
|
||||||
|
**What:** The browser POSTs printer config → server assembles files → server streams the archive back as a file download, all in one HTTP call.
|
||||||
|
**When to use:** Package generation completes in under ~10 seconds. For this domain (small driver bundles, scripted packaging), synchronous is correct.
|
||||||
|
**Trade-offs:** Simple — no job queue, no polling, no WebSocket. The downside is the HTTP connection stays open during generation; acceptable for internal tooling on a LAN.
|
||||||
|
|
||||||
|
**Example flow:**
|
||||||
|
```
|
||||||
|
POST /api/packages/generate
|
||||||
|
body: { printerId, format: "intunewin" | "ninja" }
|
||||||
|
→ Package Builder assembles temp workspace
|
||||||
|
→ PS Template Engine renders install.ps1
|
||||||
|
→ IntuneWin Builder or ZIP Builder runs
|
||||||
|
→ Response: file download (Content-Disposition: attachment)
|
||||||
|
→ Temp workspace deleted after send
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Content-Addressed Driver Storage
|
||||||
|
|
||||||
|
**What:** When a driver archive is uploaded, compute a SHA256 of the content and store it under that hash as the filename. Record the hash + original name in SQLite. Multiple printers referencing the same driver point to one file.
|
||||||
|
**When to use:** Always — avoids duplicate large files on the volume and makes driver references stable across renames.
|
||||||
|
**Trade-offs:** Slight upload cost for hashing. Cleanup requires reference counting (a row delete check before GC).
|
||||||
|
|
||||||
|
### Pattern 3: Template-Driven Script Generation
|
||||||
|
|
||||||
|
**What:** PowerShell install script is a Jinja2 (Python) or Handlebars/nunjucks (Node) template. Printer parameters are injected as variables. The template handles the elevation logic, port setup, and driver install commands in one canonical place.
|
||||||
|
**When to use:** Always — never build the PS script by string concatenation in code. The template is testable, auditable, and editable without touching Python/Node.
|
||||||
|
**Trade-offs:** Adds a template engine dependency (Jinja2 is stdlib-adjacent in Python; nunjucks is tiny in Node).
|
||||||
|
|
||||||
|
**Template sketch:**
|
||||||
|
```powershell
|
||||||
|
# printer_install.ps1.j2
|
||||||
|
$PrinterName = "{{ printer.name }}"
|
||||||
|
$PrinterIP = "{{ printer.ip }}"
|
||||||
|
$DriverName = "{{ printer.driver_name }}"
|
||||||
|
$PortName = "IP_{{ printer.ip }}"
|
||||||
|
|
||||||
|
# Elevation guard
|
||||||
|
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
||||||
|
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||||
|
Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
# ... driver install, port add, printer add ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Subprocess Isolation for IntuneWinAppUtil
|
||||||
|
|
||||||
|
**What:** IntuneWinAppUtil.exe is a Windows binary. In a Linux container it must run under Wine, or the container must be Windows-based. Isolate this call behind an interface so the builder can be swapped (e.g., native reimplementation, different tool) without touching the orchestrator.
|
||||||
|
**When to use:** Whenever calling an external binary for a critical generation step.
|
||||||
|
**Trade-offs:** Wine on Linux adds image size (~500 MB). A Windows-based container image avoids Wine but is heavier by default. A pure Python reimplementation of the .intunewin format (outer ZIP + encrypted inner ZIP + detection.xml) is feasible and removes the tool dependency entirely — recommended if the format can be locked down to what Intune actually requires.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
### Printer Package Generation Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
[Technician fills form]
|
||||||
|
↓
|
||||||
|
POST /api/packages/generate { printerId, format }
|
||||||
|
↓
|
||||||
|
API Layer validates request, loads printer config from SQLite
|
||||||
|
↓
|
||||||
|
Package Builder Service
|
||||||
|
├── Fetches driver files from Driver Store (volume path)
|
||||||
|
├── Renders install.ps1 via PS Template Engine (printer params injected)
|
||||||
|
├── Copies icon file (if Intune format)
|
||||||
|
└── Calls appropriate generator:
|
||||||
|
├── [Intune] IntuneWin Builder
|
||||||
|
│ └── subprocess: IntuneWinAppUtil.exe -c <tmpdir> -s install.ps1 -o <out>
|
||||||
|
│ OR: native Python .intunewin assembler
|
||||||
|
└── [Ninja] ZIP Builder
|
||||||
|
└── zipfile: { install.ps1, drivers/, readme.txt }
|
||||||
|
↓
|
||||||
|
Generated file written to temp workspace
|
||||||
|
↓
|
||||||
|
HTTP response streams file to browser (Content-Disposition: attachment)
|
||||||
|
↓
|
||||||
|
Temp workspace cleaned up (finally block / background task)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Driver Upload Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
[Technician uploads ZIP/INF]
|
||||||
|
↓
|
||||||
|
POST /api/drivers (multipart/form-data)
|
||||||
|
↓
|
||||||
|
Driver Service
|
||||||
|
├── Streams upload to temp file
|
||||||
|
├── Computes SHA256
|
||||||
|
├── Checks SQLite: already exists? → return existing record
|
||||||
|
├── Moves file to drivers volume at hash-named path
|
||||||
|
└── Inserts driver record (id, hash, original_name, size, uploaded_at)
|
||||||
|
↓
|
||||||
|
Response: driver record (id, name) for use in printer config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Printer Config Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
[Technician submits printer form]
|
||||||
|
↓
|
||||||
|
POST /api/printers
|
||||||
|
body: { name, ip, clientId, driverId, color, duplex, paperSize, tray, iconId }
|
||||||
|
↓
|
||||||
|
Printer Service validates params, resolves driverId FK
|
||||||
|
↓
|
||||||
|
SQLite INSERT → printer record with all settings
|
||||||
|
↓
|
||||||
|
Response: printer record
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Order (Component Dependencies)
|
||||||
|
|
||||||
|
Build these layers in order — each depends on the previous being stable:
|
||||||
|
|
||||||
|
1. **Data layer** — SQLite schema, models, connection management. Everything else reads/writes here.
|
||||||
|
2. **Storage layer** — Driver Store abstraction over the volume filesystem.
|
||||||
|
3. **Services (Printer + Driver)** — CRUD operations, driver upload with deduplication.
|
||||||
|
4. **PS Template Engine** — Standalone, can be developed and unit-tested in isolation.
|
||||||
|
5. **ZIP Builder (NinjaRMM)** — Depends on Driver Store + PS Template Engine. Simple to implement, good validation target.
|
||||||
|
6. **IntuneWin Builder** — Depends on PS Template Engine + icon handling. Most complex generator; validate ZIP Builder first.
|
||||||
|
7. **API Layer** — Thin wrappers once services are solid.
|
||||||
|
8. **UI** — Depends on all API routes being defined.
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
### Anti-Pattern 1: Generating Scripts via String Concatenation
|
||||||
|
|
||||||
|
**What people do:** Build the PowerShell script by concatenating strings in Python/Node code.
|
||||||
|
**Why it's wrong:** Injection risk (printer names with quotes, special chars), hard to read, impossible to review, breaks easily.
|
||||||
|
**Do this instead:** Use a template file (Jinja2 / nunjucks). Escape all user-provided values explicitly in the template context. Test the rendered output for known edge cases.
|
||||||
|
|
||||||
|
### Anti-Pattern 2: Storing Driver Blobs in SQLite
|
||||||
|
|
||||||
|
**What people do:** Base64-encode driver ZIPs and store them in a BLOB column.
|
||||||
|
**Why it's wrong:** SQLite performs poorly with large BLOBs; the database file balloons; backups become unwieldy; streaming is impossible.
|
||||||
|
**Do this instead:** Store files on the filesystem (Docker volume). Store only the path/hash reference in SQLite.
|
||||||
|
|
||||||
|
### Anti-Pattern 3: Blocking the Server During Package Generation
|
||||||
|
|
||||||
|
**What people do:** Call IntuneWinAppUtil.exe synchronously in the request handler with no timeout.
|
||||||
|
**Why it's wrong:** If generation hangs (tool crash, permission issue), the HTTP worker is permanently blocked. On a single-worker server this freezes the entire app.
|
||||||
|
**Do this instead:** Run the subprocess with a timeout (e.g., 60s). Use a thread pool or async subprocess so other requests can proceed. Return a 500 with a clear error if timeout is exceeded.
|
||||||
|
|
||||||
|
### Anti-Pattern 4: Leaving Temp Workspaces on Disk
|
||||||
|
|
||||||
|
**What people do:** Write package files to a temp directory and never clean up.
|
||||||
|
**Why it's wrong:** Disk fills up. Sensitive driver files and generated scripts accumulate indefinitely.
|
||||||
|
**Do this instead:** Use a try/finally pattern: generate in a uniquely-named temp dir, stream the response, then unconditionally delete the temp dir. For async flows, attach cleanup to the download-complete event.
|
||||||
|
|
||||||
|
### Anti-Pattern 5: Hardcoding the IntuneWinAppUtil Path
|
||||||
|
|
||||||
|
**What people do:** Hardcode `/app/tools/IntuneWinAppUtil.exe` in the generator code.
|
||||||
|
**Why it's wrong:** Breaks if the tool is moved, the container is Windows vs Linux with Wine, or the tool is replaced.
|
||||||
|
**Do this instead:** Configure via an environment variable (`INTUNEWIN_TOOL_PATH`). The builder service reads from config. Switching to a native Python reimplementation means changing only config, not code.
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
### External Tooling
|
||||||
|
|
||||||
|
| Tool | Integration Pattern | Notes |
|
||||||
|
|------|--------------------|-------|
|
||||||
|
| IntuneWinAppUtil.exe | subprocess call with timeout | Must be bundled in container image. On Linux, requires Windows container base image OR Wine. Wine image ~500 MB extra. Consider native reimplementation instead. |
|
||||||
|
| PowerShell (target endpoints) | Template output only — no runtime PS in container | Container never executes PS; it only generates .ps1 files. |
|
||||||
|
|
||||||
|
### Internal Boundaries
|
||||||
|
|
||||||
|
| Boundary | Communication | Notes |
|
||||||
|
|----------|--------------|-------|
|
||||||
|
| API Layer ↔ Services | Direct function call (same process) | No IPC needed; single container, single process. |
|
||||||
|
| Services ↔ DB | SQLite via ORM or raw queries | Use WAL mode for concurrent reads during long-running generates. |
|
||||||
|
| Services ↔ Driver Store | Filesystem reads via Driver Store abstraction | Abstraction allows future swap to S3 without service changes. |
|
||||||
|
| Package Builder ↔ Generators | Direct function call, returns file path | Each generator writes output to a caller-provided temp directory. |
|
||||||
|
| API Layer ↔ Temp Files | Generator returns path → API streams file → cleanup | Use streaming response to avoid loading entire archive into memory. |
|
||||||
|
|
||||||
|
## Scaling Considerations
|
||||||
|
|
||||||
|
This is an internal MSP tool. Scaling expectations are low; design for correctness and maintainability, not throughput.
|
||||||
|
|
||||||
|
| Scale | Architecture Adjustments |
|
||||||
|
|-------|--------------------------|
|
||||||
|
| 1-5 concurrent technicians | Default single-worker setup is fine. Use thread pool for subprocess calls. SQLite WAL mode handles concurrent reads. |
|
||||||
|
| 10+ concurrent technicians | Add Gunicorn/Uvicorn worker count (still single container). Background job queue (in-process, e.g. Python's concurrent.futures) if generation is slow. |
|
||||||
|
| Multi-tenant or SaaS | Out of scope. Would require auth, per-tenant isolation, external storage — full redesign. |
|
||||||
|
|
||||||
|
### Scaling Priorities
|
||||||
|
|
||||||
|
1. **First bottleneck:** IntuneWinAppUtil.exe process duration (CPU/disk). Mitigation: timeout + thread pool.
|
||||||
|
2. **Second bottleneck:** Disk I/O on the Docker volume for large driver packages. Mitigation: content-addressing avoids redundant copies.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [Microsoft: .intunewin format internals — How to decode Intune Win32 App Packages](https://msendpointmgr.com/2019/01/18/how-to-decode-intune-win32-app-packages/)
|
||||||
|
- [Microsoft Learn: Prepare a Win32 App for Intune (.intunewin packaging)](https://learn.microsoft.com/en-us/intune/intune-service/apps/apps-win32-prepare)
|
||||||
|
- [Microsoft Win32 Content Prep Tool (IntuneWinAppUtil) — GitHub](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
|
||||||
|
- [FastAPI deployment in Docker containers](https://fastapi.tiangolo.com/deployment/docker/)
|
||||||
|
- [FastAPI file upload handling](https://betterstack.com/community/guides/scaling-python/uploading-files-using-fastapi/)
|
||||||
|
- [Web-Queue-Worker Architecture Style — Azure Architecture Center](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/web-queue-worker)
|
||||||
|
- [PowerShell as Win32 installer in Intune (2026)](https://headsinthecloud.blog/2026/02/24/from-packaging-to-logic-powershell-as-the-new-win32-installer-in-intune/)
|
||||||
|
- [NinjaOne Remote Script Deployment](https://www.ninjaone.com/remote-script-deployment/)
|
||||||
|
|
||||||
|
---
|
||||||
|
*Architecture research for: ImpTune — single-container printer deployment package generator*
|
||||||
|
*Researched: 2026-04-10*
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
# Feature Research
|
||||||
|
|
||||||
|
**Domain:** Printer deployment package generator for IT admins / MSPs (Intune + NinjaRMM)
|
||||||
|
**Researched:** 2026-04-10
|
||||||
|
**Confidence:** HIGH (core features verified against multiple official and community sources)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Landscape
|
||||||
|
|
||||||
|
### Table Stakes (Users Expect These)
|
||||||
|
|
||||||
|
Features IT admins and MSPs assume exist in any printer deployment tool.
|
||||||
|
Missing these = product feels incomplete or unusable for the target workflow.
|
||||||
|
|
||||||
|
| Feature | Why Expected | Complexity | Notes |
|
||||||
|
|---------|--------------|------------|-------|
|
||||||
|
| Printer name configuration | Every deployment script requires a display name | LOW | Maps directly to `Add-Printer -Name` parameter; must match detection registry key exactly |
|
||||||
|
| IP address / hostname input | TCP/IP port creation requires the printer's network address | LOW | Used for `Add-PrinterPort -PrinterHostAddress`; accepts both IP and DNS name |
|
||||||
|
| Driver file upload (ZIP/INF) | Drivers must be bundled in the package — no external downloads at deploy time | MEDIUM | Entire extracted driver folder needed (INF + CAT + dependent files, up to 4 folders deep); partial upload breaks deployment silently |
|
||||||
|
| Driver name selection from INF | Must exactly match the driver string in the INF file | MEDIUM | Wrong driver name = silent install failure; tool should parse INF to offer a dropdown instead of free text |
|
||||||
|
| PowerShell install script generation | Core deliverable for both Intune and NinjaRMM | MEDIUM | Must call `pnputil /add-driver`, `Add-PrinterPort`, `Add-PrinterDriver`, `Add-Printer` in correct order |
|
||||||
|
| PowerShell uninstall script generation | Intune Win32 apps require an uninstall command | LOW | `Remove-Printer` + `Remove-PrinterDriver` + `Remove-PrinterPort` |
|
||||||
|
| Intune detection rule / detection script | Win32 apps require detection to know if install succeeded | MEDIUM | Registry check at `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Printers\[Name]`; tool generates this script |
|
||||||
|
| .intunewin package export | End deliverable for Intune Win32 app deployment | HIGH | AES-256 + HMAC-SHA256 encrypted ZIP-in-ZIP format; can be implemented without IntuneWinAppUtil.exe using the documented format (C# library: volodymyrsmirnov/IntuneWin or svrooij ContentPrep) |
|
||||||
|
| NinjaRMM ZIP package export | End deliverable for RMM script-based deployment | LOW | ZIP containing PS install script + driver folder; no encryption required |
|
||||||
|
| Duplex mode setting | IT admins universally configure this at deployment time | LOW | `Set-PrintConfiguration -DuplexingMode [OneSided|TwoSidedLongEdge|TwoSidedShortEdge]` |
|
||||||
|
| Color vs. grayscale default | Standard printer policy setting | LOW | `Set-PrintConfiguration -Color $true/$false` |
|
||||||
|
| Paper size default | Required for most environments (A4 vs Letter conflict is a common complaint) | LOW | `Set-PrintConfiguration -PaperSize [A4|Letter|...]` — large enum, expose only common values |
|
||||||
|
| Printer persistence across sessions | MSPs configure many printers; losing config between sessions is unacceptable | MEDIUM | SQLite or flat-file storage on Docker volume; drivers stored in volume, config in DB |
|
||||||
|
| Client/tenant organization | MSPs manage dozens of clients; printers must be grouped | MEDIUM | Simple client label/folder — no RBAC needed since no auth; flat list with client filter |
|
||||||
|
|
||||||
|
### Differentiators (Competitive Advantage)
|
||||||
|
|
||||||
|
Features that justify building ImpTune instead of manually scripting. Not expected from raw scripts, but high value for the target user.
|
||||||
|
|
||||||
|
| Feature | Value Proposition | Complexity | Notes |
|
||||||
|
|---------|-------------------|------------|-------|
|
||||||
|
| INF file parsing for driver name auto-detection | Eliminates the #1 source of silent install failures (wrong driver name string) | MEDIUM | Parse INF `[Strings]` and `[Manufacturer]` sections to extract valid driver names; present as dropdown |
|
||||||
|
| SYSTEM vs. user context self-elevation in generated script | Intune runs as SYSTEM, user-invoked scripts need elevation — single script handles both | MEDIUM | Detect `[System.Security.Principal.WindowsIdentity]::GetCurrent()` + UAC re-launch; this is the most-cited pain point in community guides |
|
||||||
|
| Custom app icon embedding for Intune | Company Portal shows the icon; admins want printer-specific icons, not generic app icon | LOW | Accept PNG upload (256x256, max 750KB per Intune spec); embed in package metadata |
|
||||||
|
| Driver file size optimization hint | Manufacturer ZIPs are often 500MB+; only a fraction is needed | MEDIUM | After INF parse, flag which files are referenced vs. unused; suggest cleanup to reduce .intunewin size |
|
||||||
|
| One-click package re-generation | When IP or settings change, regenerate without re-uploading drivers | LOW | Decouple printer config (IP, name, settings) from driver storage; re-run generation against stored drivers |
|
||||||
|
| Collate default setting | Less common but occasionally required for multifunction devices | LOW | `Set-PrintConfiguration -Collate $true/$false` |
|
||||||
|
| Package naming convention enforcement | Generated filenames should be consistent (client_printer_vN.intunewin) | LOW | Prevents file management confusion on the MSP side |
|
||||||
|
| Intune install/uninstall command preview | Show the exact command strings before export, copyable | LOW | Saves the Intune portal upload step where commands must be typed manually |
|
||||||
|
|
||||||
|
### Anti-Features (Commonly Requested, Often Problematic)
|
||||||
|
|
||||||
|
| Feature | Why Requested | Why Problematic | Alternative |
|
||||||
|
|---------|---------------|-----------------|-------------|
|
||||||
|
| Direct Intune API push (auto-upload to tenant) | Saves the manual upload step | Requires per-tenant OAuth tokens, multi-tenant app registration, and ongoing credential management — turns a stateless tool into an identity-aware service; scope and security surface explode | Export package + show exact Intune portal steps; keep the tool stateless |
|
||||||
|
| Real-time printer status / monitoring | "See if the printer is online" seems useful | Requires network access to each client site, SNMP polling, ongoing agent or VPN — far outside the deployment packaging scope | Out of scope; use RMM or dedicated monitoring tools |
|
||||||
|
| User authentication / per-technician logins | Seems professional | Adds session management, password reset flow, and auth complexity; the tool runs on a private network where single-user access is the reality | Document that the tool is internal-only, recommend network-level access control (VPN, firewall) |
|
||||||
|
| Print server migration / export (Printbrm) | Some clients have print servers to migrate | Printbrm.exe output format is entirely different from the per-printer Win32 package workflow; supporting both creates a confusing dual-path UI | Separate future feature if validated; not v1 |
|
||||||
|
| Universal Print integration | Microsoft's cloud print service is growing | Requires Azure subscription, different deployment model (no drivers, CSP), different target persona — not the MSP TCP/IP printer workflow | Document as out-of-scope; Universal Print has its own provisioning tool |
|
||||||
|
| Mobile / tablet UI | Nice to have | Target users are at a workstation doing IT work; responsive design adds cost for zero validated demand | Desktop browser only; CSS can be clean without being mobile-optimized |
|
||||||
|
| Multi-language / localization | International MSPs might want this | Zero demand signal yet; adds ongoing maintenance overhead for every string in the UI | English only for v1 |
|
||||||
|
| Full audit log / deployment history | Compliance-minded requests | State management complexity; printers are reconfigured infrequently; MSPs already have Intune/RMM logs | Out of scope for v1 per PROJECT.md |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
[Driver File Upload]
|
||||||
|
└──requires──> [INF Parsing]
|
||||||
|
└──enables──> [Driver Name Dropdown]
|
||||||
|
└──enables──> [Driver File Optimization Hint]
|
||||||
|
|
||||||
|
[Printer Config Form]
|
||||||
|
├──requires──> [Driver Name Dropdown] (needs driver to be uploaded first)
|
||||||
|
├──requires──> [IP / Hostname Input]
|
||||||
|
└──requires──> [Print Settings] (duplex, color, paper size)
|
||||||
|
|
||||||
|
[PowerShell Script Generation]
|
||||||
|
└──requires──> [Printer Config Form] (all fields must be valid)
|
||||||
|
└──requires──> [Driver File Upload] (driver files embedded in package)
|
||||||
|
|
||||||
|
[Intune Package Export (.intunewin)]
|
||||||
|
└──requires──> [PowerShell Script Generation]
|
||||||
|
└──requires──> [Detection Script Generation]
|
||||||
|
└──optionally-uses──> [Custom Icon Upload]
|
||||||
|
|
||||||
|
[NinjaRMM Package Export (ZIP)]
|
||||||
|
└──requires──> [PowerShell Script Generation]
|
||||||
|
└──independent-of──> [Detection Script Generation] (NinjaRMM doesn't need Intune detection logic)
|
||||||
|
|
||||||
|
[Client/Tenant Organization]
|
||||||
|
└──enhances──> [Printer Persistence] (printers stored per client label)
|
||||||
|
└──independent-of──> [Package Export]
|
||||||
|
|
||||||
|
[One-Click Regeneration]
|
||||||
|
└──requires──> [Printer Persistence]
|
||||||
|
└──requires──> [PowerShell Script Generation]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Notes
|
||||||
|
|
||||||
|
- **Driver Name Dropdown requires INF Parsing:** Free-text driver name is the most common cause of silent deployment failure; parsing the INF to offer valid options is a prerequisite to a reliable tool, not a nice-to-have.
|
||||||
|
- **Detection Script requires exact Printer Name match:** The registry key checked at `HKLM\SYSTEM\CurrentControlSet\Control\Print\Printers\[Name]` must exactly match the `-Name` parameter in `Add-Printer`. The tool must use the same value in both generated scripts.
|
||||||
|
- **.intunewin export requires both install and detection scripts:** Intune Win32 apps require install command, uninstall command, and detection method — all three must be generated together.
|
||||||
|
- **NinjaRMM export is independent of detection logic:** NinjaRMM script execution does not use Intune detection rules; the ZIP just needs the install script and driver folder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MVP Definition
|
||||||
|
|
||||||
|
### Launch With (v1)
|
||||||
|
|
||||||
|
Minimum to validate the core value proposition: "generate a working deployment package in minutes."
|
||||||
|
|
||||||
|
- [ ] Driver file upload (ZIP containing INF + supporting files)
|
||||||
|
- [ ] INF parsing with driver name dropdown selection
|
||||||
|
- [ ] Printer config form: name, IP/hostname, port name (auto-suggested from IP)
|
||||||
|
- [ ] Print settings: duplex mode, color/mono, paper size (A4/Letter/Legal at minimum)
|
||||||
|
- [ ] PowerShell install script generation (pnputil staging + Add-PrinterPort + Add-PrinterDriver + Add-Printer + Set-PrintConfiguration)
|
||||||
|
- [ ] PowerShell uninstall script generation (Remove-Printer + Remove-PrinterDriver + Remove-PrinterPort)
|
||||||
|
- [ ] Detection script generation (registry-based, printer name match)
|
||||||
|
- [ ] Self-elevation logic in install script (SYSTEM detection + UAC re-launch for user context)
|
||||||
|
- [ ] .intunewin package export (AES-256 encrypted ZIP-in-ZIP, no IntuneWinAppUtil.exe dependency)
|
||||||
|
- [ ] NinjaRMM ZIP export (script + driver folder)
|
||||||
|
- [ ] Client/tenant label for printer organization
|
||||||
|
- [ ] Printer config persistence (SQLite on Docker volume)
|
||||||
|
- [ ] Single Docker container runtime
|
||||||
|
|
||||||
|
### Add After Validation (v1.x)
|
||||||
|
|
||||||
|
Add once the packaging workflow is validated by real MSP use:
|
||||||
|
|
||||||
|
- [ ] Custom icon upload for Intune package (256x256 PNG, 750KB max) — trigger: MSPs ask for it after using v1
|
||||||
|
- [ ] Driver file size optimization hints (unused file flagging) — trigger: users complain about large package sizes
|
||||||
|
- [ ] One-click package regeneration from saved config — trigger: users report re-uploading drivers repeatedly
|
||||||
|
- [ ] Intune command string preview panel — trigger: friction reported during Intune portal upload step
|
||||||
|
- [ ] Collate setting — trigger: user request
|
||||||
|
|
||||||
|
### Future Consideration (v2+)
|
||||||
|
|
||||||
|
Defer until product-market fit and real usage patterns emerge:
|
||||||
|
|
||||||
|
- [ ] Bulk printer import (CSV) — defer: unknown if MSPs manage dozens at once or one at a time
|
||||||
|
- [ ] Package version history per printer — defer: adds state complexity, low signal demand
|
||||||
|
- [ ] Print server migration path (Printbrm) — defer: different workflow, validate separately
|
||||||
|
- [ ] API / CLI mode for CI/CD integration — defer: advanced use case, no demand signal yet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Prioritization Matrix
|
||||||
|
|
||||||
|
| Feature | User Value | Implementation Cost | Priority |
|
||||||
|
|---------|------------|---------------------|----------|
|
||||||
|
| INF parsing + driver name dropdown | HIGH | MEDIUM | P1 |
|
||||||
|
| PowerShell script generation (install/uninstall/detection) | HIGH | MEDIUM | P1 |
|
||||||
|
| Self-elevation logic in generated script | HIGH | LOW | P1 |
|
||||||
|
| .intunewin package export | HIGH | HIGH | P1 |
|
||||||
|
| NinjaRMM ZIP export | HIGH | LOW | P1 |
|
||||||
|
| Driver file upload + storage | HIGH | MEDIUM | P1 |
|
||||||
|
| Printer config form (name, IP, settings) | HIGH | LOW | P1 |
|
||||||
|
| Client/tenant organization | MEDIUM | LOW | P1 |
|
||||||
|
| Printer persistence (SQLite) | MEDIUM | LOW | P1 |
|
||||||
|
| Custom icon upload | MEDIUM | LOW | P2 |
|
||||||
|
| Driver size optimization hints | MEDIUM | MEDIUM | P2 |
|
||||||
|
| One-click regeneration | MEDIUM | LOW | P2 |
|
||||||
|
| Intune command preview panel | LOW | LOW | P2 |
|
||||||
|
| Collate setting | LOW | LOW | P3 |
|
||||||
|
| Bulk CSV import | LOW | MEDIUM | P3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Competitor Feature Analysis
|
||||||
|
|
||||||
|
| Feature | Manual scripting (status quo) | PrinterLogic / Vasion | ImpTune approach |
|
||||||
|
|---------|-------------------------------|----------------------|-----------------|
|
||||||
|
| Driver packaging | Manual — IT admin extracts files, writes pnputil commands | Serverless SaaS, no local driver packaging needed | Automated — upload ZIP, parse INF, bundle |
|
||||||
|
| Script generation | Manual — written per printer, error-prone | Not applicable (agent-based) | Generated from form inputs |
|
||||||
|
| Intune integration | Manual .intunewin wrapping + portal upload | Native Intune app provisioning | Export .intunewin; admin uploads to portal |
|
||||||
|
| NinjaRMM integration | Manual — custom script per RMM job | Not targeted | Export ZIP; admin pastes into NinjaRMM script |
|
||||||
|
| Multi-client support | Ad-hoc folder structure | Tenant-level in SaaS portal | Client label per printer, no auth overhead |
|
||||||
|
| Self-hosted / air-gapped | Possible but no tooling support | Cloud SaaS only | Single Docker container, fully self-hosted |
|
||||||
|
| Cost | Free (time is the cost) | Per-printer SaaS licensing (~$1-3/printer/month) | Self-hosted, no per-seat cost |
|
||||||
|
| Detection rules | Written manually, often wrong | N/A | Auto-generated, guaranteed consistent with install script |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- MSEndpointMgr: Install Network Printers with Intune Win32 apps — https://msendpointmgr.com/2022/01/03/install-network-printers-intune-win32apps-powershell/
|
||||||
|
- Call4Cloud: Deploy Intune Printer Drivers (PnPutil/Printbrm) — https://call4cloud.nl/deploy-printer-drivers-intune-win32app/
|
||||||
|
- Microsoft Learn: Set-PrintConfiguration cmdlet — https://learn.microsoft.com/en-us/powershell/module/printmanagement/set-printconfiguration?view=windowsserver2025-ps
|
||||||
|
- GitHub (Sheeves11): intune-win32-print tool — https://github.com/Sheeves11/intune-win32-print
|
||||||
|
- GitHub (volodymyrsmirnov): IntuneWin library (C#) — https://github.com/volodymyrsmirnov/IntuneWin
|
||||||
|
- Stephan van Rooij: Creating IntuneWin files programmatically in C# — https://svrooij.io/2023/10/24/create-intunewin-file/
|
||||||
|
- GitHub (jeremy-leonard-gsi): intune-print-driver-deployment scripts — https://github.com/jeremy-leonard-gsi/intune-print-driver-deployment
|
||||||
|
- NinjaOne: Network Printer Setup with PowerShell — https://www.ninjaone.com/script-hub/network-printer-setup-powershell/
|
||||||
|
- Microsoft Learn: Add and Assign Win32 Apps to Intune — https://learn.microsoft.com/en-us/intune/intune-service/apps/apps-win32-add
|
||||||
|
- PrinterLogic driver management documentation — https://help.printerlogic.com/saas/Print/Print_Mgmt/Drivers_Profiles/Driver-Management.htm
|
||||||
|
|
||||||
|
---
|
||||||
|
*Feature research for: Printer deployment package generator (Intune + NinjaRMM)*
|
||||||
|
*Researched: 2026-04-10*
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
```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:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$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:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 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
|
||||||
|
|
||||||
|
- MSEndpointMgr: [Install Network Printers via Intune Win32 Apps](https://msendpointmgr.com/2022/01/03/install-network-printers-intune-win32apps-powershell/)
|
||||||
|
- Call4Cloud: [Deploy Printer Drivers Intune — pnputil/Printbrm/PrnDrvr](https://call4cloud.nl/deploy-printer-drivers-intune-win32app/)
|
||||||
|
- Call4Cloud: [Sysnative, 64-bit, WOW6432Node in Intune](https://call4cloud.nl/sysnative-64-bit-ime-intune-syswow64-wow6432node/)
|
||||||
|
- Svrooij.io: [Open-source Intune Content Prep — cross-platform reimplementation](https://svrooij.io/2023/10/19/open-source-intune-content-prep/)
|
||||||
|
- Svrooij.io: [Analysing the Win32 Content Prep Tool format](https://svrooij.io/2023/10/04/analysing-win32-content-prep-tool/)
|
||||||
|
- Microsoft Learn: [Install a printer driver via PowerShell](https://learn.microsoft.com/en-us/answers/questions/1180091/install-a-printer-via-powershell-script)
|
||||||
|
- Microsoft Learn: [Preventing script injection in PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/security/preventing-script-injection?view=powershell-7.5)
|
||||||
|
- Microsoft Learn: [Prepare a Win32 App for Intune](https://learn.microsoft.com/en-us/intune/intune-service/apps/apps-win32-prepare)
|
||||||
|
- WOSHub: [PowerShell managing printers and drivers](https://woshub.com/powershell-managing-printers-and-their-drivers-in-windows-8/)
|
||||||
|
- SMBtotheCloud: [User vs System install behavior in Intune](https://smbtothecloud.com/user-vs-system-install-behavior-know-what-your-scripts-are-doing-and-how-to-open-powershell-as-system/)
|
||||||
|
- Patchmypc: [Intune Win32 PowerShell 64-bit switch not working](https://patchmypc.com/blog/intune-win32-powershell-script-installer-64-bit-switch-not-working/)
|
||||||
|
- Dennis Span: [Printer Drivers Installation and Troubleshooting Guide](https://dennisspan.com/printer-drivers-installation-and-troubleshooting-guide/)
|
||||||
|
- GitHub Microsoft: [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Pitfalls research for: ImpTune — printer deployment webapp*
|
||||||
|
*Researched: 2026-04-10*
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# Stack Research
|
||||||
|
|
||||||
|
**Domain:** Self-hosted single-container internal tool — Windows printer deployment package generator
|
||||||
|
**Researched:** 2026-04-10
|
||||||
|
**Confidence:** HIGH (core stack), MEDIUM (.intunewin reimplementation specifics)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Stack
|
||||||
|
|
||||||
|
### Core Technologies
|
||||||
|
|
||||||
|
| Technology | Version | Purpose | Why Recommended |
|
||||||
|
|------------|---------|---------|-----------------|
|
||||||
|
| Python | 3.12 | Runtime | LTS-stable, broad library support, zipfile/cryptography stdlib covers .intunewin needs without C extensions. 3.13 is fine too; avoid 3.11 and below (FastAPI 0.130+ requires 3.10+ but 3.12 is the sweet spot for Docker image size vs feature parity). |
|
||||||
|
| FastAPI | 0.115.x (latest stable) | HTTP framework + API | De-facto standard for Python internal tools in 2025. Async-capable, Pydantic validation built-in, native file upload/download support, StreamingResponse for generated archives. Simpler than Django for this scope; more structured than Flask. |
|
||||||
|
| Uvicorn | 0.30.x | ASGI server | FastAPI's recommended server. Handles subprocess management internally — Gunicorn is not needed for a single-container internal tool with no concurrency requirements. |
|
||||||
|
| Jinja2 | 3.1.x | Server-side HTML templating | Ships with FastAPI's template support. No build step, no Node. HTMX+Jinja2 renders the full UI from the server. |
|
||||||
|
| HTMX | 2.0.x (CDN) | Dynamic UI without JavaScript SPA | Handles partial page updates (form submissions, driver list refresh, package generation progress) with zero build tooling. Ideal for CRUD-heavy internal tools. Single `<script>` tag. |
|
||||||
|
| Alpine.js | 3.x (CDN) | Client-side UI state | Complements HTMX: dropdowns, modals, toggle states, file-input previews. Replaces React/Vue for this scope. No npm, no bundler. |
|
||||||
|
| SQLite (stdlib) | 3.x (bundled) | Persistence: driver records, printer configs | No external database process. Python's built-in `sqlite3` module is sufficient for sync operations. Single-writer model is not a concern for a single-user internal tool. Mount via Docker named volume. |
|
||||||
|
| Tailwind CSS | 4.x (CDN Play) | Utility-first CSS | For an internal tool with no public users, the CDN Play script is acceptable. It avoids a Node.js build step inside the Docker image. If CSS size ever matters, switch to the standalone Tailwind CLI binary (no Node required). |
|
||||||
|
|
||||||
|
### Supporting Libraries
|
||||||
|
|
||||||
|
| Library | Version | Purpose | When to Use |
|
||||||
|
|---------|---------|---------|-------------|
|
||||||
|
| `python-multipart` | 0.0.9 | Multipart file upload parsing | Required by FastAPI for `UploadFile`. Always install alongside FastAPI when handling file uploads. |
|
||||||
|
| `pycryptodome` | 3.20.x | AES-256-CBC + HMAC-SHA256 for .intunewin encryption | The .intunewin inner package is encrypted with AES-256-CBC; the HMAC-SHA256 digest is written to Detection.xml. Use pycryptodome (not the deprecated pycrypto). |
|
||||||
|
| `lxml` or `xml.etree.ElementTree` (stdlib) | stdlib | Generate Detection.xml metadata | Detection.xml is simple enough for stdlib ElementTree. Only reach for lxml if namespaces become complex. |
|
||||||
|
| `aiofiles` | 23.x | Async file I/O | Used with FastAPI's StreamingResponse when streaming generated ZIPs to the browser without loading the full archive into memory. |
|
||||||
|
| `python-dotenv` | 1.0.x | Environment-based configuration | Allows Docker-level config overrides (data directory, port, base URL) without rebuilding the image. |
|
||||||
|
| `peewee` | 3.17.x | ORM for SQLite | Lightweight sync ORM — perfectly matched to SQLite's single-writer model. Avoids the async overhead of SQLModel/aiosqlite, which provides no benefit with SQLite. Use only if raw `sqlite3` becomes unwieldy across multiple tables. |
|
||||||
|
|
||||||
|
### Development Tools
|
||||||
|
|
||||||
|
| Tool | Purpose | Notes |
|
||||||
|
|------|---------|-------|
|
||||||
|
| Docker (python:3.12-slim-bookworm) | Container base image | Slim variant keeps image under 200 MB. Bookworm (Debian 12) has modern glibc. Do NOT use Alpine Linux — pycryptodome and other C-extension wheels often lack musl-compatible builds, causing silent failures. |
|
||||||
|
| `uvicorn --reload` | Dev server with hot reload | Run locally during development. In Docker, use `CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]` without `--reload`. |
|
||||||
|
| Docker named volume | Driver + SQLite persistence | Mount `/data` as a named volume. Store driver ZIPs and the SQLite file there. Never store state in the container filesystem. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Core runtime
|
||||||
|
pip install fastapi==0.115.* uvicorn[standard]==0.30.* jinja2==3.1.* python-multipart==0.0.9
|
||||||
|
|
||||||
|
# File generation and encryption
|
||||||
|
pip install pycryptodome==3.20.* aiofiles==23.*
|
||||||
|
|
||||||
|
# Configuration + optional ORM
|
||||||
|
pip install python-dotenv==1.0.* peewee==3.17.*
|
||||||
|
|
||||||
|
# Dev only
|
||||||
|
pip install httpx pytest pytest-asyncio
|
||||||
|
```
|
||||||
|
|
||||||
|
Tailwind CSS and HTMX are loaded from CDN in templates — no npm install.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
| Recommended | Alternative | When to Use Alternative |
|
||||||
|
|-------------|-------------|-------------------------|
|
||||||
|
| FastAPI + Jinja2 + HTMX | Django + django-htmx | If the project grows to need Django admin, multi-tenant auth, or complex ORM migrations. Overkill for this scope. |
|
||||||
|
| FastAPI + Jinja2 + HTMX | FastAPI + React/Vue SPA | If the UI needs real-time collaborative editing or complex client-side state (drag-and-drop ordering, offline mode). Not needed here. |
|
||||||
|
| Python (FastAPI) | Go (Gin/Chi) | If single binary with no runtime dependency is the top priority. Go produces a smaller Docker image but adds complexity for the .intunewin crypto/zip logic that Python stdlib already handles. |
|
||||||
|
| pycryptodome | cryptography (PyCA) | `cryptography` is fine too and arguably better maintained. Either works for AES-CBC. Prefer `cryptography` if you want FIPS-like assurance; pycryptodome is simpler to use for this exact pattern (CBC + HMAC). |
|
||||||
|
| SQLite (builtin / peewee) | PostgreSQL | If multi-user write concurrency is ever needed. Not the case here. |
|
||||||
|
| Tailwind CDN | Tailwind standalone CLI | Use the standalone CLI binary (no Node) if you want purged, production-sized CSS. Drop the `tailwindcss` binary into the Docker image at build time. Adds ~10 MB to the image but removes CDN dependency. |
|
||||||
|
| python:3.12-slim-bookworm | python:3.12-alpine | Alpine breaks C-extension wheels. Slim Debian is the correct base for any project using pycryptodome or similar. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What NOT to Use
|
||||||
|
|
||||||
|
| Avoid | Why | Use Instead |
|
||||||
|
|-------|-----|-------------|
|
||||||
|
| IntuneWinAppUtil.exe inside Docker | It is a Windows PE binary. It will not run in a Linux container. Even Wine would add hundreds of MB and introduce instability. | Reimplement the format in Python: `zipfile` (stdlib) for inner ZIP with `ZIP_STORED`, `pycryptodome` for AES-256-CBC encryption, `xml.etree.ElementTree` for Detection.xml. The format is fully documented by reverse engineering (svrooij.io). |
|
||||||
|
| Alpine Linux base image | musl libc breaks pre-built wheels for pycryptodome and other C-extension packages, causing pip to fall back to source builds or fail silently. | `python:3.12-slim-bookworm` — Debian-based, glibc, pre-built wheels always work. |
|
||||||
|
| Celery / Redis / task queue | Massively overengineered for a single-user internal tool. .intunewin generation takes < 5 seconds per package. | FastAPI `BackgroundTasks` is sufficient for post-response cleanup; synchronous generation on the request thread is fine. |
|
||||||
|
| SQLAlchemy async (aiosqlite) | Async gives zero throughput benefit with SQLite (single-writer lock). Adds complexity for no gain. | Synchronous `sqlite3` stdlib or `peewee`. Run database calls synchronously inside `asyncio.run_in_executor` if truly needed. |
|
||||||
|
| React / Vue / Next.js | Requires a Node.js build pipeline in the Docker image, a separate frontend build stage, and a JS bundle. Unnecessary complexity for an internal CRUD tool. | HTMX + Alpine.js served from Jinja2 templates. No build step. |
|
||||||
|
| PyCrypto (original) | Unmaintained since 2012. Known vulnerabilities. | `pycryptodome` — a maintained drop-in replacement. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack Patterns by Variant
|
||||||
|
|
||||||
|
**If .intunewin generation needs to stay on the request thread (simplest path):**
|
||||||
|
- Generate the .intunewin file synchronously, stream it with `FileResponse` or `StreamingResponse`, then delete the temp file with `BackgroundTasks`.
|
||||||
|
- No async file I/O needed for packages under ~500 MB.
|
||||||
|
|
||||||
|
**If driver ZIPs are very large (>500 MB) and upload times block Uvicorn:**
|
||||||
|
- Use `aiofiles` for async reads during upload processing.
|
||||||
|
- Uvicorn is single-threaded by default in development; add `--workers 2` in production Dockerfile if needed (though unlikely for an internal tool).
|
||||||
|
|
||||||
|
**If Tailwind CDN becomes a problem (slow intranet, offline use):**
|
||||||
|
- Download the Tailwind standalone CLI (`tailwindcss-linux-x64`) into the Docker image during build.
|
||||||
|
- Run `tailwindcss -i input.css -o static/output.css --minify` as a Dockerfile `RUN` step.
|
||||||
|
- Serve the generated CSS as a static file. Zero Node.js required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## .intunewin Reimplementation (Critical Detail)
|
||||||
|
|
||||||
|
The official `IntuneWinAppUtil.exe` is Windows-only. The format is well-documented through reverse engineering:
|
||||||
|
|
||||||
|
1. **Inner ZIP** — compress source folder using `zipfile.ZipFile(..., compression=ZIP_DEFLATED)` (note: unlike the outer ZIP, the inner package uses DEFLATE not STORED)
|
||||||
|
2. **Encrypt** — AES-256-CBC with a random 32-byte key and 32-byte IV; prepend HMAC-SHA256 (32 bytes) then IV (32 bytes) to the ciphertext
|
||||||
|
3. **Detection.xml** — XML file with base64-encoded EncryptionKey, InitializationVector, Mac (HMAC), FileDigest (SHA256 of plaintext), FileDigestAlgorithm ("SHA256"), and ProfileIdentifier ("ProfileVersion1")
|
||||||
|
4. **Outer ZIP** — ZIP_STORED containing `IntuneWinPackage/Contents/IntunePackage.intunewin` (the encrypted blob) and `IntuneWinPackage/Metadata/Detection.xml`
|
||||||
|
|
||||||
|
Reference implementation: [svrooij.io — Creating IntuneWin files with C#](https://svrooij.io/2023/10/24/create-intunewin-file/) — the logic is language-agnostic and Python stdlib + pycryptodome covers all requirements. **Confidence: MEDIUM** (based on reverse-engineering documentation; validate against a real Intune upload during Phase 1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Compatibility
|
||||||
|
|
||||||
|
| Package | Compatible With | Notes |
|
||||||
|
|---------|-----------------|-------|
|
||||||
|
| fastapi 0.115.x | pydantic 2.x | FastAPI 0.115+ requires Pydantic v2. Do not mix with Pydantic v1. |
|
||||||
|
| pycryptodome 3.20.x | Python 3.12, 3.13 | Use `pycryptodome` not `pycryptodomex` (different package name, same code, different import path). Import as `from Crypto.Cipher import AES`. |
|
||||||
|
| uvicorn 0.30.x | fastapi 0.115.x | Compatible. Use `uvicorn[standard]` to include `uvloop` and `httptools` for better performance. |
|
||||||
|
| peewee 3.17.x | Python 3.12 | Sync only. Does not conflict with FastAPI's async model — call peewee from sync functions or `run_in_executor`. |
|
||||||
|
| Tailwind CDN v4 | Any browser | Play CDN is development-only per Tailwind docs. For production isolation, use standalone CLI instead. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [FastAPI Deployment with Docker — Official Docs](https://fastapi.tiangolo.com/deployment/docker/) — Docker patterns, Uvicorn configuration
|
||||||
|
- [FastAPI Templates — Official Docs](https://fastapi.tiangolo.com/advanced/templates/) — Jinja2 integration
|
||||||
|
- [svrooij.io — Creating IntuneWin files with C#](https://svrooij.io/2023/10/24/create-intunewin-file/) — .intunewin format reference (MEDIUM confidence, reverse-engineered)
|
||||||
|
- [svrooij.io — Decrypting intunewin files](https://svrooij.io/2023/10/09/decrypting-intunewin-files/) — Encryption structure validation
|
||||||
|
- [SvRooij.ContentPrep on NuGet](https://www.nuget.org/packages/SvRooij.ContentPrep) — Cross-platform C# reference implementation, last updated 2025-10-03
|
||||||
|
- [PyCryptodome docs](https://pycryptodome.readthedocs.io/en/latest/src/examples.html) — AES-CBC usage
|
||||||
|
- [Python zipfile docs](https://docs.python.org/3/library/zipfile.html) — ZIP_STORED / ZIP_DEFLATED constants
|
||||||
|
- [HTMX + FastAPI patterns 2025](https://johal.in/htmx-fastapi-patterns-hypermedia-driven-single-page-applications-2025/) — HTMX suitability for internal tools
|
||||||
|
- [Python 2025 Web Stack — Medium](https://medium.com/@hadiyolworld007/pythons-2025-web-stack-fastapi-sqlmodel-and-htmx-changed-everything-58cc2da1cf14) — FastAPI + HTMX ecosystem confirmation
|
||||||
|
- [Tailwind Play CDN docs](https://tailwindcss.com/docs/installation/play-cdn) — CDN limitations for production
|
||||||
|
- [Docker Volumes for SQLite](https://dev.to/behainguyen/python-docker-volumes-where-is-my-sqlite-database-file-48fd) — Named volume pattern
|
||||||
|
- WebSearch (multiple queries, 2026-04-10) — ecosystem verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Stack research for: ImpTune — printer deployment package generator*
|
||||||
|
*Researched: 2026-04-10*
|
||||||
Reference in New Issue
Block a user