diff --git a/CHANGELOG.md b/CHANGELOG.md index 28681d5..316425e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Docs +- Formal ADR catalog under `docs/adr/`: Accepted retrospective records + 0002–0015 for shipped topology, scanning, security, CLI, quality, and + Horizon A scope. Number 0001 reserved (draft under review, not published). + Indexes wired from Architecture, STATUS, README, QUICKSTART, CONTRIBUTING, + and plan README. + ## [0.3.0] - 2026-08-05 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3fb8060..ef3e8c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,8 @@ Then use this guide for development, quality checks, and sharing changes back. S 3. `tripwire setup` (needs `SUPABASE_DB_URL`) and `./scripts/setup-modal.sh` 4. Run checks below. -Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Scanner output notes: +Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Decisions: +[docs/adr/README.md](docs/adr/README.md). Scanner output notes: [docs/research/adapters/scanner-output-adapters.md](docs/research/adapters/scanner-output-adapters.md). ## Dev hygiene diff --git a/QUICKSTART.md b/QUICKSTART.md index 456541e..31266a5 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -137,5 +137,6 @@ setup, re-run, and maintenance command catalog. - Docs map: [docs/README.md](docs/README.md) - Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) +- Decisions: [docs/adr/README.md](docs/adr/README.md) - Capability status: [docs/STATUS.md](docs/STATUS.md) - Contributing: [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/README.md b/README.md index 0e9d05c..632fc2e 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ flowchart LR |---|---| | Run your first Live scan | [Follow the Quickstart](QUICKSTART.md#first-live-scan) | | Preview the dashboard or validate locally | [Optional local validation](QUICKSTART.md#validate-locally-optional) | -| Understand results and system shape | [Capability status](docs/STATUS.md) · [Architecture](docs/ARCHITECTURE.md) | +| Understand results and system shape | [Capability status](docs/STATUS.md) · [Architecture](docs/ARCHITECTURE.md) · [Decisions](docs/adr/README.md) | | Contribute or maintain the project | [Contributing](CONTRIBUTING.md) · [command catalog](docs/user-guide/setup-commands.md) | For the full documentation map, see [docs/README.md](docs/README.md). The diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4b8b84d..56d46b7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -130,7 +130,6 @@ from the code. ## 5. Decisions -- Planning decisions: [plan/DECISIONS.md](./plan/DECISIONS.md) +- Formal ADRs: [adr/README.md](./adr/README.md) +- Planning decisions (slice waivers, priority): [plan/DECISIONS.md](./plan/DECISIONS.md) - Slice progress: [plan/PROGRESS.md](./plan/PROGRESS.md) -- Formal ADRs (`docs/adr/`) — none yet; add when a major technology or boundary - choice needs a durable record diff --git a/docs/README.md b/docs/README.md index fdc9d67..5568e4e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,7 +12,7 @@ Use this map to move from a safe first look to the level of setup or project det |---|---|---| | Run a first Live scan | [Quickstart: ordered Live setup](../QUICKSTART.md#first-live-scan) | [Supabase setup](./user-guide/supabase-setup.md) → [Modal setup](./user-guide/modal-setup.md) → [environment keys](./user-guide/env-vars.md) → [scan and dashboard](../QUICKSTART.md#live-capabilities) | | Preview or validate locally | [Optional local validation](../QUICKSTART.md#validate-locally-optional) | [README: dashboard preview](../README.md#preview-the-dashboard-optional) | -| Understand results and system shape | [Capability status](./STATUS.md) | [Architecture](./ARCHITECTURE.md) | +| Understand results and system shape | [Capability status](./STATUS.md) | [Architecture](./ARCHITECTURE.md) · [ADRs](./adr/README.md) | | Contribute or maintain | [Contributing](../CONTRIBUTING.md) | [Setup and maintenance commands](./user-guide/setup-commands.md) | ## Setup and operation @@ -27,6 +27,7 @@ Use this map to move from a safe first look to the level of setup or project det | [user-guide/onboarding-cheatsheet.md](./user-guide/onboarding-cheatsheet.md) | Compact shared onboarding reference | | [STATUS.md](./STATUS.md) | Evidence-labelled capability claims | | [ARCHITECTURE.md](./ARCHITECTURE.md) | System diagrams, key flows, and repository layout | +| [adr/README.md](./adr/README.md) | Formal architecture decision records | | [SECURITY.md](../SECURITY.md) | Vulnerability reporting | ## Project records @@ -37,6 +38,7 @@ Use this map to move from a safe first look to the level of setup or project det | Run the documentation smoke path | [SMOKE_TESTS.md](./plan/SMOKE_TESTS.md) | | Review CI and Nightly workflows | [CI workflows](#ci-workflows) | | Inspect project planning | [plan README](./plan/README.md) · [progress](./plan/PROGRESS.md) | +| Read architecture decisions | [ADR index](./adr/README.md) | | Check scanner adapter research | [scanner output adapters](./research/adapters/scanner-output-adapters.md) | ## CI workflows diff --git a/docs/STATUS.md b/docs/STATUS.md index 5145f28..59e9f12 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -90,6 +90,20 @@ prose slice 16) is **Won't (A)** — reinstate only if a new demo need arises. --- +## DECIDED + +Architecture boundaries and quality/security trade-offs are recorded as formal +ADRs ([adr/README.md](./adr/README.md)): runtimes (0002), Modal (0003), +Supabase (0004), scanner adapters (0005), Live/Mock ACL (0006), ship UI (0007), +anon-read / service-role-write (0008), fail-closed evidence (0009), content-hash +idempotency (0010), schema bootstrap (0011), target acquisition (0012), +ship-path quality gates (0013), curated discovery (0014), and Horizon A +excluding Guard/Drift (0015). Slice waivers stay in +[plan/DECISIONS.md](./plan/DECISIONS.md). ADR number 0001 is reserved and not +published while that draft remains under review. + +--- + ## RESEARCH (not VERIFIED) Exact JSON field names in `sandbox/scanners.py` — cross-check against the pinned @@ -103,7 +117,8 @@ Adapter fixture tests (slices 8–9) are planned to tighten this. Known fixture gaps (not urgent) are listed under [fixtures/README.md](../fixtures/README.md) (“Not yet built”). Do not treat those -as shipped capabilities. +as shipped capabilities. Guard PreToolUse and Drift/trend remain Future / +Won't (A) — see [ADR-0015](./adr/0015-horizon-a-excludes-guard-and-drift.md). Coverage audit matrix: [plan/coverage-audit.md](./plan/coverage-audit.md) (slice 7 ✅). Slice stubs: [plan/README.md](./plan/README.md) (`01-A-…` … diff --git a/docs/adr/0002-node-cli-python-sandbox-split.md b/docs/adr/0002-node-cli-python-sandbox-split.md new file mode 100644 index 0000000..ce25b0e --- /dev/null +++ b/docs/adr/0002-node-cli-python-sandbox-split.md @@ -0,0 +1,55 @@ +# ADR-0002: Node CLI and Python sandbox as separate runtimes + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** runtime, cli, sandbox, languages + +## Context + +Tripwire must run on an operator workstation (discover targets, bootstrap schema, +spawn jobs) and inside an isolated scan environment (install scanner CLIs, parse +JSON, write findings). Those jobs have different language ecosystems: Node is +natural for a `npm`-distributed CLI and the HTML dashboard; Cisco / Snyk / Tessl +scanners are Python packages consumed via `pip` / `uvx`. + +A single-language stack would force either wrapping Python scanners from Node +on the operator machine, or shipping a Python CLI that still has to talk to a +browser dashboard. + +## Decision + +Keep two production runtimes: + +- **CLI** (`cli/`): Node **22** (`.nvmrc`). Discovery, content hashing, + idempotency, schema bootstrap, Modal spawn. +- **Sandbox** (`sandbox/`): Python. Modal image is Debian slim **3.11**; local + contributor pin is **3.12** (`.python-version`). Scanner adapters live here. + +The CLI never runs scanner binaries. The sandbox never owns operator UX. + +## Consequences + +- Contributors need both Node 22 and Python 3.12; CI must gate both stacks. +- Modal image independently pins Node **20.18.1** (Tessl) and Python 3.11 — + local pins and the scan image can drift and must be documented. +- Contract between sides is explicit: CLI passes `target`, `item-type`, + `item-id`, `scan-run-id`; sandbox writes to Supabase. + +## Alternatives considered + +### A. Python-only (CLI + sandbox) + +Rejected: dashboard and `npm link` CLI already existed; Node is the operator +entry the walking skeleton shipped. + +### B. Node-only, shell out to scanners from the laptop + +Rejected: scanner installs and untrusted target execution belong in isolation +(see [ADR-0003](./0003-modal-isolated-scanner-execution.md)). + +## References + +- [docs/ARCHITECTURE.md](../ARCHITECTURE.md) §2 +- [docs/user-guide/prerequisites.md](../user-guide/prerequisites.md) +- Walking skeleton: `feat: land walking skeleton` (2026-08-01) diff --git a/docs/adr/0003-modal-isolated-scanner-execution.md b/docs/adr/0003-modal-isolated-scanner-execution.md new file mode 100644 index 0000000..cb57f7a --- /dev/null +++ b/docs/adr/0003-modal-isolated-scanner-execution.md @@ -0,0 +1,62 @@ +# ADR-0003: Modal for isolated scanner execution + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** compute, isolation, modal, sandbox + +## Context + +Scanner CLIs execute untrusted skill/MCP trees, pull optional vendor tools, and +need secrets (Supabase service role, Snyk/Tessl/Cisco keys). Running that on the +operator laptop mixes credentials with target code and makes results +non-reproducible across machines. + +Live scans also need an ephemeral filesystem and a hard timeout so a hung +scanner cannot block the heatmap forever. + +## Decision + +Use **Modal** as the Live compute plane. + +- App `tripwire-scan` (`sandbox/scan_app.py`): one ephemeral sandbox per + `scan_run`, 300s hard timeout. +- Image bakes scanner packages (`cisco-ai-skill-scanner`, `cisco-ai-mcp-scanner`, + `snyk-agent-scan`) plus Node 20 for Tessl; `scanners` is copied into the image + (`add_local_python_source(..., copy=True)`). +- Secrets sync via `./scripts/setup-modal.sh` (`tripwire-supabase`, + `tripwire-scan-secrets`). +- Findings and console output are written **directly to Supabase** from the + sandbox. They are not relayed through the CLI. +- CLI spawns via `modal run sandbox/scan_app.py` (local entrypoint) so host + directories can be packed; it must not call `scan_item` directly. + +`--dry-discover` stays local and never spawns Modal. + +## Consequences + +- Live path requires a Modal account and CLI auth; Mock / dry-discover do not. +- Cold image builds and `uvx` installs can leave Tessl/Snyk unreachable; + that is reported, not hidden ([ADR-0009](./0009-fail-closed-incomplete-evidence.md)). +- Host paths are invisible on Modal’s remote FS — local targets must be tarred + ([ADR-0012](./0012-sandbox-target-acquisition.md)). +- Future packaging/deploy stories may still treat Modal as external SaaS, not + a cluster workload (draft ADR 0001 is under review and not in this catalog). + +## Alternatives considered + +### A. Local Docker / venv scanners + +Rejected for Live: weaker isolation, operator-machine drift, secrets on the +laptop next to target trees. + +### B. GitHub Actions as the scan runner + +Rejected: scan latency and auth model are operator-driven, not PR-driven. +CI remains for Tripwire’s own quality gates. + +## References + +- [docs/user-guide/modal-setup.md](../user-guide/modal-setup.md) +- [docs/STATUS.md](../STATUS.md) IMPLEMENTED +- `cli/src/modalClient.js`, `sandbox/scan_app.py` diff --git a/docs/adr/0004-supabase-system-of-record.md b/docs/adr/0004-supabase-system-of-record.md new file mode 100644 index 0000000..7cf2272 --- /dev/null +++ b/docs/adr/0004-supabase-system-of-record.md @@ -0,0 +1,60 @@ +# ADR-0004: Supabase/Postgres as system of record + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** storage, supabase, postgres, realtime + +## Context + +Scan runs, per-scanner status, and findings must be shared between the CLI +(writer), the Modal sandbox (writer), and a browser dashboard (reader) with +near-live updates. A local SQLite file cannot serve the dashboard or the +sandbox. A custom REST API would duplicate what PostgREST already provides. + +The product heatmap needs a server-side rollup so the UI does not recompute +risk from raw findings on every poll. + +## Decision + +Use **hosted Supabase (Postgres + PostgREST + Realtime)** as the Live store. + +- Canonical DDL: `db/schema.sql` (items, scan_batches, scan_runs, + scan_run_scanners, findings, coverage, config). +- Heatmap: `tripwire_rollup_item` in Postgres. `partial-failed` still scores + completed engines; `failed` / `running` / empty partial paint `error`. +- Realtime publication on `scan_runs`, `scan_run_scanners`, `findings` so the + dashboard can update in ~1s, with an 8s poll fallback. +- HTTP clients use `SUPABASE_URL` + keys; DDL uses `SUPABASE_DB_URL` + (prefer Session pooler if Direct `db.*` does not resolve). + +Raw scanner blobs in Supabase Storage were specified as a dual-write path in +adapter research; they are **not** implemented. Normalized `findings` rows are +the shipped contract. + +## Consequences + +- Live requires a Supabase project; schema and RLS must be applied together + ([ADR-0008](./0008-anon-read-service-role-write.md), + [ADR-0011](./0011-idempotent-sql-schema-bootstrap.md)). +- Application code speaks PostgREST column names; missing columns are handled + with PGRST204-safe fallbacks in the sandbox. +- Other databases are future package flavors only after the app boundary + allows it (packaging ADR draft reserved as 0001; not published yet). + +## Alternatives considered + +### A. Self-hosted Postgres + custom API + +Rejected for Horizon A: extra ops surface; PostgREST + Realtime already match +the dashboard’s read model. + +### B. SQLite / JSON files on the operator machine + +Rejected: sandbox and browser cannot share that store. + +## References + +- [docs/ARCHITECTURE.md](../ARCHITECTURE.md) §2–3 +- [docs/user-guide/supabase-setup.md](../user-guide/supabase-setup.md) +- [docs/research/adapters/scanner-output-adapters.md](../research/adapters/scanner-output-adapters.md) §0 diff --git a/docs/adr/0005-upstream-scanner-cli-adapters.md b/docs/adr/0005-upstream-scanner-cli-adapters.md new file mode 100644 index 0000000..5120648 --- /dev/null +++ b/docs/adr/0005-upstream-scanner-cli-adapters.md @@ -0,0 +1,64 @@ +# ADR-0005: Wrap upstream scanner CLIs; normalize findings + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** scanners, adapters, cisco, snyk, tessl + +## Context + +Tripwire is an assessment orchestrator, not a replacement for Cisco Skill/MCP +Scanner, Snyk Agent Scan, or Tessl. Reimplementing those engines would fork +their detection quality and lag their CLI flags. The dashboard, however, needs +one severity model (`red` / `amber` / `green`) and one `findings` table. + +Upstream JSON shapes differ and are marked experimental in places (Snyk). +Adapters must tolerate prefixed stdout, missing binaries, and missing vendor +keys without crashing the sandbox. + +## Decision + +Treat each engine as a **subprocess adapter** in `sandbox/scanners.py`. + +- Shell out with real flags (`skill-scanner`, `mcp-scanner`, `snyk-agent-scan`, + `tessl`); parse documented JSON; map into `findings` + `scan_run_scanners`. +- Collapse upstream severities into Tripwire `red` / `amber` / `green`. +- Missing vendor credentials → `skipped_missing_credential`, not a silent + “configured and clean” result. +- Nonzero exit, timeout, missing binary, or empty/malformed JSON → + `unreachable` (never a crash). Cisco findings from engines that did complete + are still persisted. +- Incremental `on_scanner_done` so the dashboard can show progress + scanner-by-scanner. +- Keep [scanner-output-adapters.md](../research/adapters/scanner-output-adapters.md) + in sync with `scanners.py`. Exact JSON field names remain RESEARCH until + fixture-round-tripped against the pinned CLI. + +Skill scans run Cisco + Tessl + Snyk. MCP scans run Cisco MCP + Snyk. + +## Consequences + +- Image and secrets must include each vendor the operator wants; absent keys + skip that engine rather than blocking the whole Live path. +- Adapter tests lock parse fixtures; they do not replace pinning CLI versions + in the Modal image when upstream output is experimental. +- Dual-write of raw blobs to Storage is specified, not shipped + ([ADR-0004](./0004-supabase-system-of-record.md)). + +## Alternatives considered + +### A. Reimplement detection in-process + +Rejected: quality and maintenance cost; product value is orchestration + +normalized review. + +### B. Call vendor SaaS HTTP APIs only + +Rejected: the shipped engines are CLIs that need the target tree (or MCP +endpoint) inside the sandbox. + +## References + +- `sandbox/scanners.py` +- [docs/STATUS.md](../STATUS.md) IMPLEMENTED / RESEARCH +- [docs/user-guide/prerequisites.md](../user-guide/prerequisites.md) five-vendor map diff --git a/docs/adr/0006-live-mock-strangler-acl.md b/docs/adr/0006-live-mock-strangler-acl.md new file mode 100644 index 0000000..691a596 --- /dev/null +++ b/docs/adr/0006-live-mock-strangler-acl.md @@ -0,0 +1,57 @@ +# ADR-0006: Live vs Mock strangler ACL + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** dashboard, acl, strangler, live, mock + +## Context + +The dashboard existed as a prototype fed by static mock data +(`tripwire-data.js`) before Live Supabase writes were reliable. Rewriting the +UI against PostgREST shapes would block shipping Detection + Sandbox. Operators +also need a no-account preview that must not be mistaken for a scan result. + +Horizon A planning locked a strangler: keep the mock-shaped UI, introduce an +anti-corruption layer for Live, and use an explicit kill switch. + +## Decision + +Keep one dashboard document and two data sources. + +- **ACL:** `prototypes/dc-dashboard/tripwire-live.js` fetches + `items` / `scan_runs` / `scan_run_scanners` / `findings` and reshapes them + into the mock item structure. +- **Kill switch:** data-source dropdown (Live vs Mock), persisted in + `sessionStorage`, default **Live**. +- Live with missing URL/anon key or a failed fetch falls back to mock data + with an honest status chip (`Missing API key`, `Connection error`, + `Live · empty`). Empty successful responses stay on Live. +- Mock is preview only; it does not replace Live setup or produce a scan. + +Shadow traffic and cut-over to a second UI are **N/A** for Horizon A. + +## Consequences + +- UI components stay coupled to the mock schema; schema changes require ACL + mapping updates and Live ACL tests (coverage gated on the four ACL modules). +- `support.js` stays out of the coverage bar (glue around the prototype). +- Operators can evaluate the UI without accounts; onboarding must say Mock is + not a scan result. + +## Alternatives considered + +### A. Rewrite the dashboard against PostgREST types + +Rejected for Horizon A: stack freeze — ship `dc-dashboard` as-is +([ADR-0007](./0007-html-prototype-ship-ui.md)). + +### B. Two separate apps (demo vs live) + +Rejected: doubles UX drift; the dropdown is the explicit seam. + +## References + +- [docs/plan/DECISIONS.md](../plan/DECISIONS.md) Strangler+ACL (2026-08-01) +- [prototypes/README.md](../../prototypes/README.md) +- `prototypes/dc-dashboard/tripwire-live.js` diff --git a/docs/adr/0007-html-prototype-ship-ui.md b/docs/adr/0007-html-prototype-ship-ui.md new file mode 100644 index 0000000..d179917 --- /dev/null +++ b/docs/adr/0007-html-prototype-ship-ui.md @@ -0,0 +1,58 @@ +# ADR-0007: HTML prototype dashboard as Horizon A ship UI + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** frontend, dashboard, prototype + +## Context + +A Data Commons HTML dashboard already existed under `prototypes/dc-dashboard/` +with heatmap, filters, and item drawers. Horizon A needed a filmable +Detection and Sandbox review surface in hours, not a productized SPA. A +frontend redesign would have consumed the same clock as Live path evidence. + +`prototypes/README.md` still says the folder is “not the shipped product UI”; +planning overrode that for Horizon A: the prototype **is** the ship UI. + +## Decision + +Ship **`prototypes/dc-dashboard` as-is**. + +- Static HTML (`Tripwire.dc.html`) + JS modules. No React/Vue rewrite, no + `/frontend-advisor` pass. +- Live client loads `@supabase/supabase-js` from an ESM CDN so the HTML + prototype needs no bundler for Realtime. +- Local preview: `node scripts/serve-dashboard.mjs` (binds 127.0.0.1, can + proxy REST so `service_role` never enters the browser). +- Direct browser → Supabase needs `SUPABASE_ANON_KEY` synced into + `tripwire-dashboard.config.js` (gitignored). +- Dashboard remains **outside** governed coverage and complexity thresholds; + normal tests stay mandatory. Live ACL four files are coverage-gated + ([ADR-0013](./0013-ship-path-quality-gates.md)). + +## Consequences + +- Path name `prototypes/` is misleading; STATUS and ARCHITECTURE call it the + Horizon A ship UI. A later product UI would be a new ADR. +- CDN + static HTML is easy to serve and brittle offline; tests inject + `loadCreateClient` so unit tests do not hit the network. +- UX debt (Guard-tab dropdown, Deep Ops aesthetic) is accepted until a + dedicated frontend wave. + +## Alternatives considered + +### A. New SPA (React/Next) as ship UI + +Rejected: stack freeze; skip frontend-advisor +([docs/plan/interview_summary.md](../plan/interview_summary.md)). + +### B. CLI-only review (no dashboard) + +Rejected: the product review step is the heatmap + finding drawers. + +## References + +- [docs/plan/DECISIONS.md](../plan/DECISIONS.md) frontend skip (2026-08-01) +- [docs/ARCHITECTURE.md](../ARCHITECTURE.md) §2 +- `scripts/serve-dashboard.mjs` diff --git a/docs/adr/0008-anon-read-service-role-write.md b/docs/adr/0008-anon-read-service-role-write.md new file mode 100644 index 0000000..a682d13 --- /dev/null +++ b/docs/adr/0008-anon-read-service-role-write.md @@ -0,0 +1,60 @@ +# ADR-0008: Anon read, service-role write + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** security, rls, auth, supabase + +## Context + +Horizon A has no multi-user identity product. The browser dashboard must read +scan data; the CLI and Modal sandbox must write it. Putting the Supabase +`service_role` key in browser-facing config would let anyone mutate or dump +the project. + +Supabase RLS defaults can lock tables without SELECT policies, which silently +breaks the anon dashboard while the CLI (service role, bypasses RLS) still +works. + +## Decision + +Split keys by trust boundary. + +- **Writes:** `SUPABASE_SERVICE_ROLE_KEY` on the CLI and in Modal secret + `tripwire-supabase`. Service role bypasses RLS. +- **Browser reads:** `SUPABASE_ANON_KEY` with RLS enabled and `anon` SELECT + policies + GRANTs on `items`, `scan_runs`, `scan_run_scanners`, `findings`. +- **Never** put `service_role` in `tripwire-dashboard.config.js`. Prefer + `serve-dashboard.mjs` local proxy when the anon key is omitted. +- **Never** toggle RLS in the Supabase UI. Apply `db/schema.sql` via + `tripwire setup --force` so policies and GRANTs land together. +- No end-user login (Auth0/Clerk/Keycloak) for Horizon A. Shared-instance auth + is deferred to future packaging work (ADR 0001 reserved; draft under review). + +Anon SELECT is `using (true)` — any holder of the project anon key can read +all scan rows. Treat the Supabase project as a single-operator trust domain. + +## Consequences + +- Leaking the anon key leaks findings for that project; leaking service role + leaks write access. `.env` and dashboard config stay gitignored. +- Operators who enable RLS in the dashboard without re-applying schema see a + “broken Live” that is actually a GRANT/policy miss + (`./scripts/check-supabase.sh`). +- Multi-tenant or shared-team instances need a new ADR before they are safe. + +## Alternatives considered + +### A. Service role in the browser + +Rejected: unrestricted write from any visitor of the dashboard origin. + +### B. Authenticated-only reads from day one + +Rejected: no identity product in Horizon A; would block the walking skeleton. + +## References + +- `db/schema.sql` RLS + GRANT block +- [docs/user-guide/supabase-setup.md](../user-guide/supabase-setup.md) +- [prototypes/README.md](../../prototypes/README.md) diff --git a/docs/adr/0009-fail-closed-incomplete-evidence.md b/docs/adr/0009-fail-closed-incomplete-evidence.md new file mode 100644 index 0000000..2d9f422 --- /dev/null +++ b/docs/adr/0009-fail-closed-incomplete-evidence.md @@ -0,0 +1,60 @@ +# ADR-0009: Fail closed on incomplete evidence + +- **Status:** Accepted +- **Date:** 2026-08-07 +- **Deciders:** Tripwire maintainers +- **Tags:** honesty, scanning, status, evidence + +## Context + +A scan that exits 0 with an empty heatmap looks “green” even when scanners +never ran, JSON was malformed, schema probe failed for auth reasons, or a +sandbox spawn died. That is worse than a noisy failure: operators and demos +treat absence of findings as safety. + +Coverage-audit acceptance tests later made this explicit: empty successful +scans, malformed scanner payloads, and schema probes that cannot see the +database must not certify success. + +## Decision + +**Fail closed.** Incomplete evidence is never a clean `complete` scan. + +- Scanner adapters: zero-exit empty/malformed JSON, timeout, or missing binary + → `unreachable`, not `completed` with zero findings. +- Any `unreachable` engine → scan_run `partial-failed`; completed engines’ + findings still persist and roll up. +- CLI: invalid concurrency / no targets / dispatch failure → nonzero exit; + failed targets are listed; the run is marked `failed` and rolled up. +- Schema probe: non-schema errors (auth, network) abort; they do not report + the database as ready. +- Missing vendor keys → `skipped_missing_credential` (honest skip), distinct + from unreachable. README: do not call the scan complete for that engine. +- Rollup: `failed` / `running` / empty `partial-failed` paint heatmap `error`, + not green. + +## Consequences + +- Live demos can show `partial-failed` / unreachable copy (“n out of m + scanners unreachable”) when Tessl/Snyk cold-install fails — that is + correct, not a dashboard bug. +- Operators must distinguish skip (no key) from unreachable (engine broke). +- CI Must Live E2E remains Won’t; unit/characterization tests encode the + fail-closed contract instead. + +## Alternatives considered + +### A. Best-effort complete (ignore broken engines) + +Rejected: silent green is a trust defect. + +### B. Fail the whole run if any engine is missing + +Rejected: optional vendor keys are part of the five-vendor map; skip is +allowed, lying about completion is not. + +## References + +- `fix(scanning): fail closed on incomplete evidence` (2026-08-07, PR #58) +- [docs/plan/coverage-audit.md](../plan/coverage-audit.md) acceptance catalog +- `sandbox/scanners.py` `overall_status`; `cli/src/orchestrator.js` diff --git a/docs/adr/0010-content-hash-idempotency.md b/docs/adr/0010-content-hash-idempotency.md new file mode 100644 index 0000000..12e3e27 --- /dev/null +++ b/docs/adr/0010-content-hash-idempotency.md @@ -0,0 +1,53 @@ +# ADR-0010: Content-hash idempotency with --force + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** cli, idempotency, hashing + +## Context + +Modal scans are slow and billable. Re-scanning an unchanged skill on every +`tripwire scan` wastes quota and floods `scan_runs`. Operators still need a +way to re-run after scanner/image changes when bytes on disk did not change. + +Git and live MCP targets cannot be hashed on the host the same way as a local +tree (clone happens inside the sandbox; introspection-only has no files). + +## Decision + +Skip spawn when the target’s **content hash** already exists on `items`. + +- Local `source_on_disk`: SHA-256 of file bytes plus path structure + (`cli/src/hash.js`). Unique on `items.content_hash`. +- Non-disk targets: placeholder `pending:` until a better hash + exists (git after clone is future work). +- Same identifier with a **new** hash updates the existing item row so the + heatmap does not accumulate duplicate cards. +- `--force` bypasses the skip and always spawns. +- `--dry-discover` never hashes for spawn; it only prints targets. + +## Consequences + +- Scanner engine upgrades do not invalidate hashes; operators must `--force`. +- Placeholder hashes collapse all non-disk targets with the same identifier + into one skip key — acceptable for Horizon A, weak for cloneable git URLs. +- Characterization tests lock skip vs `--force` Modal spawn behaviour + (slice 6). + +## Alternatives considered + +### A. Always spawn + +Rejected: cost and heatmap noise on repeated fixture scans. + +### B. Hash scanner versions into the identity + +Deferred: would re-scan on every image bump; can be added later without +changing the `--force` escape hatch. + +## References + +- `cli/src/hash.js`, `cli/src/orchestrator.js` `upsertItem` +- [docs/STATUS.md](../STATUS.md) CLI discovery / hashing / idempotency +- Slice 6 orchestrator characterization diff --git a/docs/adr/0011-idempotent-sql-schema-bootstrap.md b/docs/adr/0011-idempotent-sql-schema-bootstrap.md new file mode 100644 index 0000000..9fe1e36 --- /dev/null +++ b/docs/adr/0011-idempotent-sql-schema-bootstrap.md @@ -0,0 +1,60 @@ +# ADR-0011: Idempotent SQL schema bootstrap + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** schema, migrations, cli, supabase + +## Context + +Horizon A has one Postgres schema and a small set of additive columns +(`detail`, `console_output`, timestamps, widened scanner status). A migration +framework (Flyway, Prisma, supabase db push as the only path) would add +tooling without solving the real operator problem: a fresh project must become +queryable from `tripwire setup` or the first Live scan. + +PostgREST schema cache can lag; the CLI must probe both table existence and +migration columns (`completed_at`) before declaring ready. + +## Decision + +Treat `db/schema.sql` as an **idempotent bootstrap script**, not a versioned +migration history. + +- Apply via `tripwire setup` / first-scan auto-bootstrap (`cli/src/ensureSchema.js`) + using `SUPABASE_DB_URL` (HTTP URL cannot run DDL). +- `CREATE TABLE IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`, guarded policy + creation, `GRANT`s, replaceable rollup function, Realtime `ALTER PUBLICATION` + with exception handlers. +- `--force` re-applies when the probe sees missing tables **or** missing + columns. +- Probe distinguishes missing-schema (PGRST204/205, missing relation) from + auth/network failure ([ADR-0009](./0009-fail-closed-incomplete-evidence.md)). +- Injectable `ClientImpl` / `applySchemaFn` so unit tests never talk to live + Postgres. + +## Consequences + +- There is no down-migration. Destructive changes need a new, carefully + ordered SQL file and an operator `--force`. +- Re-running setup is safe and is the fix for RLS/GRANT drift + ([ADR-0008](./0008-anon-read-service-role-write.md)). +- Multiple environments (dev vs demo project) are just multiple `.env` files + pointing at different `SUPABASE_DB_URL`s. + +## Alternatives considered + +### A. Numbered migration files + +Deferred until schema churn is high enough to need history. Today additive +`IF NOT EXISTS` is the whole story. + +### B. Manual SQL Editor as the only apply path + +Rejected: operators miss GRANTs/Realtime; first scan must auto-heal. + +## References + +- `db/schema.sql` header comment +- `cli/src/ensureSchema.js` +- [docs/user-guide/supabase-setup.md](../user-guide/supabase-setup.md) diff --git a/docs/adr/0012-sandbox-target-acquisition.md b/docs/adr/0012-sandbox-target-acquisition.md new file mode 100644 index 0000000..4b64dee --- /dev/null +++ b/docs/adr/0012-sandbox-target-acquisition.md @@ -0,0 +1,56 @@ +# ADR-0012: Sandbox target acquisition + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** sandbox, acquisition, isolation, modal + +## Context + +Scanners need a workdir (or a protocol URL). Modal workers cannot see the +operator’s filesystem. Silently scanning an empty directory looks like a clean +skill. Path-traversal in uploaded archives must not write outside the workdir. + +Targets come in four shapes: local directory, git URL, uploaded tar from the +host, and live MCP endpoints with no source on disk. + +## Decision + +Populate the sandbox workdir in `_acquire_target` (`sandbox/scan_app.py`): + +| Shape | Action | +|---|---| +| Git URL | Shallow clone (`--depth 1`, single-branch) | +| Host local directory | `local_entrypoint` packs a gzipped tar; remote `scan_item` extracts with `filter="data"` | +| Directory already on the sandbox FS | Recursive copy (same-machine only) | +| Filesystem-looking path, no archive, not on disk | **Raise** — do not scan an empty workdir | +| MCP URL / stdio / other | Introspection-only: empty workdir; scanners use `target` via protocol | + +CLI must spawn `sandbox/scan_app.py::main`, not `scan_item` directly, or packing +is skipped. + +## Consequences + +- Local Live scans pay a pack/upload cost; that is required for isolation + ([ADR-0003](./0003-modal-isolated-scanner-execution.md)). +- Clone failures fail the `scan_run` rather than producing empty findings. +- Introspection-only MCP cannot run disk-oriented engines; those rows should + be `not_applicable` / unreachable, not green. +- Git content hashing on the host remains a placeholder + ([ADR-0010](./0010-content-hash-idempotency.md)). + +## Alternatives considered + +### A. Mount the operator disk into Modal + +Rejected: breaks isolation and is not how Modal remote functions see paths. + +### B. Always require git URLs + +Rejected: fixture and local skill scans are the primary operator path. + +## References + +- `sandbox/scan_app.py` `_acquire_target`, `cli/src/modalClient.js` +- `sandbox/tests/test_acquire_target.py` +- [docs/STATUS.md](../STATUS.md) `_acquire_target` dispatch diff --git a/docs/adr/0013-ship-path-quality-gates.md b/docs/adr/0013-ship-path-quality-gates.md new file mode 100644 index 0000000..6f1c9bd --- /dev/null +++ b/docs/adr/0013-ship-path-quality-gates.md @@ -0,0 +1,64 @@ +# ADR-0013: Ship-path quality gates; dashboard and Guard out of bar + +- **Status:** Accepted +- **Date:** 2026-08-02 +- **Deciders:** Tripwire maintainers +- **Tags:** quality, coverage, ci, nightly, hooks + +## Context + +The repo mixes a production ship path (CLI, sandbox adapters, Live ACL) with a +prototype dashboard, a Guard stub, scripts, and optional Live cloud E2E. +Applying one 95% coverage / complexity ceiling to every file either blocks on +HTML glue or lets ship-path regressions hide in untested adapters. + +Local hooks must stay fast; SAST/SCA and mutation testing are too slow to +block every push. + +## Decision + +Govern **ship path only**, with three gate altitudes. + +**Coverage (Must, CI + push when those trees change):** + +| Surface | Floor | Out of bar | +|---|---|---| +| Python `sandbox/` | `fail_under=95` | `guard/` | +| Node `cli/src` | ≥95% lines/stmts; 100% funcs; **85%** branches | — | +| Live ACL four JS modules | ≥95% lines; funcs 85 / branches 80 | `support.js` | +| `prototypes/dc-dashboard` overall | Normal tests only | coverage + complexity | +| Remotion, scripts | — | Won't | + +**Hooks vs CI vs Nightly:** + +- Commit: ruff, mypy, bandit, xenon, vulture, pylint dupes, gitleaks, fast tests, + ESLint when CLI/dashboard JS staged. +- Push: full pytest + coverage when Python changes; CLI coverage when CLI + changes; T3 gitleaks **warn-only**; full SAST/SCA is CI. +- CI: Semgrep, OSV, Meterian, CodeQL, Trivy, TruffleHog, coverage jobs. +- Nightly: mutation (mutmut, Stryker) and extra SCA are **non-gating**. +- Live Modal/Supabase E2E as CI Must = **Won't** (skip-without-config). + +## Consequences + +- Dashboard reliability still needs tests (wave G slices 21–22) but will not + fail a coverage number. +- Branch floors are lower than line floors where residual defensive branches + remain (orchestrator, CDN import). +- Green Nightly does not mean a high mutation score. + +## Alternatives considered + +### A. 95% on the whole repo including dashboard and Guard + +Rejected: Guard is not a production entry; dashboard is a prototype ship UI. + +### B. Coverage as a report only (no fail_under) + +Rejected: ship-path floors were locked as Must (slices 11–13). + +## References + +- [docs/plan/coverage-audit.md](../plan/coverage-audit.md) +- [CONTRIBUTING.md](../../CONTRIBUTING.md) Dev hygiene +- [docs/plan/DECISIONS.md](../plan/DECISIONS.md) coverage-scope / coverage-e2e diff --git a/docs/adr/0014-curated-discovery-loci.md b/docs/adr/0014-curated-discovery-loci.md new file mode 100644 index 0000000..f8eaaeb --- /dev/null +++ b/docs/adr/0014-curated-discovery-loci.md @@ -0,0 +1,60 @@ +# ADR-0014: Curated discovery loci, not a filesystem crawl + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** discovery, cli, privacy, security + +## Context + +Zero-arg `tripwire scan` is a useful operator story (“scan what this machine +already installed”). An unconstrained home-directory crawl would mix unrelated +projects, leak private paths into Supabase `items.identifier`, and surprise +people who expected a fixture scan. + +Skill and MCP install locations are conventional (Cursor, Claude, Codex agent +folders and MCP manifests), not arbitrary. + +## Decision + +Discover only **curated loci** (`cli/src/discovery.js`). + +- Default skill roots: `.cursor/skills`, `.claude/skills`, `.agents/skills`, + and the matching directories under `$HOME` for Cursor/Claude/Codex. +- Default MCP manifests: `.cursor/mcp.json`, `.mcp.json`, + `~/.cursor/mcp.json`. +- Explicit path/URL arguments and `--targets` still win. +- `--no-defaults` errors instead of scanning machine defaults when args are + empty. +- `--dry-discover` prints the same expansion without spawning. +- Type detection: `SKILL.md` → skill; git-looking HTTPS → cloneable MCP; + other HTTPS → introspection-only MCP; else local MCP heuristics. + +Zero-arg agent-loci scan is a Could, not an unconstrained search. + +## Consequences + +- Skills installed outside those folders are invisible until the operator + passes a path. +- Manifest parse failures are skipped, not fatal. +- Identifiers stored in Postgres are relative/normalized paths from those + loci — still potentially sensitive; the project is a single-operator trust + domain ([ADR-0008](./0008-anon-read-service-role-write.md)). + +## Alternatives considered + +### A. Walk `$HOME` for `SKILL.md` + +Rejected: privacy, performance, and surprise scope. + +### B. Fixtures-only discovery (no defaults) + +Rejected: the operator machine *is* a legitimate target; defaults are opt-out +via `--no-defaults`. + +## References + +- `cli/src/discovery.js` +- [docs/plan/interview_summary.md](../plan/interview_summary.md) (zero-arg not + on critical path) +- [QUICKSTART.md](../../QUICKSTART.md) dry-discover diff --git a/docs/adr/0015-horizon-a-excludes-guard-and-drift.md b/docs/adr/0015-horizon-a-excludes-guard-and-drift.md new file mode 100644 index 0000000..b0bfb92 --- /dev/null +++ b/docs/adr/0015-horizon-a-excludes-guard-and-drift.md @@ -0,0 +1,60 @@ +# ADR-0015: Horizon A excludes Guard and Drift + +- **Status:** Accepted +- **Date:** 2026-08-01 +- **Deciders:** Tripwire maintainers +- **Tags:** scope, guard, drift, horizon-a + +## Context + +The product spec includes later phases: Agent Guard (PreToolUse fail-closed +hook), Drift/trend/diff on `items.identifier`, and a Reconciler for +multi-scanner disagreement. The walking skeleton landed a `guard/` stub and +schema columns that look like those features. Treating stubs as shipped +capabilities produced false README badges (Overmind/Ossprey) and pulled Guard +into C4 diagrams. + +Horizon A was locked as Detection + Sandbox (GWT-1/2), not the full platform. + +## Decision + +**Horizon A production entry points are CLI + Modal sandbox + Supabase + +dashboard.** Everything else is Future or Won't (A). + +- `guard/` may exist on disk; it is **not** a shipped production entry. Omit + from C4 L2 and from coverage bars ([ADR-0013](./0013-ship-path-quality-gates.md)). +- Drift/trend/diff and Reconciler are out of A. `identifier` exists for later + grouping; do not claim drift behaviour. +- Demo/hackathon film day (VO/Remotion) is Won't (A); reinstate only if a new + demo need arises. +- Public badges and architecture diagrams show only partners and containers + that have a reachable path (Gate A trust strip). + +Re-opening Guard or Drift requires a new ADR (or an explicit supersede of this +one) plus STATUS evidence, not a stub file. + +## Consequences + +- Contributors seeing `guard/` must read STATUS/ARCHITECTURE Future, not the + folder name. +- Schema can be ahead of behaviour (`identifier`, `coverage` table); claims + stay evidence-labelled. +- Wave G ATDD (slices 18–22) closes CLI/sandbox/dashboard contracts inside + this Horizon A boundary; it does not revive Guard. + +## Alternatives considered + +### A. Ship Guard in Horizon A + +Rejected: no production hook path; would fake a security control. + +### B. Delete the Guard stub + +Not required: keeping a stub is fine if docs and coverage treat it as Future. + +## References + +- [docs/ARCHITECTURE.md](../ARCHITECTURE.md) §2 Future +- [docs/STATUS.md](../STATUS.md) Future +- [docs/plan/DECISIONS.md](../plan/DECISIONS.md) demo-hackathon closed; Gate A + trust strip diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..26b031d --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,33 @@ +# Architecture Decision Records + +Formal ADRs for durable technology and boundary choices. + +Planning-level decisions (slice waivers, priority, review skips, model split) +stay in [plan/DECISIONS.md](../plan/DECISIONS.md). Use an ADR when the choice +should outlive a single slice and constrain runtime topology, security, or +quality gates. + +Status values: **Proposed**, **Accepted**, **Deprecated**, **Superseded**. + +Number **0001** is reserved and omitted from this catalog while that draft +remains under review. Published records start at **0002**. + +| ADR | Title | Status | +|-----|-------|--------| +| [0002](./0002-node-cli-python-sandbox-split.md) | Node CLI and Python sandbox as separate runtimes | Accepted | +| [0003](./0003-modal-isolated-scanner-execution.md) | Modal for isolated scanner execution | Accepted | +| [0004](./0004-supabase-system-of-record.md) | Supabase/Postgres as system of record | Accepted | +| [0005](./0005-upstream-scanner-cli-adapters.md) | Wrap upstream scanner CLIs; normalize findings | Accepted | +| [0006](./0006-live-mock-strangler-acl.md) | Live vs Mock strangler ACL | Accepted | +| [0007](./0007-html-prototype-ship-ui.md) | HTML prototype dashboard as Horizon A ship UI | Accepted | +| [0008](./0008-anon-read-service-role-write.md) | Anon read, service-role write | Accepted | +| [0009](./0009-fail-closed-incomplete-evidence.md) | Fail closed on incomplete evidence | Accepted | +| [0010](./0010-content-hash-idempotency.md) | Content-hash idempotency with `--force` | Accepted | +| [0011](./0011-idempotent-sql-schema-bootstrap.md) | Idempotent SQL schema bootstrap | Accepted | +| [0012](./0012-sandbox-target-acquisition.md) | Sandbox target acquisition | Accepted | +| [0013](./0013-ship-path-quality-gates.md) | Ship-path quality gates; dashboard and Guard out of bar | Accepted | +| [0014](./0014-curated-discovery-loci.md) | Curated discovery loci, not a filesystem crawl | Accepted | +| [0015](./0015-horizon-a-excludes-guard-and-drift.md) | Horizon A excludes Guard and Drift | Accepted | + +0002–0015 are retrospective Accepted records of decisions already visible in +docs, git history, and production entry points. diff --git a/docs/plan/DECISIONS.md b/docs/plan/DECISIONS.md index 6935cbf..24c1bc2 100644 --- a/docs/plan/DECISIONS.md +++ b/docs/plan/DECISIONS.md @@ -85,3 +85,4 @@ | 2026-08-07 | planning | Added slices 18–22 | ATDD closure: CLI operator evidence, persistence contract, subprocess adapter contract, dashboard latest-state accuracy, and dashboard realtime recovery. All are Must with independent gates; source-file overlap controls execution order. PLANNED. | | 2026-08-07 | scope | Dashboard reliability | Implement latest-state and realtime recovery now; keep dashboard excluded from governed coverage/complexity thresholds while normal tests remain mandatory. USER-CONFIRMED. | | 2026-08-07 | quality-lens | 15/15 checks passed | Split dashboard latest-state and realtime recovery into slices 21 and 22 to maintain single-level abstraction; no new dependency or metric policy introduced. REVISED 1 SLICE. | +| 2026-08-12 | adr-backfill | Formal ADRs 0002–0015 | Retrospective Accepted records from docs + git + production entry points. Index: `docs/adr/README.md`. Number 0001 reserved and omitted while that draft stays under review. Slice waivers remain in this file. | diff --git a/docs/plan/README.md b/docs/plan/README.md index 9f8a876..70b1932 100644 --- a/docs/plan/README.md +++ b/docs/plan/README.md @@ -17,7 +17,8 @@ Horizon A trackers and slice stubs. **Slice files live in wave folders** (execut | [EMOJI_LEGEND.md](EMOJI_LEGEND.md) | Canonical meaning of status emojis used by tracker docs | | [TRAIL.md](TRAIL.md) | Full slice index + execute priority | | [PROGRESS.md](PROGRESS.md) | Status + open execution order | -| [DECISIONS.md](DECISIONS.md) | Planning decisions | +| [DECISIONS.md](DECISIONS.md) | Planning decisions (slice waivers, priority) | +| [../adr/README.md](../adr/README.md) | Formal ADRs (runtime/topology/security) | | [GATE_CONTRACT.md](GATE_CONTRACT.md) | Before/After close rule | | [coverage-audit.md](coverage-audit.md) | Ship-path coverage matrix (slice 7) | | [gate-evidence/](gate-evidence/) | Per-slice evidence JSON |