# S11 band-3 deterministic legs — deps (pnpm audit) + security headers [#39]

audience: AI coding agents first. BLUF-ordered. Light plan (two legs), per advisor: "deterministic, no dice,
clean done-definition, no user steer — bounded, low rabbit-hole risk." NOT the USER-GATED generalization
frontier (#16/#36). Builds the last named non-LLM class.

## Goal
Build the two v1-scope deterministic band-3 detectors for S11 so the class leaves the bench blind-spot list with
MEASURED (deterministic) recall: (1) dependency advisories via `pnpm audit`, (2) missing OWASP secure response
headers. RED-on-vuln / GREEN-on-safe, hermetic, regression-gated.

## Decisions [settled — falsification in brackets]
- **D1 — detectors in Python**, standalone-runnable + importable (`scan()` fn + `__main__` CLI), mirroring how
  `oracle2.ts` runs standalone. [delete → must add a TS runtime for pure JSON/text work; gate.py is already Python.]
- **D2 — deps determinism via captured audit OUTPUT.** `pnpm audit` hits the registry advisory DB (network +
  time-varying) → it is NOT hermetic and CANNOT be a git-fix-pinned cell. The detector splits I/O from logic:
  live mode runs `pnpm audit --json <dir>`; hermetic mode parses `--audit-json <file>`. Cells pin a REAL captured
  audit JSON (the `url-regex` GHSA-v4rh-8p82-6h5w high ReDoS, captured 2026-06-17). [delete the split → cells
  become non-deterministic / network-bound; the regression gate flaps with the advisory DB.]
- **D3 — band-3 cells live WITH their detector**, `domains/security/detectors/{deps,headers}/cells/`, NOT in
  `domains/security/corpus/`. The corpus contract is git-FIX-pinned `vuln.ts`/`safe.ts` (LLM-band ground truth);
  captured-audit JSON + config fixtures would violate it. [delete → pollutes the git-pinned LLM corpus with
  non-git fixtures, breaking corpus/README.md's contract.]
- **D4 — bench is the single coverage ledger.** `bench.py --inventory` also globs the band-3 detector cells and
  counts them; `blind_spots` stays a CURATED set (not computed-from-corpus — S4/S6 are pilot-measured with no
  corpus cells, S9 is oracle/held-out, so naive compute would wrongly flag them). After S11 ships,
  `blind_spots = {}` with a note pointing to the band-3 detectors + test. [delete the curation note → a future
  editor recomputes blind from corpus cells and silently re-flags S4/S6/S9.]
- **D5 — severity threshold default `high`** (report high+critical), `--min-severity` configurable; `--fail-on
  <sev>` exits non-zero for gate use. Default run just reports. [delete threshold → low/info advisory noise
  drowns the signal.]

## pnpm audit --json schema [MEASURED 2026-06-17, real run in ~/Projects/multideal]
`{ actions:[], advisories: { "<id>": { severity, module_name, title, vulnerable_versions, cwe:[], cves:[],
url, recommendation, cvss:{score,vectorString}, findings:[{version, paths:[...]}] } }, metadata }`.
Severity ladder: `info < low < moderate < high < critical`. Clean = `advisories: {}`.
Parse `advisories` (primary). Defensively also accept a top-level `vulnerabilities` map if a future pnpm emits it.

## OWASP secure response headers — the required set (v1 presence check)
Recognized OWASP Secure Headers Project set: `Content-Security-Policy`, `X-Content-Type-Options`,
`Strict-Transport-Security`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`. Equivalence: a CSP with
`frame-ancestors` satisfies `X-Frame-Options` (don't double-flag). v1 = report MISSING required (presence, not
value-grading — value grading is scope creep, explicitly deferred). Also flag a PRESENT `X-Powered-By` (info
disclosure). Honest scope note in the detector header.

## Wave Plan
| Wave | Tasks | Files | Parallel? |
|------|-------|-------|-----------|
| 1 | T1 deps detector + cells, T2 headers detector + cells | disjoint dirs `detectors/deps/**`, `detectors/headers/**` | ✅ no overlap |
| 2 | T3 regression test | `tests/test_s11_band3.py` | single |
| 3 | T4 bench + taxonomy + corpus-README pointer | `bench.py`, `docs/taxonomy/security.md`, `domains/security/corpus/README.md` | single |

## T1 — deps detector (`domains/security/detectors/deps/deps_audit.py` + `cells/`)
- `severity_rank(s) -> int`; `audit_findings(audit: dict, min_severity="high") -> list[str]` iterating
  `advisories`, filtering `rank(sev) >= rank(min)`, formatting:
  `[<severity>] <module_name>@<vulnerable_versions> :: <title> (<cwe|cve>) <url>`.
- CLI: `<dir>` (run `pnpm audit --json` via subprocess, cwd=dir) | `--audit-json <file>` (hermetic);
  `--min-severity`, `--fail-on`. Prints `=== DEPS AUDIT (pnpm) ===` + one finding/line + `advisories flagged: N`.
- Cells: `cells/audit_vuln.json` (REAL url-regex high advisory, captured), `cells/audit_safe.json`
  (`{"actions":[],"advisories":{},"metadata":{...}}`), `cells/canonical.json`
  (`canonical_symbol:"url-regex"`, band 3, shape `deps-audit`, note: captured-not-git-pinned + why).

## T2 — headers detector (`domains/security/detectors/headers/headers_scan.py` + `cells/`)
- `REQUIRED` set (above); `missing_headers(text) -> list[str]` case-insensitive presence; CSP-frame-ancestors
  equivalence; `X-Powered-By` present → finding. Format: `[high] missing security response header: <Name>`.
- CLI: `<file>`, `--fail-on`. Prints `=== HEADERS SCAN ===` + findings + `missing required: N`.
- Cells: `cells/headers_vuln.ts` (handler setting none/weak — flags missing), `cells/headers_safe.ts`
  (sets all required via helmet-style config — silent), `cells/canonical.json`
  (`canonical_symbol:"missing security response header"`, band 3, shape `headers-config`).

## T3 — regression test (`tests/test_s11_band3.py`, hermetic, deterministic)
Import each detector's `scan`/`audit_findings`/`missing_headers`; assert RED flags the canonical_symbol + GREEN is
silent, for BOTH legs. Mirrors `tests/test_bench.py`. No network (deps uses the captured JSON fixture).

## T4 — ledger + taxonomy
- `bench.py`: `load_cells()` also globs `domains/security/detectors/*/cells/canonical.json`; `cmd_inventory`
  `blind = sorted(set())` (S11 now MEASURED via band-3) + curation note (why not computed); add
  `band3_detectors:["deps","headers"]`.
- `docs/taxonomy/security.md`: S11 row `[INFERRED]` → `[MEASURED]` band-3 BUILT (deps+headers, RED/GREEN, test);
  coverage-summary blind-spots line → NONE.
- `domains/security/corpus/README.md`: one-line pointer that band-3 (S11) cells live with their detectors
  (captured-audit / config fixtures), not here (keeps the git-fix-pinned contract scoped to corpus/).

## Acceptance (the build's pass/fail)
1. `python tests/test_s11_band3.py`-equivalent (pytest) PASSES: deps RED flags `url-regex` / GREEN silent;
   headers RED flags a missing required header / GREEN silent.
2. `python bench.py --inventory` exits 0, lists the 2 band-3 cells, `blind_spots == []`.
3. `pytest tests/ -q` stays green (no regression).
4. Each detector runs standalone (`python deps_audit.py --audit-json cells/audit_vuln.json`;
   `python headers_scan.py cells/headers_vuln.ts`).
```
