import { basename } from 'node:path'; import type { CveFinding, ScanReport, VerifiedFinding } from '../index.js'; import { makeVSCodeLink } from './vscode-links.js'; const SEVERITY_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'UNDEFINED'] as const; type Severity = (typeof SEVERITY_ORDER)[number]; const S_EDITION_VERSION = '1.0.0'; const SEVERITY_COLORS: Record = { CRITICAL: '#ff4444', HIGH: '#ff8c00', MEDIUM: '#ffd700', LOW: '#4caf50', UNDEFINED: '#6e7681', }; /** * Generates a standalone, file://-compatible HTML report for S-Edition scan results. */ export function generateHTML(report: ScanReport, options?: { isRaw?: boolean }): string { const isRaw = options?.isRaw === true; const projectName = basename(report.meta.project_path) || report.meta.project_path; const confirmedBySeverity = countBySeverity(report.confirmed_findings); const confirmedFindings = sortBySeverity(report.confirmed_findings); const dependencyCves = sortBySeverity(report.dependency_cves); const filteredFindings = sortBySeverity(report.filtered_fp); const confirmedCards = confirmedFindings.map((finding) => renderConfirmedCard(finding, report.meta.project_path)).join('\n'); const cveCards = dependencyCves.map((finding) => renderCveCard(finding)).join('\n'); const filteredCards = filteredFindings.map((finding) => renderFilteredCard(finding, report.meta.project_path)).join('\n'); const confirmedSummary = `${report.summary.confirmed} confirmed · ${report.summary.false_positives_filtered} false positives filtered · ${report.summary.dependency_cves} CVEs`; return ` S-Edition Verification Report ${ isRaw ? '
⚠️ RAW REPORT — Not verified for false positives. See s-edition-report-{ts}.html for verified results.
' : '' }

${escapeHtml(projectName)} Security Verification Report

${escapeHtml(report.meta.project_path)}
Scan date: ${escapeHtml(formatTimestamp(report.meta.timestamp))} · supreme2l v${escapeHtml(report.meta.supreme2l_version)} · S-Edition v${S_EDITION_VERSION} · Duration ${escapeHtml(String(report.meta.duration_seconds))}s
${renderSeverityPill('CRITICAL', confirmedBySeverity.CRITICAL ?? 0)} ${renderSeverityPill('HIGH', confirmedBySeverity.HIGH ?? 0)} ${renderSeverityPill('MEDIUM', confirmedBySeverity.MEDIUM ?? 0)} ${renderSeverityPill('LOW', confirmedBySeverity.LOW ?? 0)}
${escapeHtml(confirmedSummary)}
${ isRaw ? '' : `` }

Confirmed Vulnerabilities

${confirmedCards || '
No confirmed vulnerabilities.
'}

Dependency CVEs

${cveCards || '
No dependency CVEs.
'}
`; } function renderSeverityPill(severity: string, value: number): string { const color = SEVERITY_COLORS[severity] ?? '#6e7681'; return `
${escapeHtml(severity)}${value}
`; } function renderConfirmedCard(finding: VerifiedFinding, projectPath: string): string { const severity = normalizeSeverity(finding.severity); const line = typeof finding.line === 'number' ? finding.line : 1; const vscodeLink = finding.vscode_link || makeVSCodeLink(finding.file, line); const cweBlock = renderCweBlock(finding.cwe); const relativePath = toRelativePath(finding.file, projectPath); const confidence = Math.round(finding.fp_analysis.confidence); const explanation = finding.fp_analysis.explanation.trim() || 'No explanation provided.'; return `
${escapeHtml(severity)}

${escapeHtml(truncateText(finding.issue, 80))}

Scanner: ${escapeHtml(finding.scanner)}
File: ${escapeHtml(relativePath)} · Line: ${line}
Code Snippet
${renderSnippet(finding.snippet)}
Qwen Analysis

${escapeHtml(explanation)}

Confidence: ${Number.isFinite(confidence) ? confidence : 0}%
${cweBlock}
`; } function renderCveCard(finding: CveFinding): string { const severity = normalizeSeverity(finding.severity); const links = collectAdvisoryLinks(finding); const advisoryButtons = links .map((link) => `${escapeHtml(link.label)} ↗`) .join(''); return `
${escapeHtml(severity)}

${escapeHtml(finding.issue)}

Scanner: ${escapeHtml(finding.scanner)}
${advisoryButtons || 'No advisory links available.'}
`; } function renderFilteredCard(finding: VerifiedFinding, projectPath: string): string { const severity = normalizeSeverity(finding.severity); const line = typeof finding.line === 'number' ? finding.line : 1; const relativePath = toRelativePath(finding.file, projectPath); const reason = (finding.fp_analysis.reason ?? 'No reason provided').trim(); const explanation = finding.fp_analysis.explanation.trim() || 'No explanation provided.'; return `
${escapeHtml(severity)}

${escapeHtml(finding.issue)}

File: ${escapeHtml(relativePath)} · Line: ${line}
Qwen FP Analysis

Reason: ${escapeHtml(reason)}

${escapeHtml(explanation)}

`; } function renderSnippet(snippet: string | null): string { if (snippet === null) { return '
(snippet unavailable)
'; } const lines = snippet.split('\n'); const formatted = lines .map((line) => { const className = line.startsWith('→ ') ? 'snippet-line vuln' : 'snippet-line'; return `${escapeHtml(line)}`; }) .join(''); return `
${formatted}
`; } function renderCweBlock(cwe: string | null): string { if (cwe === null) { return ''; } const match = cwe.match(/CWE-(\d+)/i); if (match === null) { return `
CWE: ${escapeHtml(cwe)}
`; } const cweId = `CWE-${match[1]}`; const href = `https://cwe.mitre.org/data/definitions/${match[1]}.html`; return `
CWE: ${escapeHtml(cweId)} ↗
`; } function collectAdvisoryLinks(finding: CveFinding): Array<{ label: string; href: string }> { const links: Array<{ label: string; href: string }> = []; for (const id of finding.cve_ids) { links.push({ label: 'View on NVD', href: `https://nvd.nist.gov/vuln/detail/${id}`, }); } for (const id of finding.ghsa_ids) { links.push({ label: 'View on GitHub Advisory', href: `https://github.com/advisories/${id}`, }); } if (links.length === 0) { for (const advisoryLink of finding.advisory_links) { if (advisoryLink.includes('nvd.nist.gov')) { links.push({ label: 'View on NVD', href: advisoryLink }); } else if (advisoryLink.includes('github.com/advisories')) { links.push({ label: 'View on GitHub Advisory', href: advisoryLink }); } } } const deduped = new Map(); for (const link of links) { deduped.set(link.href, link); } return [...deduped.values()]; } function countBySeverity(findings: Array<{ severity: string }>): Record { const counts: Record = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0, UNDEFINED: 0, }; for (const finding of findings) { const severity = normalizeSeverity(finding.severity); counts[severity] = (counts[severity] ?? 0) + 1; } return counts; } function sortBySeverity(findings: T[]): T[] { return [...findings].sort((left, right) => { const leftIndex = SEVERITY_ORDER.indexOf(normalizeSeverity(left.severity)); const rightIndex = SEVERITY_ORDER.indexOf(normalizeSeverity(right.severity)); return leftIndex - rightIndex; }); } function normalizeSeverity(input: string): Severity { const upper = input.toUpperCase(); if (SEVERITY_ORDER.includes(upper as Severity)) { return upper as Severity; } return 'UNDEFINED'; } function toRelativePath(filePath: string, projectPath: string): string { const normalizedFile = filePath.replace(/\\/g, '/'); const normalizedProject = projectPath.replace(/\\/g, '/').replace(/\/+$/, ''); const prefix = `${normalizedProject}/`; if (normalizedFile.startsWith(prefix)) { return normalizedFile.slice(prefix.length); } return filePath; } function truncateText(value: string, maxLength: number): string { if (value.length <= maxLength) { return value; } return `${value.slice(0, maxLength - 1)}…`; } function formatTimestamp(value: string): string { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return date.toLocaleString(); } function escapeHtml(value: string): string { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escapeAttribute(value: string): string { return escapeHtml(value).replace(/`/g, '`'); }