Files
YPMS/docs/nist-mapping.md
Chris TonjesandClaude Opus 5.5 e3c4cb0cdc
ci / test (push) Has been cancelled
Phase 0 milestone 0.9: docs + acceptance
- 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>
2026-10-01 14:37:40 -04:00

51 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.