vettcode-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +20 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/ast-extractor.js +519 -0
- package/dist/cli-scan-orchestrator.js +336 -0
- package/dist/cli.js +208 -0
- package/dist/control-flow-analyzer.js +184 -0
- package/dist/data-flow-analyzer.js +197 -0
- package/dist/enhanced-patterns.js +457 -0
- package/dist/file-collector.js +132 -0
- package/dist/ignore-patterns.js +225 -0
- package/dist/openrouter.js +311 -0
- package/dist/patterns.js +248 -0
- package/dist/prompts.js +144 -0
- package/dist/reference-graph.js +415 -0
- package/dist/report-generator.js +128 -0
- package/dist/scan-priority.js +49 -0
- package/dist/smart-scan-orchestrator.js +878 -0
- package/dist/static-analyzer.js +1681 -0
- package/dist/types.js +2 -0
- package/dist/verification-layer.js +525 -0
- package/package.json +61 -0
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Smart Scan Orchestrator
|
|
4
|
+
* Implements the full pipeline:
|
|
5
|
+
* 1. Static Analysis → 2. AST Extraction → 3. AI Analysis → 4. Verification → 5. Report
|
|
6
|
+
* Plus additional scanners: Security (npm-audit, Snyk), Performance (SonarJS, Clinic.js), Stress (Artillery, Autocannon)
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.defaultScannerConfig = void 0;
|
|
10
|
+
exports.runSmartScan = runSmartScan;
|
|
11
|
+
const static_analyzer_1 = require("./static-analyzer");
|
|
12
|
+
const ast_extractor_1 = require("./ast-extractor");
|
|
13
|
+
const verification_layer_1 = require("./verification-layer");
|
|
14
|
+
const scan_priority_1 = require("./scan-priority");
|
|
15
|
+
const scanners_1 = require("./scanners");
|
|
16
|
+
var scanners_2 = require("./scanners");
|
|
17
|
+
Object.defineProperty(exports, "defaultScannerConfig", { enumerable: true, get: function () { return scanners_2.defaultScannerConfig; } });
|
|
18
|
+
const PARALLEL_AI_CALLS = 3; // Use all 3 API keys in parallel
|
|
19
|
+
const DEEP_SCAN_PARALLEL = 12; // Deep scan uses 12 parallel workers (4x multiplier)
|
|
20
|
+
/**
|
|
21
|
+
* Run additional scanners in parallel
|
|
22
|
+
*/
|
|
23
|
+
async function runAdditionalScanners(files, config) {
|
|
24
|
+
const results = {};
|
|
25
|
+
const scannerPromises = [];
|
|
26
|
+
if (config.enableNpmAudit) {
|
|
27
|
+
scannerPromises.push((0, scanners_1.scanWithNpmAudit)(files).then(result => {
|
|
28
|
+
results.npmAudit = result;
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
if (config.enableSnyk) {
|
|
32
|
+
scannerPromises.push((0, scanners_1.scanWithSnyk)(files).then(result => {
|
|
33
|
+
results.snyk = result;
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
if (config.enableSonarJS) {
|
|
37
|
+
scannerPromises.push((0, scanners_1.scanWithSonarJS)(files).then(result => {
|
|
38
|
+
results.sonarJS = result;
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
if (config.enableClinic) {
|
|
42
|
+
scannerPromises.push((0, scanners_1.scanWithClinic)(files).then(result => {
|
|
43
|
+
results.clinic = result;
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
if (config.enableArtillery) {
|
|
47
|
+
scannerPromises.push((0, scanners_1.scanWithArtillery)(files).then(result => {
|
|
48
|
+
results.artillery = result;
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
if (config.enableAutocannon) {
|
|
52
|
+
scannerPromises.push((0, scanners_1.scanWithAutocannon)(files).then(result => {
|
|
53
|
+
results.autocannon = result;
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
await Promise.allSettled(scannerPromises);
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Convert scanner results to verified findings format
|
|
61
|
+
*/
|
|
62
|
+
function convertScannerResultsToFindings(scannerResults) {
|
|
63
|
+
const findings = [];
|
|
64
|
+
// Convert npm-audit results
|
|
65
|
+
if (scannerResults.npmAudit) {
|
|
66
|
+
for (const vuln of scannerResults.npmAudit.vulnerabilities) {
|
|
67
|
+
findings.push({
|
|
68
|
+
id: `npm-audit-${vuln.id}`,
|
|
69
|
+
severity: vuln.severity === "critical" ? "critical" : vuln.severity === "high" ? "high" : vuln.severity === "moderate" ? "medium" : "low",
|
|
70
|
+
category: "security",
|
|
71
|
+
title: vuln.title,
|
|
72
|
+
description: `Known vulnerability in ${vuln.package}@${vuln.version}`,
|
|
73
|
+
file: "package.json",
|
|
74
|
+
line: 1,
|
|
75
|
+
evidence: `Package: ${vuln.package}, Version: ${vuln.version}`,
|
|
76
|
+
mitigation: vuln.recommendation,
|
|
77
|
+
prevention: "Regularly update dependencies and run npm audit",
|
|
78
|
+
confidence: "high",
|
|
79
|
+
verificationStatus: "confirmed",
|
|
80
|
+
verificationNotes: "Detected by npm-audit scanner",
|
|
81
|
+
sources: ["npm-audit"],
|
|
82
|
+
source: "scanner",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Convert Snyk results
|
|
87
|
+
if (scannerResults.snyk) {
|
|
88
|
+
for (const vuln of scannerResults.snyk.vulnerabilities) {
|
|
89
|
+
findings.push({
|
|
90
|
+
id: `snyk-${vuln.id}`,
|
|
91
|
+
severity: vuln.severity === "critical" ? "critical" : vuln.severity === "high" ? "high" : vuln.severity === "medium" ? "medium" : "low",
|
|
92
|
+
category: "security",
|
|
93
|
+
title: vuln.title,
|
|
94
|
+
description: vuln.description,
|
|
95
|
+
file: vuln.package,
|
|
96
|
+
line: 1,
|
|
97
|
+
evidence: `CVSS Score: ${vuln.cvssScore}, CWE: ${vuln.cwe?.join(", ")}`,
|
|
98
|
+
mitigation: vuln.remediation,
|
|
99
|
+
prevention: "Regularly update dependencies and run Snyk scans",
|
|
100
|
+
confidence: "high",
|
|
101
|
+
verificationStatus: "confirmed",
|
|
102
|
+
verificationNotes: "Detected by Snyk scanner",
|
|
103
|
+
sources: ["snyk"],
|
|
104
|
+
source: "scanner",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Convert SonarJS results
|
|
109
|
+
if (scannerResults.sonarJS) {
|
|
110
|
+
for (const issue of scannerResults.sonarJS.issues) {
|
|
111
|
+
findings.push({
|
|
112
|
+
id: `sonarjs-${issue.id}`,
|
|
113
|
+
severity: issue.severity === "suggestion" ? "low" : issue.severity === "blocker" || issue.severity === "critical" ? "critical" : issue.severity === "major" ? "high" : issue.severity === "minor" ? "medium" : "low",
|
|
114
|
+
category: issue.type === "bug" ? "logic" : issue.type === "vulnerability" ? "security" : "code-quality",
|
|
115
|
+
title: issue.message,
|
|
116
|
+
description: `${issue.rule}: ${issue.message}`,
|
|
117
|
+
file: issue.file,
|
|
118
|
+
line: issue.line,
|
|
119
|
+
evidence: `Rule: ${issue.rule}`,
|
|
120
|
+
mitigation: "Refactor code according to SonarJS recommendations",
|
|
121
|
+
prevention: "Run SonarJS regularly during development",
|
|
122
|
+
confidence: "medium",
|
|
123
|
+
verificationStatus: "confirmed",
|
|
124
|
+
verificationNotes: "Detected by SonarJS scanner",
|
|
125
|
+
sources: ["sonarjs"],
|
|
126
|
+
source: "scanner",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Convert Clinic.js results
|
|
131
|
+
if (scannerResults.clinic) {
|
|
132
|
+
for (const issue of scannerResults.clinic.performanceIssues) {
|
|
133
|
+
findings.push({
|
|
134
|
+
id: `clinic-${issue.id}`,
|
|
135
|
+
severity: issue.severity === "high" ? "high" : issue.severity === "medium" ? "medium" : "low",
|
|
136
|
+
category: "performance",
|
|
137
|
+
title: issue.message,
|
|
138
|
+
description: `${issue.type}: ${issue.message}`,
|
|
139
|
+
file: issue.file,
|
|
140
|
+
line: issue.line || 1,
|
|
141
|
+
evidence: `Type: ${issue.type}`,
|
|
142
|
+
mitigation: issue.recommendation,
|
|
143
|
+
prevention: "Run Clinic.js performance profiling regularly",
|
|
144
|
+
confidence: "medium",
|
|
145
|
+
verificationStatus: "confirmed",
|
|
146
|
+
verificationNotes: "Detected by Clinic.js scanner",
|
|
147
|
+
sources: ["clinic"],
|
|
148
|
+
source: "scanner",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Convert Artillery results
|
|
153
|
+
if (scannerResults.artillery) {
|
|
154
|
+
for (const issue of scannerResults.artillery.stressTestIssues) {
|
|
155
|
+
findings.push({
|
|
156
|
+
id: `artillery-${issue.id}`,
|
|
157
|
+
severity: issue.severity === "high" ? "high" : issue.severity === "medium" ? "medium" : "low",
|
|
158
|
+
category: "performance",
|
|
159
|
+
title: issue.message,
|
|
160
|
+
description: `${issue.type}: ${issue.message}`,
|
|
161
|
+
file: issue.file,
|
|
162
|
+
line: issue.line || 1,
|
|
163
|
+
evidence: `Type: ${issue.type}`,
|
|
164
|
+
mitigation: issue.recommendation,
|
|
165
|
+
prevention: "Run load testing with Artillery regularly",
|
|
166
|
+
confidence: "medium",
|
|
167
|
+
verificationStatus: "confirmed",
|
|
168
|
+
verificationNotes: "Detected by Artillery scanner",
|
|
169
|
+
sources: ["artillery"],
|
|
170
|
+
source: "scanner",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Convert Autocannon results
|
|
175
|
+
if (scannerResults.autocannon) {
|
|
176
|
+
for (const issue of scannerResults.autocannon.benchmarkIssues) {
|
|
177
|
+
findings.push({
|
|
178
|
+
id: `autocannon-${issue.id}`,
|
|
179
|
+
severity: issue.severity === "high" ? "high" : issue.severity === "medium" ? "medium" : "low",
|
|
180
|
+
category: "performance",
|
|
181
|
+
title: issue.message,
|
|
182
|
+
description: `${issue.type}: ${issue.message}`,
|
|
183
|
+
file: issue.file,
|
|
184
|
+
line: issue.line || 1,
|
|
185
|
+
evidence: `Type: ${issue.type}`,
|
|
186
|
+
mitigation: issue.recommendation,
|
|
187
|
+
prevention: "Run benchmarking with Autocannon regularly",
|
|
188
|
+
confidence: "medium",
|
|
189
|
+
verificationStatus: "confirmed",
|
|
190
|
+
verificationNotes: "Detected by Autocannon scanner",
|
|
191
|
+
sources: ["autocannon"],
|
|
192
|
+
source: "scanner",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return findings;
|
|
197
|
+
}
|
|
198
|
+
async function runSmartScan(projectName, files, ignoredCount, onProgress, mode = "quick", scannerConfig = scanners_1.defaultScannerConfig, // Configuration for additional scanners
|
|
199
|
+
allFilePaths // All file paths (before filtering) for building complete file tree
|
|
200
|
+
) {
|
|
201
|
+
const aiFiles = mode === "quick" ? (0, scan_priority_1.selectFilesForQuickScan)(files) : files;
|
|
202
|
+
const staticScopeLabel = mode === "quick"
|
|
203
|
+
? `${aiFiles.length} priority files (+ full-repo static pass)`
|
|
204
|
+
: `${files.length} files`;
|
|
205
|
+
// Phase 1: Static Analysis (fast, catches 70-80% of issues)
|
|
206
|
+
onProgress("Static analysis", 10, `Pattern checks across ${files.length} files…`);
|
|
207
|
+
const staticFindings = (0, static_analyzer_1.runStaticAnalysis)(files);
|
|
208
|
+
onProgress("Static analysis", 25, `${staticFindings.length} signals flagged`);
|
|
209
|
+
// Phase 1.5: Additional Scanners (Security, Performance, Stress Testing)
|
|
210
|
+
onProgress("Additional scanners", 28, "Running security, performance, and stress scanners…");
|
|
211
|
+
let scannerResults = {};
|
|
212
|
+
if (scannerConfig.enableNpmAudit || scannerConfig.enableSnyk ||
|
|
213
|
+
scannerConfig.enableSonarJS || scannerConfig.enableClinic ||
|
|
214
|
+
scannerConfig.enableArtillery || scannerConfig.enableAutocannon) {
|
|
215
|
+
scannerResults = await runAdditionalScanners(files, scannerConfig);
|
|
216
|
+
onProgress("Additional scanners", 30, "Additional scanners complete");
|
|
217
|
+
}
|
|
218
|
+
// Phase 2: AST Extraction (extract only high-risk code sections)
|
|
219
|
+
onProgress("Code extraction", 35, `Targeting ${staticScopeLabel}…`);
|
|
220
|
+
const extractedSections = [];
|
|
221
|
+
let totalOriginalChars = 0;
|
|
222
|
+
let totalExtractedChars = 0;
|
|
223
|
+
for (const file of aiFiles) {
|
|
224
|
+
if (!(0, ast_extractor_1.shouldAnalyzeFile)(file.path))
|
|
225
|
+
continue;
|
|
226
|
+
totalOriginalChars += file.content.length;
|
|
227
|
+
const extracted = (0, ast_extractor_1.extractHighRiskCode)(file.path, file.content);
|
|
228
|
+
if (extracted && extracted.sections.length > 0) {
|
|
229
|
+
extractedSections.push(extracted);
|
|
230
|
+
totalExtractedChars += extracted.sections.reduce((sum, s) => sum + s.code.length, 0);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const tokenReduction = totalOriginalChars > 0
|
|
234
|
+
? Math.round((1 - totalExtractedChars / totalOriginalChars) * 100)
|
|
235
|
+
: 0;
|
|
236
|
+
onProgress("Code extraction", 45, `${extractedSections.length} high-risk regions · ${tokenReduction}% token reduction`);
|
|
237
|
+
const staticForAi = mode === "quick"
|
|
238
|
+
? staticFindings.filter(static_analyzer_1.shouldSendToAI).slice(0, 12)
|
|
239
|
+
: staticFindings.filter(static_analyzer_1.shouldSendToAI);
|
|
240
|
+
// Phase 3: AI Analysis (deep reasoning on extracted code)
|
|
241
|
+
onProgress(mode === "quick" ? "AI review" : "AI deep review", 50, mode === "quick"
|
|
242
|
+
? "Reviewing priority surfaces…"
|
|
243
|
+
: "Parallel review across extracted regions…");
|
|
244
|
+
let aiFindings = [];
|
|
245
|
+
let scanQuality = 'excellent';
|
|
246
|
+
let aiUsed = true;
|
|
247
|
+
try {
|
|
248
|
+
aiFindings = await runAIAnalysis(projectName, extractedSections, staticForAi, onProgress, mode);
|
|
249
|
+
scanQuality = 'excellent'; // AI + Static (95% coverage)
|
|
250
|
+
aiUsed = true;
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
console.warn('[AI FALLBACK] AI failed, running ENHANCED static analysis');
|
|
254
|
+
console.error('[AI Analysis] Error:', error);
|
|
255
|
+
// Run ENHANCED static analysis (data flow + control flow + 500+ patterns)
|
|
256
|
+
onProgress("Enhanced Analysis", 50, "AI unavailable - running comprehensive static analysis (85% coverage)");
|
|
257
|
+
const enhancedResult = (0, static_analyzer_1.runEnhancedStaticAnalysis)(files);
|
|
258
|
+
// Convert enhanced findings to AI findings format
|
|
259
|
+
aiFindings = enhancedResult.findings.map(f => ({
|
|
260
|
+
id: f.id,
|
|
261
|
+
severity: f.severity,
|
|
262
|
+
category: f.category,
|
|
263
|
+
title: f.title,
|
|
264
|
+
description: f.description,
|
|
265
|
+
file: f.file,
|
|
266
|
+
line: f.line,
|
|
267
|
+
evidence: f.evidence,
|
|
268
|
+
mitigation: generateMitigation(f),
|
|
269
|
+
prevention: generatePrevention(f),
|
|
270
|
+
}));
|
|
271
|
+
scanQuality = 'enhanced'; // Enhanced static only (85% coverage)
|
|
272
|
+
aiUsed = false;
|
|
273
|
+
onProgress("Enhanced Analysis", 75, `${aiFindings.length} issues found (${enhancedResult.stats.dataFlowVulnerabilities} data flow, ${enhancedResult.stats.controlFlowIssues} control flow)`);
|
|
274
|
+
}
|
|
275
|
+
onProgress(aiUsed ? "AI review" : "Enhanced Analysis", 75, `${aiFindings.length} additional findings`);
|
|
276
|
+
// Phase 4: Verification Layer (cross-validate AI findings)
|
|
277
|
+
onProgress("Verification", 80, "Cross-checking findings…");
|
|
278
|
+
const verificationResult = (0, verification_layer_1.verifyFindings)(aiFindings, staticFindings, files);
|
|
279
|
+
onProgress("Verification Complete", 85, `${verificationResult.summary.confirmed} confirmed, ${verificationResult.summary.falsePositives} false positives removed`);
|
|
280
|
+
// Phase 5: Merge and deduplicate all findings
|
|
281
|
+
onProgress("Report", 90, "Assembling final report…");
|
|
282
|
+
// Convert static findings to verified findings
|
|
283
|
+
const verifiedStaticFindings = staticFindings.map(sf => ({
|
|
284
|
+
id: sf.id,
|
|
285
|
+
severity: sf.severity,
|
|
286
|
+
category: sf.category,
|
|
287
|
+
title: sf.title,
|
|
288
|
+
description: sf.description,
|
|
289
|
+
file: sf.file,
|
|
290
|
+
line: sf.line,
|
|
291
|
+
evidence: sf.evidence,
|
|
292
|
+
mitigation: generateMitigation(sf),
|
|
293
|
+
prevention: generatePrevention(sf),
|
|
294
|
+
confidence: sf.confidence,
|
|
295
|
+
verificationStatus: "confirmed",
|
|
296
|
+
verificationNotes: "Detected by static analysis",
|
|
297
|
+
sources: ["static-analysis"],
|
|
298
|
+
source: "static", // Tag as static finding
|
|
299
|
+
}));
|
|
300
|
+
// Merge all findings
|
|
301
|
+
const scannerFindings = convertScannerResultsToFindings(scannerResults);
|
|
302
|
+
const allFindings = [...verifiedStaticFindings, ...verificationResult.verified, ...scannerFindings];
|
|
303
|
+
const deduplicated = (0, verification_layer_1.deduplicateFindings)(allFindings);
|
|
304
|
+
// Calculate static-only score (without AI findings)
|
|
305
|
+
const staticOnlyFindings = [...verifiedStaticFindings, ...scannerFindings];
|
|
306
|
+
const staticOnlyDeduplicated = (0, verification_layer_1.deduplicateFindings)(staticOnlyFindings);
|
|
307
|
+
const staticOnlyScore = calculateStrictScore(staticOnlyDeduplicated);
|
|
308
|
+
// Calculate full score with AI findings
|
|
309
|
+
const fullScore = calculateStrictScore(deduplicated);
|
|
310
|
+
// Determine displayed score with transparency
|
|
311
|
+
let displayedScore;
|
|
312
|
+
let scoreSource;
|
|
313
|
+
let originalScore;
|
|
314
|
+
let scoreExplanation;
|
|
315
|
+
// Debug: Log the scores to understand what's happening
|
|
316
|
+
console.log(`[Score Calculation] Static-only: ${staticOnlyScore}, Full (with AI): ${fullScore}`);
|
|
317
|
+
if (fullScore !== staticOnlyScore) {
|
|
318
|
+
// Scores differ - ALWAYS use average for fairness
|
|
319
|
+
displayedScore = Math.round((fullScore + staticOnlyScore) / 2);
|
|
320
|
+
scoreSource = "average";
|
|
321
|
+
originalScore = staticOnlyScore;
|
|
322
|
+
if (fullScore > staticOnlyScore) {
|
|
323
|
+
// AI improved the score by filtering false positives
|
|
324
|
+
scoreExplanation = `AI verification improved the score by filtering false positives. Static analysis: ${staticOnlyScore}/100, AI-verified: ${fullScore}/100. Using balanced average: ${displayedScore}/100 as final score.`;
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// AI found additional real issues
|
|
328
|
+
scoreExplanation = `AI analysis discovered additional issues beyond static analysis. For fairness: Static analysis: ${staticOnlyScore}/100, AI-verified: ${fullScore}/100. Using balanced average: ${displayedScore}/100 as final score.`;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Scores are equal - both analyses agree
|
|
333
|
+
displayedScore = fullScore;
|
|
334
|
+
scoreSource = "static";
|
|
335
|
+
originalScore = staticOnlyScore;
|
|
336
|
+
scoreExplanation = `Static and AI analysis are in complete agreement. Final score: ${displayedScore}/100.`;
|
|
337
|
+
}
|
|
338
|
+
const grade = scoreToGrade(displayedScore);
|
|
339
|
+
// Generate executive verdict
|
|
340
|
+
const executiveVerdict = generateExecutiveVerdict(deduplicated, displayedScore);
|
|
341
|
+
// Identify critical blockers
|
|
342
|
+
const criticalBlockers = deduplicated
|
|
343
|
+
.filter(f => f.severity === "critical" || (f.severity === "high" && f.confidence === "high"))
|
|
344
|
+
.map(f => `${f.title} in ${f.file}:${f.line}`);
|
|
345
|
+
// Identify strengths
|
|
346
|
+
const strengths = identifyStrengths(files, deduplicated);
|
|
347
|
+
// Calculate report confidence
|
|
348
|
+
const reportConfidence = (0, verification_layer_1.calculateReportConfidence)(deduplicated);
|
|
349
|
+
onProgress("Complete", 100, "Scan complete");
|
|
350
|
+
const report = {
|
|
351
|
+
score: displayedScore,
|
|
352
|
+
grade,
|
|
353
|
+
summary: `Analyzed ${files.length} files (${files.reduce((s, f) => s + f.lines, 0)} lines). Found ${deduplicated.length} verified issues.`,
|
|
354
|
+
executiveVerdict,
|
|
355
|
+
findings: deduplicated.map(f => ({
|
|
356
|
+
id: f.id,
|
|
357
|
+
severity: f.severity,
|
|
358
|
+
category: f.category,
|
|
359
|
+
title: f.title,
|
|
360
|
+
description: f.description,
|
|
361
|
+
file: f.file,
|
|
362
|
+
line: f.line,
|
|
363
|
+
evidence: f.evidence,
|
|
364
|
+
mitigation: f.mitigation,
|
|
365
|
+
prevention: f.prevention,
|
|
366
|
+
source: f.source, // Include source tag
|
|
367
|
+
})),
|
|
368
|
+
strengths,
|
|
369
|
+
criticalBlockers,
|
|
370
|
+
metadata: {
|
|
371
|
+
projectName,
|
|
372
|
+
scannedAt: new Date().toISOString(),
|
|
373
|
+
filesScanned: files.length,
|
|
374
|
+
linesScanned: files.reduce((s, f) => s + f.lines, 0),
|
|
375
|
+
ignoredPaths: ignoredCount,
|
|
376
|
+
reportConfidence: reportConfidence.score,
|
|
377
|
+
reportConfidenceGrade: reportConfidence.grade,
|
|
378
|
+
reportConfidenceExplanation: reportConfidence.explanation,
|
|
379
|
+
fileTree: allFilePaths && allFilePaths.length > 0 ? buildFileTreeFromPaths(allFilePaths) : buildFileTree(files),
|
|
380
|
+
// AI analysis stats
|
|
381
|
+
staticFindings: deduplicated.filter(f => f.source === "static").length,
|
|
382
|
+
aiFindings: deduplicated.filter(f => f.source === "ai").length,
|
|
383
|
+
verifiedFindings: deduplicated.filter(f => f.source === "verified").length,
|
|
384
|
+
scannerFindings: deduplicated.filter(f => f.source === "scanner").length,
|
|
385
|
+
// Score breakdown
|
|
386
|
+
staticOnlyScore,
|
|
387
|
+
fullScore,
|
|
388
|
+
displayedScore,
|
|
389
|
+
originalScore,
|
|
390
|
+
scoreSource,
|
|
391
|
+
scoreExplanation, // Transparency about scoring logic
|
|
392
|
+
// Scanner results
|
|
393
|
+
scannerResults: {
|
|
394
|
+
npmAudit: scannerResults.npmAudit?.summary,
|
|
395
|
+
snyk: scannerResults.snyk?.summary,
|
|
396
|
+
sonarJS: scannerResults.sonarJS?.summary,
|
|
397
|
+
clinic: scannerResults.clinic?.summary,
|
|
398
|
+
artillery: scannerResults.artillery?.summary,
|
|
399
|
+
autocannon: scannerResults.autocannon?.summary,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
const stats = {
|
|
404
|
+
filesScanned: files.length,
|
|
405
|
+
linesScanned: files.reduce((s, f) => s + f.lines, 0),
|
|
406
|
+
staticFindings: staticFindings.length,
|
|
407
|
+
aiFindings: aiFindings.length,
|
|
408
|
+
verifiedFindings: deduplicated.length,
|
|
409
|
+
scannerFindings: scannerFindings.length,
|
|
410
|
+
falsePositives: verificationResult.summary.falsePositives,
|
|
411
|
+
tokensSaved: `${tokenReduction}% (${Math.round((totalOriginalChars - totalExtractedChars) / 1000)}K chars)`,
|
|
412
|
+
};
|
|
413
|
+
return { report, stats };
|
|
414
|
+
}
|
|
415
|
+
async function runAIAnalysis(projectName, extractedSections, lowConfidenceStaticFindings, onProgress, mode) {
|
|
416
|
+
if (extractedSections.length === 0 && lowConfidenceStaticFindings.length === 0) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
let batches = [];
|
|
420
|
+
try {
|
|
421
|
+
batches = capSmartBatches(createSmartBatches(extractedSections, lowConfidenceStaticFindings, mode), mode === "quick" ? 10 : 48);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
const aiFindings = [];
|
|
427
|
+
const phaseLabel = mode === "quick" ? "AI review" : "AI deep review";
|
|
428
|
+
// Use more parallel workers for deep scan
|
|
429
|
+
const parallelWorkers = mode === "deep" ? DEEP_SCAN_PARALLEL : PARALLEL_AI_CALLS;
|
|
430
|
+
for (let i = 0; i < batches.length; i += parallelWorkers) {
|
|
431
|
+
const slice = batches.slice(i, i + parallelWorkers);
|
|
432
|
+
const done = Math.min(i + slice.length, batches.length);
|
|
433
|
+
const progressPct = 45 + Math.round((done / batches.length) * 30);
|
|
434
|
+
onProgress(phaseLabel, progressPct, `Round ${Math.floor(i / parallelWorkers) + 1} · ${done}/${batches.length} segments · ${parallelWorkers} parallel workers`);
|
|
435
|
+
const promises = slice.map((batch, j) => analyzeBatchWithAI(projectName, i + j, batches.length, batch, j % 3).catch(error => {
|
|
436
|
+
return []; // Return empty array on error to continue with other batches
|
|
437
|
+
}));
|
|
438
|
+
try {
|
|
439
|
+
const results = await Promise.all(promises);
|
|
440
|
+
const newFindings = results.flat();
|
|
441
|
+
aiFindings.push(...newFindings);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
// Continue with next round
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return aiFindings;
|
|
448
|
+
}
|
|
449
|
+
function batchCharSize(batch) {
|
|
450
|
+
const sectionChars = batch.sections.reduce((sum, s) => sum + s.sections.reduce((n, sec) => n + sec.code.length, 0), 0);
|
|
451
|
+
const staticChars = batch.staticFindings.reduce((sum, f) => sum + f.evidence.length + f.title.length, 0);
|
|
452
|
+
return sectionChars + staticChars;
|
|
453
|
+
}
|
|
454
|
+
function mergeSmartBatches(a, b) {
|
|
455
|
+
return {
|
|
456
|
+
sections: [...a.sections, ...b.sections],
|
|
457
|
+
staticFindings: [...a.staticFindings, ...b.staticFindings],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function capSmartBatches(batches, maxBatches) {
|
|
461
|
+
if (batches.length <= maxBatches)
|
|
462
|
+
return batches;
|
|
463
|
+
const merged = [...batches];
|
|
464
|
+
while (merged.length > maxBatches) {
|
|
465
|
+
let smallest = 0;
|
|
466
|
+
for (let i = 1; i < merged.length; i++) {
|
|
467
|
+
if (batchCharSize(merged[i]) < batchCharSize(merged[smallest]))
|
|
468
|
+
smallest = i;
|
|
469
|
+
}
|
|
470
|
+
const partner = smallest === merged.length - 1 ? smallest - 1 : smallest + 1;
|
|
471
|
+
merged[smallest] = mergeSmartBatches(merged[smallest], merged[partner]);
|
|
472
|
+
merged.splice(partner, 1);
|
|
473
|
+
}
|
|
474
|
+
return merged;
|
|
475
|
+
}
|
|
476
|
+
function createSmartBatches(extractedSections, staticFindings, mode) {
|
|
477
|
+
// Increased batch sizes - parallel workers can handle more
|
|
478
|
+
const MAX_CHARS_PER_BATCH = mode === "quick" ? 35000 : 40000; // Increased from 20k/25k
|
|
479
|
+
const batches = [];
|
|
480
|
+
let currentBatch = { sections: [], staticFindings: [] };
|
|
481
|
+
let currentChars = 0;
|
|
482
|
+
// Add extracted sections
|
|
483
|
+
for (const section of extractedSections) {
|
|
484
|
+
const sectionChars = section.sections.reduce((sum, s) => sum + s.code.length, 0);
|
|
485
|
+
if (currentChars + sectionChars > MAX_CHARS_PER_BATCH && currentBatch.sections.length > 0) {
|
|
486
|
+
batches.push(currentBatch);
|
|
487
|
+
currentBatch = { sections: [], staticFindings: [] };
|
|
488
|
+
currentChars = 0;
|
|
489
|
+
}
|
|
490
|
+
currentBatch.sections.push(section);
|
|
491
|
+
currentChars += sectionChars;
|
|
492
|
+
}
|
|
493
|
+
// Add static findings that need AI verification
|
|
494
|
+
const findingsText = staticFindings.map(f => `${f.file}:${f.line} - ${f.title}: ${f.evidence}`).join("\n");
|
|
495
|
+
if (findingsText.length < MAX_CHARS_PER_BATCH - currentChars) {
|
|
496
|
+
currentBatch.staticFindings = staticFindings;
|
|
497
|
+
}
|
|
498
|
+
else if (currentBatch.sections.length > 0) {
|
|
499
|
+
batches.push(currentBatch);
|
|
500
|
+
currentBatch = { sections: [], staticFindings };
|
|
501
|
+
}
|
|
502
|
+
if (currentBatch.sections.length > 0 || currentBatch.staticFindings.length > 0) {
|
|
503
|
+
batches.push(currentBatch);
|
|
504
|
+
}
|
|
505
|
+
return batches;
|
|
506
|
+
}
|
|
507
|
+
async function analyzeBatchWithAI(projectName, batchIndex, totalBatches, batch, keySlot) {
|
|
508
|
+
const MAX_RETRIES = 3;
|
|
509
|
+
let lastError = null;
|
|
510
|
+
// Get auth token from localStorage or session
|
|
511
|
+
const getAuthToken = () => {
|
|
512
|
+
if (typeof window === 'undefined')
|
|
513
|
+
return null;
|
|
514
|
+
return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
|
|
515
|
+
};
|
|
516
|
+
// Try with retries and exponential backoff
|
|
517
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
518
|
+
const controller = new AbortController();
|
|
519
|
+
const timeoutId = setTimeout(() => controller.abort(), 55000); // 55 seconds
|
|
520
|
+
try {
|
|
521
|
+
const authToken = getAuthToken();
|
|
522
|
+
const headers = {
|
|
523
|
+
"Content-Type": "application/json"
|
|
524
|
+
};
|
|
525
|
+
// Add Bearer token if available
|
|
526
|
+
if (authToken) {
|
|
527
|
+
headers["Authorization"] = `Bearer ${authToken}`;
|
|
528
|
+
}
|
|
529
|
+
const res = await fetch("/api/scan/smart-batch", {
|
|
530
|
+
method: "POST",
|
|
531
|
+
headers,
|
|
532
|
+
body: JSON.stringify({
|
|
533
|
+
projectName,
|
|
534
|
+
batchIndex,
|
|
535
|
+
totalBatches,
|
|
536
|
+
batch,
|
|
537
|
+
keySlot,
|
|
538
|
+
attempt, // Pass attempt number for server-side retry logic
|
|
539
|
+
}),
|
|
540
|
+
signal: controller.signal,
|
|
541
|
+
});
|
|
542
|
+
clearTimeout(timeoutId);
|
|
543
|
+
if (!res.ok) {
|
|
544
|
+
const errorText = await res.text();
|
|
545
|
+
let errorMessage = `HTTP ${res.status}`;
|
|
546
|
+
try {
|
|
547
|
+
const errorJson = JSON.parse(errorText);
|
|
548
|
+
errorMessage = errorJson.error || errorMessage;
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
errorMessage = errorText.substring(0, 200);
|
|
552
|
+
}
|
|
553
|
+
// Check if it's a retryable error
|
|
554
|
+
if (res.status === 429 || res.status === 503 || res.status === 504) {
|
|
555
|
+
lastError = new Error(errorMessage);
|
|
556
|
+
// Exponential backoff: 2s, 4s, 8s
|
|
557
|
+
const backoffMs = Math.pow(2, attempt) * 2000;
|
|
558
|
+
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
559
|
+
continue; // Retry
|
|
560
|
+
}
|
|
561
|
+
// Non-retryable error (including 401)
|
|
562
|
+
return []; // Skip this batch
|
|
563
|
+
}
|
|
564
|
+
const data = await res.json();
|
|
565
|
+
// Validate that findings have proper evidence field
|
|
566
|
+
const findings = (data.findings || []).map((f) => ({
|
|
567
|
+
...f,
|
|
568
|
+
evidence: typeof f.evidence === 'string' ? f.evidence : String(f.evidence || ''),
|
|
569
|
+
}));
|
|
570
|
+
return findings;
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
clearTimeout(timeoutId);
|
|
574
|
+
if (error instanceof Error) {
|
|
575
|
+
if (error.name === 'AbortError') {
|
|
576
|
+
lastError = error;
|
|
577
|
+
// Wait before retry
|
|
578
|
+
const backoffMs = Math.pow(2, attempt) * 1000;
|
|
579
|
+
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
580
|
+
continue; // Retry
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
lastError = error;
|
|
584
|
+
// Wait before retry
|
|
585
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
586
|
+
continue; // Retry
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// All retries failed - return empty array silently
|
|
592
|
+
return []; // Continue with other batches
|
|
593
|
+
}
|
|
594
|
+
function calculateStrictScore(findings) {
|
|
595
|
+
if (findings.length === 0)
|
|
596
|
+
return 95; // Not perfect, might have missed issues
|
|
597
|
+
// Weighted category-based scoring system
|
|
598
|
+
// Each severity has its own "bucket" that can be depleted independently
|
|
599
|
+
// This prevents info/low errors from tanking the entire score
|
|
600
|
+
const CATEGORY_WEIGHTS = {
|
|
601
|
+
critical: 35, // 35% of total score
|
|
602
|
+
high: 25, // 25% of total score (Critical + High = 60%)
|
|
603
|
+
medium: 25, // 25% of total score
|
|
604
|
+
low: 10, // 10% of total score
|
|
605
|
+
info: 5, // 5% of total score
|
|
606
|
+
};
|
|
607
|
+
// Count findings by severity
|
|
608
|
+
const counts = {
|
|
609
|
+
critical: 0,
|
|
610
|
+
high: 0,
|
|
611
|
+
medium: 0,
|
|
612
|
+
low: 0,
|
|
613
|
+
info: 0,
|
|
614
|
+
};
|
|
615
|
+
for (const finding of findings) {
|
|
616
|
+
// Apply confidence multiplier to count
|
|
617
|
+
let weight = 1.0;
|
|
618
|
+
switch (finding.confidence) {
|
|
619
|
+
case "high":
|
|
620
|
+
weight = 1.0;
|
|
621
|
+
break;
|
|
622
|
+
case "medium":
|
|
623
|
+
weight = 0.7;
|
|
624
|
+
break;
|
|
625
|
+
case "low":
|
|
626
|
+
weight = 0.4;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
counts[finding.severity] += weight;
|
|
630
|
+
}
|
|
631
|
+
// Calculate score for each category
|
|
632
|
+
// Each category depletes independently based on issue count
|
|
633
|
+
const categoryScores = {
|
|
634
|
+
critical: calculateCategoryScore(counts.critical, CATEGORY_WEIGHTS.critical, 0.5), // 0.5 = aggressive penalty
|
|
635
|
+
high: calculateCategoryScore(counts.high, CATEGORY_WEIGHTS.high, 0.4),
|
|
636
|
+
medium: calculateCategoryScore(counts.medium, CATEGORY_WEIGHTS.medium, 0.3),
|
|
637
|
+
low: calculateCategoryScore(counts.low, CATEGORY_WEIGHTS.low, 0.2),
|
|
638
|
+
info: calculateCategoryScore(counts.info, CATEGORY_WEIGHTS.info, 0.1), // 0.1 = gentle penalty
|
|
639
|
+
};
|
|
640
|
+
// Sum up all category scores
|
|
641
|
+
const totalScore = Math.round(categoryScores.critical +
|
|
642
|
+
categoryScores.high +
|
|
643
|
+
categoryScores.medium +
|
|
644
|
+
categoryScores.low +
|
|
645
|
+
categoryScores.info);
|
|
646
|
+
return Math.max(0, Math.min(100, totalScore));
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Calculate score for a single category using logarithmic decay
|
|
650
|
+
* This prevents a single category from being completely destroyed by many issues
|
|
651
|
+
*
|
|
652
|
+
* @param issueCount - Number of issues (weighted by confidence)
|
|
653
|
+
* @param maxPoints - Maximum points for this category
|
|
654
|
+
* @param decayRate - How fast the score decays (higher = faster decay)
|
|
655
|
+
*/
|
|
656
|
+
function calculateCategoryScore(issueCount, maxPoints, decayRate) {
|
|
657
|
+
if (issueCount === 0)
|
|
658
|
+
return maxPoints;
|
|
659
|
+
// Logarithmic decay formula: score = maxPoints * e^(-decayRate * issueCount)
|
|
660
|
+
// This means:
|
|
661
|
+
// - First few issues hurt a lot
|
|
662
|
+
// - Additional issues hurt less and less
|
|
663
|
+
// - Score approaches 0 but never quite reaches it (unless many issues)
|
|
664
|
+
const score = maxPoints * Math.exp(-decayRate * issueCount);
|
|
665
|
+
// Round to 2 decimal places for precision
|
|
666
|
+
return Math.max(0, Math.round(score * 100) / 100);
|
|
667
|
+
}
|
|
668
|
+
function scoreToGrade(score) {
|
|
669
|
+
if (score >= 95)
|
|
670
|
+
return "A+";
|
|
671
|
+
if (score >= 90)
|
|
672
|
+
return "A";
|
|
673
|
+
if (score >= 85)
|
|
674
|
+
return "A-";
|
|
675
|
+
if (score >= 80)
|
|
676
|
+
return "B+";
|
|
677
|
+
if (score >= 75)
|
|
678
|
+
return "B";
|
|
679
|
+
if (score >= 70)
|
|
680
|
+
return "B-";
|
|
681
|
+
if (score >= 65)
|
|
682
|
+
return "C+";
|
|
683
|
+
if (score >= 60)
|
|
684
|
+
return "C";
|
|
685
|
+
if (score >= 55)
|
|
686
|
+
return "C-";
|
|
687
|
+
if (score >= 50)
|
|
688
|
+
return "D+";
|
|
689
|
+
if (score >= 45)
|
|
690
|
+
return "D";
|
|
691
|
+
if (score >= 40)
|
|
692
|
+
return "D-";
|
|
693
|
+
return "F";
|
|
694
|
+
}
|
|
695
|
+
function generateExecutiveVerdict(findings, score) {
|
|
696
|
+
const critical = findings.filter(f => f.severity === "critical").length;
|
|
697
|
+
const high = findings.filter(f => f.severity === "high").length;
|
|
698
|
+
if (critical > 0) {
|
|
699
|
+
return `CRITICAL: ${critical} critical security vulnerabilities detected. This codebase is NOT production-ready and poses immediate security risks. ${high > 0 ? `Additionally, ${high} high-severity issues require urgent attention.` : ""} Immediate remediation required before any deployment.`;
|
|
700
|
+
}
|
|
701
|
+
if (high > 5) {
|
|
702
|
+
return `HIGH RISK: ${high} high-severity issues detected. While no critical vulnerabilities exist, this codebase has significant production risks including security flaws, error handling gaps, and potential data integrity issues. Recommend thorough review and fixes before production deployment.`;
|
|
703
|
+
}
|
|
704
|
+
if (score >= 80) {
|
|
705
|
+
return `GOOD: This codebase demonstrates solid engineering practices with ${findings.length} minor issues identified. The code is production-ready with recommended improvements for enhanced security and maintainability. Continue monitoring and addressing findings during regular maintenance cycles.`;
|
|
706
|
+
}
|
|
707
|
+
if (score >= 60) {
|
|
708
|
+
return `MODERATE: This codebase has ${findings.length} issues spanning security, code quality, and reliability concerns. While functional, it requires attention to several areas before being considered production-hardened. Prioritize high and medium severity findings.`;
|
|
709
|
+
}
|
|
710
|
+
return `NEEDS IMPROVEMENT: This codebase has ${findings.length} issues across multiple categories. Significant refactoring and security hardening required. Recommend addressing all high and medium severity findings before production use.`;
|
|
711
|
+
}
|
|
712
|
+
function identifyStrengths(files, findings) {
|
|
713
|
+
const strengths = [];
|
|
714
|
+
// Check for TypeScript usage
|
|
715
|
+
const tsFiles = files.filter(f => /\.tsx?$/.test(f.path));
|
|
716
|
+
if (tsFiles.length / files.length > 0.7) {
|
|
717
|
+
strengths.push("Strong type safety with TypeScript");
|
|
718
|
+
}
|
|
719
|
+
// Check for error handling
|
|
720
|
+
const errorHandlingCount = files.filter(f => /try\s*\{|\.catch\(|\.finally\(/i.test(f.content)).length;
|
|
721
|
+
if (errorHandlingCount / files.length > 0.5) {
|
|
722
|
+
strengths.push("Consistent error handling patterns");
|
|
723
|
+
}
|
|
724
|
+
// Check for environment variable usage
|
|
725
|
+
const envUsage = files.filter(f => /process\.env|import\.meta\.env/i.test(f.content)).length;
|
|
726
|
+
if (envUsage > 0 && findings.filter(f => f.title.includes("hardcoded")).length === 0) {
|
|
727
|
+
strengths.push("Proper use of environment variables for configuration");
|
|
728
|
+
}
|
|
729
|
+
// Check for testing
|
|
730
|
+
const testFiles = files.filter(f => /\.(test|spec)\.[jt]sx?$/.test(f.path));
|
|
731
|
+
if (testFiles.length > files.length * 0.2) {
|
|
732
|
+
strengths.push("Good test coverage with dedicated test files");
|
|
733
|
+
}
|
|
734
|
+
// Check for modern async/await
|
|
735
|
+
const asyncUsage = files.filter(f => /async\s+(?:function|\()/i.test(f.content)).length;
|
|
736
|
+
if (asyncUsage > 0) {
|
|
737
|
+
strengths.push("Modern async/await patterns for asynchronous operations");
|
|
738
|
+
}
|
|
739
|
+
if (strengths.length === 0) {
|
|
740
|
+
strengths.push("Codebase is functional and serves its purpose");
|
|
741
|
+
}
|
|
742
|
+
return strengths;
|
|
743
|
+
}
|
|
744
|
+
function generateMitigation(finding) {
|
|
745
|
+
// Generate specific mitigation based on finding type
|
|
746
|
+
const mitigations = {
|
|
747
|
+
"sql-injection": "Use parameterized queries or an ORM like Prisma/TypeORM",
|
|
748
|
+
"xss": "Sanitize user input with DOMPurify before rendering",
|
|
749
|
+
"hardcoded-secret": "Move to environment variables and use a secrets manager",
|
|
750
|
+
"command-injection": "Avoid shell execution or use parameterized commands",
|
|
751
|
+
"missing-auth": "Add authentication middleware to protect this endpoint",
|
|
752
|
+
"unhandled-promise": "Wrap in try-catch or add .catch() handler",
|
|
753
|
+
"n-plus-one": "Use eager loading or batch queries",
|
|
754
|
+
"console-log": "Replace with proper logging library (winston, pino)",
|
|
755
|
+
"empty-catch": "Log the error or handle it appropriately",
|
|
756
|
+
};
|
|
757
|
+
for (const [key, mitigation] of Object.entries(mitigations)) {
|
|
758
|
+
if (finding.id.includes(key)) {
|
|
759
|
+
return mitigation;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return "Review and fix according to best practices";
|
|
763
|
+
}
|
|
764
|
+
function generatePrevention(finding) {
|
|
765
|
+
const preventions = {
|
|
766
|
+
"sql-injection": "Always use ORMs or parameterized queries; never concatenate user input into SQL",
|
|
767
|
+
"xss": "Implement Content Security Policy and sanitize all user-generated content",
|
|
768
|
+
"hardcoded-secret": "Use environment variables and secret management tools; add pre-commit hooks to detect secrets",
|
|
769
|
+
"command-injection": "Avoid shell execution; if necessary, use allowlists and strict input validation",
|
|
770
|
+
"missing-auth": "Implement authentication middleware at the router level",
|
|
771
|
+
"unhandled-promise": "Enable ESLint rules for floating promises",
|
|
772
|
+
"n-plus-one": "Use database query profiling and monitoring",
|
|
773
|
+
"console-log": "Configure linting rules to prevent console statements in production code",
|
|
774
|
+
};
|
|
775
|
+
for (const [key, prevention] of Object.entries(preventions)) {
|
|
776
|
+
if (finding.id.includes(key)) {
|
|
777
|
+
return prevention;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return "Follow security and code quality best practices";
|
|
781
|
+
}
|
|
782
|
+
function buildFileTree(files) {
|
|
783
|
+
return buildFileTreeFromPaths(files.map(f => f.path));
|
|
784
|
+
}
|
|
785
|
+
function buildFileTreeFromPaths(paths) {
|
|
786
|
+
const root = new Map();
|
|
787
|
+
console.log(`[buildFileTreeFromPaths] Processing ${paths.length} paths`);
|
|
788
|
+
console.log(`[buildFileTreeFromPaths] Sample paths:`, paths.slice(0, 5));
|
|
789
|
+
for (const path of paths) {
|
|
790
|
+
const parts = path.split("/").filter(Boolean);
|
|
791
|
+
let currentLevel = root;
|
|
792
|
+
let currentPath = "";
|
|
793
|
+
for (let i = 0; i < parts.length; i++) {
|
|
794
|
+
const part = parts[i];
|
|
795
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
796
|
+
const isFile = i === parts.length - 1;
|
|
797
|
+
if (!currentLevel.has(part)) {
|
|
798
|
+
const node = {
|
|
799
|
+
name: part,
|
|
800
|
+
type: isFile ? "file" : "folder",
|
|
801
|
+
path: currentPath,
|
|
802
|
+
};
|
|
803
|
+
if (!isFile) {
|
|
804
|
+
node.children = [];
|
|
805
|
+
}
|
|
806
|
+
currentLevel.set(part, node);
|
|
807
|
+
}
|
|
808
|
+
if (!isFile) {
|
|
809
|
+
const folderNode = currentLevel.get(part);
|
|
810
|
+
if (!folderNode.children) {
|
|
811
|
+
folderNode.children = [];
|
|
812
|
+
}
|
|
813
|
+
// Create a map for the next level from the folder's children
|
|
814
|
+
const childMap = new Map();
|
|
815
|
+
for (const child of folderNode.children) {
|
|
816
|
+
childMap.set(child.name, child);
|
|
817
|
+
}
|
|
818
|
+
currentLevel = childMap;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// Rebuild the tree structure from the maps
|
|
823
|
+
// Pass paths array to inner function to avoid ReferenceError
|
|
824
|
+
const rebuildTree = (nodeMap, pathsArray) => {
|
|
825
|
+
const nodes = Array.from(nodeMap.values());
|
|
826
|
+
for (const node of nodes) {
|
|
827
|
+
if (node.type === "folder" && node.children) {
|
|
828
|
+
// The children array should already be populated from the map
|
|
829
|
+
// But we need to ensure it's properly structured
|
|
830
|
+
if (node.children.length === 0) {
|
|
831
|
+
// Try to find children from the path structure
|
|
832
|
+
const childPaths = pathsArray.filter(p => p.startsWith(node.path + "/"));
|
|
833
|
+
if (childPaths.length > 0) {
|
|
834
|
+
// This folder should have children
|
|
835
|
+
// Extract immediate children
|
|
836
|
+
const immediateChildren = new Set();
|
|
837
|
+
for (const childPath of childPaths) {
|
|
838
|
+
const relativePath = childPath.substring(node.path.length + 1);
|
|
839
|
+
const firstPart = relativePath.split("/")[0];
|
|
840
|
+
immediateChildren.add(firstPart);
|
|
841
|
+
}
|
|
842
|
+
// Create child nodes
|
|
843
|
+
node.children = Array.from(immediateChildren).map(childName => {
|
|
844
|
+
const childPath = `${node.path}/${childName}`;
|
|
845
|
+
const isFile = !pathsArray.some(p => p.startsWith(childPath + "/"));
|
|
846
|
+
return {
|
|
847
|
+
name: childName,
|
|
848
|
+
type: isFile ? "file" : "folder",
|
|
849
|
+
path: childPath,
|
|
850
|
+
children: isFile ? undefined : [],
|
|
851
|
+
};
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return nodes;
|
|
858
|
+
};
|
|
859
|
+
const treeNodes = rebuildTree(root, paths);
|
|
860
|
+
console.log(`[buildFileTreeFromPaths] Built tree with ${treeNodes.length} root nodes`);
|
|
861
|
+
console.log(`[buildFileTreeFromPaths] Root nodes:`, treeNodes.map(n => ({ name: n.name, type: n.type, childrenCount: n.children?.length || 0 })));
|
|
862
|
+
// Convert map to sorted array
|
|
863
|
+
const sortNodes = (nodes) => {
|
|
864
|
+
return nodes.sort((a, b) => {
|
|
865
|
+
// Folders first, then files
|
|
866
|
+
if (a.type !== b.type) {
|
|
867
|
+
return a.type === "folder" ? -1 : 1;
|
|
868
|
+
}
|
|
869
|
+
return a.name.localeCompare(b.name);
|
|
870
|
+
}).map(node => {
|
|
871
|
+
if (node.children) {
|
|
872
|
+
node.children = sortNodes(node.children);
|
|
873
|
+
}
|
|
874
|
+
return node;
|
|
875
|
+
});
|
|
876
|
+
};
|
|
877
|
+
return sortNodes(treeNodes);
|
|
878
|
+
}
|