Skip to content

fix: minimize durable audit metadata#753

Closed
BigSimmo wants to merge 4 commits into
mainfrom
codex/review-and-fix-top-security-defects
Closed

fix: minimize durable audit metadata#753
BigSimmo wants to merge 4 commits into
mainfrom
codex/review-and-fix-top-security-defects

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Prevent user-controlled document filenames, titles, and content hashes from being persisted in the indefinitely-retained audit_logs metadata to avoid durable patient-identifiable artifacts.

Description

  • Add a centralized allowlist at the audit write boundary (minimumAuditMetadata in src/lib/audit.ts) so only action-specific operational fields are retained (upload: fileType/fileSize; delete: storageRemoved; rename/label-change: no metadata).
  • Stop writing free-text filename/title/content-hash at call sites by removing these fields from upload/rename/delete audit calls (src/app/api/upload/route.ts, src/app/api/documents/[id]/route.ts).
  • Add a migration supabase/migrations/20260717170000_minimize_audit_log_metadata.sql to minimize historical audit rows by stripping free-text metadata when applied to the database.
  • Add regression tests exercising synthetic patient-identifying upload/rename metadata to assert the durable audit payload excludes user-controlled text (tests/audit.test.ts).
  • Update documentation to record the control and the deployment-time migration in the privacy PIA and the branch review ledger (docs/privacy-impact-assessment.md, docs/branch-review-ledger.md).

Testing

  • Ran npm audit --omit=dev --offline; result: passed (0 vulnerabilities).
  • Ran Prettier checks / formatting on modified files; result: passed and files formatted.
  • Ran git diff --check and staged-diff validation; result: passed (no whitespace/check failures).
  • Added unit tests (tests/audit.test.ts) that assert sensitive fields are omitted; local focused test runner could not be executed because the worktree is missing dev runtime dependencies (Vitest/TypeScript/Next build artifacts), so Vitest-based tests were not executed here.
  • Attempted npm run verify:cheap / full local gates; these could not complete in this environment due to missing tsx and other local dev dependencies and build output, so full unit/build/e2e verification and client-bundle secret scan were not run.

Notes and follow-ups

  • The migration to scrub historical audit metadata is included in the change set but has not been applied to the live database; operator approval and the usual Supabase migration deployment workflow are required to minimize existing rows.
  • No provider-backed actions were executed (no Supabase live writes, no OpenAI calls, no pushes to remote); no real patient data was used in testing.
  • The code change and regression tests were added and validated for formatting and basic static checks in this worktree; full test and production-readiness gates should be run in an environment with project dependencies installed before deployment.

Codex Task

Summary by CodeRabbit

  • Privacy & Security

    • Reduced sensitive data retained in audit records by limiting stored metadata for uploads, renames, and deletions.
    • Updated audit metadata to retain only safe operational details (e.g., file properties and cleanup status).
    • Added safeguards and a migration to minimize existing audit entries and prevent incomplete redaction.
  • Documentation

    • Refreshed the privacy impact assessment and added an updated security review ledger entry.
  • Tests

    • Added regression coverage to verify sensitive document fields are excluded from persisted audit metadata.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 33e26af1-e2e9-4b38-9e3d-fbe1ab8355e8

📥 Commits

Reviewing files that changed from the base of the PR and between a96567e and 0c6932b.

📒 Files selected for processing (1)
  • docs/branch-review-ledger.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/branch-review-ledger.md

📝 Walkthrough

Walkthrough

Audit logging now enforces action-specific metadata allowlists, removes sensitive fields from document-related call sites, minimizes existing audit rows through migration, and adds regression tests and privacy documentation.

Changes

Audit Metadata Privacy Hardening

