import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { analyzeArchitectureVulnerabilities } from './arch/analyzer.js'; import { runUnderstandAnything, readKnowledgeGraph } from './arch/builder.js'; import { enrichWithSecurityMetadata } from './arch/enricher.js'; import { type ArchScanReport, type CodeFinding, type ScanReport, type VerifiedFinding, } from './index.js'; import { scanDockerfiles, type DockerfileResult } from './dockerfile-scanner.js'; import { generateArchReport } from './reporter/arch-reporter.js'; import { createCombinedReport } from './reporter/combined-reporter.js'; import { generateReport } from './reporter/reporter.js'; import { resolveAuth } from './shared/auth.js'; import { parseFindings } from './scanner/parser.js'; import { scannerReader } from './scanner/reader.js'; import { scannerRunner } from './scanner/runner.js'; import { verifyFindings } from './verifier/verifier.js'; type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'UNDEFINED'; type MinSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; const SEVERITY_WEIGHT: Record = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1, UNDEFINED: 0, }; export interface CombinedReport { projectPath: string; timestamp: string; reportPath: string; reportName: string; security: { skipped: boolean; critical: number; high: number; medium: number; low: number; verified: number; total: number; } | null; architecture: { skipped: boolean; nodes: number; edges: number; } | null; dockerfile?: { skipped: boolean; skippedReason: string | null; dockerfilesFound: number; critical: number; high: number; medium: number; low: number; total: number; } | null; } export interface CombinedScanOptions { projectPath: string; verify: boolean; includeArch: boolean; includeSecurity: boolean; verifyAll?: boolean; minSeverity?: MinSeverity; llm?: boolean; skipUa?: boolean; } function canAuthenticate(): boolean { try { resolveAuth(); return true; } catch { return false; } } function shouldIncludeSeverity(severity: Severity, minSeverity: MinSeverity): boolean { return SEVERITY_WEIGHT[severity] >= SEVERITY_WEIGHT[minSeverity]; } function buildSkippedVerificationFinding(finding: CodeFinding): VerifiedFinding { return { ...finding, qwen_verified: false, fp_analysis: { is_likely_fp: false, confidence: 100, reason: 'verification_skipped', explanation: 'Qwen verification skipped (--no-verify mode).', }, }; } function formatFileTimestamp(date: Date): string { const year = String(date.getFullYear()); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}${month}${day}-${hours}${minutes}${seconds}`; } export async function runCombinedScan(options: CombinedScanOptions): Promise { const projectPath = resolve(options.projectPath); const verify = options.verify ?? true; const includeArch = options.includeArch ?? true; const includeSecurity = options.includeSecurity ?? true; const minSeverity = options.minSeverity ?? 'LOW'; const llm = options.llm ?? true; const startedAt = new Date(); const timestamp = startedAt.toISOString(); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log('🛡️ Qwen Code S-Edition Scan'); console.log(`📁 Scanning: ${projectPath}`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(''); let securityHtml: string | null = null; let archHtml: string | null = null; let securityError: unknown = null; let archError: unknown = null; let securitySummary: CombinedReport['security'] = null; let archSummary: CombinedReport['architecture'] = null; let dockerfileSummary: CombinedReport['dockerfile'] = null; let dockerfileFindings: Array<{ id: string; title: string; severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; file: string; line: number; message: string; confirmed: boolean; category: 'dockerfile'; }> = []; if (includeSecurity) { console.log('Phase 1/4: Security Scan'); try { await scannerRunner(projectPath); const rawReport = await scannerReader(projectPath); const parsed = parseFindings(rawReport); const authAvailable = canAuthenticate(); const verificationEnabled = verify && authAvailable; const verifiedFindings = verificationEnabled ? await verifyFindings(parsed.code_findings, { verifyAll: options.verifyAll === true }) : parsed.code_findings.map((finding) => buildSkippedVerificationFinding(finding)); const confirmed = verifiedFindings.filter( (finding) => finding.fp_analysis.is_likely_fp === false && finding.fp_analysis.reason !== 'verification_failed', ); const filtered = verifiedFindings.filter((finding) => finding.fp_analysis.is_likely_fp === true); const unverified = verifiedFindings.filter((finding) => finding.fp_analysis.reason === 'verification_failed'); const confirmedForReport = [...confirmed, ...unverified].filter((finding) => shouldIncludeSeverity(finding.severity, minSeverity), ); const unverifiedBySeverity = unverified.filter((finding) => shouldIncludeSeverity(finding.severity, minSeverity)); const verifiedCount = verifiedFindings.filter((finding) => finding.fp_analysis.reason !== 'verification_skipped_quota').length; const bySeverity: Record = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0, UNDEFINED: 0, }; for (const finding of confirmedForReport) { bySeverity[finding.severity] = (bySeverity[finding.severity] ?? 0) + 1; } const reportData: ScanReport = { meta: { project_path: projectPath, timestamp, supreme2l_version: rawReport.supreme2l_version, s_edition_version: '1.0.0', duration_seconds: Math.max(1, Math.round((Date.now() - startedAt.getTime()) / 1000)), raw_json_filename: '', verification_limited: verifiedFindings.some( (finding) => finding.fp_analysis.reason === 'verification_skipped_quota', ), total_findings_scanned: parsed.code_findings.length, }, summary: { total_scanned: rawReport.scan_summary.total_issues, confirmed: confirmedForReport.length, false_positives_filtered: filtered.length, unverified: unverifiedBySeverity.length, dependency_cves: parsed.cve_findings.length, by_severity: bySeverity, }, confirmed_findings: confirmedForReport, dependency_cves: parsed.cve_findings, filtered_fp: filtered, }; const generated = await generateReport(projectPath, reportData); securityHtml = await readFile(generated.verifiedPath, 'utf8'); securitySummary = { skipped: false, critical: bySeverity.CRITICAL ?? 0, high: bySeverity.HIGH ?? 0, medium: bySeverity.MEDIUM ?? 0, low: bySeverity.LOW ?? 0, verified: verifiedCount, total: confirmedForReport.length, }; console.log(`✓ Security scan complete: ${String(confirmedForReport.length)} findings (${String(verifiedCount)} verified)`); } catch (error) { securityError = error; console.error(`✗ Security scan failed: ${error instanceof Error ? error.message : String(error)}`); } console.log(''); } else { securitySummary = { skipped: true, critical: 0, high: 0, medium: 0, low: 0, verified: 0, total: 0, }; console.log('Phase 1/4: Security Scan'); console.log('↷ Security scan skipped (--no-security)'); console.log(''); } if (includeArch) { console.log('Phase 2/4: Architecture Scan'); try { await runUnderstandAnything( projectPath, { skipUa: options.skipUa === true }, (message) => console.log(` ${message}`), ); const graph = await readKnowledgeGraph(projectPath); const enrichedGraph = enrichWithSecurityMetadata(graph); const authAvailable = canAuthenticate(); const llmEnabled = llm && authAvailable; const vulnerabilities = llmEnabled ? await analyzeArchitectureVulnerabilities(enrichedGraph, projectPath) : []; const archReport: ArchScanReport = { meta: { project_path: projectPath, timestamp, s_edition_version: '1.0.0', duration_seconds: Math.max(1, Math.round((Date.now() - startedAt.getTime()) / 1000)), graph_stats: { total_nodes: enrichedGraph.nodes.length, total_edges: enrichedGraph.edges.length, used_fallback: enrichedGraph.edges.length === 0, }, }, vulnerabilities, graph: enrichedGraph, }; const archReportPath = await generateArchReport(projectPath, archReport); archHtml = await readFile(archReportPath, 'utf8'); archSummary = { skipped: false, nodes: enrichedGraph.nodes.length, edges: enrichedGraph.edges.length, }; console.log(`✓ Architecture scan complete: ${String(enrichedGraph.nodes.length)} nodes, ${String(enrichedGraph.edges.length)} edges`); } catch (error) { archError = error; console.error(`✗ Architecture scan failed: ${error instanceof Error ? error.message : String(error)}`); } console.log(''); } else { archSummary = { skipped: true, nodes: 0, edges: 0, }; console.log('Phase 2/4: Architecture Scan'); console.log('↷ Architecture scan skipped (--no-arch)'); console.log(''); } try { const dockerfileResult: DockerfileResult = await scanDockerfiles(projectPath); dockerfileFindings = dockerfileResult.findings; dockerfileSummary = { skipped: dockerfileResult.skipped, skippedReason: dockerfileResult.skippedReason, dockerfilesFound: dockerfileResult.dockerfilesFound, critical: dockerfileResult.critical, high: dockerfileResult.high, medium: dockerfileResult.medium, low: dockerfileResult.low, total: dockerfileResult.total, }; if (dockerfileResult.skipped) { console.log(`Phase 3/4: Dockerfile Scan [SKIPPED — ${dockerfileResult.skippedReason ?? 'unknown reason'}]`); } else { console.log('Phase 3/4: Dockerfile Scan'); console.log(`→ Found ${String(dockerfileResult.dockerfilesFound)} Dockerfiles`); console.log( `→ ${String(dockerfileResult.critical)} CRITICAL ${String(dockerfileResult.high)} HIGH ${String(dockerfileResult.medium)} MEDIUM ${String(dockerfileResult.low)} LOW`, ); } } catch (error) { dockerfileSummary = { skipped: true, skippedReason: `Dockerfile scan failed: ${error instanceof Error ? error.message : String(error)}`, dockerfilesFound: 0, critical: 0, high: 0, medium: 0, low: 0, total: 0, }; console.log(`Phase 3/4: Dockerfile Scan [SKIPPED — ${dockerfileSummary.skippedReason}]`); } console.log(''); const attemptedCount = Number(includeSecurity) + Number(includeArch); const failedCount = Number(includeSecurity && securityError !== null) + Number(includeArch && archError !== null); if (attemptedCount > 0 && failedCount === attemptedCount) { throw new Error('Both scan phases failed.'); } console.log('Phase 4/4: Generating combined report...'); const reportFileName = `scan-${formatFileTimestamp(new Date())}.html`; const reportPath = resolve(projectPath, '.supreme2l', 'reports', reportFileName); const combinedHtml = createCombinedReport( { projectPath, timestamp, html: securityHtml, skipped: !includeSecurity, }, { html: archHtml, skipped: !includeArch, }, { skipped: dockerfileSummary?.skipped ?? true, skippedReason: dockerfileSummary?.skippedReason ?? 'Dockerfile scan did not run', findings: dockerfileSummary?.skipped ? [] : dockerfileFindings, }, ); await mkdir(dirname(reportPath), { recursive: true }); await writeFile(reportPath, combinedHtml, 'utf8'); console.log(`✓ Report saved: .supreme2l/reports/${reportFileName}`); console.log(''); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log('📊 RESULTS SUMMARY'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); if (securitySummary !== null && securitySummary.skipped) { console.log('Security: Skipped'); } else { console.log('Security:'); console.log(` 🔴 HIGH: ${String(securitySummary?.high ?? 0)}`); console.log(` 🟠 MEDIUM: ${String(securitySummary?.medium ?? 0)}`); console.log(` 🟡 LOW: ${String(securitySummary?.low ?? 0)}`); } if (archSummary !== null && archSummary.skipped) { console.log('Architecture: Skipped'); } else { console.log('Architecture:'); console.log(` 📦 Files: ${String(archSummary?.nodes ?? 0)}`); console.log(` 🔗 Edges: ${String(archSummary?.edges ?? 0)}`); } if (dockerfileSummary !== null && dockerfileSummary.skipped) { console.log(`Dockerfile: Skipped (${dockerfileSummary.skippedReason ?? 'unknown reason'})`); } else { console.log('Dockerfile:'); console.log(` 🔴 CRITICAL: ${String(dockerfileSummary?.critical ?? 0)}`); console.log(` 🟠 HIGH: ${String(dockerfileSummary?.high ?? 0)}`); console.log(` 🟡 MEDIUM: ${String(dockerfileSummary?.medium ?? 0)}`); console.log(` 🔵 LOW: ${String(dockerfileSummary?.low ?? 0)}`); } console.log(''); console.log(`Open report: ${reportFileName}`); return { projectPath, timestamp, reportPath, reportName: reportFileName, security: securitySummary, architecture: archSummary, dockerfile: dockerfileSummary, }; }