import { z } from "zod"; const MAX_RESULTS = 5000; const MAX_MESSAGE = 2000; /** Distinct tool runs per upload; more than this is a flood, not a scan (INT-A11). */ export const MAX_RUNS = 20; const MAX_RULE_NAME = 200; const MAX_TOOL_NAME = 100; const MAX_LINE = 10_000_000; const MAX_PATH = 1024; const CRITICAL_SCORE = 9; const HIGH_SCORE = 7; const MEDIUM_SCORE = 4; const message = z.object({ text: z.string().optional(), markdown: z.string().optional() }).loose(); const region = z.object({ startLine: z.number().int().optional(), endLine: z.number().int().optional() }).loose(); const location = z .object({ physicalLocation: z .object({ artifactLocation: z.object({ uri: z.string().optional() }).loose().optional(), region: region.optional(), }) .loose() .optional(), }) .loose(); const level = z.enum(["error", "warning", "note", "none"]); const rule = z .object({ id: z.string(), name: z.string().optional(), shortDescription: message.optional(), defaultConfiguration: z.object({ level: level.optional() }).loose().optional(), properties: z.record(z.string(), z.unknown()).optional(), }) .loose(); const result = z .object({ ruleId: z.string().optional(), rule: z.object({ id: z.string().optional(), index: z.number().int().optional() }).loose().optional(), ruleIndex: z.number().int().optional(), level: level.optional(), message, locations: z.array(location).optional(), partialFingerprints: z.record(z.string(), z.string()).optional(), fingerprints: z.record(z.string(), z.string()).optional(), }) .loose(); const run = z .object({ tool: z.object({ driver: z.object({ name: z.string(), version: z.string().optional(), rules: z.array(rule).optional() }).loose(), }), results: z.array(result).optional(), }) .loose(); const sarifLog = z.object({ version: z.string(), runs: z.array(run) }).loose(); export type SarifResult = { tool: string; ruleId: string; ruleName: string | null; severity: "error" | "warning" | "note" | "none"; securitySeverity: string | null; message: string; path: string | null; startLine: number | null; endLine: number | null; fingerprint: string | null; }; function securityBucket(score: unknown): string | null { const value = typeof score === "string" ? Number(score) : typeof score === "number" ? score : Number.NaN; if (!Number.isFinite(value)) return null; if (value >= CRITICAL_SCORE) return "critical"; if (value >= HIGH_SCORE) return "high"; if (value >= MEDIUM_SCORE) return "medium"; return "low"; } function clampLine(value: number | undefined | null): number | null { if (value === undefined || value === null || !Number.isFinite(value)) return null; return Math.min(Math.max(Math.trunc(value), 1), MAX_LINE); } /** Repo-relative path: strips schemes, drive letters, leading slashes and `.`/`..` segments (INT-A31). */ export function normalizePath(uri: string | undefined): string | null { if (!uri) return null; let decoded = uri; try { decoded = decodeURIComponent(uri); } catch { // keep the raw form } const stripped = decoded .replace(/^file:\/\/+/, "") .replaceAll("\\", "/") .replace(/^\/*[A-Za-z]:\//, ""); const segments = stripped.split("/").filter((segment) => segment !== "" && segment !== "." && segment !== ".."); if (segments.length === 0) return null; return segments.join("/").slice(0, MAX_PATH); } /** Validates a SARIF 2.1 log and flattens its results, capped so one upload cannot flood D1. */ export function parseSarif(json: unknown): { results: SarifResult[]; tools: string[]; truncated: boolean } { const log = sarifLog.parse(json); const results: SarifResult[] = []; const tools = new Set(); let truncated = log.runs.length > MAX_RUNS; for (const entry of log.runs.slice(0, MAX_RUNS)) { const tool = entry.tool.driver.name.slice(0, MAX_TOOL_NAME); tools.add(tool); const rules = entry.tool.driver.rules ?? []; const rulesById = new Map(rules.map((item) => [item.id, item])); for (const item of entry.results ?? []) { if (results.length >= MAX_RESULTS) { truncated = true; break; } const index = item.ruleIndex ?? item.rule?.index; const ruleId = item.ruleId ?? item.rule?.id ?? (index !== undefined ? rules[index]?.id : undefined) ?? "unknown"; const definition = rulesById.get(ruleId) ?? (index !== undefined ? rules[index] : undefined); const physical = item.locations?.[0]?.physicalLocation; const startLine = clampLine(physical?.region?.startLine); const fingerprints = { ...item.fingerprints, ...item.partialFingerprints }; results.push({ tool, ruleId: ruleId.slice(0, 200), ruleName: (definition?.name ?? definition?.shortDescription?.text ?? null)?.slice(0, MAX_RULE_NAME) ?? null, severity: item.level ?? definition?.defaultConfiguration?.level ?? "warning", securitySeverity: securityBucket(definition?.properties?.["security-severity"]), message: (item.message.text ?? item.message.markdown ?? "").slice(0, MAX_MESSAGE), path: normalizePath(physical?.artifactLocation?.uri), startLine, endLine: clampLine(physical?.region?.endLine) ?? startLine, fingerprint: fingerprints.primaryLocationLineHash ?? Object.values(fingerprints)[0] ?? null, }); } } return { results, tools: [...tools], truncated }; }