Layer / File(s) Summary
Audit metadata allowlist and regression coverage
src/lib/audit.ts, tests/audit.test.ts
minimumAuditMetadata restricts stored fields by action, while tests verify titles, filenames, and content-derived identifiers are excluded.
Sensitive field removal at audit call sites
src/app/api/documents/[id]/route.ts, src/app/api/upload/route.ts
Rename and delete events stop retaining titles; uploads retain only file type and size.
Historical metadata minimization and documentation
supabase/migrations/20260717174000_minimize_audit_log_metadata.sql, docs/privacy-impact-assessment.md, docs/branch-review-ledger.md
Existing rows are minimized by action, verified for sensitive keys, and documented in privacy and review records.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • BigSimmo/Database#582: Both changes modify the upload POST flow, with this PR adjusting retained audit metadata.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary, testing, and follow-up notes, but omits the required Risk and rollout and Clinical Governance Preflight sections. Add the Risk and rollout section and complete the Clinical Governance Preflight checklist, including privacy/Supabase/deployment impact details.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change by describing the audit metadata minimization fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/review-and-fix-top-security-defects

Comment @coderabbitai help to get the list of available commands.

@supabase

supabase Bot commented Jul 17, 2026

Copy link
Copy Markdown

Updates to Preview Branch (codex/review-and-fix-top-security-defects) ↗︎

Deployments Status Updated
Database Fri, 17 Jul 2026 19:56:45 UTC
Services Fri, 17 Jul 2026 19:56:45 UTC
APIs Fri, 17 Jul 2026 19:56:45 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Fri, 17 Jul 2026 19:56:49 UTC
Migrations Fri, 17 Jul 2026 19:56:52 UTC
Seeding ⏸️ Fri, 17 Jul 2026 19:56:37 UTC
Edge Functions ⏸️ Fri, 17 Jul 2026 19:56:37 UTC

❌ Branch Error • Fri, 17 Jul 2026 19:56:53 UTC

ERROR: Unsafe supabase_admin default privileges; migration blocked. (SQLSTATE 42501)
{"safe": false, "entries": ["function:PUBLIC:execute", "function:anon:execute", "function:authenticated:execute", "function:postgres:execute", "function:service_role:execute", "function:supabase_admin:execute", "sequence:anon:select", "sequence:anon:update", "sequence:anon:usage", "sequence:authenticated:select", "sequence:authenticated:update", "sequence:authenticated:usage", "sequence:postgres:select", "sequence:postgres:update", "sequence:postgres:usage", "sequence:service_role:select", "sequence:service_role:update", "sequence:service_role:usage", "sequence:supabase_admin:usage", "table:anon:delete", "table:anon:insert", "table:anon:maintain", "table:anon:references", "table:anon:select", "table:anon:trigger", "table:anon:truncate", "table:anon:update", "table:authenticated:delete", "table:authenticated:insert", "table:authenticated:maintain", "table:authenticated:references", "table:authenticated:select", "table:authenticated:trigger", "table:authenticated:truncate", "table:authenticated:update", "table:postgres:delete", "table:postgres:insert", "table:postgres:maintain", "table:postgres:references", "table:postgres:select", "table:postgres:trigger", "table:postgres:truncate", "table:postgres:update", "table:service_role:delete", "table:service_role:insert", "table:service_role:maintain", "table:service_role:references", "table:service_role:select", "table:service_role:trigger", "table:service_role:truncate", "table:service_role:update", "table:supabase_admin:delete", "table:supabase_admin:insert", "table:supabase_admin:maintain", "table:supabase_admin:references", "table:supabase_admin:select", "table:supabase_admin:trigger", "table:supabase_admin:truncate", "table:supabase_admin:update"], "role_exists": true, "schema_exists": true}
At statement: 3
do $$
declare
  v_status jsonb;
