- Shipped
- August 4, 2026 at 4:19 AM UTC
- Author
- Kamo
- Commit
- 1c590de
phi_access_log has been accumulating evidence nobody looks at. That satisfies §164.312(b) — the trail exists — and satisfies nothing about §164.308(a)(1)(ii)(D), which asks for review, or §164.400-414, whose clock cannot start until somebody notices. This is the thing that notices. Five detectors over the closed hour, per actor: BULK_EXPORT 250 distinct records exported/downloaded/disclosed MASS_READ 200 distinct records read REPEATED_DENIALS 10 refused attempts (events — a refusal names no record) OFF_HOURS_ACCESS 20 distinct records outside the TENANT's own week PLATFORM_STAFF_ACCESS 1 — a covered entity is told regardless of justification Counts are DISTINCT records, not events: PhiAccessAuditor emits one LIST row per grid row, so a member reloading the same page forty times is 2,000 events and 50 individuals — and "how many individuals" is the number a breach assessment is made of. Rationale for every default is on PhiDetectionSettings; all of them are configmap-tunable, because a detector that fires constantly gets muted and a muted detector still reads as coverage. Off-hours is judged in the tenant's zone via its existing OFF_HOURS_ACCESS rule (business hours the Security screen already collects) falling back to Organization.timezone. occurred_at is UTC, so a UTC-anchored check would page a Sydney tenant every working morning and never page a New York one. Delivery reuses what exists: EmailTemplateServiceClient -> EmailService /api/email/templates/send with the new PHI_ACCESS_ALERT canonical key, to the addresses the org already registered in org_suspicious_detection_rules .notification_emails. A tenant that configured none gets the review log and nothing else — mailing a guessed address would disclose that a named workforce member is under suspicion. Every finding is logged at a dedicated PHI-DETECTION logger; only notification is throttled (6h per detector/tenant/ actor, in the Redis the login-anomaly rules already use). Alerts carry identifiers and counts, never a record. Two deliberate structural choices: - The aggregates run through an EntityManager, not Spring Data @Query. Spring Data validates a declared query by creating it at repository bootstrap, so a mistake there fails context startup — and a SecurityService that will not start takes every login with it, as on 2026-08-03. Here the worst case is a sweep that logs and retries next hour. It also removes this package from the @EnableJpaRepositories question entirely: no repository bean, nothing to forget. PhiDetectionQueryTest checks every property path reflectively since no test in this service can boot a context. - Every query is constrained by organization id first. phi_access_log's only usable index is (ORGANIZATION_ID, OCCURRED_AT); filtering on time alone full-scans an append-only table that only ever grows. Scheduling: this joins taskScheduler (4 threads, sched-), which seven sweeps already share. It does NOT touch translationExecutor — the 3-thread/500-queue @Async pool observed saturated — and submits to no executor at all. Hourly, five grouped aggregates per tenant batch, no network I/O inside the scan, and a reentrancy latch so a slow run skips the next tick rather than stacking behind it and starving the pool it shares. No entity or column was added or changed.