docs(01): create phase 1 foundation plans

Three plans covering Docker scaffold, SQLite schema, and .intunewin spike.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 11:19:05 +02:00
co-authored by Claude Opus 4.6
parent 1370635e3f
commit a8a27a44d3
4 changed files with 566 additions and 1 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ Decimal phases appear between their surrounding integers in numeric order.
2. The container has no Node.js dependency and starts from a single image 2. The container has no Node.js dependency and starts from a single image
3. A Python-generated .intunewin file uploads successfully to a real Intune tenant without format errors 3. A Python-generated .intunewin file uploads successfully to a real Intune tenant without format errors
4. SQLite database initializes automatically on first run with the correct schema 4. SQLite database initializes automatically on first run with the correct schema
**Plans**: TBD **Plans**: 3 plans
Plans: Plans:
- [ ] 01-01: Docker container scaffold (Dockerfile, python:3.12-slim-bookworm, volume, healthcheck) - [ ] 01-01: Docker container scaffold (Dockerfile, python:3.12-slim-bookworm, volume, healthcheck)
@@ -0,0 +1,238 @@
---
phase: 01-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- Dockerfile
- docker-compose.yml
- requirements.txt
- requirements-dev.txt
- imptune/main.py
- imptune/config.py
- imptune/api/__init__.py
- imptune/api/pages.py
- imptune/api/health.py
- imptune/templates/base.html
- imptune/templates/dashboard.html
- imptune/static/app.css
- tests/__init__.py
- tests/conftest.py
- tests/test_health.py
- tests/test_static.py
autonomous: true
requirements:
- INFRA-01
- INFRA-02
must_haves:
truths:
- "Running docker compose up starts the app and serves HTTP 200 on GET /health"
- "The container has no Node.js dependency and starts from a single python:3.12-slim-bookworm image"
- "All static assets (Pico CSS, HTMX, Alpine.js) are served from /static/ with zero CDN references in templates"
- "The app shell displays a sidebar with Dashboard, Drivers, Printers, Clients, Packages sections"
- "The app follows OS dark/light theme preference automatically"
artifacts:
- path: "Dockerfile"
provides: "Single-container build with baked-in static assets"
contains: "python:3.12-slim-bookworm"
- path: "docker-compose.yml"
provides: "Container orchestration with named volume"
contains: "imptune_data:/data"
- path: "imptune/main.py"
provides: "FastAPI app entrypoint with static files mount and router registration"
exports: ["app"]
- path: "imptune/api/health.py"
provides: "GET /health endpoint for Docker healthcheck"
exports: ["router"]
- path: "imptune/templates/base.html"
provides: "Layout template with sidebar navigation and static asset includes"
contains: "data-theme=\"auto\""
key_links:
- from: "Dockerfile"
to: "imptune/static/"
via: "curl downloads during build"
pattern: "curl.*pico\\.min\\.css"
- from: "imptune/main.py"
to: "imptune/api/health.py"
via: "include_router"
pattern: "include_router.*health"
- from: "imptune/templates/base.html"
to: "/static/"
via: "link and script tags"
pattern: "/static/.*\\.css|/static/.*\\.js"
---
<objective>
Create the Docker container scaffold, FastAPI app shell with sidebar navigation, health endpoint, and all baked-in static assets (Pico CSS, HTMX, Alpine.js). This is the foundation every subsequent plan builds on.
Purpose: Establish the running container and app shell that satisfies INFRA-01 (single Docker container) and INFRA-02 (no Node.js, no external DB). All subsequent phases add features to this scaffold.
Output: A buildable Docker image that starts, serves the app shell on localhost:8000, and passes healthcheck.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create Docker scaffold, FastAPI app, and app shell templates</name>
<files>
Dockerfile,
docker-compose.yml,
requirements.txt,
imptune/__init__.py,
imptune/main.py,
imptune/config.py,
imptune/api/__init__.py,
imptune/api/pages.py,
imptune/api/health.py,
imptune/templates/base.html,
imptune/templates/dashboard.html,
imptune/static/app.css
</files>
<action>
Create the full project scaffold following the architecture from RESEARCH.md. The app package is `imptune/` (not top-level modules).
**Dockerfile** (python:3.12-slim-bookworm base):
- WORKDIR /app
- Single RUN layer: apt-get install curl, mkdir -p /app/imptune/static, download Pico CSS v2 (pico.min.css), HTMX 2.x (htmx.min.js), Alpine.js 3.x (alpine.min.js) into /app/imptune/static/ using curl with --fail flag, then purge curl and clean apt cache
- COPY requirements.txt and pip install --no-cache-dir
- COPY imptune/ into /app/imptune/ and other root files
- VOLUME ["/data"]
- HEALTHCHECK using python stdlib urllib (not curl): `python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"`
- EXPOSE 8000
- CMD ["uvicorn", "imptune.main:app", "--host", "0.0.0.0", "--port", "8000"]
**docker-compose.yml**:
- Service `imptune`, build context `.`, ports 8000:8000, volume `imptune_data:/data`, restart unless-stopped, env DATA_DIR=/data
**requirements.txt** (all dependencies for phases 1-5):
- fastapi==0.115.*, uvicorn[standard]==0.30.*, jinja2==3.1.*, python-multipart==0.0.9, pycryptodome==3.20.*, python-dotenv==1.0.*, peewee==3.17.*
**imptune/config.py**:
- Load DATA_DIR from env (default "/data"), PORT from env (default 8000)
- Derive DB_PATH as DATA_DIR/imptune.db, DRIVERS_DIR as DATA_DIR/drivers
**imptune/main.py**:
- Create FastAPI app (title="ImpTune")
- Mount StaticFiles from pathlib.Path(__file__).parent / "static" at "/static"
- Set up Jinja2Templates pointing to imptune/templates/
- Include health router and pages router
- Add startup event that creates DATA_DIR and DRIVERS_DIR directories if they don't exist
**imptune/api/health.py**:
- GET /health returning {"status": "ok"}
**imptune/api/pages.py**:
- GET / returning dashboard.html template (sync def, not async)
- Pass empty recent_printers=[] and recent_packages=[] context for now
**imptune/templates/base.html**:
- html lang="en" data-theme="auto" (Pico CSS auto dark/light)
- Head: meta charset, viewport, title "ImpTune", link to /static/pico.min.css, link to /static/app.css, script defer for alpine.min.js, script for htmx.min.js
- Body: flex container with persistent left sidebar nav and main content area
- Sidebar: flat equal-weight nav links for Dashboard (/), Drivers (/drivers), Printers (/printers), Clients (/clients), Packages (/packages). Use semantic nav element. Active link highlighted.
- Main: container class wrapping {% block content %}{% endblock %}
**imptune/templates/dashboard.html**:
- Extends base.html
- Quick action buttons at top: "New Printer", "Upload Driver", "Export Package" (links, non-functional in Phase 1 — link to # with disabled state)
- Recent activity section below: empty state message "No printers configured yet" and "No packages exported yet"
**imptune/static/app.css** (under 50 lines):
- Sidebar layout: flex, sidebar fixed width ~220px, main flex-grow
- Sidebar nav styling: vertical link list, active state highlight
- Quick action button row styling
- Keep minimal — Pico CSS handles most styling
All __init__.py files: empty or minimal.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -c "from imptune.main import app; print('App created:', app.title)"</automated>
</verify>
<done>
- All files exist with correct content
- FastAPI app imports without errors
- Dockerfile builds (docker build .)
- docker-compose.yml is valid YAML
- Templates reference /static/ paths only (no CDN URLs)
- Sidebar has all 5 sections with equal weight
- data-theme="auto" is set on html element
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Create test scaffold and write health + static asset tests</name>
<files>
requirements-dev.txt,
tests/__init__.py,
tests/conftest.py,
tests/test_health.py,
tests/test_static.py
</files>
<behavior>
- test_health_returns_200: GET /health returns 200 with {"status": "ok"}
- test_static_mount_exists: app has /static mount
- test_no_cdn_urls_in_templates: scanning all .html files in imptune/templates/ finds zero references to cdn.jsdelivr.net, unpkg.com, cdnjs.com, or any https:// URL in link/script tags
- test_dashboard_returns_200: GET / returns 200
</behavior>
<action>
**requirements-dev.txt**: pytest, httpx (for FastAPI TestClient alternative — use fastapi.testclient which uses httpx internally)
**tests/conftest.py**:
- Import TestClient from fastapi.testclient (uses httpx under the hood)
- Fixture `client` that creates TestClient(app) from imptune.main
- Fixture `tmp_data_dir` using tmp_path that sets DATA_DIR env var to a temp directory before importing app, and creates the temp SQLite path
**tests/test_health.py**:
- test_health_returns_200: client.get("/health") returns 200 and JSON body {"status": "ok"}
**tests/test_static.py**:
- test_no_cdn_urls_in_templates: glob all .html files in imptune/templates/, read each, assert no matches for CDN domains (cdn.jsdelivr.net, unpkg.com, cdnjs.com) or https:// in href/src attributes
- test_dashboard_returns_200: client.get("/") returns 200
Run tests to confirm they pass (GREEN). The no-CDN test validates INFRA-02 at the template level.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pip install -r requirements-dev.txt -q && python -m pytest tests/test_health.py tests/test_static.py -x -v</automated>
</verify>
<done>
- All 4 tests pass
- Health endpoint verified via TestClient
- No CDN URLs found in any template
- Dashboard page loads successfully
</done>
</task>
</tasks>
<verification>
- `python -m pytest tests/ -x -v` — all tests pass
- `python -c "from imptune.main import app; print(app.title)"` — prints "ImpTune"
- Visually inspect templates for /static/ references only (automated by test_no_cdn_urls)
- `docker compose build` succeeds (if Docker available)
</verification>
<success_criteria>
- FastAPI app starts and serves GET /health with 200
- Dashboard page renders with sidebar navigation (5 sections)
- All static assets referenced via /static/ paths, zero CDN URLs
- Docker image builds from python:3.12-slim-bookworm with no Node.js
- Test suite passes with 4+ green tests
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`
</output>
@@ -0,0 +1,187 @@
---
phase: 01-foundation
plan: 02
type: execute
wave: 2
depends_on: ["01-01"]
files_modified:
- imptune/db/__init__.py
- imptune/db/database.py
- imptune/db/models.py
- imptune/storage/__init__.py
- imptune/storage/driver_store.py
- imptune/main.py
- tests/test_db.py
autonomous: true
requirements:
- INFRA-01
- INFRA-02
must_haves:
truths:
- "SQLite database initializes automatically on first run with all tables (Client, Driver, Printer, Icon)"
- "Database uses WAL journal mode and has foreign keys enabled"
- "Database file is created inside the DATA_DIR volume path, not inside the container filesystem"
- "Schema creation is idempotent — repeated startups do not fail or duplicate tables"
artifacts:
- path: "imptune/db/database.py"
provides: "Peewee SqliteDatabase instance with WAL mode and init_db function"
exports: ["db", "init_db"]
- path: "imptune/db/models.py"
provides: "All ORM models for phases 1-5 (BaseModel, Client, Driver, Printer, Icon)"
exports: ["BaseModel", "Client", "Driver", "Printer", "Icon"]
- path: "imptune/storage/driver_store.py"
provides: "SHA256 content-addressed file storage abstraction for driver packages"
exports: ["DriverStore"]
- path: "tests/test_db.py"
provides: "Database initialization and schema validation tests"
key_links:
- from: "imptune/main.py"
to: "imptune/db/database.py"
via: "startup event calling init_db()"
pattern: "init_db"
- from: "imptune/db/models.py"
to: "imptune/db/database.py"
via: "BaseModel.Meta.database = db"
pattern: "database = db"
- from: "imptune/db/database.py"
to: "imptune/config.py"
via: "DB_PATH from config"
pattern: "DB_PATH|DATA_DIR"
---
<objective>
Create the full SQLite schema using Peewee ORM (all tables for phases 1-5) and the content-addressed driver storage abstraction. Wire database initialization into the FastAPI startup event.
Purpose: Establish the data layer that all subsequent phases depend on. The full schema is created upfront per the locked user decision, so later phases only add routes and logic — not schema changes. Satisfies INFRA-01 (SQLite auto-init) and INFRA-02 (no external DB).
Output: Working database module with all models, driver storage helper, and startup wiring.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
<interfaces>
<!-- From plan 01-01: key exports the executor needs -->
From imptune/config.py:
```python
DATA_DIR: str # env var, default "/data"
DB_PATH: str # DATA_DIR + "/imptune.db"
DRIVERS_DIR: str # DATA_DIR + "/drivers"
```
From imptune/main.py:
```python
app = FastAPI(title="ImpTune")
# startup event already creates DATA_DIR/DRIVERS_DIR directories
# Executor must ADD init_db() call to the existing startup event
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create Peewee models, database init, and driver storage</name>
<files>
imptune/db/__init__.py,
imptune/db/database.py,
imptune/db/models.py,
imptune/storage/__init__.py,
imptune/storage/driver_store.py
</files>
<behavior>
- test_create_tables: calling init_db() creates all 4 tables (client, driver, printer, icon) in a fresh SQLite file
- test_wal_mode: after init_db(), PRAGMA journal_mode returns "wal"
- test_foreign_keys: after init_db(), PRAGMA foreign_keys returns 1
- test_idempotent: calling init_db() twice does not raise an error
- test_driver_store_save: saving bytes returns their SHA256 hex digest and creates a file at DRIVERS_DIR/{sha256}
- test_driver_store_dedup: saving the same bytes twice results in one file on disk (not two)
- test_driver_store_get_path: get_path(sha256) returns the correct file path
</behavior>
<action>
**imptune/db/database.py**:
- Import SqliteDatabase from peewee, import DB_PATH from imptune.config
- Create db = SqliteDatabase(None) (deferred init — path set at runtime so tests can override)
- init_db() function: call db.init(DB_PATH, pragmas={"journal_mode": "wal", "foreign_keys": 1}), then db.connect(reuse_if_open=True), then import all models and call db.create_tables([Client, Driver, Printer, Icon], safe=True)
- Use deferred database pattern so tests can point at a temp file
**imptune/db/models.py** (full schema for all phases per locked decision):
- BaseModel with Meta.database = db
- Client: name (CharField unique), created_at (DateTimeField default utcnow)
- Driver: sha256 (CharField unique, indexed), original_filename (CharField), size_bytes (IntegerField), uploaded_at (DateTimeField default utcnow), driver_desc (CharField null=True), inf_filename (CharField null=True), architecture (CharField null=True), has_cat_file (BooleanField default=False)
- Printer: name (CharField), ip_address (CharField), port_name (CharField), client (ForeignKeyField Client null=True backref="printers"), driver (ForeignKeyField Driver null=True backref="printers"), duplex_mode (CharField default="OneSided"), color_mode (BooleanField default=True), paper_size (CharField default="A4"), collate (BooleanField default=True), created_at, updated_at (both DateTimeField default utcnow)
- Icon: printer (ForeignKeyField Printer unique backref="icons"), sha256 (CharField), original_filename (CharField), size_bytes (IntegerField), uploaded_at (DateTimeField default utcnow)
**imptune/storage/driver_store.py**:
- Class DriverStore with __init__(self, base_dir: str)
- save(self, data: bytes) -> str: compute SHA256, write to base_dir/{sha256} if not exists, return hex digest
- get_path(self, sha256: str) -> Path: return Path(base_dir) / sha256
- exists(self, sha256: str) -> bool: check if file exists
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_db.py -x -v</automated>
</verify>
<done>
- All 7 tests pass
- init_db() creates Client, Driver, Printer, Icon tables
- WAL mode and foreign keys enabled
- Idempotent — second call is no-op
- DriverStore deduplicates by SHA256
</done>
</task>
<task type="auto">
<name>Task 2: Wire database init into FastAPI startup</name>
<files>
imptune/main.py
</files>
<action>
Modify the existing imptune/main.py (created by plan 01-01) to add database initialization on startup:
- Import init_db from imptune.db.database
- In the existing startup event handler, add a call to init_db() AFTER the directory creation logic
- This ensures the SQLite database is created inside DATA_DIR (which was just created/verified)
- Keep all existing code (StaticFiles mount, router includes, directory creation) — only ADD the init_db() call
Do NOT use async def for the startup handler — Peewee is sync-only. Use regular def with FastAPI's @app.on_event("startup") which already exists from plan 01-01.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_health.py tests/test_db.py -x -v</automated>
</verify>
<done>
- main.py imports and calls init_db() on startup
- Existing health and static tests still pass (no regression)
- Database tests pass with init triggered via app startup
</done>
</task>
</tasks>
<verification>
- `python -m pytest tests/ -x -v` — all tests pass (health + static + db)
- `python -c "from imptune.db.models import Client, Driver, Printer, Icon; print('Models OK')"` — imports without error
- `python -c "from imptune.storage.driver_store import DriverStore; print('DriverStore OK')"` — imports without error
</verification>
<success_criteria>
- SQLite database auto-creates on app startup with 4 tables
- WAL journal mode and foreign keys enabled via pragmas
- Database file lives at DATA_DIR/imptune.db (volume-mounted path)
- DriverStore saves files by SHA256 with deduplication
- All existing tests continue to pass (no regression)
- 7+ new tests pass for db and storage
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
</output>
@@ -0,0 +1,140 @@
---
phase: 01-foundation
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- imptune/generators/__init__.py
- imptune/generators/intunewin_builder.py
- tests/test_intunewin.py
autonomous: true
requirements:
- INFRA-02
must_haves:
truths:
- "A Python function produces a valid .intunewin file from a source directory and setup file name"
- "The .intunewin file contains an outer ZIP with IntuneWinPackage/Contents/IntunePackage.intunewin and IntuneWinPackage/Metadata/Detection.xml"
- "The encrypted blob uses the correct byte layout: HMAC-SHA256 (32 bytes) + IV (16 bytes) + AES-256-CBC ciphertext"
- "Detection.xml contains correct EncryptionKey, MacKey, InitializationVector, Mac, FileDigest values that match the actual encryption"
- "The inner ZIP uses DEFLATE compression and the outer ZIP uses STORED compression"
artifacts:
- path: "imptune/generators/intunewin_builder.py"
provides: "Python-native .intunewin file assembler using pycryptodome"
exports: ["build_intunewin"]
min_lines: 60
- path: "tests/test_intunewin.py"
provides: "Byte-level validation tests for .intunewin format"
min_lines: 80
key_links:
- from: "imptune/generators/intunewin_builder.py"
to: "pycryptodome"
via: "from Crypto.Cipher import AES"
pattern: "Crypto\\.Cipher"
- from: "imptune/generators/intunewin_builder.py"
to: "zipfile"
via: "stdlib zipfile for inner and outer ZIPs"
pattern: "zipfile\\.ZipFile"
---
<objective>
Implement the Python-native .intunewin file builder as a time-boxed spike. This module generates .intunewin packages using AES-256-CBC encryption with HMAC-SHA256, producing the exact byte layout Intune expects.
Purpose: Validate the highest-risk unknown in the project — can Python generate a .intunewin file that Intune accepts? This spike runs independently of the web app and produces a standalone generator module reused in Phase 5. Supports INFRA-02 (no external binary dependencies like IntuneWinAppUtil.exe).
Output: A tested build_intunewin() function and comprehensive byte-level validation tests.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Implement .intunewin builder with byte-level tests</name>
<files>
imptune/generators/__init__.py,
imptune/generators/intunewin_builder.py,
tests/test_intunewin.py
</files>
<behavior>
- test_output_is_valid_zip: build_intunewin() output file is a valid ZIP archive
- test_outer_zip_structure: outer ZIP contains exactly IntuneWinPackage/Contents/IntunePackage.intunewin and IntuneWinPackage/Metadata/Detection.xml
- test_outer_zip_stored: outer ZIP entries use ZIP_STORED compression (no extra compression on encrypted content)
- test_detection_xml_valid: Detection.xml is valid XML with ApplicationInfo root element in the correct namespace (http://schemas.microsoft.com/IntuneWin)
- test_detection_xml_fields: Detection.xml contains Name, UnencryptedContentSize, FileName, SetupFile, and full EncryptionInfo with all 8 sub-elements (EncryptionKey, MacKey, InitializationVector, Mac, MacAlgorithm, ProfileIdentifier, FileDigest, FileDigestAlgorithm)
- test_encrypted_blob_layout: the encrypted blob starts with 32 bytes (HMAC) + 16 bytes (IV) + remainder (ciphertext); total length = 48 + ciphertext length
- test_iv_is_16_bytes: IV extracted from Detection.xml base64-decodes to exactly 16 bytes (NOT 32 — critical per RESEARCH.md)
- test_encryption_key_is_32_bytes: EncryptionKey from Detection.xml base64-decodes to exactly 32 bytes
- test_mac_key_is_32_bytes: MacKey from Detection.xml base64-decodes to exactly 32 bytes
- test_hmac_matches: HMAC-SHA256 computed from MacKey over ciphertext matches the first 32 bytes of the blob AND the Mac value in Detection.xml
- test_decryption_roundtrip: using EncryptionKey and IV from Detection.xml, decrypt the ciphertext, unpad, and verify the result is a valid DEFLATE-compressed ZIP containing the original source files
- test_file_digest_matches: FileDigest in Detection.xml matches SHA256 of the decrypted plaintext ZIP
- test_unencrypted_content_size: UnencryptedContentSize in Detection.xml matches the byte length of the decrypted plaintext ZIP
- test_setup_file_in_detection_xml: SetupFile element matches the setup_file argument passed to build_intunewin
</behavior>
<action>
**imptune/generators/intunewin_builder.py**:
Implement build_intunewin(source_dir: str, setup_file: str, output_path: str) -> None following the skeleton from RESEARCH.md Pattern 3, with these specifics:
1. Create inner ZIP (DEFLATE compression) of all files in source_dir, preserving relative paths
2. Generate random keys: aes_key = os.urandom(32), mac_key = os.urandom(32), iv = os.urandom(16) — IV MUST be 16 bytes per the critical correction in RESEARCH.md
3. Encrypt with AES-256-CBC: cipher = AES.new(aes_key, AES.MODE_CBC, iv), ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
4. Compute HMAC-SHA256 of ciphertext using mac_key
5. Assemble encrypted blob: hmac_digest (32 bytes) + iv (16 bytes) + ciphertext
6. Compute file_digest = SHA256 of plaintext (the inner ZIP bytes before encryption)
7. Build Detection.xml with all required fields (see RESEARCH.md for exact schema). Use xml.etree.ElementTree for building and xml.dom.minidom for pretty printing. Set xmlns="http://schemas.microsoft.com/IntuneWin" on ApplicationInfo root.
8. Build outer ZIP (STORED compression) with two entries: IntuneWinPackage/Contents/IntunePackage.intunewin (the encrypted blob) and IntuneWinPackage/Metadata/Detection.xml
9. All base64 values in Detection.xml use standard base64 encoding (base64.b64encode)
**tests/test_intunewin.py**:
- Create a tmp_path fixture with a small test source directory (2-3 small text files, one named "install.ps1")
- Call build_intunewin(source_dir, "install.ps1", output_path) to generate the file
- Implement all tests from the behavior list above
- For the decryption roundtrip: extract EncryptionKey and IV from Detection.xml, use AES.new(key, AES.MODE_CBC, iv) to decrypt, unpad the result, verify it's a valid ZIP containing the original files
- For HMAC verification: extract MacKey from Detection.xml, compute hmac.new(mac_key, ciphertext, hashlib.sha256).digest(), compare to first 32 bytes of blob AND to Mac value in Detection.xml
The tests serve as the format specification — if they pass, the byte layout is correct. The only remaining validation is a real Intune upload (manual, Phase 5 gate).
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune && pip install pycryptodome -q && python -m pytest tests/test_intunewin.py -x -v</automated>
</verify>
<done>
- All 14 byte-level tests pass
- IV is confirmed 16 bytes (not 32)
- Decryption roundtrip succeeds: encrypt then decrypt recovers original files
- HMAC verification succeeds: computed HMAC matches blob header and Detection.xml Mac field
- Outer ZIP structure matches Intune's expected layout exactly
- Detection.xml has correct namespace and all required fields
</done>
</task>
</tasks>
<verification>
- `python -m pytest tests/test_intunewin.py -x -v` — all 14 tests pass
- `python -c "from imptune.generators.intunewin_builder import build_intunewin; print('Builder importable')"` — no import errors
- The .intunewin file produced can be opened as a ZIP and inspected manually (outer structure visible)
</verification>
<success_criteria>
- build_intunewin() produces a file with the exact byte layout Intune expects
- All crypto operations use correct key/IV sizes (32/32/16 bytes)
- HMAC and decryption roundtrip verified programmatically
- Detection.xml contains all 8 EncryptionInfo sub-elements with correct values
- Module is standalone — no dependency on the web framework or database
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`
</output>