diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a71d6c..0429359 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,22 +4,32 @@ Hard rules — mirrored from OWC's `CONTRIBUTING.md` because the two systems sha ## Repository conventions -1. **Migrations are immutable once merged.** Add a new Alembic revision, never edit `0001_baseline.py` or its successors. If a schema change requires undoing an older migration, the fix is a new revision, not a rewrite. Every migration is a paper trail an auditor can walk. +1. **Migrations are immutable once merged.** Add a new Alembic revision, never edit `0001_baseline.py` or its successors. If a schema change requires undoing an older migration, the fix is a new revision, not a rewrite. Every migration is a paper trail an auditor can walk. Every revision's SHA-256 is pinned in `migrations/IMMUTABLE.sha256` — add a line with each new migration; a test fails if a pinned file changes. Migrations never import app code. 2. **Don't add domain logic to `src/ypms/framework/` or `src/ypms/security/`.** Those are reused by every entity; entity-specific code belongs in `src/ypms//`. If a framework enhancement is genuinely needed, propose it in its own PR with a written reason. -3. **Every state-mutating route emits an audit row.** Enforced by `CRUDService` and the router factory once the framework lands. Don't write handlers that bypass either. +3. **Every state-mutating route emits an audit row.** Enforced by `CRUDService`, the router factory, and the stub router. Don't write handlers that bypass them. 4. **501 stubs still fire RBAC + audit.** Deferred actions must route through `require_permission` and emit an audit event. Permission gaps surface at Phase 0 review, not at feature-completion. 5. **Tests required.** `src/ypms/framework/` and `src/ypms/security/` need ≥ 80% coverage. Everything else: at least one happy-path test. -6. **No `--no-verify`, `# noqa`, or `# type: ignore` without an inline comment naming the specific reason.** If you need to silence a check, say why in the code. +6. **No `--no-verify`, `# noqa`, `# type: ignore`, `# nosec`, or `gitleaks:allow` without an inline comment naming the specific reason.** Suppressions are per-finding, never blanket. Don't lower a CI gate to get green. 7. **Sensitive fields never in logs.** SSN, banking, tokens, passwords, encryption keys — route through `sanitize_for_log` before emitting or don't emit. See `src/ypms/logging.py`. 8. **No commits attributed to a person who didn't write them.** Set `user.name` and `user.email` in `git config` to yourself before committing. Co-author trailers are welcome and encouraged for pairing. +9. **Reads of sensitive PII emit `pii_read`.** Any GET, list, or history route returning SSN-bearing, banking, FERPA, CFSA, or immigration data calls `framework.pii_audit` after RBAC passes (NIST AU-14). + +10. **Encrypted columns go through `security.column_crypto` and into `__audit_redact__`.** Otherwise their ciphertext lands in `audit_log` and `version_history`. Sensitive values are write-only on the model and masked in the read schema. + +11. **Every mapped table declares a retention policy.** The app refuses to start otherwise. Don't invent durations — use a `retention_classes.py` class, or an unset one citing the POA&M item that owns the decision. + +12. **New model modules go in `src/ypms/models.py`.** Alembic, the retention job, and the tests all read that one list. + +13. **Changes to vendored OWC code need a written reason in the module docstring** and get added to the "Upstream to OWC" list in the current phase status doc. + ## Change control - Every substantive change ties to a POA&M ID, a phase-plan milestone, or an issue. Include the reference in the PR description. @@ -37,9 +47,10 @@ Hard rules — mirrored from OWC's `CONTRIBUTING.md` because the two systems sha - [ ] Tests pass locally (`pytest`). - [ ] Coverage doesn't drop below 80% on `framework/` or `security/`. - [ ] `ruff check`, `mypy` clean. -- [ ] SAST (`bandit`) and SCA (`pip-audit`) clean, or documented waivers. -- [ ] Migration added (if schema changed); no edits to older migrations. -- [ ] Any new endpoint has RBAC + audit + a test. +- [ ] SAST (`bandit`), SCA (`pip-audit --strict .`), and secret scan (gitleaks) clean, or per-finding waivers with reasons. +- [ ] Migration added (if schema changed) and pinned in `migrations/IMMUTABLE.sha256`; no edits to older migrations. +- [ ] New tables have a retention policy; new model modules are in `src/ypms/models.py`. +- [ ] Any new endpoint has RBAC + audit + a test; PII reads emit `pii_read`. - [ ] Any new sensitive field routes through the app-layer encryption helpers. - [ ] `docs/nist-mapping.md` updated if the change wires a new NIST control. - [ ] POA&M item updated if the change closes or advances one. @@ -49,4 +60,4 @@ Hard rules — mirrored from OWC's `CONTRIBUTING.md` because the two systems sha - Squash-merge is the default. Merge commit messages should read like release notes for that PR. - Force-pushes to `main` are not allowed. Force-pushes to feature branches during review are fine. -- CI must be green. +- CI (Gitea Actions, `.gitea/workflows/ci.yml`) must be green. diff --git a/NIST_800_53_CONTROL_MAPPING.md b/NIST_800_53_CONTROL_MAPPING.md index e2dff8a..544549e 100644 --- a/NIST_800_53_CONTROL_MAPPING.md +++ b/NIST_800_53_CONTROL_MAPPING.md @@ -2,8 +2,8 @@ **System:** Youth Program Management System (YPMS) **System Owner:** District of Columbia Department of Employment Services (DOES) -**Document version:** 0.2 (draft — in-house build reframe) -**Date:** 2026-09-30 +**Document version:** 0.3 (draft — Phase 0 evidence populated) +**Date:** 2026-10-01 **Author:** DOES CIO **Companion to:** [`SYSTEM_SECURITY_PLAN.md`](SYSTEM_SECURITY_PLAN.md) @@ -85,6 +85,22 @@ The Moderate baseline contains ~261 controls including enhancements; the ~80 not Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materializes; refresh at every phase gate. +### Phase 0 status shift (2026-10-01) + +Rows touched by Phase 0 code. Each carries a dated **Phase 0** note with file:line evidence and the remaining gap; the same list with links is in [`docs/nist-mapping.md`](docs/nist-mapping.md). Rows whose status is 🤝 or ⚠️ keep it and gain a progress note. + +| Family | ✅ Implemented | 🟡 Partial | Note only (🤝 / ⚠️ kept) | +|---|---:|---:|---:| +| AC | 5 | 2 | — | +| AU | 4 | 1 | 1 | +| CM | — | 1 | 1 | +| IA | 1 | 3 | — | +| RA | — | 1 | — | +| SA | — | 1 | — | +| SC | — | 3 | 1 | +| SI | 2 | — | 1 | +| **Totals** | **12** | **12** | **4** | + --- ## 4. Detailed Control Mapping @@ -94,23 +110,23 @@ Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materia | Control | Status | Owner | RFP Ref | Implementation & Evidence | |---|---|---|---|---| | **AC-1 Policy and Procedures** | 📋 | D | — | DOES OCTO IT Security Policy applies (`C.5.7.53`). DOES ISSO owns YPMS access-control procedure document. **Evidence:** signed DOES Access Control Policy, RoB (`ypms/RULES_OF_BEHAVIOR.md`). | -| **AC-2 Account Management** | 📝 | O | `C.5.7.4`, `C.5.7.5`, `C.5.7.7`, `C.5.7.29`, `C.5.7.47` | Admin enable/disable; auto-disable on inactivity; 12–64 char alphanumeric IDs; group + individual assignment via Entra ID; no re-issue of deleted or previously-assigned IDs. **Evidence:** account-lifecycle event samples from Sentinel; documented account-provisioning workflow in `ypms/RULES_OF_BEHAVIOR.md`; quarterly access-recert records; DOES OIT personnel-access roster per source RFP `C.5.8.16`, `C.5.8.17` adopted as internal policy. | +| **AC-2 Account Management** | 🟡 | O | `C.5.7.4`, `C.5.7.5`, `C.5.7.7`, `C.5.7.29`, `C.5.7.47` | Admin enable/disable; auto-disable on inactivity; 12–64 char alphanumeric IDs; group + individual assignment via Entra ID; no re-issue of deleted or previously-assigned IDs. **Evidence:** account-lifecycle event samples from Sentinel; documented account-provisioning workflow in `ypms/RULES_OF_BEHAVIOR.md`; quarterly access-recert records; DOES OIT personnel-access roster per source RFP `C.5.8.16`, `C.5.8.17` adopted as internal policy. **Phase 0 (2026-10-01):** User model with active flag; inactive accounts rejected at authentication. Evidence: `src/ypms/users/models.py:48`, `src/ypms/auth/deps.py:43`. Gap: No admin enable/disable endpoint (admin module, Phase 8); inactivity auto-disable is YPMS-POAM-071. | | **AC-2(1) Automated Account Management** | 📝 | O | `C.5.7.4`, `C.5.7.31` | Automated inactivity-disable; real-time and scheduled access-right changes. **Evidence:** configuration screenshot of inactivity threshold; audit log showing an auto-disable event. | | **AC-2(3) Disable Inactive Accounts** | 📝 | O | `C.5.7.4` | Organization-defined inactivity threshold (`[TBD]` — recommend 90 days for staff, 180 days for youth accounts). **Evidence:** configured threshold value + audit sample. | | **AC-2(4) Automated Audit Actions** | 📝 | O | `C.5.7.36`, `C.5.7.39` | All account create/modify/enable/disable/remove actions auto-logged. **Evidence:** log samples. | -| **AC-3 Access Enforcement** | 📝 | O | `C.5.7.21`, `C.5.7.27`, `C.5.7.28`, `C.5.7.31`, `C.5.7.48` | Server-side authorization on every action; admin-only view/change of access rights; enforce authorizations per access-control policies. **Evidence:** access-control matrix showing every role × object; penetration-test evidence that unauthenticated and lower-role users cannot reach protected functions. | +| **AC-3 Access Enforcement** | ✅ | O | `C.5.7.21`, `C.5.7.27`, `C.5.7.28`, `C.5.7.31`, `C.5.7.48` | Server-side authorization on every action; admin-only view/change of access rights; enforce authorizations per access-control policies. **Evidence:** access-control matrix showing every role × object; penetration-test evidence that unauthenticated and lower-role users cannot reach protected functions. **Phase 0 (2026-10-01):** `require_permission` runs on every protected route, including all 208 stub routes; portal-only callers restricted to their own Registrant row. Evidence: `src/ypms/security/rbac.py:46`, `src/ypms/youth/router.py:67`, `tests/test_stubs.py`. Gap: Row-level scoping beyond Registrant is Phase 2+. | | **AC-4 Information Flow Enforcement** | 📝 | O | `C.5.7.23` | Trusted / transition / untrusted zone segmentation; untrusted systems cannot reach trusted-zone systems directly. **Evidence:** network architecture diagram (from the DOES OIT architecture document — `YPMS-POAM-040`); Azure NSG / firewall rule export. | -| **AC-6 Least Privilege** | 📝 | O | `C.5.7.32`, `C.5.7.33`, `C.5.7.34` | RBAC over defined objects; one login per staff, role-mediated access; admin maintains role-based security. CFSA foster/ward data and OSSE FERPA records reachable only by role-flagged users. **Evidence:** role catalogue with data-class access mapping. | -| **AC-6(1) Authorize Access to Security Functions** | 📝 | O | `C.5.7.28` | Only administrators can view/change access rights. **Evidence:** role catalogue. | +| **AC-6 Least Privilege** | ✅ | O | `C.5.7.32`, `C.5.7.33`, `C.5.7.34` | RBAC over defined objects; one login per staff, role-mediated access; admin maintains role-based security. CFSA foster/ward data and OSSE FERPA records reachable only by role-flagged users. **Evidence:** role catalogue with data-class access mapping. **Phase 0 (2026-10-01):** Data-driven RBAC (8 roles × 30 entities × 8 actions); FERPA/CFSA entities limited to ELIGIBILITY_EXAMINER; super-role explicit. Evidence: `src/ypms/seed/permissions_matrix.json`, `src/ypms/security/rbac.py:22`, `tests/test_stubs.py`. Gap: Four matrix review items open (PHASE0_STATUS.md). | +| **AC-6(1) Authorize Access to Security Functions** | ✅ | O | `C.5.7.28` | Only administrators can view/change access rights. **Evidence:** role catalogue. **Phase 0 (2026-10-01):** Only DOES_SYSTEM_ADMIN holds RBAC-table rights (User/Role not granted to any other role). Evidence: `src/ypms/seed/permissions_matrix.json`. | | **AC-6(2) Non-Privileged Access for Non-Security Functions** | 📝 | O | `C.5.7.33` | Staff use non-privileged accounts by default for routine work. **Evidence:** account-inventory review. | | **AC-6(5) Privileged Accounts** | 📝 | O | | Named, approved list of privileged users. **Evidence:** privileged-user roster + quarterly recert. | | **AC-6(9) Log Use of Privileged Functions** | 📝 | O | `C.5.7.36` | Auto-log all admin actions including account create/modify/enable/disable/remove. **Evidence:** log samples. | -| **AC-6(10) Prohibit Non-Privileged Users from Executing Privileged Functions** | 📝 | O | `C.5.7.27`, `C.5.7.32` | Enforced server-side. **Evidence:** pentest evidence. | -| **AC-7 Unsuccessful Logon Attempts** | 📝 | O | `C.5.7.10`, `C.5.7.11` | Lockout after 5 attempts; user notified on revocation. **Evidence:** configured threshold; audit sample of a lockout event and notification. | +| **AC-6(10) Prohibit Non-Privileged Users from Executing Privileged Functions** | ✅ | O | `C.5.7.27`, `C.5.7.32` | Enforced server-side. **Evidence:** pentest evidence. **Phase 0 (2026-10-01):** 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. Evidence: `src/ypms/security/rbac.py:29`, `tests/test_stubs.py`, `tests/youth/test_registrant_api.py`. | +| **AC-7 Unsuccessful Logon Attempts** | 🟡 | O | `C.5.7.10`, `C.5.7.11` | Lockout after 5 attempts; user notified on revocation. **Evidence:** configured threshold; audit sample of a lockout event and notification. **Phase 0 (2026-10-01):** 5 failed logins per source IP per 15 minutes → 429; every failure (bad credentials, unknown user, inactive, throttled) audited as `login_failed`. Evidence: `src/ypms/auth/rate_limit.py:47`, `src/ypms/auth/router.py:86`, `src/ypms/auth/router.py:65`. Gap: Per-account lockout (`locked_until`) and user notification not built; in-memory limiter is per-process. | | **AC-8 System Use Notification** | 📝 | O | `C.5.7.37` | Notification banner before granting access. **Evidence:** screenshot of banner; DOES-approved banner text on file. | | **AC-11 Session Lock** | 📝 | O | `C.5.7.3` | Idle-timeout session lock with re-authentication. **Evidence:** configured timeout value. | | **AC-11(1) Pattern-Hiding Displays** | 📝 | O | | Screen obscured on lock. **Evidence:** UX walkthrough. | -| **AC-12 Session Termination** | 📝 | O | `C.5.7.3` | Idle session termination with re-authentication. **Evidence:** configured timeout value; log sample of a session-terminated event. | +| **AC-12 Session Termination** | ✅ | O | `C.5.7.3` | Idle session termination with re-authentication. **Evidence:** configured timeout value; log sample of a session-terminated event. **Phase 0 (2026-10-01):** Access tokens expire (default 60 min, `exp` claim verified); refresh tokens cannot access resources. Evidence: `src/ypms/auth/security.py:59`, `src/ypms/auth/deps.py:35`. Gap: Idle-timeout UI behaviour lands with the portal (Phase 3). | | **AC-14 Permitted Actions Without Identification or Authentication** | 📝 | O | | Limited to public content (program info, job postings, application intake as unauthenticated entry). **Evidence:** enumeration of unauthenticated endpoints. | | **AC-17 Remote Access** | 🤝 | O, D | | All access is remote by architecture. MFA required for staff and admin via Entra ID conditional access (see IA-2). **Evidence:** Entra CA policy export; endpoint-compliance policy (source RFP `C.5.6.30`). | | **AC-17(1) Monitoring and Control** | 📝 | O | `C.5.6.9` #19 | SIEM monitoring of remote access. **Evidence:** SIEM feed. | @@ -135,18 +151,18 @@ Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materia | Control | Status | Owner | RFP Ref | Implementation & Evidence | |---|---|---|---|---| | **AU-1 Policy and Procedures** | 📋 | D | `C.5.7.53` | DOES OCTO logging policy applies. **Evidence:** policy. | -| **AU-2 Event Logging** | 📝 | O | `C.5.7.39`, `C.5.6.9` #4 | Comprehensive event set: root/admin actions, audit-log access/modification, successful and unsuccessful logon attempts, password changes, system-level object create/delete, all PII/sensitive-data interactions, role changes, shutdown/restart, file access authorized and unauthorized, user logon events, plus DOES-defined additions. **Evidence:** event-catalog document; SIEM sample stream. | -| **AU-3 Content of Audit Records** | 📝 | O | `C.5.7.15`, `C.5.7.48`, `C.5.7.49`, `C.5.6.9` #8, #11, #12, #13 | What/when/where/source/outcome/identity; sufficient to reconstruct events; unique transaction ID per transaction; before-and-after transaction history; communication send/access logging. **Evidence:** log-record specification; log sample. | +| **AU-2 Event Logging** | 🟡 | O | `C.5.7.39`, `C.5.6.9` #4 | Comprehensive event set: root/admin actions, audit-log access/modification, successful and unsuccessful logon attempts, password changes, system-level object create/delete, all PII/sensitive-data interactions, role changes, shutdown/restart, file access authorized and unauthorized, user logon events, plus DOES-defined additions. **Evidence:** event-catalog document; SIEM sample stream. **Phase 0 (2026-10-01):** Audited today: login success/failure, password change, every create/update/delete/restore, every PII read, every stub invocation, each retention run. Evidence: `src/ypms/audit/service.py:17`, `src/ypms/framework/crud.py:111`, `src/ypms/auth/router.py:65`, `src/ypms/auth/router.py:104`. Gap: Event catalogue items not yet applicable (role changes, shutdown, file access) arrive with their features. | +| **AU-3 Content of Audit Records** | ✅ | O | `C.5.7.15`, `C.5.7.48`, `C.5.7.49`, `C.5.6.9` #8, #11, #12, #13 | What/when/where/source/outcome/identity; sufficient to reconstruct events; unique transaction ID per transaction; before-and-after transaction history; communication send/access logging. **Evidence:** log-record specification; log sample. **Phase 0 (2026-10-01):** Each row: actor id + username, action, entity, entity id, before/after, timestamp, IP; app-layer ciphertext redacted from before/after. Evidence: `src/ypms/audit/models.py:18`, `src/ypms/framework/crud.py:46`. Gap: Request id is in logs, not yet on the audit row. | | **AU-3(1) Additional Audit Information** | 📝 | O | | Extended fields for financial-adjustment events (payroll approval, banking change, eligibility change). **Evidence:** log sample. | | **AU-4 Audit Log Storage Capacity** | 📝 | O | | Sized for `[TBD]` retention (see AU-11). **Evidence:** capacity plan. | | **AU-5 Response to Audit Logging Process Failures** | 📝 | O | `C.5.7.26`, `C.5.6.9` #23 | Alerts to admins and additional roles on security compromise; user-account lockout alerts; logging-pipeline health monitoring. **Evidence:** runbook + alert-configuration screenshot. | | **AU-6 Audit Record Review, Analysis, and Reporting** | 🤝 | O, D | `C.5.6.9` #19–22 | Sentinel aggregation into meaningful security intelligence; alerts on business-rule/workflow thresholds. DC OCTO SOC consumes the feed. **Evidence:** Sentinel correlation-rule inventory; SOC alert-triage runbook. | | **AU-7 Audit Record Reduction and Report Generation** | 📝 | O | `C.5.6.9` #20 | Configurable aggregation and reporting. **Evidence:** report templates. | -| **AU-8 Time Stamps** | 📝 | O | `C.5.7.40` | All critical system times synchronized. **Evidence:** NTP configuration; sample logs showing consistent UTC/ISO-8601 timestamps. | +| **AU-8 Time Stamps** | ✅ | O | `C.5.7.40` | All critical system times synchronized. **Evidence:** NTP configuration; sample logs showing consistent UTC/ISO-8601 timestamps. **Phase 0 (2026-10-01):** All timestamps timezone-aware UTC; Postgres container pinned to UTC. Evidence: `src/ypms/audit/models.py:24`, `docker-compose.yml:8`. Gap: NTP is inherited from Azure at deployment. | | **AU-9 Protection of Audit Information** | 📝 | O | `C.5.7.41`, `C.5.6.9` #6 | Audit information and audit tools protected from unauthorized access, modification, and deletion; role-based access to audit logs. Sentinel ingestion moves logs to a separate trust boundary. **Evidence:** ACL export on audit store; Sentinel workspace RBAC. | -| **AU-11 Audit Record Retention** | 🤝 | O, D | `C.5.7.42` | Organization-defined; **DOES recommendation: 1 year online, 3 years archived** (align to DOES records schedule — `[TBD]`). **Evidence:** written retention policy; archive location and format. | -| **AU-12 Audit Record Generation** | 📝 | O | `C.5.7.39`, `C.5.6.9` #17, #18 | Automated audit trails for all system components; system-generated security audit trail; SANS Information Logging Standard compliance. **Evidence:** component inventory × logging coverage table. | -| **AU-14 Session Audit** | 📝 | O | `C.5.6.9` #5, #7 | Searchable log of all screens accessed by user; sensitive-data screen access monitored and recorded. **Evidence:** search-tool demo; log sample. | +| **AU-11 Audit Record Retention** | 🤝 | O, D | `C.5.7.42` | Organization-defined; **DOES recommendation: 1 year online, 3 years archived** (align to DOES records schedule — `[TBD]`). **Evidence:** written retention policy; archive location and format. **Phase 0 (2026-10-01):** Retention scaffold: audit log carries the PIA §9 interim policy (archive at 1 year); report-only job. Evidence: `src/ypms/retention_classes.py`, `src/ypms/framework/retention.py:150`. Gap: Period unapproved until YPMS-POAM-054; archive tier is deployment work. | +| **AU-12 Audit Record Generation** | ✅ | O | `C.5.7.39`, `C.5.6.9` #17, #18 | Automated audit trails for all system components; system-generated security audit trail; SANS Information Logging Standard compliance. **Evidence:** component inventory × logging coverage table. **Phase 0 (2026-10-01):** Audit generation is in the framework (CRUDService, stub router, auth), not per-handler. Evidence: `src/ypms/framework/crud.py:111`, `src/ypms/framework/stub_router.py:66`. | +| **AU-14 Session Audit** | ✅ | O | `C.5.6.9` #5, #7 | Searchable log of all screens accessed by user; sensitive-data screen access monitored and recorded. **Evidence:** search-tool demo; log sample. **Phase 0 (2026-10-01):** `pii_read` audit on Registrant GET, list (ids recorded), and history. Evidence: `src/ypms/framework/pii_audit.py:36`, `src/ypms/youth/router.py:99`, `src/ypms/youth/router.py:115`. Gap: Extends to Guardian/TaxDocument/Attachment as they are built. | ### CA — Assessment, Authorization, and Monitoring @@ -170,13 +186,13 @@ Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materia | **CM-2 Baseline Configuration** | 📝 | O | `C.5.6.13`, IEEE 828-2012 | Application baseline in the repo (Dockerfile / compose / IaC); DOES-approved tool/software list. **Evidence:** baseline files in `ypms/`; CM plan (see CM-9). | | **CM-2(2) Automation Support for Accuracy and Currency** | 📝 | O | | IaC (Bicep or Terraform) maintains Azure baseline. **Evidence:** tool inventory; IaC repo path. | | **CM-2(3) Retention of Previous Configurations** | 📝 | O | | Rollback-capable baseline history via git. **Evidence:** git log. | -| **CM-3 Configuration Change Control** | 🤝 | O, D | `C.5.6.13`, `C.5.6.17` | DOES CIO approves changes; 30-day advance notice for maintenance windows; NOC coordination; emergency-change track. **Evidence:** change-record register; monthly change-review minutes. | +| **CM-3 Configuration Change Control** | 🤝 | O, D | `C.5.6.13`, `C.5.6.17` | DOES CIO approves changes; 30-day advance notice for maintenance windows; NOC coordination; emergency-change track. **Evidence:** change-record register; monthly change-review minutes. **Phase 0 (2026-10-01):** Merged migrations pinned by SHA-256; CI fails on any edit. Every change ships through a reviewed commit. Evidence: `migrations/IMMUTABLE.sha256`, `tests/test_migration_immutability.py:31`. Gap: Maintenance-window process is organizational. | | **CM-4 Impact Analysis** | 📝 | O | `C.5.8.12` | Required before database migrations affecting programs already in production. **Evidence:** impact-analysis template + sample; migration review discipline. | | **CM-5 Access Restrictions for Change** | 📝 | O | | Only DOES OIT delivery leads have merge rights on `main`; branch protection + code review required. **Evidence:** git branch-protection settings; Entra group ACL. | | **CM-6 Configuration Settings** | 📝 | O | `C.5.6.5`, `C.5.6.6`, `C.5.6.7` | Admin-configurable security controls; CA configuration; DB/space/performance configuration. DOES OIT establishes secure baseline (CIS Azure Foundations benchmark + DOES overlays). **Evidence:** baseline settings export; Azure Policy assignment. | | **CM-7 Least Functionality** | 📝 | O | | Unused features disabled. **Evidence:** enabled-feature inventory. | | **CM-7(1) Periodic Review** | 📝 | O | | Quarterly review of enabled features. **Evidence:** review record. | -| **CM-8 System Component Inventory** | 📝 | O | `C.5.6.13` | Comprehensive tool/software list with licensing and upgrade path. **Evidence:** SBOM (CycloneDX, per release, from CI) + tool inventory. | +| **CM-8 System Component Inventory** | 🟡 | O | `C.5.6.13` | Comprehensive tool/software list with licensing and upgrade path. **Evidence:** SBOM (CycloneDX, per release, from CI) + tool inventory. **Phase 0 (2026-10-01):** CycloneDX 1.6 SBOM of the runtime dependency set generated in CI. Evidence: `.gitea/workflows/ci.yml:70`. Gap: Pending first green CI run (Gitea runner); per-release retention of SBOMs not yet defined. | | **CM-8(1) Updates During Installation and Removal** | 📝 | O | | Inventory auto-updated via CI pipeline. **Evidence:** tooling. | | **CM-9 Configuration Management Plan** | 📝 | O | | `ypms/CONFIGURATION_MANAGEMENT_PLAN.md` (to be authored; also serves source RFP §C.5.12 Change Management Plan). **Evidence:** signed plan. | | **CM-10 Software Usage Restrictions** | 📋 | D | `C.5.6.13` | DOES-approved software list; license terms respected. **Evidence:** approved-software list. | @@ -205,15 +221,15 @@ Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materia | Control | Status | Owner | RFP Ref | Implementation & Evidence | |---|---|---|---|---| | **IA-1 Policy and Procedures** | 📋 | D | | DOES identity policy. **Evidence:** policy. | -| **IA-2 Identification and Authentication (Organizational Users)** | 📝 | O | `C.5.7.38`, `C.5.7.36` | Unique user ID per user; admin actions tied to individual identity. **Evidence:** IdP integration record. | +| **IA-2 Identification and Authentication (Organizational Users)** | ✅ | O | `C.5.7.38`, `C.5.7.36` | Unique user ID per user; admin actions tied to individual identity. **Evidence:** IdP integration record. **Phase 0 (2026-10-01):** Every non-public route requires a valid bearer token bound to a unique, active user. Evidence: `src/ypms/auth/deps.py:25`. Gap: MFA enhancements (IA-2(1)/(2)) arrive with Entra ID in Phase 1. | | **IA-2(1) MFA to Privileged Accounts** | ⚠️ | O | `C.5.7.6` | Source RFP `C.5.7.6` permits "unique identifier + shared secret answer" — **insufficient by NIST 800-63B AAL2**. **SSP tailors above the source RFP: MFA required for all privileged and staff accounts** via Entra ID conditional access. **Evidence:** Entra CA policy export; sample log showing MFA challenge (`YPMS-POAM-030`). | | **IA-2(2) MFA to Non-Privileged Accounts** | ⚠️ | O | | Same tailoring: MFA for all staff-role accounts. **Evidence:** as IA-2(1). | | **IA-2(8) Access to Accounts — Replay Resistant** | 📝 | O | | Modern MFA protocols. **Evidence:** protocol inventory. | | **IA-2(12) Acceptance of PIV Credentials** | 📋 | D | | DC OCTO enterprise: no PIV in scope; DOES employee credentials via DOES SSO. **Evidence:** IdP config. | | **IA-3 Device Identification and Authentication** | 📝 | O | | Device certificates for managed endpoints. **Evidence:** MDM cert deployment. | -| **IA-4 Identifier Management** | 📝 | O | `C.5.7.7`, `C.5.7.38`, `C.5.7.47` | 12-alphanumeric minimum; unique per user; no re-issue of deleted IDs. **Evidence:** IdP configuration; audit sample. | -| **IA-5 Authenticator Management** | 📝 | O | `C.5.7.8`, `C.5.7.12`, `C.5.7.13`, `C.5.7.14`, `C.5.7.45`, `C.5.7.46` | User-established and changeable passwords; admin reset with forced change; expiration; email/SMS notification on change; complexity/reuse/length policy; masked entry. **Evidence:** password-policy configuration; user-communication template. | -| **IA-5(1) Password-Based Authentication** | 📝 | O | `C.5.7.9`, `C.5.7.45` | FIPS 140-2 crypto (SSP tailors to FIPS 140-3 forward alignment); policy per RFP. **Evidence:** cryptographic-module cert; policy document. | +| **IA-4 Identifier Management** | 🟡 | O | `C.5.7.7`, `C.5.7.38`, `C.5.7.47` | 12-alphanumeric minimum; unique per user; no re-issue of deleted IDs. **Evidence:** IdP configuration; audit sample. **Phase 0 (2026-10-01):** Usernames unique (DB constraint); no user-delete path exists, so identifiers are never re-issued. Evidence: `migrations/versions/0002_framework_tables.py:54`. Gap: 12-character minimum identifier and explicit tombstoning arrive with registration (Phase 1). | +| **IA-5 Authenticator Management** | 🟡 | O | `C.5.7.8`, `C.5.7.12`, `C.5.7.13`, `C.5.7.14`, `C.5.7.45`, `C.5.7.46` | User-established and changeable passwords; admin reset with forced change; expiration; email/SMS notification on change; complexity/reuse/length policy; masked entry. **Evidence:** password-policy configuration; user-communication template. **Phase 0 (2026-10-01):** Users change their own password (12-character floor, current password required); hashes only. Evidence: `src/ypms/auth/router.py:138`, `src/ypms/auth/router.py:147`. Gap: Password-change notification (C.5.7.14) and admin reset arrive with notifications/admin. | +| **IA-5(1) Password-Based Authentication** | 🟡 | O | `C.5.7.9`, `C.5.7.45` | FIPS 140-2 crypto (SSP tailors to FIPS 140-3 forward alignment); policy per RFP. **Evidence:** cryptographic-module cert; policy document. **Phase 0 (2026-10-01):** PBKDF2-SHA256 (NIST SP 800-132) via passlib. Evidence: `src/ypms/auth/security.py:30`. Gap: FIPS 140-3 validated module is deployment-dependent (YPMS-POAM-032). | | **IA-6 Authenticator Feedback** | 📝 | O | `C.5.7.46` | Masked password entry. **Evidence:** UI walkthrough. | | **IA-7 Cryptographic Module Authentication** | 📝 | O | `C.5.7.9` | FIPS 140-2 validated modules (SSP tailors: FIPS 140-3 where available). **Evidence:** module cert; usage inventory. | | **IA-8 Identification and Authentication (Non-Organizational Users)** | ❓ | O | `C.5.3.2`, `C.5.7.30` | Youth and host authentication; LDAP-compliant; social-media login option. **Open decision (SSP §17.D):** identity model for minors — recommend social-media login for browsing/pre-application only, with IAL2-equivalent identity proofing (Login.gov or SSA verification) before banking/payroll actions. **Evidence:** identity-flow design; risk-acceptance memo. | @@ -349,7 +365,7 @@ Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materia | **RA-2 Security Categorization** | 📋 | D | | `ypms/FIPS_199_MEMO.md`. | | **RA-3 Risk Assessment** | 🤝 | O, D | | Initial RA before ATO; annually thereafter; on major change. **Evidence:** RA report. | | **RA-3(1) Supply Chain Risk Assessment** | 📝 | O | | Third-party and fourth-party disclosure in architecture document. **Evidence:** SBOM + third/fourth-party register. | -| **RA-5 Vulnerability Monitoring and Scanning** | 📝 | O | `C.5.7.52` | Cyber-threat detection and remediation; authenticated and unauthenticated scans monthly; container image scans in CI on push (`YPMS-POAM-062`). **Evidence:** scan schedule + reports. | +| **RA-5 Vulnerability Monitoring and Scanning** | 🟡 | O | `C.5.7.52` | Cyber-threat detection and remediation; authenticated and unauthenticated scans monthly; container image scans in CI on push (`YPMS-POAM-062`). **Evidence:** scan schedule + reports. **Phase 0 (2026-10-01):** pip-audit on runtime dependencies gates CI; an unfixable `ecdsa` advisory was removed by changing JWT library. Evidence: `.gitea/workflows/ci.yml:59`. Gap: Pending first green CI run; infrastructure scanning is deployment work. | | **RA-5(2) Update Vulnerabilities to be Scanned** | 📝 | O | | Feed updates. **Evidence:** feed configuration. | | **RA-5(5) Privileged Access** | 📝 | O | | Authenticated scans with privileged access. **Evidence:** scan configuration. | | **RA-7 Risk Response** | 📋 | D | | POA&M-driven. **Evidence:** POA&M. | @@ -368,7 +384,7 @@ Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materia | **SA-8 Security and Privacy Engineering Principles** | 📝 | O | | SSDLC adherence. **Evidence:** SSDLC document. | | **SA-9 External System Services** | 📝 | O | `C.5.6.3` | Payroll, LMS, address verification, social-media IdPs, DC MAR — each with security assurance. **Evidence:** vendor register + SIG-Lite / SOC 2 collection. | | **SA-10 Developer Configuration Management** | 📝 | O | `C.5.8.4`–`C.5.8.11` | Data-model deliverables (conceptual/logical/physical ERDs, data dictionary, integrity rules, CRUD matrix, denormalization rationale). **Evidence:** deliverables in `ypms/`. | -| **SA-11 Developer Testing and Evaluation** | 📝 | O | source RFP §C.5.10 | Unit / functional / non-functional / UAT with evidence. **Evidence:** test reports; CI test results. | +| **SA-11 Developer Testing and Evaluation** | 🟡 | O | source RFP §C.5.10 | Unit / functional / non-functional / UAT with evidence. **Evidence:** test reports; CI test results. **Phase 0 (2026-10-01):** 167 automated tests, 80% coverage floor, ruff, strict mypy, bandit SAST, migration-drift test. Evidence: `.gitea/workflows/ci.yml:53`, `.gitea/workflows/ci.yml:56`. Gap: Pending first green CI run. | | **SA-15 Development Process, Standards, and Tools** | 📝 | O | | DOES OIT SSDLC standards. **Evidence:** SSDLC document. | | **SA-22 Unsupported System Components** | 📝 | O | `C.5.6.13` | Third-party dependencies tracked in SBOM with upgrade path. **Evidence:** SBOM + upgrade-path statement. | @@ -384,11 +400,11 @@ Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materia | **SC-7(4) External Telecommunications Services** | 📝 | O | | Documented external services with ISAs. | | **SC-7(5) Deny by Default / Allow by Exception** | 📝 | O | | Default-deny firewall posture. **Evidence:** ruleset export. | | **SC-7(7) Split Tunneling for Remote Devices** | 📝 | O | | No split tunneling from managed devices. | -| **SC-8 Transmission Confidentiality and Integrity** | ⚠️ | O | `C.5.6.15`, `C.5.7.25`, `C.5.7.20` | Source RFP says SSL; **SSP tailors to TLS 1.2 minimum, TLS 1.3 preferred, FIPS-approved cipher suites**. All PII encrypted in transit. **Evidence:** SSL Labs–style report; TLS config (`YPMS-POAM-031`). | +| **SC-8 Transmission Confidentiality and Integrity** | ⚠️ | O | `C.5.6.15`, `C.5.7.25`, `C.5.7.20` | Source RFP says SSL; **SSP tailors to TLS 1.2 minimum, TLS 1.3 preferred, FIPS-approved cipher suites**. All PII encrypted in transit. **Evidence:** SSL Labs–style report; TLS config (`YPMS-POAM-031`). **Phase 0 (2026-10-01):** Not wired in Phase 0 (no local TLS proxy). Gap: TLS terminates at Front Door / App Gateway at deployment (YPMS-POAM-031). | | **SC-8(1) Cryptographic Protection** | 📝 | O | `C.5.7.9` | FIPS 140-2/-3 cryptography for transmission. **Evidence:** module cert. | | **SC-10 Network Disconnect** | 📝 | O | `C.5.7.3` | Idle-session network disconnect. **Evidence:** timeout config. | -| **SC-12 Cryptographic Key Establishment and Management** | 📝 | O | `C.5.7.9` | Key management via Azure Key Vault (Gov / HSM). **Evidence:** key-management design; rotation policy (`YPMS-POAM-032`). | -| **SC-13 Cryptographic Protection** | 📝 | O | `C.5.7.9`, `C.5.7.25` | FIPS 140-3 cryptography (140-2 acceptable during deprecation window) at rest and in transit. **Evidence:** module cert + usage map. | +| **SC-12 Cryptographic Key Establishment and Management** | 🟡 | O | `C.5.7.9` | Key management via Azure Key Vault (Gov / HSM). **Evidence:** key-management design; rotation policy (`YPMS-POAM-032`). **Phase 0 (2026-10-01):** Column key derived with HKDF-SHA256; ≥32-byte key material and ≥32-byte JWT secret enforced at startup. Evidence: `src/ypms/security/column_crypto.py:49`, `src/ypms/settings.py:15`. Gap: Keys come from env vars; Key Vault (Gov/HSM) in Phase 1. | +| **SC-13 Cryptographic Protection** | 🟡 | O | `C.5.7.9`, `C.5.7.25` | FIPS 140-3 cryptography (140-2 acceptable during deprecation window) at rest and in transit. **Evidence:** module cert + usage map. **Phase 0 (2026-10-01):** FIPS-approved algorithms only: AES-256-GCM, HKDF-SHA256, HMAC-SHA256 (JWT), PBKDF2-SHA256. Evidence: `src/ypms/security/column_crypto.py:55`. Gap: Validated-module status is deployment-dependent (YPMS-POAM-032). | | **SC-15 Collaborative Computing Devices** | 📝 | O | | Camera/mic access disabled by default in workspace tooling. | | **SC-17 Public Key Infrastructure Certificates** | 📝 | O | `C.5.6.5` | Certificate Authority configuration for integrations. **Evidence:** CA config. | | **SC-18 Mobile Code** | 📝 | O | | Only signed mobile code allowed. | @@ -396,7 +412,7 @@ Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materia | **SC-21 Secure Name/Address Resolution Service (Recursive)** | 🔵 | I | | Inherited. | | **SC-22 Architecture and Provisioning for Name/Address Resolution** | 🔵 | I | | Inherited. | | **SC-23 Session Authenticity** | 📝 | O | `C.5.7.20` | Anti-CSRF, secure cookies, per-transaction integrity for financial actions. **Evidence:** session-management design. | -| **SC-28 Protection of Information at Rest** | 📝 | O | `C.5.7.25`, `C.5.7.50` | Encryption at rest for all PII (Postgres TDE + envelope encryption for blobs); SSN masked/anonymized in display (`YPMS-POAM-080`). **Evidence:** DB encryption config; SSN-handling design. | +| **SC-28 Protection of Information at Rest** | 🟡 | O | `C.5.7.25`, `C.5.7.50` | Encryption at rest for all PII (Postgres TDE + envelope encryption for blobs); SSN masked/anonymized in display (`YPMS-POAM-080`). **Evidence:** DB encryption config; SSN-handling design. **Phase 0 (2026-10-01):** SSN encrypted at the app layer (AES-256-GCM, column-bound AAD); write-only on the model; masked in every response. Evidence: `src/ypms/security/column_crypto.py:55`, `src/ypms/youth/models.py:83`. Gap: Banking columns not yet built; Postgres TDE and blob encryption are deployment. | | **SC-28(1) Cryptographic Protection** | 📝 | O | `C.5.7.9` | FIPS-approved cryptography for at-rest data via Azure Key Vault. **Evidence:** module cert. | | **SC-39 Process Isolation** | 🔵 | I | | Inherited from IaaS + COTS platform. | @@ -416,9 +432,9 @@ Status distribution will shift from 📝 to ✅/🟡/❌ as the codebase materia | **SI-7 Software, Firmware, and Information Integrity** | 📝 | O | | Signed artifacts + integrity checks on deploy. | | **SI-7(1) Integrity Checks** | 📝 | O | | On startup and periodically. | | **SI-8 Spam Protection** | 🤝 | O, D | | Inbound-email spam protection at DOES enterprise level. | -| **SI-10 Information Input Validation** | 📝 | O | `C.5.7.44`, `C.5.6.2`, `C.5.6.3.b`, `C.5.6.3.d` | Character set / length / numerical range / format / content validation with trusted-source restriction; address, age, residency data checks; SSA SSN verification; DC MAR address validation. **Evidence:** input-validation rules; sample validation-failure log. | -| **SI-11 Error Handling** | 📝 | O | `C.5.7.43`, `C.5.6.9` #2, #3 | Corrective information without adversary-exploitable disclosure; consistent error handling; all errors logged. **Evidence:** error-response inventory. | -| **SI-12 Information Management and Retention** | 🤝 | O, D | | Aligned with DOES records schedule. **Evidence:** retention policy. | +| **SI-10 Information Input Validation** | ✅ | O | `C.5.7.44`, `C.5.6.2`, `C.5.6.3.b`, `C.5.6.3.d` | Character set / length / numerical range / format / content validation with trusted-source restriction; address, age, residency data checks; SSA SSN verification; DC MAR address validation. **Evidence:** input-validation rules; sample validation-failure log. **Phase 0 (2026-10-01):** Pydantic validation on every request body; SSN format and never-issued ranges, DOB, ZIP, email, lengths. Evidence: `src/ypms/youth/schemas.py:21`. | +| **SI-11 Error Handling** | ✅ | O | `C.5.7.43`, `C.5.6.9` #2, #3 | Corrective information without adversary-exploitable disclosure; consistent error handling; all errors logged. **Evidence:** error-response inventory. **Phase 0 (2026-10-01):** Unhandled errors return a generic body with request id; logs record type and frames, not messages; SQL parameters hidden. Evidence: `src/ypms/main.py:56`, `src/ypms/db.py:24`, `tests/test_app_guards.py`. | +| **SI-12 Information Management and Retention** | 🤝 | O, D | | Aligned with DOES records schedule. **Evidence:** retention policy. **Phase 0 (2026-10-01):** Every table carries a retention policy; app refuses to boot otherwise; nightly report-only job. Evidence: `src/ypms/framework/retention.py:135`, `src/ypms/main.py:34`. Gap: Disposal execution is Phase 5; periods await YPMS-POAM-054. | | **SI-16 Memory Protection** | 🔵 | I | | Inherited from IaaS / OS. | ### SR — Supply Chain Risk Management diff --git a/PHASE0_PLAN.md b/PHASE0_PLAN.md index d53a3ca..8be7071 100644 --- a/PHASE0_PLAN.md +++ b/PHASE0_PLAN.md @@ -79,7 +79,7 @@ Every Phase 0 component maps to a source-RFP requirement or a NIST control. Tabl | **0.6** | 501-stub routers for remaining entities | ✅ **Complete 2026-09-30.** 26 stub entities registered in `src/ypms/stubs.py`, each with a router from `framework/stub_router.py` exposing one route per matrix action (8). Every route runs the real RBAC check and writes an audit row before returning 501. `tests/test_stubs.py` sweeps entity × role × route against the matrix JSON (501 + one audit row when granted; 403 + none when not). Registrant restore moved to the matrix `Restore` action. 138 tests, 95.05% coverage. | | **0.7** | Retention scaffold | ✅ **Complete 2026-10-01.** Every mapped table resolves a `RetentionPolicy` (entity models declare `retention_policy`; vendored framework tables mapped by name). PIA §9 interim periods encoded in `src/ypms/retention_classes.py`, all unapproved pending `YPMS-POAM-054`; classes without a PIA basis report as `unset`. Report-only job in `src/ypms/framework/retention.py` (`python -m ypms.framework.retention`) logs "0 records to purge" and writes a `retention_run` audit row. Stub entity models + migration `0004_stub_entity_tables`. Migration-vs-model drift test added. 156 tests, 95.91% coverage. | | **0.8** | CI pipeline | 🟡 **Pipeline complete 2026-10-01; green-on-`main` pending a Gitea runner.** `.gitea/workflows/ci.yml`: ruff, mypy, pytest (80% floor, includes migration-drift test), bandit, pip-audit (runtime deps), gitleaks (pinned + sha256-verified, full history), CycloneDX SBOM artifact. Every step passes locally. Running the gates fixed a broken pip-audit invocation, removed `ecdsa` by swapping python-jose → PyJWT, and enforced a 32-byte `JWT_SECRET`. Closes when the first run on `main` is green. | -| **0.9** | Docs + acceptance | `README.md` with runnable quickstart; `CONTRIBUTING.md` with hard rules; `docs/nist-mapping.md` skeleton; this file marked complete. | +| **0.9** | Docs + acceptance | ✅ **Complete 2026-10-01.** README rewritten (Docker + native quickstart, try-it flow, gates, add-an-entity steps); CONTRIBUTING gains rules 9–13; `docs/nist-mapping.md` with file:line evidence; control mapping v0.3 (12 ✅, 12 🟡, 4 progress notes). Closing §6 gaps added: failed-login audit (AU-2), opaque 500 handler + hidden SQL params (SI-11), startup retention check (§7 risk), migration hash pinning (CM-3). Definition of Done in §8: 12 of 14 checked; remaining two need Docker and a Gitea runner. 167 tests. | Each milestone independently mergeable. @@ -255,20 +255,20 @@ Everything else in the control mapping stays 📝 until later phases wire it. Phase 0 is complete when: -- [ ] `docker compose up` boots app + Postgres cleanly; `/healthz` returns 200. -- [ ] `alembic upgrade head` applies the baseline migration. -- [ ] `POST /auth/login` issues a JWT with `iam.assurance_level` claim. -- [ ] `POST /registrants` creates a Registrant with masked SSN in the response, emits an audit row, requires `OYP_STAFF` or higher. -- [ ] `GET /registrants/{id}` reads the record, fires the PII-read audit decorator, masks SSN, requires appropriate role. -- [ ] `PATCH /registrants/{id}` updates fields, emits before/after in audit row, requires appropriate role. -- [ ] `DELETE /registrants/{id}` soft-deletes, emits audit, requires appropriate role. -- [ ] Every stub entity's endpoints return 501 but still emit an audit row and enforce RBAC. -- [ ] Tests green in CI: lint, types, pytest, SAST, SCA, secret scan, SBOM. -- [ ] Coverage ≥ 80% on `framework/` and `security/`; every stub has one test. -- [ ] `README.md` quickstart works on a clean checkout. -- [ ] `CONTRIBUTING.md` published with hard rules mirrored from OWC. -- [ ] `docs/nist-mapping.md` lists the controls wired above, with file:line evidence. -- [ ] Control mapping `NIST_800_53_CONTROL_MAPPING.md` updated: the Phase-0-wired controls flip from 📝 to ✅ (or 🟡 for partials); file:line evidence populated. +- [ ] `docker compose up` boots app + Postgres cleanly; `/healthz` returns 200. — **Open:** no Docker on the verifying workstation. Native boot (uvicorn + SQLite) verified; `/healthz` and `/readyz` 200. +- [x] `alembic upgrade head` applies the baseline migration. — All four revisions, SQLite; also exercised by the drift test on every CI run. Postgres run rides with the Docker item. +- [x] `POST /auth/login` issues a JWT with `iam.assurance_level` claim. (verified 2026-10-01: clean clone, live uvicorn, HTTP) +- [x] `POST /registrants` creates a Registrant with masked SSN in the response, emits an audit row, requires `OYP_STAFF` or higher. (verified 2026-10-01: clean clone, live uvicorn, HTTP) +- [x] `GET /registrants/{id}` reads the record, fires the PII-read audit decorator, masks SSN, requires appropriate role. (verified 2026-10-01: clean clone, live uvicorn, HTTP) +- [x] `PATCH /registrants/{id}` updates fields, emits before/after in audit row, requires appropriate role. (verified 2026-10-01: clean clone, live uvicorn, HTTP) +- [x] `DELETE /registrants/{id}` soft-deletes, emits audit, requires appropriate role. (verified 2026-10-01: clean clone, live uvicorn, HTTP) Only the super-role holds `Delete` on Registrant today (matrix review item). +- [x] Every stub entity's endpoints return 501 but still emit an audit row and enforce RBAC. — `tests/test_stubs.py` sweeps 26 entities × 8 roles × 8 routes; also seen live. +- [ ] Tests green in CI: lint, types, pytest, SAST, SCA, secret scan, SBOM. — **Open:** every gate passes locally; Gitea runs queue until an `act_runner` is registered. +- [x] Coverage ≥ 80% on `framework/` and `security/`; every stub has one test. — ~96% overall; `framework/router.py` still excluded (first real caller arrives with a domain entity). +- [x] `README.md` quickstart works on a clean checkout. — Native path verified from a fresh clone; Docker path rides with the Docker item. +- [x] `CONTRIBUTING.md` published with hard rules mirrored from OWC (plus YPMS rules 9–13). +- [x] `docs/nist-mapping.md` lists the controls wired above, with file:line evidence. +- [x] Control mapping `NIST_800_53_CONTROL_MAPPING.md` updated: the Phase-0-wired controls flip from 📝 to ✅ (or 🟡 for partials); file:line evidence populated. — v0.3: 12 ✅, 12 🟡; SC-8 stays ⚠️ (TLS is deployment-phase). --- diff --git a/PHASE0_STATUS.md b/PHASE0_STATUS.md index 9e2dd86..8091376 100644 --- a/PHASE0_STATUS.md +++ b/PHASE0_STATUS.md @@ -1,7 +1,7 @@ # Phase 0 — Handoff status **Last updated:** 2026-10-01 -**Phase:** 0 — Foundations (in flight) +**Phase:** 0 — Foundations (built; exit pending two infra checks) **Owner:** Chris Tonjes, DOES CIO (System Owner / ISSO / AO) **Companion docs:** [`PHASE0_PLAN.md`](PHASE0_PLAN.md) (scope + milestones), [`ARCHITECTURE.md`](ARCHITECTURE.md) (design), [`README.md`](README.md) (runnable quickstart) @@ -9,7 +9,7 @@ ## TL;DR -Phase 0 milestones **0.1 → 0.7** are shipped; **0.8 (CI)** is complete except for its green-on-`main` exit check, which needs a Gitea Actions runner registered on `gitea.doesworks.net`. **0.9 (docs + acceptance)** is next. All ATO artifacts (SSP, control mapping, POA&M, PIA) are drafted at v0.1–v0.3 and awaiting review signatures. The repo builds, tests are green (**158 passing, 95.83% coverage**), and Docker Compose boots. `main` is pushed to Gitea. +All nine Phase 0 milestones are built. **Phase 0 exits on two infrastructure checks**, neither of which can run on the dev workstation: (1) the first green Gitea Actions run on `main` — needs an `act_runner` registered on `gitea.doesworks.net`; (2) `docker compose up` against real Postgres. Definition of Done (PHASE0_PLAN §8) is 12 of 14 checked; everything else was verified end to end over HTTP from a clean clone. Tests green (**167 passing, ~96% coverage**); every CI gate passes locally. Control mapping is at v0.3 with file:line evidence (12 ✅ / 12 🟡). `main` is pushed to Gitea. --- @@ -17,15 +17,15 @@ Phase 0 milestones **0.1 → 0.7** are shipped; **0.8 (CI)** is complete except | Dimension | Status | |---|---| -| Phase | 0 (Foundations), milestones 0.1–0.7 shipped; 0.8 pending runner | +| Phase | 0 (Foundations), milestones 0.1–0.9 built; exit pending CI runner + Docker/Postgres boot | | Git branch | `main`, tracking `origin/main` (Gitea `https://gitea.doesworks.net/chris.tonjes/YPMS.git`) | -| Tests | **158 passing**, **95.83% coverage** (floor 80% enforced in CI) | +| Tests | **167 passing**, **~96% coverage** (floor 80% enforced in CI) | | CI | `.gitea/workflows/ci.yml` — all gates pass locally; runs queue as "Waiting" until a runner is registered | | Lint / types | `ruff` clean, `mypy` clean (43 files, strict) | -| Runtime | FastAPI + Postgres on Docker Compose; boots to `/healthz` + `/readyz`; `/auth/*` and `/registrants/*` live; 26 stub entities return 501 behind RBAC + audit | +| Runtime | Verified booting natively (uvicorn + SQLite) from a clean clone; Docker Compose + Postgres not yet run; `/auth/*` and `/registrants/*` live; 26 stub entities return 501 behind RBAC + audit | | Migrations | 4 revisions (`0001`–`0004_stub_entity_tables`); all apply cleanly to SQLite in the drift test; live Postgres run pending (no Docker on the dev workstation) | | Seed | 8 roles + permissions matrix + bootstrap admin (`chris` / `DOES_SYSTEM_ADMIN`) | -| ATO docs | SSP v0.3, control mapping v0.2, POA&M v0.1, PIA v0.1 — all drafts, awaiting signatures | +| ATO docs | SSP v0.3, control mapping **v0.3 (Phase 0 evidence)**, POA&M v0.1, PIA v0.1 — all drafts, awaiting signatures | --- @@ -115,15 +115,29 @@ Guardrails baked in: `OYP_STAFF` can't see FERPA/CFSA (that's `ELIGIBILITY_EXAMI - Every step passes locally. Gitea has queued runs #1–#5 (and this push) as **Waiting** — no runner is online. - **To close 0.8:** register an `act_runner` against `gitea.doesworks.net` with an `ubuntu-latest` label mapped to an act-compatible image (e.g. `catthehacker/ubuntu:act-22.04`), outbound to github.com + PyPI. Then confirm the newest run on `main` is green. +### 0.9 — Docs + acceptance ✅ + +- **README** rewritten: Docker and native (no-Docker) quickstarts, a try-it curl flow, all gate commands, and step-by-step "promote a stub to a real entity". +- **CONTRIBUTING** rules 9–13: PII reads emit `pii_read`; encrypted columns go in `__audit_redact__`; every table has a retention policy; model modules registered in `models.py`; vendored-code changes need a reason + upstream list entry. PR checklist and suppression rule updated. +- **`docs/nist-mapping.md`** — per-control code evidence with linked file:line and the remaining gap. **Control mapping v0.3** carries the same notes per row plus a status-shift table: 12 ✅, 12 🟡; AU-11, CM-3, SI-12 keep 🤝 and SC-8 keeps ⚠️, each with a progress note. +- **Closed gaps found while preparing evidence** (each promised by PHASE0_PLAN §6/§7 but not yet built): + - **Failed logins weren't audited** (AU-2, C.5.7.39). Now `login_failed` with reason `bad_credentials` / `unknown_user` / `inactive` / `throttled`. The typed username is stored only when it matches a real account. + - **SI-11:** app-wide handler returns `{"error":"internal_error","request_id":…}`; logs type + stack frames, never the message (driver errors echo PII); engine `hide_parameters=True`. + - **Startup retention check:** `create_app` refuses to boot if any table lacks a policy. + - **CM-3:** `migrations/IMMUTABLE.sha256` pins merged revisions; test fails on edits. + - Alembic `fileConfig(disable_existing_loggers=False)` — in-process migrations were silencing app loggers in tests. +- **End-to-end check** from a clean clone: migrate → seed → uvicorn → login, Registrant create/read/update/delete, stub 501, audit trail, ciphertext-only at rest, no SSN in the server log. + --- ## What's next -### Immediate next milestone: 0.9 — Docs + acceptance +### Phase 0 exit — two infrastructure checks -`README.md` with runnable quickstart (expand the existing one), `CONTRIBUTING.md` hard rules (add the new ones: every table needs a retention policy; new model modules go in `models.py`; suppressions are per-finding), `docs/nist-mapping.md` skeleton, and flip `NIST_800_53_CONTROL_MAPPING.md` rows for the Phase-0-wired controls from 📝 to ✅/🟡 with file evidence (PHASE0_PLAN §7 acceptance). Phase 0 is done when 0.8's runner is green and 0.9's checklist is signed. +1. **CI green on `main`.** Register an `act_runner` on `gitea.doesworks.net` (label `ubuntu-latest` → an act-compatible image such as `catthehacker/ubuntu:act-22.04`; outbound to github.com and PyPI). The queued runs will start; confirm the newest on `main` passes. Closes 0.8 and the CI item in PLAN §8. +2. **Docker + Postgres boot.** On a Docker-capable machine: `cp .env.example .env && docker compose up --build`, then the README "Try it" flow. Closes the last PLAN §8 item and gives the first live Postgres run of migrations 0001–0004. If Postgres disagrees with SQLite anywhere, fix in a new migration. -Then Phase 1 (applications + identity + parental consent) starts. +Then: sign off Phase 0, and draft `PHASE1_PLAN.md` (applications + identity + parental consent — ARCH §12). Decisions that should be made before Phase 1 starts: the four permission-matrix review items below, the 0002 constraint follow-up, and `YPMS-POAM-050` (youth identity model), which Phase 1 is built around. ### Upstream to OWC (accumulated) @@ -336,9 +350,9 @@ Then the ATO artifacts as needed: --- -## POA&M state (as of 2026-09-30) +## POA&M state (as of 2026-10-01) -- **62 items total** — 2 fully closed, 4 partial closures under `YPMS-POAM-040` +- **62 items total** — 2 fully closed; 9 Phase 0 milestone partial closures logged under `YPMS-POAM-040`; `YPMS-POAM-042` (CI/SBOM) in progress pending the runner - **6 open decisions** (see Open Questions table above) - **Immediate remaining action items:** - PIA route to Privacy Officer + OGC review @@ -348,14 +362,13 @@ Then the ATO artifacts as needed: --- -## Signals to watch for as we approach milestone 0.9 +## Signals to watch for at Phase 0 exit / Phase 1 start -- Control-mapping status flips need file:line evidence, not "implemented in Phase 0". Auditors trace the path. -- Don't weaken a gate to get green (no blanket bandit skips, no `--cov-fail-under` drop). Suppressions are per-finding with a reason — e.g. the inline `gitleaks:allow` on the test JWT secret in `tests/conftest.py`. - If the first real CI run fails on something local runs didn't catch (runner image, network), fix the workflow, not the gate. +- Phase 1 promotes Guardian and ConsentRecord from stubs: follow README "Adding an entity", and give both the Registrant treatment (`pii_read`, `__audit_redact__` for anything encrypted, coverage doc). +- PARENT_USER access to a child's Registrant hangs on the Guardian link — replace the Phase 0 "parents see nothing" rule in `youth/router.py`, don't layer around it. +- Keep `docs/nist-mapping.md` and the control-mapping rows in step; line numbers drift, so refresh them at each phase gate. - Retention execution (actual disposal) is Phase 5 work, alongside the OSSE/CFSA ingest pipelines. Don't add it in passing. -- Any entity that will hold PII in Phase 1+ (Guardian, TaxDocument, Attachment) should get the Registrant treatment then — `pii_read` on reads, `__audit_redact__` on encrypted columns. - --- -**End of handoff v0.5.** +**End of handoff v0.6.** diff --git a/POAM.md b/POAM.md index 7d79ef4..eb88f81 100644 --- a/POAM.md +++ b/POAM.md @@ -180,6 +180,7 @@ Closures retained for traceability. | — | Phase 0 milestone 0.6 shipped (partial closure of YPMS-POAM-040) | 2026-09-30 | 501-stub routers for 26 entities; every route enforces RBAC and writes an audit row before returning 501 (source RFP C.5.7.27; NIST AC-3, AU-12). Matrix-driven test sweep covers every entity × role × action. Permission-matrix review items recorded in PHASE0_STATUS.md. 138 tests passing, 95.05% coverage. | | — | Phase 0 milestone 0.7 shipped (partial closure of YPMS-POAM-040) | 2026-10-01 | Retention scaffold (NIST AU-11, SI-12; source RFP C.5.7.42): every table carries a policy; PIA §9 interim periods encoded but unapproved; report-only nightly job logs eligible counts and audits each run. Implementation is ready to consume the signed schedule from YPMS-POAM-054 (flip `approved`, cite authority). 156 tests passing, 95.91% coverage. | | — | Phase 0 milestone 0.8 pipeline complete (partial closure of YPMS-POAM-040; advances YPMS-POAM-042) | 2026-10-01 | Gitea Actions workflow with lint, strict types, tests + coverage floor, migration drift test, SAST, SCA on runtime deps, full-history secret scan, CycloneDX SBOM. All gates pass locally; green-on-main pending runner registration. Removed `ecdsa` (PYSEC-2026-1325) by moving JWT to PyJWT; enforced 32-byte JWT secret (RFC 7518). 158 tests passing, 95.83% coverage. | +| — | Phase 0 milestone 0.9 shipped (partial closure of YPMS-POAM-040) | 2026-10-01 | Docs + acceptance: README, CONTRIBUTING rules 9–13, `docs/nist-mapping.md`, control mapping v0.3 with file:line evidence (12 ✅ / 12 🟡). Closed Phase 0 control gaps: failed-login audit (AU-2, C.5.7.39), opaque error handler + hidden SQL parameters (SI-11, C.5.7.43), startup retention-policy check, migration hash pinning (CM-3). End-to-end HTTP verification from a clean clone. Phase 0 exit remaining: Docker/Postgres boot check and first green Gitea CI run. 167 tests passing. | --- diff --git a/README.md b/README.md index 272e471..23ad7b4 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ DC Department of Employment Services / Office of Youth Programs — in-house build replacing seven legacy tools (MBSYEP intake, TMS timekeeping, PF3 admin, manual payroll scripts, MicroStrategy, and two supporting apps) with a single Python / FastAPI / Postgres system on Azure Government. -**Status:** Pre-development. Phase 0 (foundations) in flight. +**Status:** Phase 0 (foundations) — framework, auth, RBAC, audit, Registrant vertical slice, 501 stubs for every other entity, retention scaffold, CI. See [`PHASE0_STATUS.md`](PHASE0_STATUS.md). **System owner + ISSO + AO:** Chris Tonjes, DOES CIO. **Full context:** [`OVERVIEW.md`](OVERVIEW.md), [`ARCHITECTURE.md`](ARCHITECTURE.md), [`PHASE0_PLAN.md`](PHASE0_PLAN.md). -## Quickstart (local) +## Quickstart — Docker (canonical) Requires Docker Desktop (or equivalent). Ports 8000 (app), 5433 (Postgres), 8081 (Adminer) must be free. @@ -15,68 +15,107 @@ cp .env.example .env docker compose up --build ``` -App at http://localhost:8000. OpenAPI docs at http://localhost:8000/docs. +Compose runs `alembic upgrade head`, seeds roles + permissions + the bootstrap admin (`chris` / `change-me-immediately`, or `SEED_ADMIN_PASSWORD`), then starts the app. + +- App: http://localhost:8000 — OpenAPI at http://localhost:8000/docs +- Adminer: http://localhost:8081 — System PostgreSQL, server `db`, user/password/db `ypms` +- Stop with `Ctrl+C`; wipe the local DB with `docker compose down -v`. + +## Quickstart — native, no Docker + +Uses SQLite in place of Postgres. Fine for poking at the API; Postgres-specific behaviour needs the Docker path. ```bash -curl http://localhost:8000/healthz # → {"status":"ok"} -curl http://localhost:8000/readyz # → {"status":"ok"} once db is up -``` - -Adminer (DB explorer) at http://localhost:8081. Connect with: -- System: PostgreSQL -- Server: `db` -- Username / Password / Database: `ypms` / `ypms` / `ypms` - -Stop with `Ctrl+C`; wipe local DB with `docker compose down -v`. - -## Tests - -Local (no Docker required for the tests): - -```bash -python -m venv .venv && source .venv/bin/activate +python3.12 -m venv .venv && source .venv/bin/activate pip install -e '.[dev]' -pytest + +export DATABASE_URL=sqlite:///./ypms-dev.db +export JWT_SECRET=dev-jwt-secret-change-me-32-bytes-min # >= 32 bytes or the app won't start +export COLUMN_ENCRYPTION_KEY=dev-col-enc-key-change-me-32bytes-______ +export SEED_ADMIN_PASSWORD=pick-a-local-password + +alembic upgrade head +python -m ypms.seed.run +uvicorn ypms.main:app --reload ``` -Coverage: +## Try it ```bash -pytest --cov=src/ypms --cov-report=term-missing +curl http://localhost:8000/healthz # {"status":"ok"} +curl http://localhost:8000/readyz # {"status":"ok"} once the DB is reachable + +TOKEN=$(curl -s -X POST http://localhost:8000/auth/login \ + -d 'username=chris&password=change-me-immediately' | python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])') + +curl -s -X POST http://localhost:8000/registrants \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"first_name":"Test","last_name":"Youth","dob":"2010-05-01","ssn":"123-45-6789"}' +# → ssn_masked "XXX-XX-6789"; the SSN itself is never returned + +curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/guardians -H "Authorization: Bearer $TOKEN" +# → 501 (stub: RBAC checked and audited, no logic yet) ``` -Tests use SQLite in-memory for speed. Postgres-specific behaviour gets its own Postgres-only fixture when a migration or query needs it (see [`ARCHITECTURE.md`](ARCHITECTURE.md) §4.8). +Change the bootstrap password right away: `POST /auth/change-password`. + +The retention job is report-only in Phase 0: + +```bash +python -m ypms.framework.retention # logs "0 records to purge" and writes an audit row +``` + +## Tests and gates + +```bash +pytest # SQLite in-memory, ~10s +pytest --cov=src/ypms --cov-report=term-missing --cov-fail-under=80 +ruff check src tests +mypy src +bandit -q -r src -c pyproject.toml +pip-audit --strict . # runtime dependencies +``` + +CI runs all of these plus gitleaks and a CycloneDX SBOM on Gitea Actions — [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml). ## Repo shape Full tree in [`ARCHITECTURE.md`](ARCHITECTURE.md) §5.1. High points: -- `src/ypms/main.py` — FastAPI factory -- `src/ypms/settings.py` — pydantic-settings config -- `src/ypms/db.py` — SQLAlchemy engine, session, `Base` -- `src/ypms/logging.py` — structlog + sensitive-field sanitizer -- `src/ypms/middleware.py` — request id -- `src/ypms/framework/` — vendored from OWC, milestone 0.2 -- `src/ypms/security/` — auth + RBAC + column encryption, milestones 0.3–0.4 -- `src/ypms/youth/` — Registrant entity (vertical slice), milestone 0.5 -- Other entity packages — 501 stubs, milestone 0.6 -- `migrations/` — Alembic, immutable-once-merged -- `tests/` — pytest, SQLite by default +- `src/ypms/main.py` — app factory; refuses to start if any table lacks a retention policy; opaque 500 handler +- `src/ypms/settings.py` — pydantic-settings config (secret-length floors enforced) +- `src/ypms/framework/` — vendored from OWC, plus YPMS additions: `pii_audit.py`, `stub_router.py`, `retention.py` +- `src/ypms/security/` — RBAC, `column_crypto.py` (AES-256-GCM app-layer encryption) +- `src/ypms/auth/` — login, JWT (PyJWT), refresh, change-password, login rate limit +- `src/ypms/youth/` — Registrant, the real vertical-slice entity +- `src/ypms/stubs.py`, `src/ypms/stub_models.py` — the 26 entities still at 501 +- `src/ypms/retention_classes.py` — PIA §9 retention policy catalog +- `src/ypms/models.py` — the one list of model modules +- `src/ypms/seed/` — roles, permissions matrix, bootstrap admin +- `migrations/` — Alembic; merged revisions pinned in `IMMUTABLE.sha256` +- `docs/coverage/` — per-entity requirement traces; `docs/nist-mapping.md` — control evidence index -## Adding an entity +## Adding an entity (promoting a stub to real) -Once the framework lands in milestone 0.2, adding a new entity follows the OWC add-entity pattern (model → schemas → router → main.py registration → migration → permissions seed entry → test → coverage doc). Full instructions land here at milestone 0.2 completion. +1. Move the class from `src/ypms/stub_models.py` into `src/ypms//models.py`; keep `__tablename__` and `retention_policy`. Add the module to `src/ypms/models.py`. +2. Add columns in a **new** migration; append its hash to `migrations/IMMUTABLE.sha256`. +3. Encrypted columns: write through `security.column_crypto`, list them in `__audit_redact__`. +4. Schemas in `/schemas.py`; the read schema masks anything sensitive. +5. Router: `framework.router.make_router` for plain CRUD, or a hand-built router on `CRUDService` (see `youth/router.py`) when you need PII-read audit or row scoping. Register it in `main.py` and remove the entity from `src/ypms/stubs.py`. +6. Check the permissions matrix rows for the entity — they're policy; review them as such. +7. Tests, then a coverage doc at `docs/coverage/.md` (see `registrant.md`). ## Contributing -See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the hard rules. Highlights: migrations are immutable once merged, every state-mutating route emits an audit row, framework/security modules must not carry domain logic, no `--no-verify` or `# type: ignore` without a written reason. +See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the hard rules. ## Compliance -- [`SYSTEM_SECURITY_PLAN.md`](SYSTEM_SECURITY_PLAN.md) v0.3 — NIST 800-53 Moderate SSP -- [`NIST_800_53_CONTROL_MAPPING.md`](NIST_800_53_CONTROL_MAPPING.md) v0.2 — full control-family map -- [`PRIVACY_IMPACT_ASSESSMENT.md`](PRIVACY_IMPACT_ASSESSMENT.md) v0.1 — PIA covering minors, SSN, banking, FERPA, CFSA -- [`POAM.md`](POAM.md) v0.1 — POA&M with 62 items and 2 closures as of 2026-09-30 +- [`SYSTEM_SECURITY_PLAN.md`](SYSTEM_SECURITY_PLAN.md) — NIST 800-53 Moderate SSP +- [`NIST_800_53_CONTROL_MAPPING.md`](NIST_800_53_CONTROL_MAPPING.md) v0.3 — full control map with Phase 0 evidence +- [`docs/nist-mapping.md`](docs/nist-mapping.md) — Phase 0 control evidence index (file:line) +- [`PRIVACY_IMPACT_ASSESSMENT.md`](PRIVACY_IMPACT_ASSESSMENT.md) — PIA covering minors, SSN, banking, FERPA, CFSA +- [`POAM.md`](POAM.md) — plan of action and milestones ## License / usage diff --git a/docs/nist-mapping.md b/docs/nist-mapping.md new file mode 100644 index 0000000..8f80808 --- /dev/null +++ b/docs/nist-mapping.md @@ -0,0 +1,50 @@ +# 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)
[`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)
[`src/ypms/youth/router.py:67`](../src/ypms/youth/router.py#L67)
[`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)
[`src/ypms/security/rbac.py:22`](../src/ypms/security/rbac.py#L22)
[`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)
[`tests/test_stubs.py`](../tests/test_stubs.py)
[`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)
[`src/ypms/auth/router.py:86`](../src/ypms/auth/router.py#L86)
[`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)
[`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)
[`src/ypms/framework/crud.py:111`](../src/ypms/framework/crud.py#L111)
[`src/ypms/auth/router.py:65`](../src/ypms/auth/router.py#L65)
[`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)
[`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)
[`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)
[`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)
[`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)
[`src/ypms/youth/router.py:99`](../src/ypms/youth/router.py#L99)
[`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)
[`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)
[`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)
[`.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)
[`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)
[`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)
[`src/ypms/db.py:24`](../src/ypms/db.py#L24)
[`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)
[`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.