All completion criteria verified green — no defects found, nothing to fix. Final report: **Phase 82 (security headers) — final verification pass: all green** - Verified prior-run implementation: `app/core/security_headers.py` (pure-ASGI, header-only, exact A1 CSP), registration in `app/main.py` after `configure_caching` (outermost), unit/integration/E2E suites. - Deviation confirmed sound: `data:`-URI favicon (blocked by locked CSP) → static `frontend/assets/favicon.svg` in 5 templates + Containerfile `cp`; SVG element byte-identical to the old data-URI (verified programmatically); serves 200 with all three headers. - Curl check (server booted like e2e conftest, log: `/tmp/curl_security_headers_final.log`): `/`, `/api/health`, `/assets/styles.css`, `/nope` (404) → all three headers, CSP exactly `default-src 'self'; base-uri 'none'; frame-ancestors 'none'`. - `uv run pytest tests/unit/test_security_headers.py tests/integration/test_security_headers.py -v --no-cov` → 13 passed (incl. SSE byte-identity pin). - `uv run pytest tests/e2e/test_security_headers.py -v --no-cov` (isolated) → 2 passed (headers + zero CSP violations + painted page). - SSE tripwire `uv run pytest tests/e2e/test_chat_rag.py -v --no-cov` → 3 passed. - `uv run pytest --cov=app --cov-report=term-missing` → 1665 passed, app/ 99% (>90%); `uv run ruff check . && uv run pyright` → clean (0 errors). - `git diff --stat` limited to phase-82 files + the two documented deviations (favicon set, `tests/unit/__init__.py`); no `pyproject.toml`/`uv.lock`/JS diffs. - Commit + phase-dir move left to the harness per pipeline rules (not executed by me). Next pending phase: `83_chat_save_payload_limits`.
2.2 KiB
2.2 KiB
Task 01 — The pure-ASGI header middleware + unit suite
Phase: 82_security_headers · Story: n/a (security hardening — audit SEC-04)
Objective
app/core/security_headers.py exists exactly per the phase design block, and tests/unit/test_security_headers.py pins the header values, the 404 path, the SSE streaming passthrough (byte-identical body + headers present), and the non-http scope passthrough.
Work
app/core/security_headers.py(new) — implement per the phase overview's design block: theCSPconstant (the exact A1 string),SecurityHeadersMiddlewareas a pure-ASGI class wrappingsend(headers written onhttp.response.startviastarlette.datastructures.MutableHeaders(scope=message)), thescope["type"] != "http"fast path, and the module docstring content listed in the design block (audit basis, the No-CDN verification, the pure-ASGI/SSE rationale).tests/unit/test_security_headers.py(new):- a tiny ASGI
test_app(a plain 200Response-like dict sequence, or starlette'sResponsewrapped in an ASGI callable) + the middleware: assert all three headers with exact values; - a 404-shaped response (status 404) → same three headers;
- a streaming response (an async generator yielding
data: a\n\nthendata: b\n\n,media_type="text/event-stream") → consume the full body via the ASGIreceive/sendprotocol (drive it with a small async client loop, orhttpx.AsyncClient(transport=ASGITransport(...))if already used in the unit suite) → body is exactly the two frames in order AND the three headers are present (the SSE pin); - a
websocketscope → the app is called, no crash, no header injection attempt.
- a tiny ASGI
Testing & Quality
uv run pytest tests/unit/test_security_headers.py -vgreen.- Coverage: >90% on
app/core/security_headers.py(every branch: http start, http other messages, non-http scope).
Completion Criteria
uv run pytest tests/unit/test_security_headers.py -v— all tests green, including the SSE byte-identity pin.uv run ruff check app/core/security_headers.py tests/unit/test_security_headers.py && uv run pyrightclean.git diff --statfor this task: exactly the two new files.