import { basename } from 'node:path'; export interface CombinedSecurityView { projectPath: string; timestamp: string; html: string | null; skipped: boolean; } export interface CombinedArchView { html: string | null; skipped: boolean; } type DockerfileSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; export interface CombinedDockerfileFinding { id: string; title: string; severity: DockerfileSeverity; file: string; line: number; message: string; confirmed: boolean; category: 'dockerfile'; } export interface CombinedDockerfileView { skipped: boolean; skippedReason: string | null; findings: CombinedDockerfileFinding[]; } export function createCombinedReport( securityReport: CombinedSecurityView, archGraph: CombinedArchView, dockerfileReport: CombinedDockerfileView, ): string { const projectName = basename(securityReport.projectPath) || securityReport.projectPath; const securityDisabled = securityReport.skipped || securityReport.html === null; const archDisabled = archGraph.skipped || archGraph.html === null; const dockerDisabled = dockerfileReport.skipped; const defaultTab = !securityDisabled ? 'security' : !archDisabled ? 'arch' : 'docker'; return ` Qwen Code S-Edition Combined Report
Qwen Code S-Edition | ${escapeHtml(projectName)} | ${escapeHtml(securityReport.timestamp)}
${escapeHtml(securityReport.projectPath)}
${securityDisabled ? '
Skipped
' : ``}
${archDisabled ? '
Skipped
' : ``}
${renderDockerfilePanel(dockerfileReport)}
`; } function renderDockerfilePanel(dockerfileReport: CombinedDockerfileView): string { if (dockerfileReport.skipped) { const reason = dockerfileReport.skippedReason ?? 'Dockerfile scan skipped'; return `
Skipped — ${escapeHtml(reason)}
`; } const findings = dockerfileReport.findings; if (findings.length === 0) { return [ '
', '
No hadolint findings.
', '
No Dockerfile issues detected.
', '
', ].join(''); } const rows = findings .map((finding) => { const severityClass = finding.severity === 'CRITICAL' ? 'severity-critical' : finding.severity === 'HIGH' ? 'severity-high' : finding.severity === 'MEDIUM' ? 'severity-medium' : 'severity-low'; return [ '', `${escapeHtml(finding.severity)}`, `${escapeHtml(finding.id)}`, `${escapeHtml(finding.file)}:${String(finding.line)}`, `${escapeHtml(finding.message)}`, '', ].join(''); }) .join(''); return [ '
', `
${String(findings.length)} finding(s)
`, '', '', `${rows}`, '
SeverityRule IDFile:LineMessage
', '
', ].join(''); } function escapeHtml(value: string): string { return value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function escapeAttribute(value: string): string { return value .replaceAll('&', '&') .replaceAll('"', '"') .replaceAll('<', '<') .replaceAll('>', '>'); }