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

10 KiB
Raw Permalink Blame History

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 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/auth/deps.py:43
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/youth/router.py:67
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/security/rbac.py:22
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 —
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
tests/test_stubs.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/router.py:86
src/ypms/auth/router.py:65
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/deps.py:35
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/framework/crud.py:111
src/ypms/auth/router.py:65
src/ypms/auth/router.py:104
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/framework/crud.py:46
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
docker-compose.yml:8
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/framework/retention.py:150
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/stub_router.py:66
—
AU-14 ✅ pii_read audit on Registrant GET, list (ids recorded), and history. src/ypms/framework/pii_audit.py:36
src/ypms/youth/router.py:99
src/ypms/youth/router.py:115
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
tests/test_migration_immutability.py:31
Maintenance-window process is organizational.
CM-8 🟡 CycloneDX 1.6 SBOM of the runtime dependency set generated in CI. .gitea/workflows/ci.yml:70 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 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 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:147
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 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 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:56
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/settings.py:15
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 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/youth/models.py:83
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 —
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/db.py:24
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/main.py:34
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.