AWS evidence collectors and IAM
Part of epic 6213. Child C3 of 9. Depends on C1 (store, bucket) and C2 (the `Collector` contract, `run_collector`, alarm prefix, and the migration head this child's migration chains after).
## 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 :)"
**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).
- A check that fails or drifts raises the existing alarm/paging path — never silent.
- The same collectors serve ISO 27001/42001 evidence where controls overlap (no duplicate collectors).
Operator decision Q4: **Read-only, own account** — a new read-only IAM statement on the api task role, scoped to the account's own services; no cross-account role.
## Scope (architecture §3.2)
Seven collectors on C2's contract (literal `_system(EVIDENCE_<ID>, …)` per collector, kebab-case module constants, no loop; staggered daily 04:00–05:30 UTC), using the container's own AWS credentials:
| id | source | folder | criteria | drift |
|---|---|---|---|---|
| `cloudwatch-alarms` | `cloudwatch:DescribeAlarms` | `alarms` | CC7.2, A1.1 | an alarm in ALARM not listed in an accepted risk row |
| `paging-subscriptions` | `sns:ListSubscriptionsByTopic` on paging + cp-alarms topics | `alarms` | CC2.2 | no confirmed subscription |
| `securityhub` | `securityhub:GetFindings` (own account, FAILED/ACTIVE/HIGH+CRITICAL, not SUPPRESSED) | `control-checks` | CC6.6, CC7.1 | any finding |
| `edge-tls` | `elasticloadbalancing:Describe*`, `wafv2:ListWebACLs`, `wafv2:GetWebACLForResource` | `control-checks` | CC6.6, CC6.7 | listener not on pinned TLS policy, port-80 listener not redirecting, or public ALB without WAF |
| `backups` | `backup:ListBackupJobs`, `backup:ListCopyJobs`, `rds:DescribeDBInstances` | `control-checks` | CC9.1, A1.2 | no COMPLETED job/copy in 26 h, or DB not Multi-AZ |
| `storage-lifecycle` | `s3:GetLifecycleConfiguration` on transcripts bucket | `offboarding` | C1.2 | the 180-day rule absent |
| `ecr-scan` | `ecr:DescribeRepositories`, `ecr:DescribeImageScanFindings` | `ecr-scan` | CC6.8 | HIGH/CRITICAL finding past SLA |
**IAM**: new `data "aws_iam_policy_document" "task_compliance_evidence"` in `infra/jaah/terraform/modules/ecs/main.tf`, attached beside `task_security_archive`; exactly the read actions above, `Resource="*"` (only valid scope for these Describe/List APIs) except `s3:GetLifecycleConfiguration` scoped to the transcripts bucket ARN; NO write actions. Check `workload_permissions_boundary` (declared in this repo) permits these reads; if not, widen it with exactly these read actions in the same PR (otherwise prod apply fails at promotion).
**Seeding**: own alembic migration seeding the 7 rows `is_system=true`, `enabled=true`, same ON CONFLICT shape; **down_revision = C2's migration** (one head). Add rows to `apps/api/tests/test_cron_system_activities.py` `_EXPECTED`/`_SEEDED_ENABLED` additively only. Add the 7 ids to the readiness-checklist collector→criterion section C2 created.
Expected on dev: several collectors will drift (no paging email, no WAF…) — intended; the dedup key keeps one alarm Task per collector, resolved on recovery.
Credentials & reach: AC4, AC5: Crons page, trigger collector, see run detail with `s3://` key and alarm Task | `jaah/debug/main/operator-login` (operator login, valid on dev and main) / operator / dev CP `app.dev.jaah.ai` | **verified**: credential fetch succeeded; `GET https://app.dev.jaah.ai/api/public/settings` answered 200 from `h2`. Terraform plan for dev/main | 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**. AC-X2 | runner's own forge login via `jaah-forge` | **verified**.
Out of scope: NO write IAM actions, NO cross-account role, NO management/security-account access (entitlement export and aggregated Security Hub stay owner-run). Do NOT change the C2 framework contract except additively. Do NOT alter existing IAM statements, alarms, WAF, TLS or backup config to make a collector pass — drift is evidence. Epic-wide (PRD §6, verbatim): "**Do not** obtain, buy, sign or contract anything; **do not** contact outside parties. **Do not** claim any certification as held. **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** put instance data (people, account ids) in reusable docs. **Do not** add a new evidence-store folder outside `tools/evidence_store.py`'s list."
Done means:
- AC4 (R2) Each unattended-runnable readiness check runs on its schedule on the **dev** control plane and a dated artifact appears in the evidence store per run. States: pass → artifact filed; fail/drift → artifact filed AND alarm fires; collector error → alarm fires (never silent). → verify: on dev console Crons page, trigger a collector, observe a success journal entry and the new evidence key; force a failing check (e.g. a test fixture or a known-failing dev setting) and observe the alarm. (Every one of the 7 triggered on dev; none ends in an AccessDenied error leg — that would mean IAM is wrong.)
- AC5 (R2) The collectors are ON by default in the real deployment (registered system crons, enabled), not behind a flag left off. → verify: dev console lists them enabled with a next-run time.
- Terraform plan (dev and main) shows only the added read-only policy (and boundary reads if needed), no write action — oracle: plan excerpt in PR body; `alembic heads` = one head.
- AC-X1 (A3) Nothing breaks: `./dc check full` green on every PR; existing crons behave unchanged. → verify: gate output; existing cron tests pass unchanged.
- AC-X2 (A3) Every PR's Jaah Deploy succeeds on dev. → verify: GitHub deploy run status.
Outcome
7 AWS SOC 2 evidence collectors + read-only IAM shipped (#5397; fixes #5400, #5407), full gate green ×3, dev deploys ok. Dev boundary applied via org/ (inputs now in repo). All 7 run on dev: 3 pass, 4 drift, none AccessDenied.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Awaiting response
- Awaiting response
- Runner resumed
- Runner ended (done)