ci / test (push) Has been cancelled
- README rewritten: Docker and native quickstarts, try-it flow, gate commands, promote-a-stub steps. - CONTRIBUTING rules 9-13 (pii_read on PII reads, __audit_redact__ for encrypted columns, retention policy per table, models.py registry, vendored-code change discipline); PR checklist updated. - docs/nist-mapping.md: per-control file:line evidence and remaining gap. - NIST_800_53_CONTROL_MAPPING.md v0.3: Phase 0 notes on 28 rows (12 implemented, 12 partial, 4 progress notes on hybrid/tailored rows) plus a status-shift table. - PHASE0_PLAN: 0.9 complete; Definition of Done 12/14 checked with how each was verified. Open: first green Gitea CI run, Docker/Postgres boot. - POA&M and status handoff updated; status TL;DR no longer claims an unverified Docker boot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 lines
10 KiB
Markdown
51 lines
10 KiB
Markdown
# NIST 800-53 — Phase 0 code evidence
|
||
|
||
**As of:** 2026-10-01 · **Scope:** controls that Phase 0 code implements or partially implements (PHASE0_PLAN.md §6) · **Authoritative mapping:** [`NIST_800_53_CONTROL_MAPPING.md`](../NIST_800_53_CONTROL_MAPPING.md) v0.3
|
||
|
||
This is the developer-facing index: for each control, where the code is and what's still missing. The control mapping carries the same notes in each row for assessors. When a change wires or advances a control, update both (CONTRIBUTING PR checklist).
|
||
|
||
Line numbers are as of the commit that last touched this file. If they drift, the function or name in the summary is the stable anchor.
|
||
|
||
| Control | Status | What the code does | Evidence | Remaining gap |
|
||
|---|---|---|---|---|
|
||
| **AC-2** | 🟡 | User model with active flag; inactive accounts rejected at authentication. | [`src/ypms/users/models.py:48`](../src/ypms/users/models.py#L48)<br>[`src/ypms/auth/deps.py:43`](../src/ypms/auth/deps.py#L43) | No admin enable/disable endpoint (admin module, Phase 8); inactivity auto-disable is YPMS-POAM-071. |
|
||
| **AC-3** | ✅ | `require_permission` runs on every protected route, including all 208 stub routes; portal-only callers restricted to their own Registrant row. | [`src/ypms/security/rbac.py:46`](../src/ypms/security/rbac.py#L46)<br>[`src/ypms/youth/router.py:67`](../src/ypms/youth/router.py#L67)<br>[`tests/test_stubs.py`](../tests/test_stubs.py) | Row-level scoping beyond Registrant is Phase 2+. |
|
||
| **AC-6** | ✅ | Data-driven RBAC (8 roles × 30 entities × 8 actions); FERPA/CFSA entities limited to ELIGIBILITY_EXAMINER; super-role explicit. | [`src/ypms/seed/permissions_matrix.json`](../src/ypms/seed/permissions_matrix.json)<br>[`src/ypms/security/rbac.py:22`](../src/ypms/security/rbac.py#L22)<br>[`tests/test_stubs.py`](../tests/test_stubs.py) | Four matrix review items open (PHASE0_STATUS.md). |
|
||
| **AC-6(1)** | ✅ | Only DOES_SYSTEM_ADMIN holds RBAC-table rights (User/Role not granted to any other role). | [`src/ypms/seed/permissions_matrix.json`](../src/ypms/seed/permissions_matrix.json) | — |
|
||
| **AC-6(10)** | ✅ | Delete and Restore (super-role only today) and Registrant VersionHistory (OYP_STAFF and ELIGIBILITY_EXAMINER only) enforced server-side; 403 asserted for every other role. | [`src/ypms/security/rbac.py:29`](../src/ypms/security/rbac.py#L29)<br>[`tests/test_stubs.py`](../tests/test_stubs.py)<br>[`tests/youth/test_registrant_api.py`](../tests/youth/test_registrant_api.py) | — |
|
||
| **AC-7** | 🟡 | 5 failed logins per source IP per 15 minutes → 429; every failure (bad credentials, unknown user, inactive, throttled) audited as `login_failed`. | [`src/ypms/auth/rate_limit.py:47`](../src/ypms/auth/rate_limit.py#L47)<br>[`src/ypms/auth/router.py:86`](../src/ypms/auth/router.py#L86)<br>[`src/ypms/auth/router.py:65`](../src/ypms/auth/router.py#L65) | Per-account lockout (`locked_until`) and user notification not built; in-memory limiter is per-process. |
|
||
| **AC-12** | ✅ | Access tokens expire (default 60 min, `exp` claim verified); refresh tokens cannot access resources. | [`src/ypms/auth/security.py:59`](../src/ypms/auth/security.py#L59)<br>[`src/ypms/auth/deps.py:35`](../src/ypms/auth/deps.py#L35) | Idle-timeout UI behaviour lands with the portal (Phase 3). |
|
||
| **AU-2** | 🟡 | Audited today: login success/failure, password change, every create/update/delete/restore, every PII read, every stub invocation, each retention run. | [`src/ypms/audit/service.py:17`](../src/ypms/audit/service.py#L17)<br>[`src/ypms/framework/crud.py:111`](../src/ypms/framework/crud.py#L111)<br>[`src/ypms/auth/router.py:65`](../src/ypms/auth/router.py#L65)<br>[`src/ypms/auth/router.py:104`](../src/ypms/auth/router.py#L104) | Event catalogue items not yet applicable (role changes, shutdown, file access) arrive with their features. |
|
||
| **AU-3** | ✅ | Each row: actor id + username, action, entity, entity id, before/after, timestamp, IP; app-layer ciphertext redacted from before/after. | [`src/ypms/audit/models.py:18`](../src/ypms/audit/models.py#L18)<br>[`src/ypms/framework/crud.py:46`](../src/ypms/framework/crud.py#L46) | Request id is in logs, not yet on the audit row. |
|
||
| **AU-8** | ✅ | All timestamps timezone-aware UTC; Postgres container pinned to UTC. | [`src/ypms/audit/models.py:24`](../src/ypms/audit/models.py#L24)<br>[`docker-compose.yml:8`](../docker-compose.yml#L8) | NTP is inherited from Azure at deployment. |
|
||
| **AU-11** | 🤝 | Retention scaffold: audit log carries the PIA §9 interim policy (archive at 1 year); report-only job. | [`src/ypms/retention_classes.py`](../src/ypms/retention_classes.py)<br>[`src/ypms/framework/retention.py:150`](../src/ypms/framework/retention.py#L150) | Period unapproved until YPMS-POAM-054; archive tier is deployment work. |
|
||
| **AU-12** | ✅ | Audit generation is in the framework (CRUDService, stub router, auth), not per-handler. | [`src/ypms/framework/crud.py:111`](../src/ypms/framework/crud.py#L111)<br>[`src/ypms/framework/stub_router.py:66`](../src/ypms/framework/stub_router.py#L66) | — |
|
||
| **AU-14** | ✅ | `pii_read` audit on Registrant GET, list (ids recorded), and history. | [`src/ypms/framework/pii_audit.py:36`](../src/ypms/framework/pii_audit.py#L36)<br>[`src/ypms/youth/router.py:99`](../src/ypms/youth/router.py#L99)<br>[`src/ypms/youth/router.py:115`](../src/ypms/youth/router.py#L115) | Extends to Guardian/TaxDocument/Attachment as they are built. |
|
||
| **CM-3** | 🤝 | Merged migrations pinned by SHA-256; CI fails on any edit. Every change ships through a reviewed commit. | [`migrations/IMMUTABLE.sha256`](../migrations/IMMUTABLE.sha256)<br>[`tests/test_migration_immutability.py:31`](../tests/test_migration_immutability.py#L31) | Maintenance-window process is organizational. |
|
||
| **CM-8** | 🟡 | CycloneDX 1.6 SBOM of the runtime dependency set generated in CI. | [`.gitea/workflows/ci.yml:70`](../.gitea/workflows/ci.yml#L70) | Pending first green CI run (Gitea runner); per-release retention of SBOMs not yet defined. |
|
||
| **IA-2** | ✅ | Every non-public route requires a valid bearer token bound to a unique, active user. | [`src/ypms/auth/deps.py:25`](../src/ypms/auth/deps.py#L25) | MFA enhancements (IA-2(1)/(2)) arrive with Entra ID in Phase 1. |
|
||
| **IA-4** | 🟡 | Usernames unique (DB constraint); no user-delete path exists, so identifiers are never re-issued. | [`migrations/versions/0002_framework_tables.py:54`](../migrations/versions/0002_framework_tables.py#L54) | 12-character minimum identifier and explicit tombstoning arrive with registration (Phase 1). |
|
||
| **IA-5** | 🟡 | Users change their own password (12-character floor, current password required); hashes only. | [`src/ypms/auth/router.py:138`](../src/ypms/auth/router.py#L138)<br>[`src/ypms/auth/router.py:147`](../src/ypms/auth/router.py#L147) | Password-change notification (C.5.7.14) and admin reset arrive with notifications/admin. |
|
||
| **IA-5(1)** | 🟡 | PBKDF2-SHA256 (NIST SP 800-132) via passlib. | [`src/ypms/auth/security.py:30`](../src/ypms/auth/security.py#L30) | FIPS 140-3 validated module is deployment-dependent (YPMS-POAM-032). |
|
||
| **RA-5** | 🟡 | pip-audit on runtime dependencies gates CI; an unfixable `ecdsa` advisory was removed by changing JWT library. | [`.gitea/workflows/ci.yml:59`](../.gitea/workflows/ci.yml#L59) | Pending first green CI run; infrastructure scanning is deployment work. |
|
||
| **SA-11** | 🟡 | 167 automated tests, 80% coverage floor, ruff, strict mypy, bandit SAST, migration-drift test. | [`.gitea/workflows/ci.yml:53`](../.gitea/workflows/ci.yml#L53)<br>[`.gitea/workflows/ci.yml:56`](../.gitea/workflows/ci.yml#L56) | Pending first green CI run. |
|
||
| **SC-8** | ⚠️ | Not wired in Phase 0 (no local TLS proxy). | — | TLS terminates at Front Door / App Gateway at deployment (YPMS-POAM-031). |
|
||
| **SC-12** | 🟡 | Column key derived with HKDF-SHA256; ≥32-byte key material and ≥32-byte JWT secret enforced at startup. | [`src/ypms/security/column_crypto.py:49`](../src/ypms/security/column_crypto.py#L49)<br>[`src/ypms/settings.py:15`](../src/ypms/settings.py#L15) | Keys come from env vars; Key Vault (Gov/HSM) in Phase 1. |
|
||
| **SC-13** | 🟡 | FIPS-approved algorithms only: AES-256-GCM, HKDF-SHA256, HMAC-SHA256 (JWT), PBKDF2-SHA256. | [`src/ypms/security/column_crypto.py:55`](../src/ypms/security/column_crypto.py#L55) | Validated-module status is deployment-dependent (YPMS-POAM-032). |
|
||
| **SC-28** | 🟡 | SSN encrypted at the app layer (AES-256-GCM, column-bound AAD); write-only on the model; masked in every response. | [`src/ypms/security/column_crypto.py:55`](../src/ypms/security/column_crypto.py#L55)<br>[`src/ypms/youth/models.py:83`](../src/ypms/youth/models.py#L83) | Banking columns not yet built; Postgres TDE and blob encryption are deployment. |
|
||
| **SI-10** | ✅ | Pydantic validation on every request body; SSN format and never-issued ranges, DOB, ZIP, email, lengths. | [`src/ypms/youth/schemas.py:21`](../src/ypms/youth/schemas.py#L21) | — |
|
||
| **SI-11** | ✅ | Unhandled errors return a generic body with request id; logs record type and frames, not messages; SQL parameters hidden. | [`src/ypms/main.py:56`](../src/ypms/main.py#L56)<br>[`src/ypms/db.py:24`](../src/ypms/db.py#L24)<br>[`tests/test_app_guards.py`](../tests/test_app_guards.py) | — |
|
||
| **SI-12** | 🤝 | Every table carries a retention policy; app refuses to boot otherwise; nightly report-only job. | [`src/ypms/framework/retention.py:135`](../src/ypms/framework/retention.py#L135)<br>[`src/ypms/main.py:34`](../src/ypms/main.py#L34) | Disposal execution is Phase 5; periods await YPMS-POAM-054. |
|
||
|
||
## Verified end to end (2026-10-01)
|
||
|
||
Clean clone → fresh venv → `alembic upgrade head` → `python -m ypms.seed.run` → `uvicorn`, then over HTTP:
|
||
|
||
- `POST /auth/login` → JWT with `iam.assurance_level`; no token → 401.
|
||
- `POST /registrants` → 201, `ssn_masked: XXX-XX-6789`, no `ssn` field; `GET` → `pii_read` audit row; `PATCH` → version 2; `DELETE` → `deleted_at` set.
|
||
- `GET /guardians/5` → 501 with a `View` audit row.
|
||
- `audit_log`: `login`, `create`, `pii_read`, `update`, `delete`, `View` — every `ssn_encrypted` value `[REDACTED]`.
|
||
- `registrants.ssn_encrypted` holds `v1.` ciphertext only; the plaintext SSN appears nowhere in the DB or server log.
|
||
|
||
SQLite stood in for Postgres (no Docker on the verifying workstation). The Postgres run is still open.
|