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,415 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reference Graph Builder
|
|
4
|
+
*
|
|
5
|
+
* Builds a comprehensive cross-file reference map to enable intelligent
|
|
6
|
+
* context-aware validation with 97%+ accuracy.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Import/Export tracking
|
|
10
|
+
* - Constant definitions (MAX_SIZE, etc.)
|
|
11
|
+
* - Function definitions and calls
|
|
12
|
+
* - Component usage tracking
|
|
13
|
+
* - Data flow analysis
|
|
14
|
+
* - Security validation tracking
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.buildReferenceGraph = buildReferenceGraph;
|
|
18
|
+
exports.hasSizeValidationInChain = hasSizeValidationInChain;
|
|
19
|
+
exports.hasAuthValidationInChain = hasAuthValidationInChain;
|
|
20
|
+
exports.hasInputSanitizationInChain = hasInputSanitizationInChain;
|
|
21
|
+
exports.isUIWiring = isUIWiring;
|
|
22
|
+
exports.getAccessibleSecurityConstants = getAccessibleSecurityConstants;
|
|
23
|
+
/**
|
|
24
|
+
* Build a comprehensive reference graph from all files
|
|
25
|
+
*/
|
|
26
|
+
function buildReferenceGraph(files) {
|
|
27
|
+
const graph = {
|
|
28
|
+
files: new Map(),
|
|
29
|
+
constantsByName: new Map(),
|
|
30
|
+
functionsByName: new Map(),
|
|
31
|
+
componentsByName: new Map(),
|
|
32
|
+
dependencyChains: new Map(),
|
|
33
|
+
};
|
|
34
|
+
// Pass 1: Extract all exports and definitions
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const ref = analyzeFile(file.path, file.content);
|
|
37
|
+
graph.files.set(file.path, ref);
|
|
38
|
+
// Index constants
|
|
39
|
+
for (const constant of ref.securityConstants) {
|
|
40
|
+
const existing = graph.constantsByName.get(constant.name) || [];
|
|
41
|
+
existing.push(file.path);
|
|
42
|
+
graph.constantsByName.set(constant.name, existing);
|
|
43
|
+
}
|
|
44
|
+
// Index functions
|
|
45
|
+
for (const func of ref.exports.functions) {
|
|
46
|
+
const existing = graph.functionsByName.get(func) || [];
|
|
47
|
+
existing.push(file.path);
|
|
48
|
+
graph.functionsByName.set(func, existing);
|
|
49
|
+
}
|
|
50
|
+
// Index components
|
|
51
|
+
for (const comp of ref.exports.components) {
|
|
52
|
+
const existing = graph.componentsByName.get(comp) || [];
|
|
53
|
+
existing.push(file.path);
|
|
54
|
+
graph.componentsByName.set(comp, existing);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Pass 2: Build import relationships and usedBy
|
|
58
|
+
for (const [filePath, ref] of graph.files) {
|
|
59
|
+
for (const imp of ref.imports) {
|
|
60
|
+
// Find the file that exports these items
|
|
61
|
+
const importedFile = resolveImport(imp.from, filePath, graph);
|
|
62
|
+
if (importedFile) {
|
|
63
|
+
const importedRef = graph.files.get(importedFile);
|
|
64
|
+
if (importedRef) {
|
|
65
|
+
importedRef.usedBy.push(filePath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Pass 3: Build dependency chains (recursive)
|
|
71
|
+
for (const [filePath] of graph.files) {
|
|
72
|
+
graph.dependencyChains.set(filePath, buildDependencyChain(filePath, graph, new Set()));
|
|
73
|
+
}
|
|
74
|
+
return graph;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Analyze a single file and extract all relevant information
|
|
78
|
+
*/
|
|
79
|
+
function analyzeFile(path, content) {
|
|
80
|
+
const ref = {
|
|
81
|
+
path,
|
|
82
|
+
exports: {
|
|
83
|
+
functions: [],
|
|
84
|
+
constants: [],
|
|
85
|
+
components: [],
|
|
86
|
+
types: [],
|
|
87
|
+
},
|
|
88
|
+
imports: [],
|
|
89
|
+
securityConstants: [],
|
|
90
|
+
validationFunctions: [],
|
|
91
|
+
usedBy: [],
|
|
92
|
+
metadata: {
|
|
93
|
+
isComponent: /\.tsx$/.test(path) && !/\.test\./.test(path),
|
|
94
|
+
isPage: /\/pages?\/|\/app\/.*page\.tsx/.test(path),
|
|
95
|
+
isAPI: /\/api\//.test(path),
|
|
96
|
+
isUtil: /\/utils?\/|\/lib\/|\/helpers?\//.test(path),
|
|
97
|
+
isTest: /\.(test|spec)\.[jt]sx?$/.test(path),
|
|
98
|
+
isConfig: /\.(config|rc)\.[jt]s$/.test(path),
|
|
99
|
+
purpose: 'unknown',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
// Determine file purpose
|
|
103
|
+
if (ref.metadata.isTest)
|
|
104
|
+
ref.metadata.purpose = 'test';
|
|
105
|
+
else if (ref.metadata.isConfig)
|
|
106
|
+
ref.metadata.purpose = 'config';
|
|
107
|
+
else if (ref.metadata.isAPI)
|
|
108
|
+
ref.metadata.purpose = 'api';
|
|
109
|
+
else if (ref.metadata.isPage || ref.metadata.isComponent)
|
|
110
|
+
ref.metadata.purpose = 'ui';
|
|
111
|
+
else if (ref.metadata.isUtil)
|
|
112
|
+
ref.metadata.purpose = 'util';
|
|
113
|
+
else if (/static-analyzer|ast-extractor|scanner|verification/.test(path))
|
|
114
|
+
ref.metadata.purpose = 'scanner';
|
|
115
|
+
// Extract imports
|
|
116
|
+
const importMatches = content.matchAll(/import\s+(?:{([^}]+)}|(\w+))\s+from\s+['"]([^'"]+)['"]/g);
|
|
117
|
+
for (const match of importMatches) {
|
|
118
|
+
const items = match[1]
|
|
119
|
+
? match[1].split(',').map(s => s.trim().replace(/\s+as\s+\w+/, ''))
|
|
120
|
+
: [match[2]];
|
|
121
|
+
ref.imports.push({
|
|
122
|
+
from: match[3],
|
|
123
|
+
items: items.filter(Boolean),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Extract exports
|
|
127
|
+
// Named exports: export const/function/class
|
|
128
|
+
const namedExports = content.matchAll(/export\s+(?:const|let|var|function|class)\s+(\w+)/g);
|
|
129
|
+
for (const match of namedExports) {
|
|
130
|
+
const name = match[1];
|
|
131
|
+
// Determine type
|
|
132
|
+
if (/^[A-Z]/.test(name)) {
|
|
133
|
+
if (content.includes(`function ${name}`) || content.includes(`const ${name} = (`)) {
|
|
134
|
+
ref.exports.components.push(name);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
ref.exports.constants.push(name);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (content.includes(`function ${name}`) || content.includes(`const ${name} = (`)) {
|
|
141
|
+
ref.exports.functions.push(name);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
ref.exports.constants.push(name);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Default exports
|
|
148
|
+
const defaultExport = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
|
|
149
|
+
if (defaultExport) {
|
|
150
|
+
const name = defaultExport[1];
|
|
151
|
+
if (/^[A-Z]/.test(name)) {
|
|
152
|
+
ref.exports.components.push(name);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
ref.exports.functions.push(name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Extract security constants (size limits, timeouts, etc.)
|
|
159
|
+
const sizeConstants = content.matchAll(/const\s+(MAX_[A-Z_]*(?:SIZE|BYTES|LENGTH|LIMIT))\s*=\s*([^;]+)/gi);
|
|
160
|
+
for (const match of sizeConstants) {
|
|
161
|
+
ref.securityConstants.push({
|
|
162
|
+
name: match[1],
|
|
163
|
+
type: 'size_limit',
|
|
164
|
+
value: match[2].trim(),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const timeoutConstants = content.matchAll(/const\s+(.*?TIMEOUT.*?)\s*=\s*([^;]+)/gi);
|
|
168
|
+
for (const match of timeoutConstants) {
|
|
169
|
+
ref.securityConstants.push({
|
|
170
|
+
name: match[1],
|
|
171
|
+
type: 'timeout',
|
|
172
|
+
value: match[2].trim(),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const rateLimitConstants = content.matchAll(/const\s+(.*?(?:RATE|LIMIT|RETRY).*?)\s*=\s*([^;]+)/gi);
|
|
176
|
+
for (const match of rateLimitConstants) {
|
|
177
|
+
ref.securityConstants.push({
|
|
178
|
+
name: match[1],
|
|
179
|
+
type: 'rate_limit',
|
|
180
|
+
value: match[2].trim(),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Extract validation functions
|
|
184
|
+
// Size validation
|
|
185
|
+
if (/file\.size|byteLength|contentLength/.test(content)) {
|
|
186
|
+
const sizeValidators = content.matchAll(/(?:function|const)\s+(\w*[Vv]alidate\w*[Ss]ize\w*)/g);
|
|
187
|
+
for (const match of sizeValidators) {
|
|
188
|
+
ref.validationFunctions.push({
|
|
189
|
+
name: match[1],
|
|
190
|
+
validates: 'size',
|
|
191
|
+
pattern: match[0],
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Auth validation
|
|
196
|
+
if (/auth|token|bearer|jwt/i.test(content)) {
|
|
197
|
+
const authValidators = content.matchAll(/(?:function|const)\s+(\w*[Aa]uth\w*|\w*[Tt]oken\w*|\w*[Vv]erify\w*)/g);
|
|
198
|
+
for (const match of authValidators) {
|
|
199
|
+
ref.validationFunctions.push({
|
|
200
|
+
name: match[1],
|
|
201
|
+
validates: 'auth',
|
|
202
|
+
pattern: match[0],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Input validation
|
|
207
|
+
if (/sanitize|validate|escape|clean/i.test(content)) {
|
|
208
|
+
const inputValidators = content.matchAll(/(?:function|const)\s+(\w*[Ss]anitize\w*|\w*[Vv]alidate\w*|\w*[Ee]scape\w*|\w*[Cc]lean\w*)/g);
|
|
209
|
+
for (const match of inputValidators) {
|
|
210
|
+
ref.validationFunctions.push({
|
|
211
|
+
name: match[1],
|
|
212
|
+
validates: 'input',
|
|
213
|
+
pattern: match[0],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return ref;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Resolve an import path to an actual file path
|
|
221
|
+
*/
|
|
222
|
+
function resolveImport(importPath, fromFile, graph) {
|
|
223
|
+
// Skip external packages
|
|
224
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
// Relative import
|
|
228
|
+
if (importPath.startsWith('.')) {
|
|
229
|
+
const fromDir = fromFile.split('/').slice(0, -1).join('/');
|
|
230
|
+
let resolved = importPath.startsWith('./')
|
|
231
|
+
? `${fromDir}/${importPath.slice(2)}`
|
|
232
|
+
: importPath;
|
|
233
|
+
// Try different extensions
|
|
234
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx'];
|
|
235
|
+
for (const ext of extensions) {
|
|
236
|
+
const candidate = resolved + ext;
|
|
237
|
+
if (graph.files.has(candidate)) {
|
|
238
|
+
return candidate;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Absolute import (from src/)
|
|
243
|
+
if (importPath.startsWith('@/')) {
|
|
244
|
+
const withoutAlias = importPath.replace('@/', 'src/');
|
|
245
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx'];
|
|
246
|
+
for (const ext of extensions) {
|
|
247
|
+
const candidate = withoutAlias + ext;
|
|
248
|
+
if (graph.files.has(candidate)) {
|
|
249
|
+
return candidate;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Build recursive dependency chain for a file with depth limiting
|
|
257
|
+
*/
|
|
258
|
+
function buildDependencyChain(filePath, graph, visited, depth = 0, maxDepth = 50 // Prevent stack overflow
|
|
259
|
+
) {
|
|
260
|
+
// Depth check to prevent infinite recursion
|
|
261
|
+
if (depth >= maxDepth) {
|
|
262
|
+
console.warn(`[Dependency Chain] Max depth ${maxDepth} reached for ${filePath}`);
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
if (visited.has(filePath))
|
|
266
|
+
return [];
|
|
267
|
+
visited.add(filePath);
|
|
268
|
+
const ref = graph.files.get(filePath);
|
|
269
|
+
if (!ref)
|
|
270
|
+
return [];
|
|
271
|
+
const chain = [];
|
|
272
|
+
for (const imp of ref.imports) {
|
|
273
|
+
const importedFile = resolveImport(imp.from, filePath, graph);
|
|
274
|
+
if (importedFile) {
|
|
275
|
+
chain.push(importedFile);
|
|
276
|
+
// Pass depth + 1 to track recursion level
|
|
277
|
+
chain.push(...buildDependencyChain(importedFile, graph, visited, depth + 1, maxDepth));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return [...new Set(chain)]; // Deduplicate
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Check if a file or its dependencies have size validation with depth limiting
|
|
284
|
+
*/
|
|
285
|
+
function hasSizeValidationInChain(filePath, graph, maxDepth = 20 // Limit dependency traversal depth
|
|
286
|
+
) {
|
|
287
|
+
const visited = new Set();
|
|
288
|
+
function checkWithDepth(file, depth) {
|
|
289
|
+
// Prevent excessive traversal
|
|
290
|
+
if (depth >= maxDepth || visited.has(file)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
visited.add(file);
|
|
294
|
+
const ref = graph.files.get(file);
|
|
295
|
+
if (!ref)
|
|
296
|
+
return false;
|
|
297
|
+
// Check current file
|
|
298
|
+
if (ref.securityConstants.some(c => c.type === 'size_limit')) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
if (ref.validationFunctions.some(v => v.validates === 'size')) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
// Check dependencies with depth limit
|
|
305
|
+
const deps = graph.dependencyChains.get(file) || [];
|
|
306
|
+
// Limit number of dependencies to check per level
|
|
307
|
+
for (const dep of deps.slice(0, 50)) {
|
|
308
|
+
if (checkWithDepth(dep, depth + 1)) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
return checkWithDepth(filePath, 0);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check if a file or its dependencies have auth validation
|
|
318
|
+
*/
|
|
319
|
+
function hasAuthValidationInChain(filePath, graph) {
|
|
320
|
+
const ref = graph.files.get(filePath);
|
|
321
|
+
if (!ref)
|
|
322
|
+
return false;
|
|
323
|
+
// Check current file
|
|
324
|
+
if (ref.validationFunctions.some(v => v.validates === 'auth')) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
// Check dependencies
|
|
328
|
+
const deps = graph.dependencyChains.get(filePath) || [];
|
|
329
|
+
for (const dep of deps) {
|
|
330
|
+
const depRef = graph.files.get(dep);
|
|
331
|
+
if (!depRef)
|
|
332
|
+
continue;
|
|
333
|
+
if (depRef.validationFunctions.some(v => v.validates === 'auth')) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Check if a file or its dependencies have input sanitization
|
|
341
|
+
*/
|
|
342
|
+
function hasInputSanitizationInChain(filePath, graph) {
|
|
343
|
+
const ref = graph.files.get(filePath);
|
|
344
|
+
if (!ref)
|
|
345
|
+
return false;
|
|
346
|
+
// Check current file
|
|
347
|
+
if (ref.validationFunctions.some(v => v.validates === 'input')) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
// Check dependencies
|
|
351
|
+
const deps = graph.dependencyChains.get(filePath) || [];
|
|
352
|
+
for (const dep of deps) {
|
|
353
|
+
const depRef = graph.files.get(dep);
|
|
354
|
+
if (!depRef)
|
|
355
|
+
continue;
|
|
356
|
+
if (depRef.validationFunctions.some(v => v.validates === 'input')) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Check if a file is just UI wiring (delegates to other components)
|
|
364
|
+
*/
|
|
365
|
+
function isUIWiring(filePath, graph) {
|
|
366
|
+
const ref = graph.files.get(filePath);
|
|
367
|
+
if (!ref)
|
|
368
|
+
return false;
|
|
369
|
+
// Must be a UI component
|
|
370
|
+
if (!ref.metadata.isComponent && !ref.metadata.isPage) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
// Check if it imports validation/upload components
|
|
374
|
+
const importsValidationComponents = ref.imports.some(imp => /UploadZone|FileUpload|Dropzone|Upload/.test(imp.items.join(',')));
|
|
375
|
+
if (importsValidationComponents) {
|
|
376
|
+
return true; // Delegates to upload component
|
|
377
|
+
}
|
|
378
|
+
// Check if it's used by other components (intermediate layer)
|
|
379
|
+
if (ref.usedBy.length > 0 && ref.metadata.isComponent) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get all security constants accessible to a file (including imports)
|
|
386
|
+
*/
|
|
387
|
+
function getAccessibleSecurityConstants(filePath, graph) {
|
|
388
|
+
const constants = [];
|
|
389
|
+
const ref = graph.files.get(filePath);
|
|
390
|
+
if (!ref)
|
|
391
|
+
return constants;
|
|
392
|
+
// Add constants from current file
|
|
393
|
+
for (const constant of ref.securityConstants) {
|
|
394
|
+
constants.push({
|
|
395
|
+
name: constant.name,
|
|
396
|
+
type: constant.type,
|
|
397
|
+
source: filePath,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
// Add constants from dependencies
|
|
401
|
+
const deps = graph.dependencyChains.get(filePath) || [];
|
|
402
|
+
for (const dep of deps) {
|
|
403
|
+
const depRef = graph.files.get(dep);
|
|
404
|
+
if (!depRef)
|
|
405
|
+
continue;
|
|
406
|
+
for (const constant of depRef.securityConstants) {
|
|
407
|
+
constants.push({
|
|
408
|
+
name: constant.name,
|
|
409
|
+
type: constant.type,
|
|
410
|
+
source: dep,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return constants;
|
|
415
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Report Generator - Generates security reports from findings
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateReport = generateReport;
|
|
7
|
+
function generateReport(staticFindings, filesScanned, linesScanned, projectName) {
|
|
8
|
+
// Convert static findings to findings
|
|
9
|
+
const findings = staticFindings.map(f => ({
|
|
10
|
+
id: f.id,
|
|
11
|
+
severity: f.severity,
|
|
12
|
+
category: f.category,
|
|
13
|
+
title: f.title,
|
|
14
|
+
description: f.description,
|
|
15
|
+
file: f.file,
|
|
16
|
+
line: f.line,
|
|
17
|
+
evidence: f.evidence,
|
|
18
|
+
mitigation: "",
|
|
19
|
+
prevention: "",
|
|
20
|
+
source: "static",
|
|
21
|
+
}));
|
|
22
|
+
// Calculate score
|
|
23
|
+
const score = calculateScore(findings);
|
|
24
|
+
const grade = calculateGrade(score);
|
|
25
|
+
// Generate summary
|
|
26
|
+
const summary = `Analyzed ${filesScanned} files (${linesScanned} lines). Found ${findings.length} issues.`;
|
|
27
|
+
// Generate executive verdict
|
|
28
|
+
const executiveVerdict = generateExecutiveVerdict(score, findings);
|
|
29
|
+
// Extract critical blockers
|
|
30
|
+
const criticalBlockers = findings
|
|
31
|
+
.filter(f => f.severity === "critical")
|
|
32
|
+
.map(f => `${f.title} in ${f.file}:${f.line}`);
|
|
33
|
+
// Generate strengths
|
|
34
|
+
const strengths = generateStrengths(findings);
|
|
35
|
+
return {
|
|
36
|
+
score,
|
|
37
|
+
grade,
|
|
38
|
+
summary,
|
|
39
|
+
executiveVerdict,
|
|
40
|
+
findings,
|
|
41
|
+
strengths,
|
|
42
|
+
criticalBlockers,
|
|
43
|
+
metadata: {
|
|
44
|
+
projectName,
|
|
45
|
+
scannedAt: new Date().toISOString(),
|
|
46
|
+
filesScanned,
|
|
47
|
+
linesScanned,
|
|
48
|
+
ignoredPaths: 0,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function calculateScore(findings) {
|
|
53
|
+
if (findings.length === 0)
|
|
54
|
+
return 100;
|
|
55
|
+
let score = 100;
|
|
56
|
+
const severityWeights = {
|
|
57
|
+
critical: 25,
|
|
58
|
+
high: 15,
|
|
59
|
+
medium: 8,
|
|
60
|
+
low: 3,
|
|
61
|
+
info: 1,
|
|
62
|
+
};
|
|
63
|
+
for (const finding of findings) {
|
|
64
|
+
score -= severityWeights[finding.severity];
|
|
65
|
+
}
|
|
66
|
+
return Math.max(0, score);
|
|
67
|
+
}
|
|
68
|
+
function calculateGrade(score) {
|
|
69
|
+
if (score >= 95)
|
|
70
|
+
return "A+";
|
|
71
|
+
if (score >= 90)
|
|
72
|
+
return "A";
|
|
73
|
+
if (score >= 85)
|
|
74
|
+
return "A-";
|
|
75
|
+
if (score >= 80)
|
|
76
|
+
return "B+";
|
|
77
|
+
if (score >= 75)
|
|
78
|
+
return "B";
|
|
79
|
+
if (score >= 70)
|
|
80
|
+
return "B-";
|
|
81
|
+
if (score >= 65)
|
|
82
|
+
return "C+";
|
|
83
|
+
if (score >= 60)
|
|
84
|
+
return "C";
|
|
85
|
+
if (score >= 55)
|
|
86
|
+
return "C-";
|
|
87
|
+
if (score >= 50)
|
|
88
|
+
return "D+";
|
|
89
|
+
if (score >= 45)
|
|
90
|
+
return "D";
|
|
91
|
+
if (score >= 40)
|
|
92
|
+
return "D-";
|
|
93
|
+
return "F";
|
|
94
|
+
}
|
|
95
|
+
function generateExecutiveVerdict(score, findings) {
|
|
96
|
+
const criticalCount = findings.filter(f => f.severity === "critical").length;
|
|
97
|
+
const highCount = findings.filter(f => f.severity === "high").length;
|
|
98
|
+
if (criticalCount > 0) {
|
|
99
|
+
return `CRITICAL: This codebase has ${criticalCount} critical security vulnerability(ies) that must be fixed immediately before production deployment.`;
|
|
100
|
+
}
|
|
101
|
+
if (highCount > 3) {
|
|
102
|
+
return `HIGH RISK: This codebase has ${highCount} high-severity issues that should be addressed before production.`;
|
|
103
|
+
}
|
|
104
|
+
if (score >= 80) {
|
|
105
|
+
return "GOOD: This codebase has good security practices with minor issues that can be improved.";
|
|
106
|
+
}
|
|
107
|
+
if (score >= 60) {
|
|
108
|
+
return "MODERATE: This codebase has some security and quality concerns that should be addressed.";
|
|
109
|
+
}
|
|
110
|
+
return "POOR: This codebase has significant security and quality issues that require attention.";
|
|
111
|
+
}
|
|
112
|
+
function generateStrengths(findings) {
|
|
113
|
+
const strengths = [];
|
|
114
|
+
const categories = new Set(findings.map(f => f.category));
|
|
115
|
+
if (!categories.has("security")) {
|
|
116
|
+
strengths.push("No obvious security vulnerabilities detected");
|
|
117
|
+
}
|
|
118
|
+
if (!categories.has("production")) {
|
|
119
|
+
strengths.push("Good error handling practices");
|
|
120
|
+
}
|
|
121
|
+
if (!findings.some(f => f.severity === "critical")) {
|
|
122
|
+
strengths.push("No critical severity issues found");
|
|
123
|
+
}
|
|
124
|
+
if (strengths.length === 0) {
|
|
125
|
+
strengths.push("Codebase structure is analyzable");
|
|
126
|
+
}
|
|
127
|
+
return strengths;
|
|
128
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filePriorityScore = filePriorityScore;
|
|
4
|
+
exports.selectFilesForQuickScan = selectFilesForQuickScan;
|
|
5
|
+
exports.estimateScanMinutes = estimateScanMinutes;
|
|
6
|
+
const ast_extractor_1 = require("./ast-extractor");
|
|
7
|
+
/** Higher score = analyzed first in quick mode. */
|
|
8
|
+
function filePriorityScore(filepath) {
|
|
9
|
+
const p = filepath.replace(/\\/g, "/").toLowerCase();
|
|
10
|
+
let score = 0;
|
|
11
|
+
if (/\/(api|routes|controllers|middleware|auth|security)\//.test(p))
|
|
12
|
+
score += 100;
|
|
13
|
+
if (/\/(app|src|server|backend|lib)\//.test(p))
|
|
14
|
+
score += 40;
|
|
15
|
+
if (/(route|controller|middleware|auth|login|signup|session|jwt|oauth)/i.test(p))
|
|
16
|
+
score += 60;
|
|
17
|
+
if (/(handler|resolver|service|repository|model|schema)/i.test(p))
|
|
18
|
+
score += 30;
|
|
19
|
+
if (/(config|env|settings|database|db|prisma|drizzle|sequelize)/i.test(p))
|
|
20
|
+
score += 50;
|
|
21
|
+
if (/(index|main|app|server|entry)\.[jt]sx?$/.test(p))
|
|
22
|
+
score += 45;
|
|
23
|
+
if (p.includes("package.json") || p.endsWith(".env.example"))
|
|
24
|
+
score += 35;
|
|
25
|
+
if (/\/(pages|views|components)\//.test(p))
|
|
26
|
+
score += 15;
|
|
27
|
+
if (p.includes("node_modules") || p.includes("dist/") || p.includes(".test."))
|
|
28
|
+
score -= 200;
|
|
29
|
+
return score;
|
|
30
|
+
}
|
|
31
|
+
const QUICK_MAX_FILES = 42;
|
|
32
|
+
function selectFilesForQuickScan(files) {
|
|
33
|
+
const candidates = files.filter((f) => (0, ast_extractor_1.shouldAnalyzeFile)(f.path));
|
|
34
|
+
if (candidates.length <= QUICK_MAX_FILES)
|
|
35
|
+
return candidates;
|
|
36
|
+
return [...candidates]
|
|
37
|
+
.sort((a, b) => filePriorityScore(b.path) - filePriorityScore(a.path))
|
|
38
|
+
.slice(0, QUICK_MAX_FILES);
|
|
39
|
+
}
|
|
40
|
+
function estimateScanMinutes(mode, aiBatchCount) {
|
|
41
|
+
const perBatch = mode === "quick" ? 0.35 : 0.55;
|
|
42
|
+
const parallel = 3;
|
|
43
|
+
const rounds = Math.ceil(aiBatchCount / parallel);
|
|
44
|
+
const minutes = (rounds * perBatch) + (mode === "quick" ? 0.25 : 0.5);
|
|
45
|
+
return {
|
|
46
|
+
min: Math.max(1, Math.floor(minutes * 0.7)),
|
|
47
|
+
max: Math.ceil(minutes * 1.4) + 1,
|
|
48
|
+
};
|
|
49
|
+
}
|