/** * Pure helpers for shaping Codex Security scan results into agent-friendly * text. Kept free of SDK imports so unit tests run with node --experimental-strip-types. */ export const SEVERITY_LEVELS = [ "critical", "high", "medium", "low", "informational", ] as const; export type Severity = (typeof SEVERITY_LEVELS)[number]; const SEVERITY_RANK: Record = { critical: 0, high: 1, medium: 2, low: 3, informational: 4, }; /** Minimal structural view of a finding — matches the SDK's Finding shape. */ export interface FindingLike { findingId?: string; title?: string; summary?: string; severity?: { level?: string }; confidence?: { level?: string }; taxonomy?: { category?: string; cwe?: string[] }; locations?: { path?: string; startLine?: number; endLine?: number }[]; remediation?: string; } export function normalizeSeverity(level: unknown): Severity { if (typeof level === "string") { const lower = level.toLowerCase(); for (const severity of SEVERITY_LEVELS) { if (severity === lower) return severity; } } return "informational"; } export function countBySeverity( findings: readonly FindingLike[], ): Record { const counts: Record = { critical: 0, high: 0, medium: 0, low: 0, informational: 0, }; for (const finding of findings) { counts[normalizeSeverity(finding.severity?.level)] += 1; } return counts; } export function meetsMinSeverity( finding: FindingLike, minSeverity: Severity, ): boolean { return ( SEVERITY_RANK[normalizeSeverity(finding.severity?.level)] <= SEVERITY_RANK[minSeverity] ); } export function sortFindings( findings: readonly FindingLike[], ): FindingLike[] { return [...findings].sort((a, b) => { const bySeverity = SEVERITY_RANK[normalizeSeverity(a.severity?.level)] - SEVERITY_RANK[normalizeSeverity(b.severity?.level)]; if (bySeverity !== 0) return bySeverity; return (a.title ?? "").localeCompare(b.title ?? ""); }); } export function formatLocation(finding: FindingLike): string { const location = finding.locations?.[0]; if (!location?.path) return "(no location)"; const start = location.startLine ?? 0; const end = location.endLine !== undefined && location.endLine !== start ? `-${location.endLine}` : ""; return `${location.path}:${start}${end}`; } function formatCountsLine(counts: Record): string { const parts = SEVERITY_LEVELS.filter((s) => counts[s] > 0).map( (s) => `${counts[s]} ${s}`, ); return parts.length > 0 ? parts.join(", ") : "no findings"; } export interface FormatScanReportOptions { findings: readonly FindingLike[]; minSeverity: Severity; /** Max findings to list individually. Counts always cover all findings. */ maxListed?: number; reportPath: string; findingsPath: string; sarifPath: string | null; scanDir: string; costText?: string; } /** Build the markdown report returned to the agent after a scan. */ export function formatScanReport(options: FormatScanReportOptions): string { const { findings, minSeverity, maxListed = 20, reportPath, findingsPath, sarifPath, scanDir, costText, } = options; const counts = countBySeverity(findings); const lines: string[] = []; if (findings.length === 0) { lines.push("## Codex Security scan: no findings", ""); lines.push("The scan completed and reported no vulnerabilities."); } else { lines.push( `## Codex Security scan: ${findings.length} finding(s) — ${formatCountsLine(counts)}`, "", ); const eligible = sortFindings( findings.filter((f) => meetsMinSeverity(f, minSeverity)), ); const listed = eligible.slice(0, maxListed); for (const [index, finding] of listed.entries()) { const severity = normalizeSeverity(finding.severity?.level); const confidence = finding.confidence?.level ?? "unknown"; const cwes = finding.taxonomy?.cwe?.length ? ` (${finding.taxonomy.cwe.join(", ")})` : ""; lines.push( `### ${index + 1}. [${severity.toUpperCase()}] ${finding.title ?? "Untitled finding"}`, "", `- Location: \`${formatLocation(finding)}\``, `- Confidence: ${confidence}${cwes}`, ); if (finding.summary) lines.push(`- Summary: ${finding.summary}`); if (finding.remediation) lines.push(`- Remediation: ${finding.remediation}`); lines.push(""); } const remaining = eligible.length - listed.length; if (remaining > 0) { lines.push( `…and ${remaining} more finding(s) at or above ${minSeverity} severity. Full list in ${findingsPath}.`, "", ); } const belowMin = findings.length - eligible.length; if (belowMin > 0) { lines.push( `(${belowMin} finding(s) below ${minSeverity} severity not listed.)`, "", ); } } lines.push("### Artifacts", ""); lines.push(`- Report: ${reportPath}`); lines.push(`- Findings JSON: ${findingsPath}`); if (sarifPath) lines.push(`- SARIF: ${sarifPath}`); lines.push(`- Scan directory: ${scanDir}`); if (costText) lines.push(`- Estimated cost: ${costText}`); lines.push(""); lines.push( "Read the report for full details before fixing. After fixing, re-run security_scan to verify.", ); return lines.join("\n"); } export function formatCost(cost: { model?: string; estimatedUsd?: number; inputTokens?: number; outputTokens?: number; }): string | undefined { if (typeof cost.estimatedUsd !== "number") return undefined; const tokens = typeof cost.inputTokens === "number" && typeof cost.outputTokens === "number" ? ` (${cost.inputTokens + cost.outputTokens} tokens)` : ""; return `$${cost.estimatedUsd.toFixed(4)}${cost.model ? ` on ${cost.model}` : ""}${tokens}`; }