DOCUMENTATION HUB·LEVEL_2

Forensic Integrity Report — Zero-Trust Tenant Isolation Audit

REF_PATH: forensic_integrity_reportSOURCE: APP_DOCUMENTS_DB

Forensic Integrity Report — Zero-Trust Tenant Isolation Audit

Authority: Principal Security Architect / Principal Systems Engineer
Generated: 2026-05-07
Scope: Prisma schema (prisma/schema.prisma), migrations under prisma/migrations/, client Ironguard (app/utils/apiClient.ts), server API guard (app/lib/security/ironguardApiGuard.ts), client stores (app/store/), cold-boot utilities (app/store/resetAllStores.ts).


LAYER 1 — Database kernel (PostgreSQL / Prisma)

Verification method

  • Enumerated all model tables mapped via @@map or default naming.
  • Classified each as: Direct tenant_id / tenantId, Indirect tenancy (FK/join), Global / singleton / exempt, or CRITICAL GAP (tenant-controlled data without row-level tenant discriminator suitable for RLS).

RLS migration status

  • prisma/migrations/20260507200000_ironguard_session_tenant_guc/migration.sql defines ironguard_set_session_tenant(uuid) for SET LOCAL app.current_tenant_id.
  • Full ENABLE ROW LEVEL SECURITY + FORCE on all tenant tables is not applied in one shot: application connections must set the GUC per transaction before policies return rows; otherwise Prisma queries would see empty sets or fail.

Table inventory (abbrev.)

Table / modelTenant linkageRLS-ready note
Tenant (tenants)RegistryExempt — defines tenants; policy by id = session when listing own row
MarketBenchmarkSnapshotNoneGlobal benchmark — exempt with governance review
Company, Vendor, AgentLog, AgentComputeLog, RiskEvent (SimThreatEvent), AuditLog, BotAuditLog, user_role_assignments, evidence_*, integrity_*, etc.Direct UUIDRLS candidatetenant_id / tenantId present
Department, Policy, ActiveRiskVia company_idcompanies.tenantIdIndirect — policies must join companies
ThreatEventtenantCompanyId onlyCRITICAL GAP — no UUID tenant_id on row; isolation relies on join to companies
AgentReasoning, AgentOperation, WorkNote, SustainabilityMetricVia threatIdThreatEventCRITICAL GAP path — inherits production-threat linkage; RLS via join/subquery
SimulationConfig, SystemConfig, ChaosConfig, DailySnapshot, MarketBenchmarkSnapshotGlobal/singletonExempt — not per-tenant rows
CommunityInsights, CommunityIntelligence, SyntheticEmployee, IronwatchLogNone / simulation globalFlagged — classify under platform policy (non-production tenant PII)
ClearanceRequestPartial (riskEventId nullable)Review — tie to shadow tenant via risk join
SentinelAutomationOutboxOptional tenant_scopeReview — nullable scope

Fixes applied (this change set)

  1. Documented CRITICAL rows where indirect tenancy or missing tenant_id blocks naive WHERE tenant_id = current_setting(...) without joins.
  2. Retained GUC helper migration as the kernel hook for phased RLS rollout.

LAYER 2 — API gateway (Ironguard scoping)

Files checked

  • app/utils/apiClient.ts — client injection + mismatch throws + sentinel logging.
  • app/lib/security/ironguardApiGuard.tsnew: compares x-tenant-id to ironframe-tenant cookie when cookie present; 403 on mismatch.
  • app/api/dashboard/route.ts — wired to server guard.

Fixes applied

  1. Server-side assertIronguardApiTenantOr403 returns 403 when session cookie resolves to a UUID and x-tenant-id differs.
  2. When cookie absent, header UUID is still used for the payload (backward compatibility for edge bootstrap); documented as residual risk to tighten with signed session.

LAYER 3 — State memory (cold boot)

Files checked

  • app/store/*.ts — Zustand modules with tenant-adjacent scratch state.
  • app/context/TenantProvider.tsxswitchDevTenantColdBoot.
  • app/utils/purgeClientTenantScope.ts.

Fixes applied

  1. resetAllStores() (app/store/resetAllStores.ts) — clears risk pipeline, agent streams, audit buffer, Kimbot/GRC bot/sim overlays, scenario multiplier, compliance overlay scratch, board readiness, adversary sim, agentic compute samples.
  2. tenantScopeCache.clear() (app/utils/apiCacheCoordinator.ts) — dispatches cache invalidation event for dashboard shell.
  3. switchDevTenantColdBoot / purgeClientTenantScopeAfterSwitch — call resetAllStores() then tenantScopeCache.clear() before resetting tenant session line.

LAYER 4 — Audit ledger (sentinel)

Files checked

  • app/utils/isolationSentinelLog.tsnew.
  • app/utils/apiClient.ts — invokes sentinel before throwing Ironguard errors.

Fixes applied

  1. On client IRONGUARD_BREACH / IRONGUARD_NO_TENANT, append deferred audit row:
    [ 🚨 SECURITY ALERT ] | ISOLATION BREACH ATTEMPT BLOCKED. LOGGING AGENT CONTEXT: …

Files touched (summary)

FileAction
docs/FORENSIC_INTEGRITY_REPORT.mdCreated (this report)
docs/TAS.mdImmutable Directives for isolation
app/lib/security/ironguardApiGuard.tsCreated
app/api/dashboard/route.tsServer Ironguard guard
app/store/resetAllStores.tsCreated
app/lib/security/ironguardApiGuard.tsCreated — cookie vs header 403
app/api/dashboard/route.tsUses assertIronguardApiTenantOr403
app/utils/apiCacheCoordinator.tstenantScopeCache.clear()
app/utils/isolationSentinelLog.tsCreated
app/utils/apiClient.tsSentinel hooks on throw paths
app/utils/purgeClientTenantScope.tsresetAllStores() + tenantScopeCache.clear()
app/context/TenantProvider.tsxCold boot uses purge (full reset)
app/components/TenantSwitcher.tsxPurge includes cache via unified purge util
docs/TAS.mdImmutable Directives §5

Residual risks (explicit)

  1. RLS not enabled on all tables until DB session sets app.current_tenant_id on every connection path.
  2. ThreatEvent lacks UUID tenant_id; cross-tenant leakage at SQL layer requires join policies.
  3. Cookie absent: API guard allows header-only tenant UUID when cookie missing — tighten with mandatory session for production.