- Create 11-01-SUMMARY.md with full execution record
- Update STATE.md: advance plan to 3, add decisions, update session
- Update ROADMAP.md: phase 11 progress to 1/4 summaries (In Progress)
The reference implementation (svrooij/ContentPrep Zipper.cs DecryptFileAsync)
reads the first 32 bytes as the stored HMAC, then hashes the *remaining* bytes
— i.e. IV (16 bytes) || ciphertext — to verify integrity. ImpTune was computing
HMAC(mac_key, ciphertext) which omits the IV. Intune's server-side HMAC check
would therefore always fail, manifesting as the same silent symptom as the
Detection.xml bug: empty wizard fields, greyed OK button, no error banner.
The blob layout is unchanged: [HMAC(32)] + [IV(16)] + [ciphertext].
Only the hash input is corrected: iv + ciphertext instead of ciphertext.
The Mac field in Detection.xml is also updated accordingly (it stores the same
HMAC value that is prepended to the blob).
Tests updated: test_hmac_matches now verifies HMAC over blob[32:] (= IV+ciphertext),
which is exactly what the reference decryption algorithm verifies against.
All 114 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four structural defects in the generated Detection.xml caused Intune's upload
wizard to silently fail metadata parsing (empty form, OK button greyed):
1. Missing ToolVersion="1.8.6.0" XML attribute on ApplicationInfo — the wizard
uses this to validate the package was produced by a compatible tool.
2. Spurious xmlns="http://schemas.microsoft.com/IntuneWin" namespace — changes
element identity for Intune's XML parser (reference emits no namespace).
3. <?xml version="1.0" ?> declaration header — reference uses OmitXmlDeclaration=true.
4. Extra <MacAlgorithm> child element inside EncryptionInfo — not present in
the reference FileEncryptionInfo model (svrooij/ContentPrep verified).
Fix: switched from toprettyxml() to tostring(xml_declaration=False)+indent(),
added ToolVersion attribute, removed xmlns and MacAlgorithm.
Tests updated to assert the corrected reference format; all 114 pass.
Root cause verified against svrooij/ContentPrep Packager.cs + ApplicationInfo.cs
(open-source C# reference implementation of IntuneWinAppUtil.exe).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Real INFs (e.g. Ricoh oemsetup.inf) include [SourceDisksFiles] entries
with bare filename lines (no '='), which strict configparser rejects
with ParsingError, surfacing as a 500 on /drivers/upload.
Pre-process the INF text to rewrite bare lines into synthetic
__bare_N = <line> entries before parsing, and filter those synthetic
keys out of DriverDesc extraction so they cannot leak into driver_names.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Import Form from fastapi for mixed multipart + form field support
- Add caller: str = Form('') parameter to upload_driver handler
- Capture new_driver from Driver.get_or_create() return tuple
- Branch on caller == 'printer_form' to emit OOB-enabled response
- Create partials/driver_upload_with_oob.html with primary driver_list
include + hx-swap-oob select targeting id=printer-form-driver-select
- New driver is auto-selected via new_driver_id context variable
All 13 driver upload tests pass including 4 new OOB contract tests.
Full non-e2e suite: 111 passed.
- Dashboard queries 5 most recent Printer records and 5 most recent driver-assigned ones
- New /packages route lists printers with drivers, with intunewin/ninja download links
- packages.html template extends base.html with Pico grid table
- Dashboard printer/package items now link to detail pages
- Quick action buttons wired to real routes (removed aria-disabled)
- Add shutil import and Icon import to packages.py
- After driver extraction, look up Icon record for printer
- If found and file exists on disk, copy to tmpdir/icon.png
- Missing icon (no DB record or missing file) silently skipped — export still succeeds
- PKG-04 requirement satisfied
- Update printer_detail() to pass install_cmd, uninstall_cmd, has_driver, has_icon to template
- Rewrite printer_detail.html with Intune Commands, Export, and Icon sections
- Show command strings with Alpine.js copy-to-clipboard buttons (install-cmd, uninstall-cmd)
- Show NinjaRMM ZIP and .intunewin download links when driver assigned
- Add HTMX icon upload form with #icon-status swap target
- Remove disabled placeholder Regenerate Package button
- Add TestCommandPreview class (4 tests) to test_packages.py
- All 94 tests pass
- Create imptune/api/icons.py with POST /printers/{printer_id}/icon
- Validate PNG format, 256x256 dimensions, 750KB max size
- Store icons SHA256-addressed under cfg.DATA_DIR/icons/
- Replace existing Icon record on re-upload
- Register icons.router in main.py with ICONS_DIR makedirs
- Patch cfg.ICONS_DIR in conftest.py for tests
- All 6 icon upload tests pass
- GET /printers/{id}/packages/ninja: in-memory ZIP with install.ps1 + driver files in named subfolder
- GET /printers/{id}/packages/intunewin: temp dir build of .intunewin via build_intunewin()
- _get_printer_and_driver() helper validates printer, driver, inf, desc
- Driver ZIP file existence check before processing (RESEARCH pitfall 3)
- TemporaryDirectory context manager for auto-cleanup (RESEARCH pitfall 1)
- Router registered in main.py after scripts router
- All 9 package tests pass, 84 total tests green
- Add GET /printers/{printer_id} route to pages.py with LEFT OUTER JOINs on Client and Driver
- Create printer_detail.html full-page template showing all config fields and driver info
- Display "No driver assigned" when driver FK is null
- Add disabled "Regenerate Package" button (Phase 4 placeholder)
- Make printer names in printer_list.html clickable links to detail page
- imptune/api/printers.py: POST /printers (all form fields, checkbox->bool,
FK resolution), DELETE /printers/{id}, grouped list renderer with LEFT JOIN
- imptune/api/clients.py: POST /clients with duplicate-name handling
- imptune/api/pages.py: GET /printers and GET /clients page routes
- imptune/main.py: register printers + clients routers; close db on shutdown
- imptune/db/database.py: close existing connection before re-init (test isolation)
- templates: printers.html, clients.html, printer_form.html (Alpine.js port
auto-derivation), printer_list.html (grouped by client), client_list.html
- tests/conftest.py: close test-thread db connection in fixture teardown
- tests/test_printer_crud.py: updated to use list(select().where()) for DB
queries (avoids Peewee thread-local cursor caching across test boundaries)
Auto-fix [Rule 1 - Bug]: DB test isolation — Peewee thread-local connections
persisted across tests causing stale DB reads; fixed via conftest teardown and
lifespan db.close() on shutdown.
- Import init_db from imptune.db.database
- Call init_db() after DATA_DIR/DRIVERS_DIR creation in lifespan
- Database auto-initializes on every app startup via volume-mounted path
- imptune/db/database.py: deferred SqliteDatabase with WAL + foreign_keys pragmas and init_db()
- imptune/db/models.py: full schema (Client, Driver, Printer, Icon) for phases 1-5
- imptune/storage/driver_store.py: SHA256 content-addressed DriverStore with dedup
- tests/test_db.py: 7 TDD tests covering all db and storage behaviors
- requirements-dev.txt with pytest and httpx
- tests/conftest.py: client and tmp_data_dir fixtures
- tests/test_health.py: GET /health returns 200 with {"status": "ok"}
- tests/test_static.py: no CDN URLs in templates, dashboard returns 200
- Fix imptune/api/pages.py: use request= kwarg in TemplateResponse (Starlette compat)
- Fix imptune/main.py: replace deprecated on_event with asynccontextmanager lifespan
- AES-256-CBC encryption with PKCS7 padding using pycryptodome
- HMAC-SHA256 of ciphertext prepended to blob (mac_key, 32 bytes)
- Encrypted blob layout: HMAC(32) + IV(16) + ciphertext
- IV is 16 bytes (corrected from STACK.md documentation error of 32 bytes)
- Detection.xml with all 8 EncryptionInfo sub-elements and correct namespace
- Inner ZIP uses DEFLATE compression; outer ZIP uses STORED compression
- All 14 byte-level tests pass including crypto roundtrip verification
- Dockerfile with python:3.12-slim-bookworm, curl downloads Pico CSS, HTMX, Alpine.js at build time
- docker-compose.yml with imptune_data:/data named volume and restart policy
- requirements.txt with all phase 1-5 dependencies
- imptune/config.py loading DATA_DIR/PORT from env with DB_PATH and DRIVERS_DIR derived paths
- imptune/main.py: FastAPI app with StaticFiles mount, health+pages routers, startup dir creation
- imptune/api/health.py: GET /health returning {"status": "ok"}
- imptune/api/pages.py: GET / returning dashboard.html with sync def handler
- imptune/templates/base.html: data-theme="auto", sidebar with 5 nav sections, /static/ paths only
- imptune/templates/dashboard.html: quick actions + empty state recent activity
- imptune/static/app.css: sidebar layout, active link highlight, quick action buttons
- 14 tests covering outer ZIP structure, Detection.xml schema, key sizes
- Cryptographic roundtrip: HMAC-SHA256, AES-256-CBC decryption, file digest
- IV size assertion: must be 16 bytes (critical correction from RESEARCH.md)
- Tests serve as format specification for build_intunewin() implementation