Detection and alerting over the record-access audit trail

FeatureSecurityService
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.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing