hipaa, HIPAA evidence (Enterprise)

hipaa runs seccover and adds the audit-log check. For each table it shows whether every INSERT, UPDATE and DELETE is recorded, where, with which values, and whether that record can be altered. The check only reads. It writes one HIPAA report covering access control, audit controls, integrity, the Security Rule mapping and an evidence stamp. Enterprise edition.

unitautogen hipaa --schema public --out unitautogen_tests

Options and output

  • The seccover options, --schema, --out, --sec-gate and the rest
  • --config FILE declares audit or PHI tables the tool missed. ./unitautogen.toml is read automatically.
  • --storage also checks Supabase Storage policies and public buckets, see below
  • The report is <out>/hipaa/<schema>/hipaa_report.html, with .json beside it. It links the seccover and unit reports for detail.
  • On Supabase, unitautogen --supabase hipaa --schema public writes to supabase/tests/seccover/hipaa/<schema>/ and turns on --storage
  • The run installs its pgTAP test schemas and drops them at the end. TG_KEEP_TEST_SCHEMAS=1 keeps them.

Where its tests are

hipaa writes no tests of its own. The tests behind its report are these.

EvidenceFolderWritten by
Access control, functions, views and triggers<out>/security/<schema>/functions/, views/, triggers/seccover
Access control, RLS policies<out>/security/<schema>/rls/seccover
Access control, column-level security<out>/security/<schema>/cls/seccover
Audit controls and integrity<out>/unit/<schema>/triggers/trigger unit tests
PII in error messages<out>/pii_tests/pii
  • The report's Evidence links open these files, and the evidence stamp records each file's SHA-256
  • To re-run the evidence, run the suites in security/ and unit/, see CI and CD
  • With --no-emit-tests the suites run and are then removed, and the report says so

The HIPAA report

The report reads like a short book. A contents table shows each chapter, what it answers and which chapters found a gap. Each chapter opens with an In brief box before its detail tables. Above chapter 1, a Bounds table lists the roles the matrices cannot bind, such as superusers, the schema owner, BYPASSRLS roles and TRUNCATE holders, and the pgaudit state. The scope is the database layer.

HIPAA report with its contents, bounds, safeguard summary and terms
A run on a fixture schema built to fail, with gaps in 4 of 4 safeguards. The contents table shows each chapter's status, and the summary says why each safeguard has a gap.
ChapterWhat it shows
1. SummaryOne row per safeguard, 164.312(a)(1) access control, (b) audit controls, (c)(1) integrity of the audit record and (d) authentication, marked gap found, partly tested, no gaps found, not applicable or not tested, with the reason.
2. What to fixEvery gap with its priority, HIPAA rule, object, finding, the database change that closes it and the chapter with the evidence. Fixes that exist only because PostgreSQL grants EXECUTE to PUBLIC by default are marked and explained once, with the REVOKE that closes them all.
3. ReportsOne row per report this one draws on, with what it covers and a link.
4. Access control164.312(a)(1) and (d). The RLS test identities, the RLS policy verdict per table, and every SECURITY DEFINER function, view and trigger a client reaches, with its verdict. Plus RLS bypass surfaces, column-grant leaks and PII-in-error findings.
5. Audit controls164.312(b). One row per data table with a cell per INSERT, UPDATE and DELETE. A cell reads recorded when a passing trigger test made the change and asserted the audit row. It reads partly recorded when the audit table has no actor column, the row is not asserted, or a condition decides whether it is written. Other cells read cancelled, not tested, not recorded or audit trigger off. The row also lists what the audit table has no column for, such as actor, timestamp, operation or old and new values. When pgaudit is installed, the report reads its settings and adds what it logs per role.
6. Trigger side effectsTriggers that change the ePHI rows themselves. Rewriting a column, deleting or updating other rows, writing into another schema, cancelling or rejecting the change, or failing open.
7. Integrity164.312(c)(1). For each audit table and client role, whether it can forge, alter, erase or truncate audit rows, or add a trigger that does. Verdicts are tamper-proof, forgeable, editable, misattributable, unresolved, unreachable or unknown.
8. HIPAA Security RuleOne row per safeguard the database layer evidences, 164.312(a)(1), (b), (c)(1), (d) and 164.308(a)(8), with links to the chapters that hold the evidence. Safeguards outside the database layer are not listed.
9. Evidence stampDatabase, schema, server version, the database-clock timestamp, the connection role, tool version and licence. The SHA-256 of the report, of every source file it read and of every emitted test suite. An edited copy no longer matches.

Audit tables the tool did not find

An audit table is one that a trigger writes a record of each change into. The tool finds these itself. A table a trigger writes into counts as an audit table when its name contains audit, log, history, journal, trail, change or archive, or ends in event, version or revision. It also counts when it has two or more audit-style columns, such as changed_by, changed_at, operation, old_data, new_data, actor or record_id.

If your audit table has a different name and columns, say public.activity, the trigger's write is treated as an ordinary write. The changes it records do not count as recorded under Audit controls. List the missed tables in the config file.

[audit]
tables = ["public.activity", "billing.ledger"]
  • The same section removes a wrong guess with not, and [audit.map] states which audit table a table must log to
  • It only matters for a table a trigger writes into
  • TG_HIPAA_AUDIT_TABLES=public.activity,billing.ledger still works and only adds
  • To check it worked, look at the table's box in the Integrity chapter. Its recognised by line reads from config file.

Which tables and columns hold PHI is decided automatically. Where the tool gets it wrong, correct it in the config file under [phi].

Supabase Storage (--storage)

Supabase keeps a row in storage.objects for every stored file, such as photos, PDFs and scans. The RLS policies on that table decide who can open, upload, change or delete each file. A public bucket lets anyone with a file's link download it, and its policies are not checked for downloads. Add --storage to check them. --supabase turns it on by itself.

unitautogen hipaa --schema public --storage
unitautogen seccover --schema public --storage
  • It runs the RLS policy tests on storage.objects and storage.buckets, and the public-bucket check. Nothing else, since those functions and tables are Supabase's own.
  • Reports go to <out>/security/storage/rls_report.html. With hipaa, <out>/hipaa/storage/hipaa_report.html covers access control only.
  • For a public bucket it also tests who can list its files. It plants one test file owned by one user, rolled back afterwards, and tries to list it as an anonymous user and as another signed-in user.
  • If a stranger can list it, that is fix now, since anyone can find and download every file. If listing is denied to both, that is review, since the bucket may be public on purpose.
  • The fix is UPDATE storage.buckets SET public = false WHERE id = '<bucket>';, and for fix now also narrowing the SELECT policy on storage.objects so only a file's owner can list it.
  • --storage needs seccover or hipaa. A database without the storage tables is skipped with a message.

Use it to

  • Hand an assessor reproducible technical evidence for access control, audit controls and integrity at the database layer
  • Get a ranked list of the database changes that close each gap

For what the report proves and what stays with you, see the HIPAA evidence page.