-
Notifications
You must be signed in to change notification settings - Fork 0
fix: minimize durable audit metadata #753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
59a7aaf
5b1ce7c
a96567e
0c6932b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,34 @@ export type AuditLogEntry = { | |
| metadata?: Record<string, unknown>; | ||
| }; | ||
|
|
||
| /** | ||
| * Audit rows are retained indefinitely, so their metadata must be an operational | ||
| * event summary rather than a second copy of document metadata. In particular, | ||
| * upload filenames and rename/delete titles are user-controlled and can contain | ||
| * patient identifiers. Keep this allowlist at the durable-write boundary so a | ||
| * future caller cannot accidentally reintroduce free text into `audit_logs`. | ||
| */ | ||
| function minimumAuditMetadata(entry: AuditLogEntry): Record<string, boolean | number | string> { | ||
| const metadata = entry.metadata ?? {}; | ||
|
|
||
| switch (entry.action) { | ||
| case "document_upload": { | ||
| const fileType = typeof metadata.fileType === "string" ? metadata.fileType : undefined; | ||
| const fileSize = | ||
| typeof metadata.fileSize === "number" && Number.isFinite(metadata.fileSize) ? metadata.fileSize : undefined; | ||
| return { | ||
| ...(fileType ? { fileType } : {}), | ||
| ...(fileSize !== undefined ? { fileSize } : {}), | ||
| }; | ||
| } | ||
| case "document_delete": | ||
| return typeof metadata.storageRemoved === "boolean" ? { storageRemoved: metadata.storageRemoved } : {}; | ||
|
Comment on lines
+38
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Preserve the numeric delete-cleanup result. Line 39 accepts only booleans, but the DELETE route passes numeric Proposed fix case "document_delete":
- return typeof metadata.storageRemoved === "boolean" ? { storageRemoved: metadata.storageRemoved } : {};
+ return typeof metadata.storageRemoved === "number" &&
+ Number.isSafeInteger(metadata.storageRemoved) &&
+ metadata.storageRemoved >= 0
+ ? { storageRemoved: metadata.storageRemoved }
+ : {};Also applies to: 60-60 🤖 Prompt for AI Agents |
||
| case "document_rename": | ||
| case "document_label_change": | ||
| return {}; | ||
| } | ||
| } | ||
|
|
||
| // Minimal structural type so we do not couple to the full Supabase client type. | ||
| type AuditInsertClient = { | ||
| from: (table: string) => { | ||
|
|
@@ -29,7 +57,7 @@ export async function writeAuditLog(supabase: AuditInsertClient, entry: AuditLog | |
| action: entry.action, | ||
| resource_type: entry.resourceType ?? null, | ||
| resource_id: entry.resourceId ?? null, | ||
| metadata: entry.metadata ?? {}, | ||
| metadata: minimumAuditMetadata(entry), | ||
| }); | ||
| if (error) { | ||
| logger.error("Audit log write failed", { action: entry.action, message: error.message }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| -- Audit logs are retained indefinitely for clinical governance. Historical writes | ||
| -- included user-controlled document names/titles and content hashes in metadata, | ||
| -- which could preserve patient identifiers after the source document was renamed | ||
| -- or deleted. Keep the immutable event/resource record but remove free text and | ||
| -- retain only operational facts needed for audit triage. | ||
|
|
||
| update public.audit_logs | ||
| set metadata = case action | ||
| when 'document_upload' then jsonb_strip_nulls(jsonb_build_object( | ||
| 'fileType', case when jsonb_typeof(metadata -> 'fileType') = 'string' then metadata -> 'fileType' end, | ||
| 'fileSize', case when jsonb_typeof(metadata -> 'fileSize') = 'number' then metadata -> 'fileSize' end | ||
| )) | ||
| when 'document_delete' then jsonb_strip_nulls(jsonb_build_object( | ||
| 'storageRemoved', case when jsonb_typeof(metadata -> 'storageRemoved') = 'boolean' then metadata -> 'storageRemoved' end | ||
| )) | ||
| else '{}'::jsonb | ||
| end | ||
| where metadata <> '{}'::jsonb; | ||
|
|
||
| do $$ | ||
| declare | ||
| unsafe_metadata_count integer; | ||
| begin | ||
| select count(*) into unsafe_metadata_count | ||
| from public.audit_logs | ||
| where metadata ?| array['fileName', 'contentHash', 'previousTitle', 'newTitle', 'title']; | ||
|
|
||
| if unsafe_metadata_count > 0 then | ||
| raise exception 'audit-log metadata minimization incomplete: % rows still contain user-controlled document text', unsafe_metadata_count; | ||
| end if; | ||
| end $$; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the migration reference from
20260717170000to20260717174000.The privacy assessment references migration
20260717170000, but the actual migration file is20260717174000_minimize_audit_log_metadata.sql. The timestamp was changed to avoid a collision with an existing migration onmain. This stale reference could mislead operators during deployment or audit trail review.📝 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents