Evidence layout and bucket everywhere

Task 6220 · Done · Coder · created · updated

Part of epic 6213 (close competitor security/compliance certification gaps). Child C1 of 9. ## Original requirement (verbatim) > "Identify which security and compliance certifications Jaah's competitors have — SOC 2, HIPAA, ISO 27001, etc. — and close the gap. Without breaking anything :)" BA requirement this child enables — **R2 — SOC 2 Type II readiness: in-house recurring evidence (D-4)** (verbatim): - Every SOC 2 check in the readiness checklist that an agent can run unattended runs on a schedule as a Jaah system cron and files a dated artifact to the evidence store through `tools/evidence_store.py` (existing folders only, or a folder added there). - The same collectors serve ISO 27001/42001 evidence where controls overlap (no duplicate collectors). Operator decision Q1 (architecture): dev gets an **unlocked** evidence bucket. The evidence bucket exists on every environment; Object Lock only where `soc2_boundary` is set (same as `security_archive`). Dev's api gets `EVIDENCE_BUCKET`. Dev evidence stays "not audit evidence". ## Scope (architecture §2.1, §2.2, §3.2 'Evidence bucket on every environment') 1. **New `packages/evidence/__init__.py`** (stdlib only) — single home of: - `CONTROLS: frozenset[str]` = the current 16 folders from `tools/evidence_store.py` plus ONE new folder **`control-checks`**. - `object_key(control, filename, *, date, draft=False) -> str` returning `<control>/<YYYY-MM-DD>/<file>`; raises `ValueError` on unknown control / bad filename / bad date using the same regexes as today. - `draft_name(filename) -> str`. 2. **`tools/evidence_store.py`** keeps its CLI and `write()` signature unchanged; imports `CONTROLS`, `object_key`, `draft_name` from `packages/evidence` (put `packages/` on `sys.path` the way `tools/architectural_guard.py` does). Existing callers (`github_settings.py`, `mfa_check.py`, `tenant_offboard.py`, `entitlements_export.py`, `hipaa_crosswalk.py` reading `CONTROLS`) do not change. 3. **`apps/api/app/compliance_evidence/store.py`** (new package): ```python def evidence_target() -> Optional[attachment_storage.Target] # None when EVIDENCE_BUCKET is unset (local ./dc stack). Raises when the bucket is set but no # credentials resolve — same rule as security_archive.archive_target(). def file(control: str, filename: str, body: bytes, *, today: date) -> str # key = packages.evidence.object_key(...); attachment_storage.put(target, key, body, content_md5=True) # returns "s3://<bucket>/<key>"; with no target returns "unconfigured:<key>" and writes nothing. ``` Model on `apps/api/app/security_archive.py:77-97` (`archive_target`, `attachment_storage.put` + container credentials). NOT via `tools/evidence_store.py` (it shells out to the aws CLI the api image lacks; apps never import `tools/`). `EVIDENCE_BUCKET` read via `app/config.py` settings (add `evidence_bucket`), not `os.environ`. Make sure `packages/evidence` is importable in the api image (check `infra/local/Dockerfile.api` COPY list, as other `packages/` are). 4. **Terraform** `infra/jaah/terraform/security-archive.tf`: `aws_s3_bucket.evidence` drops `count = var.soc2_boundary ? 1 : 0`; `object_lock_enabled = var.soc2_boundary`; the object-lock configuration keeps its count gate. `main.tf`'s `evidence_bucket = one(aws_s3_bucket.evidence[*].bucket)` becomes a plain reference. Add `moved { from = aws_s3_bucket.evidence[0] to = aws_s3_bucket.evidence }` for EVERY evidence resource that loses its count, so prod's locked bucket is never destroyed/recreated. CloudTrail data-event selector stays boundary-gated (dev has no trail); verify the bucket's SSE-KMS key exists on dev (security_archive's key is not gated). Update `outputs.tf`/`variables.tf` descriptions. Ensure dev's api task gets `EVIDENCE_BUCKET` and the task role's PutObject on it (`modules/ecs/main.tf:567`). 5. Docs: `packages/README.md` gains a row; `docs/compliance/README.md` evidence-store table gains the row `control-checks` — "Automated recurring control checks (Type II operating evidence); collector id leads the filename" — and its sentence "One S3 bucket per boundary environment" is corrected to reflect the bucket on every environment (locked only on the boundary). This child creates NO collectors and NO cron — C2/C3 consume `store.file` and `CONTROLS`. Credentials & reach: AC-X2: Jaah Deploy succeeds per PR | runner's own forge login via `jaah-forge` (no roster credential needed) / GitHub `jaah-ai/jaah` | **verified**: `jaah-forge api get repos/jaah-ai/jaah/actions/workflows` lists "Jaah Deploy". Risk 2: terraform plan for dev/main showing 0 destroys | none needed: the plan runs in Jaah Deploy's own CI with its OIDC role; the runner reads the plan output with `jaah-forge` | **verified** (same forge reach). Unit tests / gate | nothing beyond the worktree | **verified** (worktree-only). Out of scope: Do NOT write any collector, cron, migration or alarm (C2/C3). Do NOT change CloudTrail, KMS key policy or Object Lock semantics on prod. Do NOT change `tools/evidence_store.py`'s CLI or `write()` signature or its existing callers. Epic-wide fences (BA PRD §6, verbatim): "**Do not** obtain, buy, sign or contract anything (cert body, auditor, Drata, SafeBase, pentest firm, NDAs); **do not** contact outside parties. Those are owner actions only. **Do not** claim any certification as held (trust page, security page, marketing, PRs). **Do not** start FedRAMP, PCI, TISAX, Cyber Essentials, ISO 27017/27018/27701 or CSA STAR work. **Do not** change HIPAA mode behaviour, the BAA text, or the HIPAA crosswalk except to add pointers. **Do not** change existing legal pages' text/URLs, the publish switch semantics, or `UNPUBLISHED_SIGNUP_LINKS` / sign-up flow. **Do not** flip the **prod** legal publish switch or promote to `master` (owner action). **Do not** widen the audit boundary to dev; **do not** alter existing controls' behaviour (MFA, sessions, retention, IAM) to satisfy ISO — record a gap row instead unless the change is clearly non-breaking and additive. **Do not** copy SOC 2 policy prose into ISO documents — pointer per row (single-home); no parallel policy set. **Do not** put instance data (people, account ids) in reusable docs — `portfolio/projects/jaah.md` only. **Do not** add a new evidence-store folder outside `tools/evidence_store.py`'s list." (The `control-checks` folder is that list's new member, now single-homed in `packages/evidence`.) Done means: - `packages/evidence` is the single home of `CONTROLS`/`object_key`/`draft_name`; `tools/evidence_store.py` imports them and its existing tests pass unchanged — oracle: `grep -n 'CONTROLS = ' tools/ packages/ -r` shows exactly one definition; `./dc check full` green. - `store.file` writes `<control>/<YYYY-MM-DD>/<file>` with content MD5 and returns the `s3://` URI; returns `unconfigured:<key>` and writes nothing when `EVIDENCE_BUCKET` is unset; rejects an unknown control — oracle: new unit tests for all three legs pass. - Terraform plan for BOTH dev and main shows 0 destroys/replacements of `aws_s3_bucket.evidence*` and creates the dev bucket without Object Lock — oracle: quote the plan lines from the Jaah Deploy run in the PR body. - After deploy, dev's api has `EVIDENCE_BUCKET` set — oracle: Jaah Deploy run log / task definition shows it. - AC-X1 (A3) Nothing breaks: `./dc check full` green on every PR; existing legal pages, sign-up links, HIPAA mode and existing crons behave unchanged. → verify: gate output; existing legal-page and cron tests pass unchanged. - AC-X2 (A3) Every PR's Jaah Deploy succeeds on dev. → verify: GitHub deploy run status.

Outcome

packages/evidence single-homes CONTROLS (+control-checks); CP store.file; evidence bucket on every env (moved blocks keep main's lock). Full gate green; PR #5383 merged; dev deploy applied (bucket unlocked, EVIDENCE_BUCKET set). Main plan unobserved (owner-gated).

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)