begin
  if not exists (select 1 from pg_catalog.pg_roles where rolname = 'supabase_admin') then
    raise notice 'role supabase_admin does not exist; default-privilege assertion is not applicable';
    return;
  end if;

  begin
    -- Local/Superuser-capable environments can assume the target role even
    -- when the migration role cannot use ALTER DEFAULT PRIVILEGES FOR ROLE
    -- directly. Hosted environments that cannot assume it fall through to the
    -- catalog assertion and block with operator instructions.
    execute 'set local role supabase_admin';
    -- Revokes must be global: per-schema ACLs cannot subtract privileges from
    -- built-in or previously granted global defaults.
    alter default privileges for role supabase_admin
      revoke all privileges on tables from public, anon, authenticated, service_role;
    alter default privileges for role supabase_admin in schema public
      revoke all privileges on tables from public, anon, authenticated, service_role;
    alter default privileges for role supabase_admin
      revoke all privileges on sequences from public, anon, authenticated, service_role;
    alter default privileges for role supabase_admin in schema public
      revoke all privileges on sequences from public, anon, authenticated, service_role;
    alter default privileges for role supabase_admin
      revoke execute on functions from public, anon, authenticated, service_role;
    alter default privileges for role supabase_admin in schema public
      revoke execute on functions from public, anon, authenticated, service_role;
    alter default privileges for role supabase_admin in schema public
      grant select, insert, update, delete on tables to service_role;
    alter default privileges for role supabase_admin in schema public
      grant usage, select on sequences to service_role;
    alter default privileges for role supabase_admin in schema public
      grant execute on functions to service_role;
    execute 'reset role';
  exception when insufficient_privilege then
    begin execute 'reset role'; exception when others then null; end;
    raise notice 'current role % cannot remediate supabase_admin default privileges; asserting the catalog postcondition', current_user;
  end;

  v_status := public.default_privileges_status('supabase_admin', 'public');
  if not coalesce((v_status->>'safe')::boolean, false) then
    raise exception using
      errcode = '42501',
      message = 'Unsafe supabase_admin default privileges; migration blocked.',
      detail = v_status::text,
      hint = E'Run these six statements as supabase_admin, then retry the migration:\n'
        'DO $remediate$ BEGIN ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin REVOKE ALL PRIVILEGES ON TABLES FROM PUBLIC, anon, authenticated, service_role; ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL PRIVILEGES ON TABLES FROM PUBLIC, anon, authenticated, service_role; END $remediate$;\n'
        'DO $remediate$ BEGIN ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin REVOKE ALL PRIVILEGES ON SEQUENCES FROM PUBLIC, anon, authenticated, service_role; ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL PRIVILEGES ON SEQUENCES FROM PUBLIC, anon, authenticated, service_role; END $remediate$;\n'
        'DO $remediate$ BEGIN ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC, anon, authenticated, service_role; ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC, anon, authenticated, service_role; END $remediate$;\n'
        'ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO service_role;\n'
        'ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO service_role;\n'
        'ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO service_role;';
  end if;
end;
$$

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

CI triage

CI failed on this PR. Automated classification of the 2 failed job(s):

  • Unit coverageneeds investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.
  • PR requiredneeds investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.

Compared with main CI run #2957 (success).

Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger.

@BigSimmo
BigSimmo marked this pull request as ready for review July 17, 2026 19:36
@BigSimmo
BigSimmo enabled auto-merge July 17, 2026 19:36
20260717170000_minimize_audit_log_metadata.sql conflicted with
20260717170000_registry_projection_cleanup.sql already on main.
Supabase CLI uses the numeric timestamp prefix as the schema_migrations
primary key, so two files sharing the same prefix cause a duplicate-key
error during migration replay CI.

