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
${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)
`,
'
',
'| Severity | Rule ID | File:Line | Message |
',
`${rows}`,
'
',
'
',
].join('');
}
function escapeHtml(value: string): string {
return value
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
function escapeAttribute(value: string): string {
return value
.replaceAll('&', '&')
.replaceAll('"', '"')
.replaceAll('<', '<')
.replaceAll('>', '>');
}