HIPAA Basic Compliance Report v0.8.0 (+ pre-merge review fixes) - #41
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 forusage_events(RAG query log source), commit226b99a, not yet pushed/opened as its own PR.Pre-merge review fixes (commits
1ad35db,2f16a9d)csv_export.pynow sanitizes every cell through one central path;Organization.namehas no character-class validation upstream, so this was a real, exploitable vector via a crafted org name.sources_unavailablein JSON/PDF/CSV/UI. A confirmed-nonexistentorg_idnow 404s instead of silently rendering an empty placeholder report.generated_by/generated_atare no longer part of the cached payload; a cache hit could previously attribute a report to whichever admin triggered the original cache-miss computation.security_incidents(implied a legal/HIPAA determination this report never makes) split intofailed_login_attempts(routine noise) andsecurity_events_requiring_review(actual denials/escalation attempts).compliance_report_accessedevent to the existingaudit:eventsstream (actor, org, date range, format) — reusesomnibioai-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-billingfull suite: 364/364 passed (unaffected by this repo's changes)omnibioai-control-centerbackend full suite: 1259/1259 passed, 99.85% coverage (98% gate)omnibioai-control-centerfrontend full suite: 495/495 passed (40 files)tsc -b: cleanReview 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 twov0.9-deferred items above.🤖 Generated with Claude Code