Skip to main content

Compliance control matrix

What this is, and what it is not

This maps control families from HIPAA, GDPR, NDPR, ISO 27001, SOC 2 and PCI-DSS to the artifact in this repository that implements them, with a file path or a test name.

⚠⚠ It does not claim compliance with any of them. Compliance is a finding an auditor makes about an organisation, not a property a codebase can assert about itself. What a codebase can say is "this control is implemented here, and this is how you verify it", which is what every row below does.

The PARTIAL rows are the point. A matrix listing only what is done is marketing; an auditor will find the gaps anyway, and finding them undocumented is worse than finding them named. Each PARTIAL row states the gap and, where one exists, the issue tracking it. They are also the next milestone's backlog.

Rows marked with an MR number cite work that is merged or in review at the time of writing. A matrix is only as honest as its citations, so a path that does not yet exist on main says which merge request brings it rather than reading as an existing control.

Status vocabulary

StatusMeans
IMPLEMENTEDThe control exists in this repo, at the cited path, with the cited test
PARTIALSomething real exists and a named gap remains
EXTERNALOwned by ehr/deployment or the infrastructure, verified by M33-015's conformance test
NOT APPLICABLEOut of scope, with the reason

⚠ Note the difference between EXTERNAL and IMPLEMENTED. A control living in ehr/deployment is never IMPLEMENTED here — this repo cannot know whether it is switched on. It is EXTERNAL, and it earns that only when a conformance check can fail on it.

Encryption at rest

ControlStatusEvidence
PHI encrypted at rest (HIPAA §164.312(a)(2)(iv); GDPR Art.32(1)(a))IMPLEMENTEDcommon/.../security/crypto/PiiProtector.java, PiiCipher.java — AES-256-GCM pii:v1:<keyId>:<iv>:<ct+tag>
Every PHI column classified and enforcedIMPLEMENTEDdocs/site/docs/operations/phi-data-classes.md is the enforced list; PhiDataClassConformanceTest fails on any unclassified column in a governed table
Demographics, identifiers, clinical values, form payloads, notificationsIMPLEMENTEDSEC-001, M33-007/008/009/010 — see the registry for the per-column disposition
Searchability under encryptionIMPLEMENTEDPiiBlindIndex.java — keyed HMAC bi:<keyId>:<base64>, versioned (M33-005B)
Key rotation without data lossIMPLEMENTEDPiiKeyring.java; every envelope carries its key id, so old rows stay readable
health_card identifiersPARTIALcard_number, qr_code_value, nfc_identifier are plaintext and the table is outside GOVERNED_TABLES#403
Database/volume encryption (TDE)PARTIALInfra-owned and not verifiable from herepg_stat_ssl describes the wire, not the disk. Needs an infrastructure attestation; see infrastructure-security-requirements.md (!531)
Backup encryption, separate keys, tested restorePARTIALSame — no conformance check is possible from an application connection. ehr/deployment#2

Encryption in transit

ControlStatusEvidence
Response hardening headersIMPLEMENTEDcommon/.../security/SecurityResponseHeaders.java; SecurityHeadersIntegrationTest reads a real response through the running chain
HSTSIMPLEMENTED, inert until TLSWithheld from a request the app considers insecure — deliberate, and it means HSTS never fires until the ingress forwards X-Forwarded-Proto: https
Ingress TLS with a valid chainEXTERNALDeployedEnvironmentSecurityConformanceTest.ingressServesValidTls (!531) — ⚠ failing today: PKIX path building failed. ehr/deployment#2
Plain HTTP refusedEXTERNAL…plainHttpIsNotServed — ⚠ failing today: plain HTTP returns 200
App→database TLSEXTERNAL…databaseConnectionIsEncryptedInTransit asserts pg_stat_ssl.ssl for the app's own backend

Access control and accountability

ControlStatusEvidence
Unique user identification (HIPAA §164.312(a)(2)(i))IMPLEMENTEDuser_account; every audit row attributes to a user_account_id
Role- and object-level authorizationIMPLEMENTED@RequiresAccess + AccessDecisionService; RequiresAccessArchitectureTest fails any write mapping that omits it
Minimum necessary / patient-scoped readsIMPLEMENTEDcore/.../clinical/service/PatientAccessGuard.java — service layer, because engines call in-process and a controller check would be bypassed
Emergency access ("break the glass", §164.312(a)(2)(ii))IMPLEMENTEDBreakGlassServiceImpl.java — reason, elevated permission, time box, audit, SecurityEventType.BREAK_GLASS_GRANTED
Audit controls (§164.312(b))IMPLEMENTEDaudit_event / access_log / security_event; AuditEventAttribution.java refuses caller-supplied identity
Audit trail carries no PHIIMPLEMENTEDRegistry marks audit free text REDACTED, never ENCRYPTED — encrypting it would make it unreadable to the people investigating an incident
Service-to-service identityIMPLEMENTEDSEC-018 — service_identity + OAuth2 client credentials; signed claims, not the caller-controlled header

Authentication

