#!/usr/bin/env tsx import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; export const REPORT_PATH = "docs/security/tenant-isolation-report.md"; export const S04_SUMMARY_PATH = "docs/security/tenant-isolation-s04-summary.md"; export const REQUIRED_ISOLATION_TEST_PATHS = [ "apps/web/app/test/isolation/admin-routes.test.ts", "apps/web/app/test/isolation/fixtures/user-session-isolation.test.ts", ] as const; export const ISOLATION_TEST_ROOTS = ["apps/web/app/test/isolation", "apps/api/src/isolation"] as const; type EvidenceSource = { path: string; content: string; }; export type IsolationTestEvidence = { path: string; testCount: number; boundaries: string[]; denyPathEvidence: string[]; }; export type TenantIsolationEvidence = { generatedFrom: string[]; tests: IsolationTestEvidence[]; s04: { path: string; verificationStatus: string; summary: string; patterns: string[]; }; }; export type CheckTenantIsolationReportResult = { ok: boolean; reportPath: string; failures: string[]; }; type CliOptions = { rootDir: string; check: boolean; }; const TEST_CASE_PATTERN = /\b(?:it|test)\(\s*(["'`])([\s\S]*?)\1\s*,/g; const TENANT_ID_PATTERN = /\b(?:tenant|row|relationship|api[_-]?key|log|user|session|token|bearer)[_-](?:session[_-])?(?:iso[_-])?[a-z0-9_-]+\b/gi; const EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi; const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi; const TOKEN_ASSIGNMENT_PATTERN = /\b(?:token|apiKey|api_key|secret|password|bearer)\s*[:=]\s*["'`][^"'`]+["'`]/gi; const LONG_SECRET_PATTERN = /\b[A-Za-z0-9_-]{24,}\b/g; const SOURCE_SNIPPET_PATTERN = /[`{};]/g; const BOUNDARY_RULES: readonly { pattern: RegExp; label: string }[] = [ { pattern: /admin permission|admin auth|adminOnly|verifyUserHasPermission/i, label: "admin permission gate" }, { pattern: /resolver tenant|tenant, user, and entity|tenantId|userId/i, label: "tenant and user scoped resolver" }, { pattern: /relationship resolver|parent and child|authorized rows/i, label: "relationship parent/child authorization" }, { pattern: /row logs|API-key log|setApiKeyLogStatus|status log/i, label: "API-key status logging boundary" }, { pattern: /reportUsage|usage/i, label: "tenant-scoped usage reporting" }, ]; const DENY_RULES: readonly { pattern: RegExp; label: string }[] = [ { pattern: /permission is denied|auth is denied|denied/i, label: "denied credentials stop downstream access" }, { pattern: /returns 403|status: 403|, 403/i, label: "403 response for unauthorized tenant access" }, { pattern: /not\.toHaveBeenCalled|skips|not called/i, label: "denied path skips data mutation or lookup" }, { pattern: /foreign|tenant B|cross-tenant/i, label: "foreign tenant resource denied" }, ]; function normalizeRelativePath(relativePath: string): string { return relativePath.split(path.sep).join("/"); } function uniqueSorted(values: Iterable): string[] { return [...new Set(values)].sort((a, b) => a.localeCompare(b)); } function readRequiredFile(rootDir: string, relativePath: string): EvidenceSource { const normalizedPath = normalizeRelativePath(relativePath); const absolutePath = path.join(rootDir, normalizedPath); if (!fs.existsSync(absolutePath)) { throw new Error(`missing expected isolation evidence file: ${normalizedPath}`); } return { path: normalizedPath, content: fs.readFileSync(absolutePath, "utf8") }; } function walkTestFiles(rootDir: string, relativeRoot: string): string[] { const absoluteRoot = path.join(rootDir, relativeRoot); if (!fs.existsSync(absoluteRoot)) return []; const discovered: string[] = []; const walk = (absoluteDir: string) => { for (const entry of fs.readdirSync(absoluteDir, { withFileTypes: true })) { const absolutePath = path.join(absoluteDir, entry.name); if (entry.isDirectory()) { walk(absolutePath); } else if (entry.isFile() && /\.test\.[cm]?[tj]sx?$/.test(entry.name)) { discovered.push(normalizeRelativePath(path.relative(rootDir, absolutePath))); } } }; walk(absoluteRoot); return discovered; } export function discoverIsolationTestPaths(rootDir = process.cwd()): string[] { return uniqueSorted([ ...REQUIRED_ISOLATION_TEST_PATHS, ...ISOLATION_TEST_ROOTS.flatMap((testRoot) => walkTestFiles(rootDir, testRoot)), ]); } function collectLabels(content: string, rules: readonly { pattern: RegExp; label: string }[]): string[] { return uniqueSorted(rules.filter((rule) => rule.pattern.test(content)).map((rule) => rule.label)); } function extractTestNames(content: string): string[] { return [...content.matchAll(TEST_CASE_PATTERN)].map((match) => match[2].replace(/\s+/g, " ").trim()).filter(Boolean); } function parseS04Summary(source: EvidenceSource): TenantIsolationEvidence["s04"] { const verificationStatus = source.content.match(/^verification_result:\s*(.+)$/m)?.[1]?.trim() ?? "unknown"; const headingSummary = source.content.match(/^\*\*(.+?)\*\*/m)?.[1]?.trim() ?? "S04 summary available."; const patternBlock = source.content.match(/^patterns_established:\n([\s\S]*?)(?:\n[a-z_]+:|\n---)/m)?.[1] ?? ""; const patterns = uniqueSorted([...patternBlock.matchAll(/^\s*-\s+(.+)$/gm)].map((match) => sanitizeSummaryText(match[1]))); return { path: source.path, verificationStatus: sanitizeSummaryText(verificationStatus), summary: sanitizeSummaryText(headingSummary), patterns, }; } function sanitizeSummaryText(value: string): string { return value .replace(EMAIL_PATTERN, "[redacted email]") .replace(TOKEN_ASSIGNMENT_PATTERN, "[redacted credential]") .replace(UUID_PATTERN, "[redacted id]") .replace(TENANT_ID_PATTERN, (match) => categoricalLabelFor(match)) .replace(LONG_SECRET_PATTERN, "[redacted token]") .replace(SOURCE_SNIPPET_PATTERN, "") .replace(/\s+/g, " ") .trim(); } function categoricalLabelFor(value: string): string { const lower = value.toLowerCase(); if (lower.includes("tenant") && /(?:_a|tenanta|tenant-a| a\b)/i.test(value)) return "tenant A"; if (lower.includes("tenant") && /(?:_b|tenantb|tenant-b| b\b)/i.test(value)) return "tenant B"; if (lower.includes("admin")) return "admin"; if (lower.includes("relationship")) return "relationship record"; if (lower.includes("row")) return "row record"; if (lower.includes("api")) return "API credential"; if (lower.includes("user")) return "user session"; return "[redacted id]"; } function summarizeIsolationTest(source: EvidenceSource): IsolationTestEvidence { const testNames = extractTestNames(source.content).map(sanitizeSummaryText); return { path: source.path, testCount: testNames.length, boundaries: collectLabels(source.content, BOUNDARY_RULES), denyPathEvidence: uniqueSorted([...testNames.filter((name) => /denied|403|foreign|skips/i.test(name)), ...collectLabels(source.content, DENY_RULES)]), }; } export function collectTenantIsolationEvidence(rootDir = process.cwd()): TenantIsolationEvidence { const tests = discoverIsolationTestPaths(rootDir).map((testPath) => summarizeIsolationTest(readRequiredFile(rootDir, testPath))).sort((a, b) => a.path.localeCompare(b.path), ); const s04Source = readRequiredFile(rootDir, S04_SUMMARY_PATH); const generatedFrom = uniqueSorted([...tests.map((test) => test.path), s04Source.path]); return { generatedFrom, tests, s04: parseS04Summary(s04Source) }; } function renderList(values: string[]): string { if (values.length === 0) return "- none observed"; return values.map((value) => `- ${value}`).join("\n"); } export function renderTenantIsolationReport(evidence: TenantIsolationEvidence): string { const testRows = evidence.tests.map((test) => { return `| ${test.path} | ${test.testCount} | ${test.boundaries.join("; ") || "none observed"} | ${test.denyPathEvidence.join("; ") || "none observed"} |`; }); return [ "# Tenant Isolation Report", "", "", "", "## Scope", "", "This report summarizes committed Tenant Isolation tenant-isolation tests and S04 evidence using categorical labels only. Fixture identifiers, credentials, row IDs, relationship IDs, tenant IDs, emails, tokens, and source snippets are intentionally redacted from this document.", "", "## Source Evidence", "", renderList(evidence.generatedFrom), "", "## Boundary Matrix", "", "| Source | Test count | Checked access boundaries | Deny-path evidence |", "| --- | ---: | --- | --- |", ...testRows, "", "## Prior Slice Evidence", "", `- Source: ${evidence.s04.path}`, `- Verification status: ${evidence.s04.verificationStatus}`, `- Summary: ${evidence.s04.summary}`, "- Established patterns:", ...evidence.s04.patterns.map((pattern) => ` - ${pattern}`), "", "## Deny-Path Coverage", "", "The boundary matrix records denial outcomes for unauthorized credentials, foreign tenant resources, skipped downstream mutations/lookups, and 403 responses. These entries are derived from test names and assertion patterns rather than concrete fixture values.", "", "## Redaction Rules", "", "The renderer reports tenant A, tenant B, admin, user session, API credential, row record, and relationship record categories instead of concrete fixture values.", "", "## Verification Commands", "", "```sh", "pnpm exec eslint scripts/tenant-isolation-report.ts scripts/tenant-isolation-report.test.ts --max-warnings=0", "pnpm exec vitest run scripts/tenant-isolation-report.test.ts", "pnpm exec tsx scripts/tenant-isolation-report.ts --check", "pnpm exec tsx scripts/docs-link-check.ts", "```", "", "## Redaction Contract", "", "The renderer reports tenant A, tenant B, admin, user session, API credential, row record, and relationship record categories instead of concrete fixture values.", "", ].join("\n"); } export function writeTenantIsolationReport(rootDir = process.cwd()): string { const reportPath = path.join(rootDir, REPORT_PATH); const report = renderTenantIsolationReport(collectTenantIsolationEvidence(rootDir)); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, report, "utf8"); return reportPath; } export function checkTenantIsolationReport(rootDir = process.cwd()): CheckTenantIsolationReportResult { const reportPath = path.join(rootDir, REPORT_PATH); const expected = renderTenantIsolationReport(collectTenantIsolationEvidence(rootDir)); if (!fs.existsSync(reportPath)) { return { ok: false, reportPath, failures: [`missing generated report: ${REPORT_PATH}`] }; } const actual = fs.readFileSync(reportPath, "utf8"); if (actual !== expected) { return { ok: false, reportPath, failures: [`stale generated report: ${REPORT_PATH}`] }; } return { ok: true, reportPath, failures: [] }; } function parseCliOptions(args: string[]): CliOptions { const options: CliOptions = { rootDir: process.cwd(), check: false }; for (const arg of args) { if (arg === "--check") { options.check = true; } else if (arg.startsWith("--root=")) { options.rootDir = path.resolve(arg.slice("--root=".length)); } else { throw new Error(`unknown argument: ${arg}`); } } return options; } function formatCliResult(status: "PASS" | "FAIL", reportPath: string, details: string[]): string { const relativeReportPath = normalizeRelativePath(path.relative(process.cwd(), reportPath)); return [`${status} tenant-isolation-report ${relativeReportPath}`, ...details].join("\n") + "\n"; } export function runTenantIsolationReportCli(args = process.argv.slice(2)): number { try { const options = parseCliOptions(args); if (options.check) { const result = checkTenantIsolationReport(options.rootDir); const output = formatCliResult(result.ok ? "PASS" : "FAIL", result.reportPath, result.failures.map((failure) => `FAIL ${failure}`)); (result.ok ? process.stdout : process.stderr).write(output); return result.ok ? 0 : 1; } const reportPath = writeTenantIsolationReport(options.rootDir); process.stdout.write(formatCliResult("PASS", reportPath, ["wrote generated report"])); return 0; } catch (error) { const message = error instanceof Error ? error.message : "unknown tenant isolation report failure"; process.stderr.write(`FAIL tenant-isolation-report ${message}\n`); return 1; } } if (process.argv[1] === fileURLToPath(import.meta.url)) { process.exitCode = runTenantIsolationReportCli(); }