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.
@@ -0,0 +1,336 @@
1
+ "use strict";
2
+ /**
3
+ * CLI Scan Orchestrator
4
+ * Adapts the smart scan orchestrator for CLI use without web API calls
5
+ * Uses OpenRouter client directly for AI analysis
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.runSmartScan = runSmartScan;
9
+ const static_analyzer_1 = require("./static-analyzer");
10
+ const ast_extractor_1 = require("./ast-extractor");
11
+ const verification_layer_1 = require("./verification-layer");
12
+ const scan_priority_1 = require("./scan-priority");
13
+ const openrouter_1 = require("./openrouter");
14
+ const prompts_1 = require("./prompts");
15
+ const PARALLEL_AI_CALLS = 3;
16
+ async function runSmartScan(projectName, files, ignoredCount, onProgress, mode = "quick") {
17
+ const aiFiles = mode === "quick" ? (0, scan_priority_1.selectFilesForQuickScan)(files) : files;
18
+ const staticScopeLabel = mode === "quick" ? `${aiFiles.length} priority files` : `${files.length} files`;
19
+ // Phase 1: Static Analysis
20
+ onProgress("Static analysis", 10, `Pattern checks across ${files.length} files…`);
21
+ const staticFindings = (0, static_analyzer_1.runStaticAnalysis)(files);
22
+ onProgress("Static analysis", 25, `${staticFindings.length} signals flagged`);
23
+ // Phase 2: AST Extraction
24
+ onProgress("Code extraction", 35, `Targeting ${staticScopeLabel}…`);
25
+ const extractedSections = [];
26
+ let totalOriginalChars = 0;
27
+ let totalExtractedChars = 0;
28
+ for (const file of aiFiles) {
29
+ if (!(0, ast_extractor_1.shouldAnalyzeFile)(file.path))
30
+ continue;
31
+ totalOriginalChars += file.content.length;
32
+ const extracted = (0, ast_extractor_1.extractHighRiskCode)(file.path, file.content);
33
+ if (extracted && extracted.sections.length > 0) {
34
+ extractedSections.push(extracted);
35
+ totalExtractedChars += extracted.sections.reduce((sum, s) => sum + s.code.length, 0);
36
+ }
37
+ }
38
+ const tokenReduction = totalOriginalChars > 0
39
+ ? Math.round((1 - totalExtractedChars / totalOriginalChars) * 100)
40
+ : 0;
41
+ onProgress("Code extraction", 45, `${extractedSections.length} high-risk regions · ${tokenReduction}% token reduction`);
42
+ // Phase 3: AI Analysis (direct OpenRouter calls)
43
+ onProgress("AI review", 50, "Analyzing extracted code with AI…");
44
+ let aiFindings = [];
45
+ let aiUsed = true;
46
+ try {
47
+ aiFindings = await runAIAnalysisCLI(projectName, extractedSections, staticFindings, onProgress, mode);
48
+ aiUsed = true;
49
+ }
50
+ catch (error) {
51
+ console.warn('[AI FALLBACK] AI failed, running ENHANCED static analysis');
52
+ console.error('[AI Analysis] Error:', error);
53
+ // Run ENHANCED static analysis as fallback
54
+ onProgress("Enhanced Analysis", 50, "AI unavailable - running comprehensive static analysis (85% coverage)");
55
+ const enhancedResult = (0, static_analyzer_1.runEnhancedStaticAnalysis)(files);
56
+ aiFindings = enhancedResult.findings.map(f => ({
57
+ id: f.id,
58
+ severity: f.severity,
59
+ category: f.category,
60
+ title: f.title,
61
+ description: f.description,
62
+ file: f.file,
63
+ line: f.line,
64
+ evidence: f.evidence,
65
+ mitigation: generateMitigation(f),
66
+ prevention: generatePrevention(f),
67
+ }));
68
+ aiUsed = false;
69
+ onProgress("Enhanced Analysis", 75, `${aiFindings.length} issues found`);
70
+ }
71
+ onProgress(aiUsed ? "AI review" : "Enhanced Analysis", 75, `${aiFindings.length} additional findings`);
72
+ // Phase 4: Verification Layer
73
+ onProgress("Verification", 80, "Cross-checking findings…");
74
+ const verificationResult = (0, verification_layer_1.verifyFindings)(aiFindings, staticFindings, files);
75
+ onProgress("Verification Complete", 85, `${verificationResult.summary.confirmed} confirmed, ${verificationResult.summary.falsePositives} false positives removed`);
76
+ // Phase 5: Merge and deduplicate
77
+ onProgress("Report", 90, "Assembling final report…");
78
+ const verifiedStaticFindings = staticFindings.map(sf => ({
79
+ id: sf.id,
80
+ severity: sf.severity,
81
+ category: sf.category,
82
+ title: sf.title,
83
+ description: sf.description,
84
+ file: sf.file,
85
+ line: sf.line,
86
+ evidence: sf.evidence,
87
+ mitigation: generateMitigation(sf),
88
+ prevention: generatePrevention(sf),
89
+ confidence: sf.confidence,
90
+ verificationStatus: "confirmed",
91
+ verificationNotes: "Detected by static analysis",
92
+ sources: ["static-analysis"],
93
+ source: "static",
94
+ }));
95
+ const allFindings = [...verifiedStaticFindings, ...verificationResult.verified];
96
+ const deduplicated = (0, verification_layer_1.deduplicateFindings)(allFindings);
97
+ const score = calculateStrictScore(deduplicated);
98
+ const grade = scoreToGrade(score);
99
+ const executiveVerdict = generateExecutiveVerdict(deduplicated, score);
100
+ const criticalBlockers = deduplicated
101
+ .filter(f => f.severity === "critical" || (f.severity === "high" && f.confidence === "high"))
102
+ .map(f => `${f.title} in ${f.file}:${f.line}`);
103
+ const strengths = identifyStrengths(files, deduplicated);
104
+ const reportConfidence = (0, verification_layer_1.calculateReportConfidence)(deduplicated);
105
+ onProgress("Complete", 100, "Scan complete");
106
+ const report = {
107
+ score,
108
+ grade,
109
+ summary: `Analyzed ${files.length} files (${files.reduce((s, f) => s + f.lines, 0)} lines). Found ${deduplicated.length} verified issues.`,
110
+ executiveVerdict,
111
+ findings: deduplicated.map(f => ({
112
+ id: f.id,
113
+ severity: f.severity,
114
+ category: f.category,
115
+ title: f.title,
116
+ description: f.description,
117
+ file: f.file,
118
+ line: f.line,
119
+ evidence: f.evidence,
120
+ mitigation: f.mitigation,
121
+ prevention: f.prevention,
122
+ source: f.source,
123
+ })),
124
+ strengths,
125
+ criticalBlockers,
126
+ metadata: {
127
+ projectName,
128
+ scannedAt: new Date().toISOString(),
129
+ filesScanned: files.length,
130
+ linesScanned: files.reduce((s, f) => s + f.lines, 0),
131
+ ignoredPaths: ignoredCount,
132
+ reportConfidence: reportConfidence.score,
133
+ reportConfidenceGrade: reportConfidence.grade,
134
+ reportConfidenceExplanation: reportConfidence.explanation,
135
+ staticFindings: deduplicated.filter(f => f.source === "static").length,
136
+ aiFindings: deduplicated.filter(f => f.source === "ai").length,
137
+ verifiedFindings: deduplicated.filter(f => f.source === "verified").length,
138
+ },
139
+ };
140
+ const stats = {
141
+ filesScanned: files.length,
142
+ linesScanned: files.reduce((s, f) => s + f.lines, 0),
143
+ staticFindings: staticFindings.length,
144
+ aiFindings: aiFindings.length,
145
+ verifiedFindings: deduplicated.length,
146
+ falsePositives: verificationResult.summary.falsePositives,
147
+ tokensSaved: `${tokenReduction}% (${Math.round((totalOriginalChars - totalExtractedChars) / 1000)}K chars)`,
148
+ };
149
+ return { report, stats };
150
+ }
151
+ async function runAIAnalysisCLI(projectName, extractedSections, staticFindings, onProgress, mode) {
152
+ if (extractedSections.length === 0 && staticFindings.length === 0) {
153
+ return [];
154
+ }
155
+ const apiKeys = (0, openrouter_1.getApiKeys)();
156
+ if (apiKeys.length === 0) {
157
+ throw new Error("No OpenRouter API keys configured. Set OPENROUTER_API_KEY_1, _2, _3 or OPENROUTER_API_KEYS.");
158
+ }
159
+ const aiFindings = [];
160
+ const batchSize = mode === "quick" ? 3 : 5;
161
+ const batches = createBatches(extractedSections, staticFindings, batchSize);
162
+ for (let i = 0; i < batches.length; i++) {
163
+ const progressPct = 50 + Math.round(((i + 1) / batches.length) * 25);
164
+ onProgress("AI review", progressPct, `Processing batch ${i + 1}/${batches.length}…`);
165
+ try {
166
+ const findings = await analyzeBatchWithAI(projectName, batches[i], i % apiKeys.length, i);
167
+ aiFindings.push(...findings);
168
+ }
169
+ catch (error) {
170
+ console.warn(`Batch ${i + 1} failed, continuing...`);
171
+ }
172
+ }
173
+ return aiFindings;
174
+ }
175
+ function createBatches(extractedSections, staticFindings, maxBatches) {
176
+ const MAX_CHARS_PER_BATCH = 30000;
177
+ const batches = [];
178
+ let currentBatch = { sections: [], staticFindings: [] };
179
+ let currentChars = 0;
180
+ for (const section of extractedSections) {
181
+ const sectionChars = section.sections.reduce((sum, s) => sum + s.code.length, 0);
182
+ if (currentChars + sectionChars > MAX_CHARS_PER_BATCH && currentBatch.sections.length > 0) {
183
+ batches.push(currentBatch);
184
+ currentBatch = { sections: [], staticFindings: [] };
185
+ currentChars = 0;
186
+ }
187
+ currentBatch.sections.push(section);
188
+ currentChars += sectionChars;
189
+ }
190
+ if (staticFindings.length > 0) {
191
+ currentBatch.staticFindings = staticFindings.slice(0, 20);
192
+ }
193
+ if (currentBatch.sections.length > 0 || currentBatch.staticFindings.length > 0) {
194
+ batches.push(currentBatch);
195
+ }
196
+ return batches.slice(0, maxBatches);
197
+ }
198
+ async function analyzeBatchWithAI(projectName, batch, keyIndex, batchIndex) {
199
+ const prompt = (0, prompts_1.getAnalysisPrompt)(projectName, batch, batchIndex);
200
+ const messages = [
201
+ { role: "system", content: "You are a security code analysis expert. Analyze the provided code for vulnerabilities and provide findings in JSON format." },
202
+ { role: "user", content: prompt }
203
+ ];
204
+ const result = await (0, openrouter_1.chatCompletion)(messages, undefined, 2);
205
+ try {
206
+ const parsed = (0, openrouter_1.parseJsonFromModel)(result.content);
207
+ return parsed.findings || [];
208
+ }
209
+ catch (error) {
210
+ console.error("Failed to parse AI response:", error);
211
+ return [];
212
+ }
213
+ }
214
+ function calculateStrictScore(findings) {
215
+ if (findings.length === 0)
216
+ return 95;
217
+ const CATEGORY_WEIGHTS = {
218
+ critical: 35,
219
+ high: 25,
220
+ medium: 25,
221
+ low: 10,
222
+ info: 5,
223
+ };
224
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
225
+ for (const finding of findings) {
226
+ let weight = 1.0;
227
+ switch (finding.confidence) {
228
+ case "high":
229
+ weight = 1.0;
230
+ break;
231
+ case "medium":
232
+ weight = 0.7;
233
+ break;
234
+ case "low":
235
+ weight = 0.4;
236
+ break;
237
+ }
238
+ counts[finding.severity] += weight;
239
+ }
240
+ const categoryScores = {
241
+ critical: calculateCategoryScore(counts.critical, CATEGORY_WEIGHTS.critical, 0.5),
242
+ high: calculateCategoryScore(counts.high, CATEGORY_WEIGHTS.high, 0.4),
243
+ medium: calculateCategoryScore(counts.medium, CATEGORY_WEIGHTS.medium, 0.3),
244
+ low: calculateCategoryScore(counts.low, CATEGORY_WEIGHTS.low, 0.2),
245
+ info: calculateCategoryScore(counts.info, CATEGORY_WEIGHTS.info, 0.1),
246
+ };
247
+ const totalScore = Math.round(categoryScores.critical + categoryScores.high +
248
+ categoryScores.medium + categoryScores.low + categoryScores.info);
249
+ return Math.max(0, Math.min(100, totalScore));
250
+ }
251
+ function calculateCategoryScore(issueCount, maxPoints, decayRate) {
252
+ if (issueCount === 0)
253
+ return maxPoints;
254
+ const score = maxPoints * Math.exp(-decayRate * issueCount);
255
+ return Math.max(0, Math.round(score * 100) / 100);
256
+ }
257
+ function scoreToGrade(score) {
258
+ if (score >= 95)
259
+ return "A+";
260
+ if (score >= 90)
261
+ return "A";
262
+ if (score >= 85)
263
+ return "A-";
264
+ if (score >= 80)
265
+ return "B+";
266
+ if (score >= 75)
267
+ return "B";
268
+ if (score >= 70)
269
+ return "B-";
270
+ if (score >= 65)
271
+ return "C+";
272
+ if (score >= 60)
273
+ return "C";
274
+ if (score >= 55)
275
+ return "C-";
276
+ if (score >= 50)
277
+ return "D+";
278
+ if (score >= 45)
279
+ return "D";
280
+ if (score >= 40)
281
+ return "D-";
282
+ return "F";
283
+ }
284
+ function generateExecutiveVerdict(findings, score) {
285
+ const critical = findings.filter(f => f.severity === "critical").length;
286
+ const high = findings.filter(f => f.severity === "high").length;
287
+ if (critical > 0) {
288
+ return `CRITICAL: ${critical} critical security vulnerabilities detected. This codebase is NOT production-ready. Immediate remediation required.`;
289
+ }
290
+ if (high > 5) {
291
+ return `HIGH RISK: ${high} high-severity issues require urgent attention before production deployment.`;
292
+ }
293
+ if (score >= 80) {
294
+ return "GOOD: This codebase has good security practices with minor issues that can be improved.";
295
+ }
296
+ if (score >= 60) {
297
+ return "MODERATE: This codebase has some security and quality concerns that should be addressed.";
298
+ }
299
+ return "POOR: This codebase has significant security and quality issues that require attention.";
300
+ }
301
+ function identifyStrengths(files, findings) {
302
+ const strengths = [];
303
+ const categories = new Set(findings.map(f => f.category));
304
+ if (!categories.has("security")) {
305
+ strengths.push("No obvious security vulnerabilities detected");
306
+ }
307
+ if (!categories.has("production")) {
308
+ strengths.push("Good error handling practices");
309
+ }
310
+ if (!findings.some(f => f.severity === "critical")) {
311
+ strengths.push("No critical severity issues found");
312
+ }
313
+ if (strengths.length === 0) {
314
+ strengths.push("Codebase structure is analyzable");
315
+ }
316
+ return strengths;
317
+ }
318
+ function generateMitigation(finding) {
319
+ const mitigations = {
320
+ "sql-injection": "Use parameterized queries or ORM with built-in SQL injection protection",
321
+ "xss": "Sanitize user input with DOMPurify or use React's automatic escaping",
322
+ "command-injection": "Avoid executing shell commands with user input, use safe alternatives",
323
+ "hardcoded-secret": "Move secrets to environment variables",
324
+ "missing-auth": "Add authentication middleware to protect sensitive endpoints",
325
+ "empty-catch": "Add proper error handling and logging in catch blocks",
326
+ };
327
+ for (const [key, value] of Object.entries(mitigations)) {
328
+ if (finding.title.toLowerCase().includes(key)) {
329
+ return value;
330
+ }
331
+ }
332
+ return "Review and fix the identified issue following security best practices";
333
+ }
334
+ function generatePrevention(finding) {
335
+ return "Implement security best practices and code review processes to prevent similar issues";
336
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * VettCode CLI - Terminal-based code security scanner
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ const commander_1 = require("commander");
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const chalk_1 = __importDefault(require("chalk"));
47
+ const ora_1 = __importDefault(require("ora"));
48
+ const cli_table3_1 = __importDefault(require("cli-table3"));
49
+ const file_collector_1 = require("./file-collector");
50
+ const cli_scan_orchestrator_1 = require("./cli-scan-orchestrator");
51
+ const dotenv = __importStar(require("dotenv"));
52
+ const program = new commander_1.Command();
53
+ program
54
+ .name("vettcode")
55
+ .description("AI-powered codebase security and quality scanner")
56
+ .version("1.0.0");
57
+ program
58
+ .argument("<directory>", "Directory to scan")
59
+ .option("-o, --output <file>", "Output report to JSON file")
60
+ .option("-i, --ignore <patterns>", "Comma-separated ignore patterns")
61
+ .option("--json", "Output JSON format")
62
+ .option("--mode <mode>", "Scan mode: quick or deep (default: quick)")
63
+ .option("--no-ai", "Disable AI analysis (static only)")
64
+ .action(async (directory, options) => {
65
+ try {
66
+ // Load environment variables
67
+ dotenv.config();
68
+ console.log(chalk_1.default.bold.cyan("\n🔍 VettCode CLI - Security Scanner\n"));
69
+ // Validate directory
70
+ const resolvedPath = path.resolve(directory);
71
+ if (!fs.existsSync(resolvedPath)) {
72
+ console.error(chalk_1.default.red(`❌ Error: Directory not found: ${directory}`));
73
+ process.exit(1);
74
+ }
75
+ // Parse ignore patterns
76
+ const ignorePatterns = options.ignore
77
+ ? options.ignore.split(",").map((p) => p.trim())
78
+ : undefined;
79
+ // Collect files
80
+ const collectSpinner = (0, ora_1.default)("Collecting files...").start();
81
+ const files = (0, file_collector_1.collectFiles)(resolvedPath, ignorePatterns);
82
+ collectSpinner.succeed(`Collected ${files.length} files`);
83
+ if (files.length === 0) {
84
+ console.warn(chalk_1.default.yellow("⚠️ No code files found to scan"));
85
+ process.exit(0);
86
+ }
87
+ const projectName = path.basename(resolvedPath);
88
+ const scanMode = (options.mode === "deep" ? "deep" : "quick");
89
+ // Run smart scan
90
+ const scanSpinner = (0, ora_1.default)("Running smart scan...").start();
91
+ const { report, stats } = await (0, cli_scan_orchestrator_1.runSmartScan)(projectName, files, 0, (phase, pct, detail) => {
92
+ scanSpinner.text = `${phase} (${pct}%)${detail ? ` - ${detail}` : ''}`;
93
+ }, scanMode);
94
+ scanSpinner.succeed(`Scan complete: ${stats.verifiedFindings} verified issues found`);
95
+ // Display results
96
+ if (options.json) {
97
+ console.log(JSON.stringify(report, null, 2));
98
+ }
99
+ else {
100
+ displayReport(report, stats);
101
+ }
102
+ // Save to file if requested
103
+ if (options.output) {
104
+ const outputPath = path.resolve(options.output);
105
+ fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
106
+ console.log(chalk_1.default.green(`\n📄 Report saved to: ${outputPath}`));
107
+ }
108
+ // Exit with error code if critical issues found
109
+ const criticalCount = report.findings.filter(f => f.severity === "critical").length;
110
+ if (criticalCount > 0) {
111
+ process.exit(1);
112
+ }
113
+ }
114
+ catch (error) {
115
+ console.error(chalk_1.default.red(`\n❌ Error: ${error instanceof Error ? error.message : String(error)}`));
116
+ process.exit(1);
117
+ }
118
+ });
119
+ program.parse();
120
+ function displayReport(report, stats) {
121
+ // Score header
122
+ console.log("\n" + chalk_1.default.bold("═".repeat(60)));
123
+ console.log(chalk_1.default.bold("SCAN RESULTS".padStart(60 - "SCAN RESULTS".length / 2).padEnd(60)));
124
+ console.log(chalk_1.default.bold("═".repeat(60)));
125
+ // Score with color
126
+ const scoreColor = report.score >= 80 ? "green" : report.score >= 60 ? "yellow" : "red";
127
+ console.log(`\n${chalk_1.default.bold("Score:")} ${chalk_1.default[scoreColor].bold(report.score + "/100")} ${chalk_1.default.bold(`(${report.grade})`)}`);
128
+ // Summary
129
+ console.log(chalk_1.default.gray(`\n${report.summary}`));
130
+ // Executive verdict
131
+ console.log(chalk_1.default.bold.cyan(`\n📋 Executive Verdict:`));
132
+ console.log(chalk_1.default.white(report.executiveVerdict));
133
+ // Findings by severity
134
+ const findingsBySeverity = {
135
+ critical: report.findings.filter(f => f.severity === "critical"),
136
+ high: report.findings.filter(f => f.severity === "high"),
137
+ medium: report.findings.filter(f => f.severity === "medium"),
138
+ low: report.findings.filter(f => f.severity === "low"),
139
+ info: report.findings.filter(f => f.severity === "info"),
140
+ };
141
+ console.log(chalk_1.default.bold.cyan(`\n🔍 Findings by Severity:`));
142
+ console.log(` ${chalk_1.default.red.bold(findingsBySeverity.critical.length)} Critical`);
143
+ console.log(` ${chalk_1.default.red(findingsBySeverity.high.length)} High`);
144
+ console.log(` ${chalk_1.default.yellow(findingsBySeverity.medium.length)} Medium`);
145
+ console.log(` ${chalk_1.default.gray(findingsBySeverity.low.length)} Low`);
146
+ console.log(` ${chalk_1.default.gray(findingsBySeverity.info.length)} Info`);
147
+ // Critical blockers
148
+ if (report.criticalBlockers.length > 0) {
149
+ console.log(chalk_1.default.bold.red(`\n🚨 Critical Blockers:`));
150
+ report.criticalBlockers.forEach(blocker => {
151
+ console.log(chalk_1.default.red(` • ${blocker}`));
152
+ });
153
+ }
154
+ // Strengths
155
+ if (report.strengths.length > 0) {
156
+ console.log(chalk_1.default.bold.green(`\n✅ Strengths:`));
157
+ report.strengths.forEach(strength => {
158
+ console.log(chalk_1.default.green(` • ${strength}`));
159
+ });
160
+ }
161
+ // Detailed findings table
162
+ if (report.findings.length > 0) {
163
+ console.log(chalk_1.default.bold.cyan(`\n📝 Detailed Findings:`));
164
+ const table = new cli_table3_1.default({
165
+ head: [
166
+ chalk_1.default.bold("Severity"),
167
+ chalk_1.default.bold("Category"),
168
+ chalk_1.default.bold("Title"),
169
+ chalk_1.default.bold("File"),
170
+ chalk_1.default.bold("Line")
171
+ ],
172
+ colWidths: [10, 15, 30, 25, 6],
173
+ wordWrap: true,
174
+ });
175
+ for (const finding of report.findings.slice(0, 20)) { // Limit to first 20
176
+ const severityColor = finding.severity === "critical" ? "red"
177
+ : finding.severity === "high" ? "red"
178
+ : finding.severity === "medium" ? "yellow"
179
+ : "gray";
180
+ table.push([
181
+ chalk_1.default[severityColor](finding.severity.toUpperCase()),
182
+ finding.category,
183
+ finding.title.substring(0, 28),
184
+ finding.file?.substring(0, 23) || "",
185
+ finding.line?.toString() || ""
186
+ ]);
187
+ }
188
+ console.log(table.toString());
189
+ if (report.findings.length > 20) {
190
+ console.log(chalk_1.default.gray(`\n... and ${report.findings.length - 20} more findings`));
191
+ }
192
+ }
193
+ // Metadata
194
+ console.log(chalk_1.default.bold.gray(`\n📊 Scan Metadata:`));
195
+ console.log(chalk_1.default.gray(` Project: ${report.metadata?.projectName}`));
196
+ console.log(chalk_1.default.gray(` Files Scanned: ${stats?.filesScanned || report.metadata?.filesScanned}`));
197
+ console.log(chalk_1.default.gray(` Lines Scanned: ${stats?.linesScanned || report.metadata?.linesScanned}`));
198
+ console.log(chalk_1.default.gray(` Static Findings: ${stats?.staticFindings || report.metadata?.staticFindings}`));
199
+ console.log(chalk_1.default.gray(` AI Findings: ${stats?.aiFindings || report.metadata?.aiFindings}`));
200
+ console.log(chalk_1.default.gray(` Verified Findings: ${stats?.verifiedFindings || report.findings.length}`));
201
+ console.log(chalk_1.default.gray(` False Positives Removed: ${stats?.falsePositives || "N/A"}`));
202
+ if (stats?.tokensSaved) {
203
+ console.log(chalk_1.default.gray(` Tokens Saved: ${stats.tokensSaved}`));
204
+ }
205
+ console.log(chalk_1.default.gray(` Report Confidence: ${report.metadata?.reportConfidence}% (${report.metadata?.reportConfidenceGrade})`));
206
+ console.log(chalk_1.default.gray(` Scanned At: ${report.metadata?.scannedAt}`));
207
+ console.log(chalk_1.default.bold("═".repeat(60) + "\n"));
208
+ }