ControlStatusEvidence
Credential storageIMPLEMENTEDBCrypt (user_account.password_hash)
Brute-force resistanceIMPLEMENTEDAccount lockout + RateLimitProperties.java per-IP and per-(IP, username) buckets
Multi-factor authenticationIMPLEMENTED, opt-inM33-012 — TotpVerifier.java (RFC 6238 vectors asserted), MfaServiceImpl.java. ⚠ PARTIAL as a policy: MFA_REQUIRED_FOR_ALL_USERS defaults false, and enforcing needs FE-364's enrolment screen
Session token integrityIMPLEMENTEDM33-011 — ES256 with a kid, JWKS at /api/v1/auth/jwks.json. ⚠ ES256 activates only once the EC key pair is configured; otherwise HS256 with a startup warning
Refresh-token theft detectionIMPLEMENTEDM33-004 — reuse revokes the whole token family; SecurityEventType.REFRESH_TOKEN_REUSE
MFA in Keycloak modeNOT APPLICABLE (documented)The password grant has nowhere to interpose a factor; KeycloakAuthService.completeMfaLogin refuses rather than pretending. Deployed profile is KEYCLOAK_ENABLED=false

Browser trust boundary

ControlStatusEvidence
Content Security PolicyPARTIALReport-Only today. Violations are collected and triaged (M33-012B, csp_violation_group, !530); enforcement is M33-013 (#336), gated on a representative window of evidence
CSP violation evidenceIMPLEMENTEDcore/.../engines/security/helpers/CspUriReducer.java + CspReportIngestServiceImpl.java (!530); ⚠ receives nothing until FE-368 adds report-to
CORSPASSThe platform is deployed same-origin, so no CORS policy exists and none is needed. ⚠ The dead CORS_ALLOWED_ORIGINS values were removed 2026-09-10 (M33-013) — they were set in the Helm values and the local runbook, read by no Java code, and had already been mistaken for a live control. Two guards keep it that way: CorsIsDeliberatelyAbsentTest for the Java, CorsConfigStaysDeadGateTest for every other file. Adopting split-host means replacing both deliberately
CSRFNOT APPLICABLE, deliberatelyEvery state-changing endpoint authenticates by bearer token, which a cross-site form cannot set; the one cookie-authenticated endpoint (/auth/refresh) is SameSite=Lax. ⚠ This reasoning stops holding the moment CORS allows credentials from another origin — which is why #336 decides both together

Data residency (NDPR, GDPR Ch.V)

ControlStatusEvidence
One cell per country; blast radius is the cellPARTIAL (designed)MILESTONE29_PLAN.md; DECISIONS_2026-08-16.md D19
Key material never leaves the countryPARTIAL (designed)Per-cell keys; ⚠ AWS has no Nigeria region, which is why cell-NG runs in-country storage and key custody rather than a foreign region
No PHI to third-party processorsIMPLEMENTEDM33-012B self-hosts CSP collection — report-uri.com and Sentry would both be PHI egress to a processor with no BAA
Residency enforced rather than declaredPARTIALThe conformance hook exists (ProductionSecurityPostureValidator.java); the per-cell endpoint allow-list is M29 work

PCI-DSS

ControlStatusEvidence
Cardholder data protectionNOT APPLICABLENo payment card data is stored or transmitted. GatewayChargeDto carries gatewayReference and an authorizationUrl for redirect checkout; the schema has no card_number/cvv/expiry columns for payment cards. Card data goes to the gateway, never through this system
— the exception that looks like onesee abovehealth_card.card_number is an insurance card, not a payment card. It is a HIPAA identifier problem (#403), not a PCI one

⚠ Saying "not applicable, and here is the reason" is stronger than silence. A reviewer who greps for card_number will find health_card and needs to be told immediately which of the two things it is.

ISO 27001 / SOC 2 mappings

FamilyStatusEvidence
A.9 / CC6 Access controlIMPLEMENTEDsee Access control and accountability
A.10 / CC6.7 CryptographyIMPLEMENTED (app) + PARTIAL (infra)see Encryption at rest
A.12.4 / CC7.2 Logging and monitoringIMPLEMENTEDaudit_event, security_event; M28-010's configuration-health endpoint
A.14 / CC8 Secure developmentIMPLEMENTEDCheckstyle, PMD, SpotBugs, JaCoCo ≥80% enforced on every module; ArchUnit rules for authorization, scheduling and PHI classification
A.17 / CC7.5 Availability, backupsPARTIALInfra-owned; no restore test is evidenced. ehr/deployment#2
CC6.1 Boundary protectionPARTIALIngress TLS failing today — see Encryption in transit

How to answer a question with this

"Is observation.value_text encrypted, and how do I verify it?"

  1. phi-data-classes.md## observation classifies it FREE_TEXT / ENCRYPTED.
  2. PhiDataClassConformanceTest fails if that column were dropped from the registry.
  3. PiiProtector.protect writes the pii:v1: envelope; PiiEncryptionAtRestTest asserts ciphertext reaches the database.
  4. M33-007's ticket log records why value_coded_non_coded and value_numeric are deliberately not encrypted, and what would break if they were.

That chain — registry, enforcing test, implementation, recorded reasoning — is what every IMPLEMENTED row above is claiming.