Rename to 20260717174000 (next safe slot after 20260717173000).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/privacy-impact-assessment.md`:
- Line 201: Update the migration reference in the audit_logs row to use
20260717174000 instead of 20260717170000, matching the actual
minimize-audit-log-metadata migration while leaving the surrounding privacy
assessment content unchanged.

In `@src/lib/audit.ts`:
- Around line 38-39: Update the document_delete handling in the metadata
normalization logic to accept finite, non-negative numeric storageRemoved values
in addition to booleans, preserving the numeric cleanup count in the returned
object. Add a regression test covering a nonzero numeric value and retain the
empty-object behavior for unsupported values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a7d9490-42b2-4c56-9dcc-8ab5abf6d25c

📥 Commits

Reviewing files that changed from the base of the PR and between b04c95f and a96567e.

📒 Files selected for processing (7)
  • docs/branch-review-ledger.md
  • docs/privacy-impact-assessment.md
  • src/app/api/documents/[id]/route.ts
  • src/app/api/upload/route.ts
  • src/lib/audit.ts
  • supabase/migrations/20260717174000_minimize_audit_log_metadata.sql
  • tests/audit.test.ts

| `rag_queries` | No (hash placeholder) | `queryTextForStorage` / `normalizedQueryTextForStorage` ([query-privacy.ts:33-39](src/lib/query-privacy.ts)); centralized write in `insertRagQuery` | `answer` is null by default and stored only with explicit `RAG_PERSIST_ANSWER_TEXT=true`; `source_chunk_ids` (own data) | owner-read, [schema.sql:3932](supabase/schema.sql) |
| `rag_query_misses` | No (hash placeholder) | same helpers; writes in [search/route.ts:558-559](src/app/api/search/route.ts), [interaction/route.ts:88-89](src/app/api/search/interaction/route.ts) | `metadata.query_hash` | owner-read, [schema.sql:3935](supabase/schema.sql) |
| `rag_retrieval_logs` | No (hash placeholder) | same helpers; write at [search/route.ts:556-559](src/app/api/search/route.ts) | retrieval telemetry only | owner-read, [schema.sql:3938](supabase/schema.sql) |
| `audit_logs` | N/A (no query text) | action/resource metadata only; the write boundary allowlists operational metadata and excludes user-controlled filenames/titles/content hashes ([audit.ts](../src/lib/audit.ts)). Migration `20260717170000` minimizes existing rows on deployment. | `owner_id`, `action`, `resource_id` | service-role-only, [schema.sql:3959](supabase/schema.sql) |

Copy link
Copy Markdown
Contributor

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 20260717170000 to 20260717174000.

The privacy assessment references migration 20260717170000, but the actual migration file is 20260717174000_minimize_audit_log_metadata.sql. The timestamp was changed to avoid a collision with an existing migration on main. This stale reference could mislead operators during deployment or audit trail review.

📝 Proposed fix
-| `audit_logs`         | N/A (no query text)   | action/resource metadata only; the write boundary allowlists operational metadata and excludes user-controlled filenames/titles/content hashes ([audit.ts](../src/lib/audit.ts)). Migration `20260717170000` minimizes existing rows on deployment. | `owner_id`, `action`, `resource_id`                                                                                     | service-role-only, [schema.sql:3959](supabase/schema.sql) |
+| `audit_logs`         | N/A (no query text)   | action/resource metadata only; the write boundary allowlists operational metadata and excludes user-controlled filenames/titles/content hashes ([audit.ts](../src/lib/audit.ts)). Migration `20260717174000` minimizes existing rows on deployment. | `owner_id`, `action`, `resource_id`                                                                                     | service-role-only, [schema.sql:3959](supabase/schema.sql) |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `audit_logs` | N/A (no query text) | action/resource metadata only; the write boundary allowlists operational metadata and excludes user-controlled filenames/titles/content hashes ([audit.ts](../src/lib/audit.ts)). Migration `20260717170000` minimizes existing rows on deployment. | `owner_id`, `action`, `resource_id` | service-role-only, [schema.sql:3959](supabase/schema.sql) |
| `audit_logs` | N/A (no query text) | action/resource metadata only; the write boundary allowlists operational metadata and excludes user-controlled filenames/titles/content hashes ([audit.ts](../src/lib/audit.ts)). Migration `20260717174000` minimizes existing rows on deployment. | `owner_id`, `action`, `resource_id` | service-role-only, [schema.sql:3959](supabase/schema.sql) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/privacy-impact-assessment.md` at line 201, Update the migration
reference in the audit_logs row to use 20260717174000 instead of 20260717170000,
matching the actual minimize-audit-log-metadata migration while leaving the
surrounding privacy assessment content unchanged.

Comment thread src/lib/audit.ts
Comment on lines +38 to +39
case "document_delete":
return typeof metadata.storageRemoved === "boolean" ? { storageRemoved: metadata.storageRemoved } : {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 cleanup.storageRemoved; every delete audit row therefore stores {} instead of the required operational result. Accept a finite non-negative count and add a regression for a nonzero value.

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/audit.ts` around lines 38 - 39, Update the document_delete handling
in the metadata normalization logic to accept finite, non-negative numeric
storageRemoved values in addition to booleans, preserving the numeric cleanup
count in the returned object. Add a regression test covering a nonzero numeric
value and retain the empty-object behavior for unsupported values.

@BigSimmo BigSimmo closed this Jul 18, 2026
auto-merge was automatically disabled July 18, 2026 06:12

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants