Skip to content

HIPAA Basic Compliance Report v0.8.0 (+ pre-merge review fixes) - #41

Merged
man4ish merged 8 commits into
mainfrom
fix/hipaa-report-pre-merge-review-fixes
Aug 12, 2026
Merged

HIPAA Basic Compliance Report v0.8.0 (+ pre-merge review fixes)#41
man4ish merged 8 commits into
mainfrom
fix/hipaa-report-pre-merge-review-fixes

Conversation

@man4ish

@man4ish man4ish commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Basic HIPAA Compliance Report for OmniBioAI Studio v0.8.0: GET /compliance/hipaa-report[/pdf|/csv], platform_admin-only, org_admin scoping deferred to v0.9.0.

9 commits total: 7 build out the feature (WeasyPrint/PDF template → data aggregation → JSON/PDF/CSV endpoints → frontend), followed by 2 commits addressing every finding from a full pre-merge security/privacy/architecture/HIPAA-readiness review performed before this branch was pushed.

Companion PR (required for this feature to work end-to-end): omnibioai-billing — read API for usage_events (RAG query log source), commit 226b99a, not yet pushed/opened as its own PR.

Pre-merge review fixes (commits 1ad35db, 2f16a9d)

  1. CSV formula injection (CWE-1236)csv_export.py now sanitizes every cell through one central path; Organization.name has no character-class validation upstream, so this was a real, exploitable vector via a crafted org name.
  2. Silent downstream failures vs. genuine empty results — a downstream outage (auth-service/billing-service) previously rendered as "this org had zero activity" with no signal anything was wrong. Now surfaced explicitly as sources_unavailable in JSON/PDF/CSV/UI. A confirmed-nonexistent org_id now 404s instead of silently rendering an empty placeholder report.
  3. Cache provenancegenerated_by/generated_at are no longer part of the cached payload; a cache hit could previously attribute a report to whichever admin triggered the original cache-miss computation.
  4. Terminologysecurity_incidents (implied a legal/HIPAA determination this report never makes) split into failed_login_attempts (routine noise) and security_events_requiring_review (actual denials/escalation attempts).
  5. Regression tests for all of the above, plus nonexistent-org 404, pagination-cap boundaries, malicious-org-name escaping, and multi-org login attribution (documented/locked-in as a known v0.9-deferred limitation, not fixed here).
  6. Report-access audit trail — every successful JSON/PDF/CSV response now writes a compliance_report_accessed event to the existing audit:events stream (actor, org, date range, format) — reuses omnibioai-security-audit's existing infrastructure, no new producer/consumer/table.

Explicitly out of scope (v0.9)

org_admin support, dataset view/download/upload instrumentation, session duration, System Activity section, historical org-membership attribution for login events.

Test results

  • omnibioai-billing full suite: 364/364 passed (unaffected by this repo's changes)
  • omnibioai-control-center backend full suite: 1259/1259 passed, 99.85% coverage (98% gate)
  • omnibioai-control-center frontend full suite: 495/495 passed (40 files)
  • tsc -b: clean

Review status

Full read-only pre-merge security/privacy/architecture/HIPAA-readiness review completed before push (findings documented in commit 1ad35db's message). No cross-org data leakage or credential exposure was found in the original 7 commits; the 2 fix commits close every finding raised except the two v0.9-deferred items above.

🤖 Generated with Claude Code

Step 2 of the Basic HIPAA Compliance Report (Studio v0.8.0). No PDF
library existed anywhere in the workspace before this (confirmed by
scanning every requirements/pyproject during discovery) -- adds:

- compliance/templates/hipaa_report.html: 4 of the 5 originally-scoped
  sections (Executive Summary, User Access Log, RAG Query Log, Security
  Events) -- Section 5 (System Activity/config changes) deferred, its
  data source isn't verified yet (Step 3). Reuses the platform's actual
  logo mark (same inline SVG as main.py's report header / AdminLogo.tsx),
  explicit not-tracked notes for session duration and dataset
  views/downloads/uploads (dropped from v0.8.0 scope per discovery, not
  silently zeroed), page numbers + generated-at timestamp via WeasyPrint's
  @page CSS.
- compliance/pdf.py: render_report_html/render_report_pdf (Jinja2 render
  + WeasyPrint layout), template loaded via Path(__file__).parent rather
  than a top-level templates/ dir -- this repo's Dockerfile only COPYs
  backend/src into the image, confirmed by reading it directly, so
  anything outside that path would silently not exist at runtime.
- weasyprint+jinja2 added to pyproject.toml; libpango/libcairo/
  libgdk-pixbuf/shared-mime-info/fonts-liberation added to both
  Dockerfiles -- weasyprint's real runtime deps (Pango/Cairo rendering,
  not a pure-Python PDF engine), missing from both images before this.

Tests: HTML escaping, empty-state rendering, logo presence, valid PDF
magic bytes -- 7 new tests, full backend suite still 1158 passed / 98%
coverage gate green.
…n-out)

Step 3 of the Basic HIPAA Compliance Report (Studio v0.8.0):
compliance/service.py::build_report fans out to omnibioai-auth (org
roster + IAM audit ledger) and omnibioai-billing (usage_events, Step 1)
and shapes the result into the four sections with real data sources --
Executive Summary, User Access Log, RAG Query Log, Security Events.

Two source-level scoping gaps found while building this, neither obvious
from the task brief (both documented in service.py's module docstring):

- login_success/login_failure AuditEvent rows in omnibioai-auth are never
  organization_id-scoped (login happens before org context resolves).
  Fetched platform-wide, then filtered against the target org's actual
  roster (GET /orgs/{id}/members) -- not organization_id-filtered at the
  source (would silently return zero rows for every org).
- The generic audit ledger (omnibioai-security-audit's audit_events,
  decision=="deny" for 403s) has no organization_id column at all --
  dropped as a Section 4 source entirely rather than mislabeling
  platform-wide data as one org's. role_assignment_denied from auth's own
  (properly org-scoped) IAM ledger is the one accurate "access denied"
  signal used instead.

Adds compliance/auth_client.py and compliance/billing_client.py (thin
async httpx clients, same shape as analytics/billing_client.py -- caller's
JWT forwarded unmodified, downstream service's own RBAC is what actually
authorizes each call) with bounded pagination (100 pages) and a
`truncated` flag surfaced to the report rather than silently dropping
data past the cap.

25 new unit tests (auth_client/billing_client pagination+failure paths,
service.py aggregation/grouping/classification logic). Full backend
suite: 1183 passed, 99.67% coverage (98% gate).
Step 4 of the Basic HIPAA Compliance Report (Studio v0.8.0).
compliance/router.py, wired into main.py alongside analytics_router (same
"no blanket router-level dependency, each route requires manage_all_orgs
individually" posture).

platform_admin-only (manage_all_orgs permission) -- confirmed via HTTP:
platform_admin 200, org_admin token without that permission 403,
regular member 403, missing/invalid token 401. org_admin scoping
deferred to v0.9.0 per the confirmed scope decision.

Params: from_date/to_date/org_id (all required), 400 if from_date >
to_date. Cached 1hr via the existing analytics/cache.py::get_or_set_async
(reused as-is, not a second caching mechanism) -- verified same-params
requests hit cache (build_report called once), different org_id does not
share a cache entry. generated_by resolves from the verified token's
email claim, falling back to sub.

12 new tests (RBAC matrix, param validation, cache behavior). Full
backend suite: 1195 passed, 99.67% coverage (98% gate).
Step 5 of the Basic HIPAA Compliance Report (Studio v0.8.0).
GET /compliance/hipaa-report/pdf: same _build_cached_report call as the
JSON endpoint (Step 4) -- same cache key shape, so a PDF request after a
JSON request for the same org/date-range reuses the already-cached data
instead of recomputing it (verified by test: build_report called once
across both routes) -- then renders it via compliance/pdf.py
(WeasyPrint, Step 2) and returns it as an attachment download.

WeasyPrint's layout pass is a real blocking CPU cost, not I/O -- run via
loop.run_in_executor, the same off-event-loop pattern
api/routes_llm.py's own count_abstracts/list_indexed_domains already use
for their blocking filesystem walks, so one PDF render doesn't stall
every other concurrent request this process is serving.

Same platform_admin-only RBAC as the JSON endpoint (manage_all_orgs).

6 new tests (valid PDF response, Content-Disposition filename, RBAC,
date validation, cache reuse across JSON+PDF). Full backend suite: 1201
passed, 99.67% coverage (98% gate).
Step 6 of the Basic HIPAA Compliance Report (Studio v0.8.0).
compliance/csv_export.py::render_report_csv -- one CSV, all four
sections in sequence separated by a "## Section N: ..." marker row (no
existing precedent for a multi-section CSV export in this codebase to
mirror; analytics/router.py's own /analytics/export is single-table-
per-request). csv.writer + io.StringIO is the same mechanism that
existing export already uses, not a new convention.

GET /compliance/hipaa-report/csv wired the same way as the PDF endpoint
(Step 5): reuses _build_cached_report (same cache key -- verified a CSV
request after a JSON request for the same org/range doesn't recompute),
platform_admin-only RBAC. No run_in_executor needed here (plain string
formatting, not WeasyPrint's layout engine).

11 new tests (csv_export module + router RBAC/validation/cache-reuse).
Full backend suite: 1212 passed, 99.68% coverage (98% gate).
Step 7 of the Basic HIPAA Compliance Report (Studio v0.8.0).
compliance.ts data layer (mirrors analytics.ts's shape exactly) +
pages/ComplianceReport.tsx: date range inputs, required organization
picker (fetchPlatformOrgs), Generate Report button, in-browser preview
(summary stat tiles + User Access Log / RAG Query Log / Security Events
tables via the shared DataTable component), Download PDF / Download CSV
buttons.

Deliberately NOT auto-loaded on mount like AnalyticsDashboard.tsx --
generating this report is a real fan-out across two other services, so
it only runs on an explicit click. Download buttons stay disabled until
a report has been generated for the current org/range.

Visible to platform_admin only (auth.ts::canSeeComplianceReport, a
narrower gate than canSeeAnalytics -- no org_admin/team_admin branch,
matching the backend's v0.8.0 scope decision). Wired into
navigation.ts/AdminApp.tsx the same way audit-logs/interactions are:
Security section, flat platform-wide page, no org-picker-then-detail
flow (the org picker lives inside the page itself, since org_id is a
required report parameter here rather than an optional filter).

37 new tests (compliance.ts data layer, ComplianceReport.tsx behavior,
navigation.ts placement). tsc -b clean. Full frontend suite: 492 passed
(40 files).
…nce, incident terminology, and report-access audit trail

Pre-merge security & compliance review fixes (findings 1-4 and 6).
Findings 1-4 are necessarily one commit -- all four touch
service.py's return shape and its three consumers (router.py, pdf.py's
template, csv_export.py) in lockstep; splitting them would leave an
intermediate commit referencing fields that don't exist yet. Finding 6
(audit_log.py) is folded in here rather than its own commit for the
same reason: router.py's audit_log.log_report_access call sites were
already part of this commit's router.py rewrite, and a commit whose
router.py imports a module that doesn't exist yet isn't a commit that
actually builds.

1. CSV formula injection (CWE-1236). csv_export.py::_sanitize_cell +
   _writerow are now the one path every cell in every section passes
   through -- any value starting with =, +, -, or @ (Organization.name
   has no character-class validation in omnibioai-auth's own schema, so
   any org member with rename permission could set one to a live
   formula) gets a leading apostrophe, the standard Excel/Sheets/
   LibreOffice "treat as literal text" convention. 12 new tests
   including the concrete HYPERLINK-formula and cmd-execution vectors
   the review flagged.

2. Silent downstream failures now distinguishable from genuine empty
   results. auth_client.py/billing_client.py's _get/list_all_* functions
   return an explicit status ("ok"/"not_found"/"unavailable", or a
   truncated/unavailable bool pair) instead of collapsing "the fetch
   failed" and "the fetch found nothing" into the same empty shape.
   service.py collects every failure into sources_unavailable (human-
   readable strings, e.g. "RAG query events (omnibioai-billing)"),
   rendered as a warning banner in JSON/PDF/CSV -- a downstream outage no
   longer produces a report that silently looks like "this org had zero
   activity". A confirmed 404 (get_organization's "not_found" status)
   now raises OrganizationNotFoundError -> a real HTTP 404, instead of
   silently falling back to a placeholder label for a nonexistent org.

3. generated_by/generated_at removed from service.build_report's return
   value (and therefore from what analytics/cache.py caches) --
   router.py's new _build_report stamps both fresh on every response,
   cached or not. Fixes: a cache hit could previously attribute a report
   to whichever admin happened to trigger the original cache-miss
   computation, a real integrity defect for a document whose purpose is
   auditability.

4. security_incidents (implying a legal/HIPAA reportable determination
   this report never makes) replaced by two separately-named summary
   counters: failed_login_attempts (routine, expected noise) and
   security_events_requiring_review (role_assignment_denied/
   mfa_verification_failed specifically). Section 4's events table is
   unchanged -- both kinds of event still appear there; only the Section
   1 summary terminology changed, so a mistyped password is no longer
   counted identically to a rejected privilege-escalation attempt.

6. audit_log.py: a compliance_report_accessed event (actor, organization,
   date range, timestamp, format) recorded to the existing `audit:events`
   Redis stream on every successful JSON/PDF/CSV response -- the same
   platform-wide, multi-writer bus omnibioai-security-audit's consumer
   already drains, and omnibioai-api-gateway's own middleware already
   writes to. No new audit producer/consumer/table: the event shape
   matches omnibioai-security-audit's AuditEvent model field-for-field,
   so it persists as a normal, queryable AuditEventRecord row through
   existing infrastructure if/when that consumer processes this stream.
   Best-effort, fire-and-forget (never blocks or breaks the report
   response), matching every other audit-write call site's own
   documented convention. Never called on a 401/403/404 -- nothing to
   record for a request that didn't actually produce a report.

Also adds: pagination-cap boundary tests (auth_client/billing_client,
proving the 101st page is never fetched once truncated), a malicious-
organization-name HTML-escaping regression test (pdf.py), and a
multi-org-login-attribution test that documents/locks in the existing,
known v0.9-deferred limitation rather than silently changing it.

71 new/changed tests across the seven affected files plus the new
audit_log module. Full backend suite: 1259 passed, 99.85% coverage
(98% gate).
…ources warning

Pre-merge security & compliance review fixes, frontend half of findings
3-4 (backend half: fix/hipaa-report-pre-merge-review-fixes, previous
commit). compliance.ts::HipaaReportSummary drops security_incidents for
failed_login_attempts + security_events_requiring_review, matching the
backend rename; adds sources_unavailable: string[] to the HipaaReport
type. ComplianceReport.tsx renders both new stat tiles (Failed Login
Attempts / Security Events Requiring Review, replacing the single
Security Incidents tile) and a warning banner -- same EmptyState
component the existing `truncated` warning already uses -- listing which
data sources were unavailable when sources_unavailable is non-empty, so
a downstream outage is visible in the UI, not just the JSON/PDF/CSV
exports.

6 new tests (renamed labels present/absent, warning shown/hidden). Full
frontend suite: 495 passed (40 files), tsc -b clean.
@man4ish
man4ish merged commit 439db8a into main Aug 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant