feat(sp11): tamper-evident hash-chained audit_log
- New audit_log table (migration 0009, user_version=9):
* id, event_type, entity_type, entity_id, actor, payload_json,
created_at, prev_hash, hash
* Indexes on (entity_type, entity_id), event_type, created_at
- New cyclone.audit_log module:
* append_event(session, AuditEvent) — appends one chained row
* verify_chain(session) — walks the chain, returns first bad id
* SHA-256 hash over canonical row form (unit-separator delimited)
* Genesis prev_hash = 64 zeros (Bitcoin-style sentinel)
- New AuditLog ORM model
- New admin API endpoints:
* GET /api/admin/audit-log (paginated, filterable)
* GET /api/admin/audit-log/verify (returns ok/first_bad_id/reason)
- Hooked into existing endpoints to append events:
* /api/parse-999 → 'claim.rejected' per matched claim
* /api/parse-277ca → 'claim.payer_rejected' per matched claim
* /api/clearhouse/submit → 'clearhouse.submitted' per claim
- HIPAA §164.316(b)(2) compliance note in docs
Tests: 688 -> 705 (9 audit + 8 audit-API). All 705 backend tests pass.
This commit is contained in:
@@ -147,6 +147,37 @@ All CMS POS codes `01`–`99` are accepted. The canonical list lives in
|
||||
`cyclone/parsers/payer.py` as `CMS_PLACE_OF_SERVICE_CODES` and is the
|
||||
source of truth for validation and any UI dropdowns.
|
||||
|
||||
## Audit log (SP11)
|
||||
|
||||
Cyclone persists every state-changing event to a tamper-evident
|
||||
hash-chained audit log. Each row carries a SHA-256 hash of
|
||||
`(id, event_type, entity_type, entity_id, actor, payload_json,
|
||||
created_at, prev_hash)`, where `prev_hash` is the previous row's
|
||||
hash. Modifying any row's payload invalidates every subsequent row's
|
||||
hash.
|
||||
|
||||
To list events:
|
||||
|
||||
```bash
|
||||
curl 'http://localhost:8000/api/admin/audit-log?entity_type=claim&entity_id=C-123'
|
||||
```
|
||||
|
||||
To verify the chain (run nightly or on demand):
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/api/admin/audit-log/verify
|
||||
# → {"ok": true, "checked": 1234}
|
||||
# → {"ok": false, "checked": 1180, "first_bad_id": 1181, "reason": "hash mismatch ..."}
|
||||
```
|
||||
|
||||
Events written today:
|
||||
- `claim.rejected` (999 ACK AK5 R/E)
|
||||
- `claim.payer_rejected` (277CA STC A4/A6/A7)
|
||||
- `clearhouse.submitted` (SFTP submit)
|
||||
|
||||
Compliance: HIPAA §164.316(b)(2) requires 6-year retention. The
|
||||
schema doesn't enforce retention — that's a separate vacuum job.
|
||||
|
||||
## 277CA Claim Acknowledgment (SP10)
|
||||
|
||||
After Gainwell accepts our 837P file (999 AK5=A) and adjudicates the
|
||||
|
||||
Reference in New Issue
Block a user