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
package/dist/types.js
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Verification Layer
|
|
4
|
+
* Validates AI findings to prevent hallucinations and false positives
|
|
5
|
+
* Cross-references with static analysis and code context
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.verifyFindings = verifyFindings;
|
|
9
|
+
exports.deduplicateFindings = deduplicateFindings;
|
|
10
|
+
exports.calculateReportConfidence = calculateReportConfidence;
|
|
11
|
+
/**
|
|
12
|
+
* Cross-verify AI findings with static analysis results
|
|
13
|
+
*/
|
|
14
|
+
function verifyFindings(aiFindings, staticFindings, codeFiles) {
|
|
15
|
+
const verified = [];
|
|
16
|
+
const falsePositives = [];
|
|
17
|
+
for (const aiFinding of aiFindings) {
|
|
18
|
+
const verification = verifyIndividualFinding(aiFinding, staticFindings, codeFiles);
|
|
19
|
+
if (verification.verificationStatus === "false-positive") {
|
|
20
|
+
falsePositives.push(aiFinding);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
verified.push(verification);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Sort by confidence and severity
|
|
27
|
+
verified.sort((a, b) => {
|
|
28
|
+
const confidenceScore = { high: 3, medium: 2, low: 1 };
|
|
29
|
+
const severityScore = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
|
|
30
|
+
const aScore = confidenceScore[a.confidence] + severityScore[a.severity];
|
|
31
|
+
const bScore = confidenceScore[b.confidence] + severityScore[b.severity];
|
|
32
|
+
return bScore - aScore;
|
|
33
|
+
});
|
|
34
|
+
const summary = {
|
|
35
|
+
totalFindings: aiFindings.length,
|
|
36
|
+
confirmed: verified.filter(f => f.verificationStatus === "confirmed").length,
|
|
37
|
+
likely: verified.filter(f => f.verificationStatus === "likely").length,
|
|
38
|
+
uncertain: verified.filter(f => f.verificationStatus === "uncertain").length,
|
|
39
|
+
falsePositives: falsePositives.length,
|
|
40
|
+
};
|
|
41
|
+
return { verified, falsePositives, summary };
|
|
42
|
+
}
|
|
43
|
+
function verifyIndividualFinding(aiFinding, staticFindings, codeFiles) {
|
|
44
|
+
const sources = ["ai-analysis"];
|
|
45
|
+
let confidence = "medium";
|
|
46
|
+
let verificationStatus = "likely";
|
|
47
|
+
let verificationNotes = "";
|
|
48
|
+
// 1. Check if static analysis also found this issue
|
|
49
|
+
const matchingStatic = staticFindings.find(sf => sf.file === aiFinding.file &&
|
|
50
|
+
Math.abs(sf.line - aiFinding.line) <= 3 && // Within 3 lines
|
|
51
|
+
sf.category === aiFinding.category);
|
|
52
|
+
if (matchingStatic) {
|
|
53
|
+
sources.push("static-analysis");
|
|
54
|
+
confidence = "high";
|
|
55
|
+
verificationStatus = "confirmed";
|
|
56
|
+
verificationNotes = "Confirmed by static analysis";
|
|
57
|
+
}
|
|
58
|
+
// 2. Verify the evidence actually exists in the code
|
|
59
|
+
const file = codeFiles.find(f => f.path === aiFinding.file);
|
|
60
|
+
if (file) {
|
|
61
|
+
const lines = file.content.split("\n");
|
|
62
|
+
const targetLine = lines[aiFinding.line - 1];
|
|
63
|
+
if (!targetLine) {
|
|
64
|
+
verificationStatus = "false-positive";
|
|
65
|
+
verificationNotes = "Line number does not exist in file";
|
|
66
|
+
confidence = "low";
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Check if evidence matches actual code
|
|
70
|
+
// Ensure evidence is a string before calling trim()
|
|
71
|
+
const evidenceStr = typeof aiFinding.evidence === 'string' ? aiFinding.evidence : String(aiFinding.evidence || '');
|
|
72
|
+
const evidenceNormalized = evidenceStr.trim().replace(/\s+/g, " ");
|
|
73
|
+
const lineNormalized = targetLine.trim().replace(/\s+/g, " ");
|
|
74
|
+
if (evidenceNormalized && (lineNormalized.includes(evidenceNormalized) || evidenceNormalized.includes(lineNormalized))) {
|
|
75
|
+
sources.push("pattern-match");
|
|
76
|
+
if (verificationStatus === "likely") {
|
|
77
|
+
verificationStatus = "confirmed";
|
|
78
|
+
verificationNotes = "Evidence matches code exactly";
|
|
79
|
+
confidence = "high";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Evidence doesn't match - might be hallucination
|
|
84
|
+
if (verificationStatus !== "confirmed") {
|
|
85
|
+
verificationStatus = "uncertain";
|
|
86
|
+
verificationNotes = "Evidence does not match actual code at specified line";
|
|
87
|
+
confidence = "low";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
verificationStatus = "false-positive";
|
|
94
|
+
verificationNotes = "File not found in codebase";
|
|
95
|
+
confidence = "low";
|
|
96
|
+
}
|
|
97
|
+
// 3. Severity-specific validation
|
|
98
|
+
if (aiFinding.severity === "critical" || aiFinding.severity === "high") {
|
|
99
|
+
// High-severity findings need stronger evidence
|
|
100
|
+
if (sources.length < 2 && verificationStatus !== "confirmed") {
|
|
101
|
+
confidence = confidence === "high" ? "medium" : "low";
|
|
102
|
+
verificationNotes += "; High-severity finding needs stronger evidence";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// 4. Category-specific validation
|
|
106
|
+
const categoryValidation = validateByCategory(aiFinding, file?.content);
|
|
107
|
+
if (categoryValidation.isFalsePositive) {
|
|
108
|
+
verificationStatus = "false-positive";
|
|
109
|
+
verificationNotes = categoryValidation.reason || "Marked as false positive";
|
|
110
|
+
confidence = "low";
|
|
111
|
+
}
|
|
112
|
+
else if (categoryValidation.adjustConfidence) {
|
|
113
|
+
confidence = categoryValidation.adjustConfidence;
|
|
114
|
+
verificationNotes += categoryValidation.reason ? `; ${categoryValidation.reason}` : "";
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
...aiFinding,
|
|
118
|
+
confidence,
|
|
119
|
+
verificationStatus,
|
|
120
|
+
verificationNotes,
|
|
121
|
+
sources,
|
|
122
|
+
source: sources.includes("static-analysis") ? "verified" : "ai", // Tag AI-discovered findings
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function validateByCategory(finding, fileContent) {
|
|
126
|
+
if (!fileContent)
|
|
127
|
+
return { isFalsePositive: false };
|
|
128
|
+
const lines = fileContent.split("\n");
|
|
129
|
+
const contextStart = Math.max(0, finding.line - 5);
|
|
130
|
+
const contextEnd = Math.min(lines.length, finding.line + 5);
|
|
131
|
+
const context = lines.slice(contextStart, contextEnd).join("\n");
|
|
132
|
+
const fullContext = lines.slice(Math.max(0, finding.line - 20), Math.min(lines.length, finding.line + 20)).join("\n");
|
|
133
|
+
switch (finding.category) {
|
|
134
|
+
case "security":
|
|
135
|
+
// SQL Injection validation
|
|
136
|
+
if (finding.title.toLowerCase().includes("sql injection")) {
|
|
137
|
+
// Check if parameterized queries are used
|
|
138
|
+
if (/\$\d+|\?|:[\w]+/.test(context)) {
|
|
139
|
+
return {
|
|
140
|
+
isFalsePositive: true,
|
|
141
|
+
reason: "Uses parameterized queries, not vulnerable to SQL injection",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Check for ORM usage (Prisma, TypeORM, Sequelize, Mongoose, etc.)
|
|
145
|
+
if (/prisma\.|typeorm\.|sequelize\.|mongoose\./i.test(context)) {
|
|
146
|
+
return {
|
|
147
|
+
isFalsePositive: true,
|
|
148
|
+
reason: "Uses ORM with built-in SQL injection protection",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Check for query builders (Knex, etc.)
|
|
152
|
+
if (/knex\(|\.where\(|\.select\(|\.insert\(/i.test(context)) {
|
|
153
|
+
return {
|
|
154
|
+
isFalsePositive: true,
|
|
155
|
+
reason: "Uses query builder with parameterization",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// XSS validation
|
|
160
|
+
if (finding.title.toLowerCase().includes("xss") || finding.title.toLowerCase().includes("cross-site scripting")) {
|
|
161
|
+
// Check for sanitization
|
|
162
|
+
if (/DOMPurify|sanitize|escape|encodeURIComponent|textContent/i.test(fullContext)) {
|
|
163
|
+
return {
|
|
164
|
+
isFalsePositive: true,
|
|
165
|
+
reason: "Content is sanitized before rendering",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// React/Next.js automatically escapes by default
|
|
169
|
+
if (/\{.*\}|dangerouslySetInnerHTML/.test(context)) {
|
|
170
|
+
if (!/dangerouslySetInnerHTML/.test(context)) {
|
|
171
|
+
return {
|
|
172
|
+
isFalsePositive: true,
|
|
173
|
+
reason: "React/Next.js automatically escapes JSX expressions",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Hardcoded secrets validation
|
|
179
|
+
if (finding.title.toLowerCase().includes("hardcoded") || finding.title.toLowerCase().includes("secret") || finding.title.toLowerCase().includes("api key")) {
|
|
180
|
+
// Check if it's actually an env variable or placeholder
|
|
181
|
+
if (/process\.env|import\.meta\.env|YOUR_|XXX|PLACEHOLDER|EXAMPLE|TEST_|DEMO_/i.test(context)) {
|
|
182
|
+
return {
|
|
183
|
+
isFalsePositive: true,
|
|
184
|
+
reason: "Uses environment variable or is a placeholder/example",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Check for common test/example patterns
|
|
188
|
+
if (/sk_test_|pk_test_|test-|demo-|example-/i.test(finding.evidence)) {
|
|
189
|
+
return {
|
|
190
|
+
isFalsePositive: true,
|
|
191
|
+
reason: "Test or example key, not production secret",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Authentication bypass validation
|
|
196
|
+
if (finding.title.toLowerCase().includes("authentication") && finding.title.toLowerCase().includes("bypass")) {
|
|
197
|
+
// Check if there's actual auth middleware or JWT validation
|
|
198
|
+
if (/authenticateToken|isAuthenticated|requireAuth|protect|verifyToken|jwt\.verify|jsonwebtoken|authHeader|Authorization|Bearer/i.test(fullContext)) {
|
|
199
|
+
return {
|
|
200
|
+
isFalsePositive: true,
|
|
201
|
+
reason: "Authentication middleware or token validation is present",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Missing authentication check validation
|
|
206
|
+
if (finding.title.toLowerCase().includes("missing") && finding.title.toLowerCase().includes("authentication")) {
|
|
207
|
+
// Check if auth is actually implemented
|
|
208
|
+
if (/req\.headers\.get\(['"]authorization['"]\)|authHeader|Bearer|jwt\.verify|verifyToken|requireAuth|isAuthenticated/i.test(fullContext)) {
|
|
209
|
+
return {
|
|
210
|
+
isFalsePositive: true,
|
|
211
|
+
reason: "Authentication check is present in the code",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Check if it's a public endpoint (login, register, health, etc.)
|
|
215
|
+
if (/\/login|\/register|\/signup|\/health|\/ping|\/public|\/webhook/i.test(fileContent)) {
|
|
216
|
+
return {
|
|
217
|
+
isFalsePositive: true,
|
|
218
|
+
reason: "Public endpoint, authentication not required",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Rate limiting validation
|
|
223
|
+
if (finding.title.toLowerCase().includes("rate limit")) {
|
|
224
|
+
// Check for rate limiting implementation
|
|
225
|
+
if (/rateLimit|limiter|throttle|maxRequests|keyLock|requestCount|resetAt/i.test(fullContext)) {
|
|
226
|
+
return {
|
|
227
|
+
isFalsePositive: true,
|
|
228
|
+
reason: "Rate limiting is implemented",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// API key exposure validation
|
|
233
|
+
if (finding.title.toLowerCase().includes("api key") && finding.title.toLowerCase().includes("exposed")) {
|
|
234
|
+
// Check if it's using environment variables
|
|
235
|
+
if (/process\.env|import\.meta\.env|Deno\.env/i.test(context)) {
|
|
236
|
+
return {
|
|
237
|
+
isFalsePositive: true,
|
|
238
|
+
reason: "API keys are loaded from environment variables, not exposed",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// JSON injection validation
|
|
243
|
+
if (finding.title.toLowerCase().includes("json injection")) {
|
|
244
|
+
// Check for input validation or sanitization
|
|
245
|
+
if (/JSON\.parse\([^)]*\).*try|try.*JSON\.parse|\.replace\(|sanitize|validate|DOMPurify/i.test(fullContext)) {
|
|
246
|
+
return {
|
|
247
|
+
isFalsePositive: true,
|
|
248
|
+
reason: "JSON parsing is wrapped in error handling or input is sanitized",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// Check for malicious pattern detection
|
|
252
|
+
if (/__proto__|constructor.*prototype|<script|javascript:|on\w+=/i.test(fullContext)) {
|
|
253
|
+
return {
|
|
254
|
+
isFalsePositive: false,
|
|
255
|
+
adjustConfidence: "high",
|
|
256
|
+
reason: "Code checks for malicious patterns, but verify completeness",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Race condition validation
|
|
261
|
+
if (finding.title.toLowerCase().includes("race condition")) {
|
|
262
|
+
// Check for mutex, locks, or queue implementation
|
|
263
|
+
if (/mutex|lock|queue|await\s+queue|semaphore|Promise\.all\(.*map/i.test(fullContext)) {
|
|
264
|
+
return {
|
|
265
|
+
isFalsePositive: true,
|
|
266
|
+
reason: "Synchronization mechanism (mutex/lock/queue) is implemented",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
case "production":
|
|
272
|
+
// Unhandled promise validation
|
|
273
|
+
if (finding.title.toLowerCase().includes("unhandled")) {
|
|
274
|
+
// Check for try-catch or .catch() or finally
|
|
275
|
+
if (/try\s*\{|\.catch\(|\.finally\(|\.then\([^,)]+,/i.test(fullContext)) {
|
|
276
|
+
return {
|
|
277
|
+
isFalsePositive: true,
|
|
278
|
+
reason: "Error handling is present (try-catch or .catch() or promise rejection handler)",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Check if it's wrapped in another async function with error handling
|
|
282
|
+
if (/async\s+function/.test(fullContext) && /try\s*\{/.test(fullContext)) {
|
|
283
|
+
return {
|
|
284
|
+
isFalsePositive: true,
|
|
285
|
+
reason: "Async function with try-catch error handling",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Missing validation validation
|
|
290
|
+
if (finding.title.toLowerCase().includes("missing") && finding.title.toLowerCase().includes("validation")) {
|
|
291
|
+
// Check for validation libraries (Zod, Joi, Yup, etc.)
|
|
292
|
+
if (/\.parse\(|\.validate\(|\.schema\(|z\.|Joi\.|yup\./i.test(fullContext)) {
|
|
293
|
+
return {
|
|
294
|
+
isFalsePositive: true,
|
|
295
|
+
reason: "Validation is implemented using validation library",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Check for manual validation
|
|
299
|
+
if (/if\s*\([^)]*(?:!|typeof|instanceof|Array\.isArray)\s*[^)]*\)\s*\{?\s*(?:throw|return)/i.test(fullContext)) {
|
|
300
|
+
return {
|
|
301
|
+
isFalsePositive: false,
|
|
302
|
+
adjustConfidence: "medium",
|
|
303
|
+
reason: "Manual validation is present, review for completeness",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
case "database":
|
|
309
|
+
// N+1 query validation
|
|
310
|
+
if (finding.title.toLowerCase().includes("n+1") || finding.title.toLowerCase().includes("n-plus-1")) {
|
|
311
|
+
// Check if it's actually a problem (small arrays are fine)
|
|
312
|
+
if (/\.slice\(0,\s*[1-9]\)|\.take\([1-9]\)|\.limit\([1-9]\)/i.test(context)) {
|
|
313
|
+
return {
|
|
314
|
+
isFalsePositive: true,
|
|
315
|
+
reason: "Limited to small number of items, not a performance issue",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// Check for eager loading (Prisma include, TypeORM relations, etc.)
|
|
319
|
+
if (/include\s*:|relations\s*:|populate\(/i.test(fullContext)) {
|
|
320
|
+
return {
|
|
321
|
+
isFalsePositive: true,
|
|
322
|
+
reason: "Uses eager loading to prevent N+1 queries",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Missing index validation
|
|
327
|
+
if (finding.title.toLowerCase().includes("index") || finding.title.toLowerCase().includes("slow query")) {
|
|
328
|
+
// Check if it's a read-only or small table
|
|
329
|
+
if (/\.findMany\(\)\s*\.length\s*<\s*\d+|LIMIT\s+\d+/i.test(context)) {
|
|
330
|
+
return {
|
|
331
|
+
isFalsePositive: false,
|
|
332
|
+
adjustConfidence: "low",
|
|
333
|
+
reason: "Query appears to be limited, may not need index",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
case "code-quality":
|
|
339
|
+
// AI placeholder code validation
|
|
340
|
+
if (finding.title.toLowerCase().includes("placeholder") || finding.title.toLowerCase().includes("ai-generated")) {
|
|
341
|
+
// Check if the code has actual implementation
|
|
342
|
+
if (/const\s+\w+\s*=\s*await|return\s+\w+|throw\s+new\s+Error|if\s*\(|for\s*\(|while\s*\(/i.test(context)) {
|
|
343
|
+
return {
|
|
344
|
+
isFalsePositive: true,
|
|
345
|
+
reason: "Code has actual implementation, not a placeholder",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// Check for TODO or FIXME comments which indicate it's genuinely incomplete
|
|
349
|
+
if (!/TODO|FIXME|PLACEHOLDER|FIX THIS|IMPLEMENT THIS/i.test(fullContext)) {
|
|
350
|
+
return {
|
|
351
|
+
isFalsePositive: true,
|
|
352
|
+
reason: "No placeholder markers found, appears to be complete code",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Magic number validation
|
|
357
|
+
if (finding.title.toLowerCase().includes("magic number")) {
|
|
358
|
+
// HTTP status codes are acceptable
|
|
359
|
+
if (/\b(?:200|201|204|301|302|304|400|401|403|404|409|422|500|502|503)\b/.test(finding.evidence)) {
|
|
360
|
+
return {
|
|
361
|
+
isFalsePositive: true,
|
|
362
|
+
reason: "HTTP status code, not a magic number",
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
// Common constants (0, 1, -1, 100, 1000) are often acceptable
|
|
366
|
+
if (/\b(?:0|1|-1|100|1000)\b/.test(finding.evidence) && !/\*\s*(?:0|1|-1|100|1000)|(?:0|1|-1|100|1000)\s*\*/.test(context)) {
|
|
367
|
+
return {
|
|
368
|
+
isFalsePositive: false,
|
|
369
|
+
adjustConfidence: "low",
|
|
370
|
+
reason: "Common constant, review if it should be extracted",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Console.log validation
|
|
375
|
+
if (finding.title.toLowerCase().includes("console") && finding.title.toLowerCase().includes("log")) {
|
|
376
|
+
// Check if it's debug logging wrapped in condition
|
|
377
|
+
if (/if\s*\([^)]*(?:debug|dev|development)[^)]*\)/i.test(fullContext)) {
|
|
378
|
+
return {
|
|
379
|
+
isFalsePositive: false,
|
|
380
|
+
adjustConfidence: "low",
|
|
381
|
+
reason: "Conditional debug logging, may be intentional",
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
// Check if it's in a development file
|
|
385
|
+
if (finding.file.includes("dev") || finding.file.includes("test") || finding.file.includes("debug")) {
|
|
386
|
+
return {
|
|
387
|
+
isFalsePositive: true,
|
|
388
|
+
reason: "Development/test file, console.log is acceptable",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// TODO comments validation
|
|
393
|
+
if (finding.title.toLowerCase().includes("todo")) {
|
|
394
|
+
// TODOs in test files are less critical
|
|
395
|
+
if (finding.file.includes("test") || finding.file.includes("spec") || finding.file.includes("__tests__")) {
|
|
396
|
+
return {
|
|
397
|
+
isFalsePositive: false,
|
|
398
|
+
adjustConfidence: "low",
|
|
399
|
+
reason: "TODO in test file, lower priority",
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
case "react":
|
|
405
|
+
// Missing key prop validation
|
|
406
|
+
if (finding.title.toLowerCase().includes("key")) {
|
|
407
|
+
// Check if key is actually present nearby
|
|
408
|
+
if (/key\s*=/i.test(context)) {
|
|
409
|
+
return {
|
|
410
|
+
isFalsePositive: true,
|
|
411
|
+
reason: "Key prop is present",
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
// Check if it's mapping a static array
|
|
415
|
+
if (/\[(["'][^"']+["'],?\s*){1,5}\]\.map/i.test(fullContext)) {
|
|
416
|
+
return {
|
|
417
|
+
isFalsePositive: false,
|
|
418
|
+
adjustConfidence: "low",
|
|
419
|
+
reason: "Static array with few items, key may not be critical",
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// useEffect dependency validation
|
|
424
|
+
if (finding.title.toLowerCase().includes("useeffect") && finding.title.toLowerCase().includes("dependency")) {
|
|
425
|
+
// Check if dependency is explicitly omitted with eslint-disable
|
|
426
|
+
if (/eslint-disable|eslint-disable-next-line/.test(context)) {
|
|
427
|
+
return {
|
|
428
|
+
isFalsePositive: false,
|
|
429
|
+
adjustConfidence: "medium",
|
|
430
|
+
reason: "Dependency intentionally omitted with ESLint disable",
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
case "performance":
|
|
436
|
+
// Large bundle size validation
|
|
437
|
+
if (finding.title.toLowerCase().includes("bundle") || finding.title.toLowerCase().includes("import")) {
|
|
438
|
+
// Check for dynamic imports
|
|
439
|
+
if (/import\(|React\.lazy|next\/dynamic/i.test(fullContext)) {
|
|
440
|
+
return {
|
|
441
|
+
isFalsePositive: true,
|
|
442
|
+
reason: "Uses dynamic imports for code splitting",
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
case "typing":
|
|
448
|
+
// Missing type validation
|
|
449
|
+
if (finding.title.toLowerCase().includes("type") && (finding.title.toLowerCase().includes("missing") || finding.title.toLowerCase().includes("any"))) {
|
|
450
|
+
// Check if it's JavaScript file (types not required)
|
|
451
|
+
if (finding.file.endsWith(".js") || finding.file.endsWith(".jsx")) {
|
|
452
|
+
return {
|
|
453
|
+
isFalsePositive: true,
|
|
454
|
+
reason: "JavaScript file, types not required",
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
// Check if any is intentional with comment
|
|
458
|
+
if (/\/\/\s*@ts-ignore|\/\*.*any.*\*\/|eslint-disable/i.test(context)) {
|
|
459
|
+
return {
|
|
460
|
+
isFalsePositive: false,
|
|
461
|
+
adjustConfidence: "medium",
|
|
462
|
+
reason: "Type explicitly ignored or documented",
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
return { isFalsePositive: false };
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Merge duplicate findings (same issue reported multiple times)
|
|
472
|
+
*/
|
|
473
|
+
function deduplicateFindings(findings) {
|
|
474
|
+
const seen = new Map();
|
|
475
|
+
for (const finding of findings) {
|
|
476
|
+
// Create a key based on file, line, and category
|
|
477
|
+
const key = `${finding.file}:${finding.line}:${finding.category}`;
|
|
478
|
+
const existing = seen.get(key);
|
|
479
|
+
if (existing) {
|
|
480
|
+
// Keep the one with higher confidence
|
|
481
|
+
if (finding.confidence === "high" && existing.confidence !== "high") {
|
|
482
|
+
seen.set(key, finding);
|
|
483
|
+
}
|
|
484
|
+
else if (finding.verificationStatus === "confirmed" && existing.verificationStatus !== "confirmed") {
|
|
485
|
+
seen.set(key, finding);
|
|
486
|
+
}
|
|
487
|
+
// Merge sources
|
|
488
|
+
existing.sources = [...new Set([...existing.sources, ...finding.sources])];
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
seen.set(key, finding);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return Array.from(seen.values());
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Calculate overall confidence score for the entire report
|
|
498
|
+
*/
|
|
499
|
+
function calculateReportConfidence(findings) {
|
|
500
|
+
if (findings.length === 0) {
|
|
501
|
+
return {
|
|
502
|
+
score: 100,
|
|
503
|
+
grade: "A+",
|
|
504
|
+
explanation: "No findings to verify",
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const confirmedCount = findings.filter(f => f.verificationStatus === "confirmed").length;
|
|
508
|
+
const likelyCount = findings.filter(f => f.verificationStatus === "likely").length;
|
|
509
|
+
const uncertainCount = findings.filter(f => f.verificationStatus === "uncertain").length;
|
|
510
|
+
// Weighted confidence score
|
|
511
|
+
const score = Math.round(((confirmedCount * 1.0 + likelyCount * 0.7 + uncertainCount * 0.3) / findings.length) * 100);
|
|
512
|
+
let grade = "F";
|
|
513
|
+
if (score >= 90)
|
|
514
|
+
grade = "A+";
|
|
515
|
+
else if (score >= 80)
|
|
516
|
+
grade = "A";
|
|
517
|
+
else if (score >= 70)
|
|
518
|
+
grade = "B";
|
|
519
|
+
else if (score >= 60)
|
|
520
|
+
grade = "C";
|
|
521
|
+
else if (score >= 50)
|
|
522
|
+
grade = "D";
|
|
523
|
+
const explanation = `${confirmedCount} confirmed, ${likelyCount} likely, ${uncertainCount} uncertain out of ${findings.length} total findings`;
|
|
524
|
+
return { score, grade, explanation };
|
|
525
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vettcode-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI version of VettCode - AI-powered codebase security and quality scanner",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vettcode": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc && node dist/cli.js",
|
|
12
|
+
"start": "node dist/cli.js",
|
|
13
|
+
"lint": "eslint src --ext .ts",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"security",
|
|
18
|
+
"code-scanner",
|
|
19
|
+
"vulnerability",
|
|
20
|
+
"static-analysis",
|
|
21
|
+
"cli",
|
|
22
|
+
"ast",
|
|
23
|
+
"ai",
|
|
24
|
+
"code-review",
|
|
25
|
+
"sast",
|
|
26
|
+
"security-audit"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/mixifys33/vettcode-cli.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/mixifys33/vettcode-cli#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/mixifys33/vettcode-cli/issues"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
".env.example",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=16.0.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@babel/parser": "^7.26.3",
|
|
49
|
+
"@babel/traverse": "^7.26.5",
|
|
50
|
+
"chalk": "^4.1.2",
|
|
51
|
+
"commander": "^11.1.0",
|
|
52
|
+
"ora": "^5.4.1",
|
|
53
|
+
"cli-table3": "^0.6.3",
|
|
54
|
+
"dotenv": "^16.4.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/babel__traverse": "^7.20.6",
|
|
58
|
+
"@types/node": "^22.10.0",
|
|
59
|
+
"typescript": "^5.7.0"
|
|
60
|
+
}
|
|
61
|
+
}
|