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,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Control Flow Analyzer
|
|
4
|
+
* Finds unhandled errors, missing validation, race conditions
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.analyzeControlFlow = analyzeControlFlow;
|
|
8
|
+
/**
|
|
9
|
+
* Analyze control flow for error handling and validation issues
|
|
10
|
+
*/
|
|
11
|
+
function analyzeControlFlow(files) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
// DISABLED: findUnhandledAsyncErrors - fundamentally flawed
|
|
15
|
+
// It flags function DECLARATIONS instead of actual unhandled promise CALLS
|
|
16
|
+
// Exported async functions that throw errors are a VALID pattern (error propagation)
|
|
17
|
+
// The caller is responsible for handling errors, not the function itself
|
|
18
|
+
// findings.push(...findUnhandledAsyncErrors(file.path, file.content));
|
|
19
|
+
findings.push(...findMissingValidation(file.path, file.content));
|
|
20
|
+
findings.push(...findRaceConditions(file.path, file.content));
|
|
21
|
+
findings.push(...findMissingNullChecks(file.path, file.content));
|
|
22
|
+
}
|
|
23
|
+
return findings;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Find async functions without error handling
|
|
27
|
+
*/
|
|
28
|
+
function findUnhandledAsyncErrors(filePath, content) {
|
|
29
|
+
const findings = [];
|
|
30
|
+
const lines = content.split('\n');
|
|
31
|
+
// Find async functions
|
|
32
|
+
const asyncFunctions = content.matchAll(/async\s+function\s+(\w+)|const\s+(\w+)\s*=\s*async/gi);
|
|
33
|
+
for (const match of asyncFunctions) {
|
|
34
|
+
if (!match.index)
|
|
35
|
+
continue;
|
|
36
|
+
const funcName = match[1] || match[2];
|
|
37
|
+
const lineNumber = content.slice(0, match.index).split('\n').length;
|
|
38
|
+
// Get function body (approximate)
|
|
39
|
+
const funcStart = match.index;
|
|
40
|
+
const funcEnd = findFunctionEnd(content, funcStart);
|
|
41
|
+
const funcBody = content.slice(funcStart, funcEnd);
|
|
42
|
+
// Check if function has error handling
|
|
43
|
+
const hasTryCatch = /try\s*\{[\s\S]*\}\s*catch/.test(funcBody);
|
|
44
|
+
const hasCatchChain = /\.catch\s*\(/.test(funcBody);
|
|
45
|
+
const throwsError = /throw\s+(?:new\s+)?Error/.test(funcBody);
|
|
46
|
+
if (!hasTryCatch && !hasCatchChain && !throwsError) {
|
|
47
|
+
findings.push({
|
|
48
|
+
id: `unhandled-async-${filePath}-${lineNumber}`,
|
|
49
|
+
severity: 'high',
|
|
50
|
+
category: 'production',
|
|
51
|
+
title: 'Async Function Without Error Handling',
|
|
52
|
+
description: `Function '${funcName}' is async but has no error handling`,
|
|
53
|
+
file: filePath,
|
|
54
|
+
line: lineNumber,
|
|
55
|
+
evidence: lines[lineNumber - 1]?.trim() || '',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return findings;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Find API endpoints without input validation
|
|
63
|
+
*/
|
|
64
|
+
function findMissingValidation(filePath, content) {
|
|
65
|
+
const findings = [];
|
|
66
|
+
const lines = content.split('\n');
|
|
67
|
+
// Find API route handlers
|
|
68
|
+
const routes = content.matchAll(/router\.(get|post|put|delete|patch)\s*\(/gi);
|
|
69
|
+
for (const match of routes) {
|
|
70
|
+
if (!match.index)
|
|
71
|
+
continue;
|
|
72
|
+
const method = match[1];
|
|
73
|
+
const lineNumber = content.slice(0, match.index).split('\n').length;
|
|
74
|
+
// Get route handler body
|
|
75
|
+
const handlerStart = match.index;
|
|
76
|
+
const handlerEnd = findFunctionEnd(content, handlerStart);
|
|
77
|
+
const handlerBody = content.slice(handlerStart, handlerEnd);
|
|
78
|
+
// Check if handler validates input
|
|
79
|
+
const hasValidation = /validate|schema|zod|joi|yup/.test(handlerBody) ||
|
|
80
|
+
/if\s*\(!.*\)/.test(handlerBody) ||
|
|
81
|
+
/throw.*Error/.test(handlerBody);
|
|
82
|
+
if (!hasValidation && (method === 'post' || method === 'put' || method === 'patch')) {
|
|
83
|
+
findings.push({
|
|
84
|
+
id: `missing-validation-${filePath}-${lineNumber}`,
|
|
85
|
+
severity: 'high',
|
|
86
|
+
category: 'security',
|
|
87
|
+
title: 'API Endpoint Without Input Validation',
|
|
88
|
+
description: `${method.toUpperCase()} endpoint lacks input validation`,
|
|
89
|
+
file: filePath,
|
|
90
|
+
line: lineNumber,
|
|
91
|
+
evidence: lines[lineNumber - 1]?.trim() || '',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return findings;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Find potential race conditions
|
|
99
|
+
*/
|
|
100
|
+
function findRaceConditions(filePath, content) {
|
|
101
|
+
const findings = [];
|
|
102
|
+
const lines = content.split('\n');
|
|
103
|
+
// Find concurrent operations without proper synchronization
|
|
104
|
+
const promiseAlls = content.matchAll(/Promise\.all\s*\(/gi);
|
|
105
|
+
for (const match of promiseAlls) {
|
|
106
|
+
if (!match.index)
|
|
107
|
+
continue;
|
|
108
|
+
const lineNumber = content.slice(0, match.index).split('\n').length;
|
|
109
|
+
const line = lines[lineNumber - 1] || '';
|
|
110
|
+
// Check if Promise.all involves database writes
|
|
111
|
+
if (/(?:update|insert|delete|create|save)/.test(line)) {
|
|
112
|
+
findings.push({
|
|
113
|
+
id: `race-condition-${filePath}-${lineNumber}`,
|
|
114
|
+
severity: 'high',
|
|
115
|
+
category: 'production',
|
|
116
|
+
title: 'Potential Race Condition in Concurrent Writes',
|
|
117
|
+
description: 'Concurrent database writes may cause race conditions',
|
|
118
|
+
file: filePath,
|
|
119
|
+
line: lineNumber,
|
|
120
|
+
evidence: line.trim(),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return findings;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Find missing null/undefined checks
|
|
128
|
+
*/
|
|
129
|
+
function findMissingNullChecks(filePath, content) {
|
|
130
|
+
const findings = [];
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
// Find property access that might be null/undefined
|
|
133
|
+
const propertyAccess = content.matchAll(/(\w+)\.(\w+)(?!\?\.)/gi);
|
|
134
|
+
for (const match of propertyAccess) {
|
|
135
|
+
if (!match.index)
|
|
136
|
+
continue;
|
|
137
|
+
const varName = match[1];
|
|
138
|
+
const lineNumber = content.slice(0, match.index).split('\n').length;
|
|
139
|
+
const line = lines[lineNumber - 1] || '';
|
|
140
|
+
// Skip if already using optional chaining
|
|
141
|
+
if (line.includes('?.'))
|
|
142
|
+
continue;
|
|
143
|
+
// Skip if there's a null check before
|
|
144
|
+
const beforeLines = lines.slice(Math.max(0, lineNumber - 5), lineNumber).join('\n');
|
|
145
|
+
if (new RegExp(`if\\s*\\(${varName}\\)|${varName}\\s*&&|${varName}\\s*\\?`).test(beforeLines)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Check if variable comes from external source
|
|
149
|
+
if (/req\.|params\.|query\.|body\.|find|get/.test(line)) {
|
|
150
|
+
findings.push({
|
|
151
|
+
id: `missing-null-check-${filePath}-${lineNumber}`,
|
|
152
|
+
severity: 'medium',
|
|
153
|
+
category: 'production',
|
|
154
|
+
title: 'Missing Null/Undefined Check',
|
|
155
|
+
description: `Property access on '${varName}' without null check`,
|
|
156
|
+
file: filePath,
|
|
157
|
+
line: lineNumber,
|
|
158
|
+
evidence: line.trim(),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return findings;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Helper: Find the end of a function body
|
|
166
|
+
*/
|
|
167
|
+
function findFunctionEnd(content, start) {
|
|
168
|
+
let braceCount = 0;
|
|
169
|
+
let inFunction = false;
|
|
170
|
+
for (let i = start; i < content.length; i++) {
|
|
171
|
+
const char = content[i];
|
|
172
|
+
if (char === '{') {
|
|
173
|
+
braceCount++;
|
|
174
|
+
inFunction = true;
|
|
175
|
+
}
|
|
176
|
+
else if (char === '}') {
|
|
177
|
+
braceCount--;
|
|
178
|
+
if (inFunction && braceCount === 0) {
|
|
179
|
+
return i + 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return content.length;
|
|
184
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Data Flow Analyzer
|
|
4
|
+
* Tracks user input from sources to dangerous sinks
|
|
5
|
+
* Detects injection vulnerabilities without AI
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.analyzeDataFlow = analyzeDataFlow;
|
|
9
|
+
// User input sources (tainted data)
|
|
10
|
+
const INPUT_SOURCES = [
|
|
11
|
+
/req\.body/gi,
|
|
12
|
+
/req\.query/gi,
|
|
13
|
+
/req\.params/gi,
|
|
14
|
+
/req\.headers/gi,
|
|
15
|
+
/req\.cookies/gi,
|
|
16
|
+
/searchParams\.get/gi,
|
|
17
|
+
/formData\.get/gi,
|
|
18
|
+
/process\.env/gi, // Can be tainted in some contexts
|
|
19
|
+
];
|
|
20
|
+
// Dangerous sinks (where tainted data causes vulnerabilities)
|
|
21
|
+
const DANGEROUS_SINKS = {
|
|
22
|
+
sql: [
|
|
23
|
+
/\.execute\s*\(/gi,
|
|
24
|
+
/\.query\s*\(/gi,
|
|
25
|
+
/\.raw\s*\(/gi,
|
|
26
|
+
/sql`/gi,
|
|
27
|
+
],
|
|
28
|
+
command: [
|
|
29
|
+
/exec\s*\(/gi,
|
|
30
|
+
/execSync\s*\(/gi,
|
|
31
|
+
/spawn\s*\(/gi,
|
|
32
|
+
/spawnSync\s*\(/gi,
|
|
33
|
+
/eval\s*\(/gi,
|
|
34
|
+
],
|
|
35
|
+
path: [
|
|
36
|
+
/fs\.readFile/gi,
|
|
37
|
+
/fs\.writeFile/gi,
|
|
38
|
+
/fs\.unlink/gi,
|
|
39
|
+
/fs\.rm/gi,
|
|
40
|
+
/require\s*\(/gi,
|
|
41
|
+
/import\s*\(/gi,
|
|
42
|
+
],
|
|
43
|
+
xss: [
|
|
44
|
+
/\.innerHTML\s*=/gi,
|
|
45
|
+
/\.outerHTML\s*=/gi,
|
|
46
|
+
/document\.write/gi,
|
|
47
|
+
/dangerouslySetInnerHTML/gi,
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
// Sanitization functions (clean tainted data)
|
|
51
|
+
const SANITIZERS = [
|
|
52
|
+
/DOMPurify\.sanitize/gi,
|
|
53
|
+
/escape/gi,
|
|
54
|
+
/sanitize/gi,
|
|
55
|
+
/validate/gi,
|
|
56
|
+
/parseInt/gi,
|
|
57
|
+
/parseFloat/gi,
|
|
58
|
+
/Number\(/gi,
|
|
59
|
+
/\.trim\(\)/gi,
|
|
60
|
+
/\.replace\(/gi,
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* Analyze data flow from user inputs to dangerous sinks
|
|
64
|
+
*/
|
|
65
|
+
function analyzeDataFlow(files) {
|
|
66
|
+
const findings = [];
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const fileFindings = analyzeFileDataFlow(file.path, file.content);
|
|
69
|
+
findings.push(...fileFindings);
|
|
70
|
+
}
|
|
71
|
+
return findings;
|
|
72
|
+
}
|
|
73
|
+
function analyzeFileDataFlow(filePath, content) {
|
|
74
|
+
const findings = [];
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
// Find all user input sources
|
|
77
|
+
const sources = findInputSources(content, lines);
|
|
78
|
+
// Find all dangerous sinks
|
|
79
|
+
const sinks = findDangerousSinks(content, lines);
|
|
80
|
+
// Trace data flow from sources to sinks
|
|
81
|
+
for (const source of sources) {
|
|
82
|
+
for (const sink of sinks) {
|
|
83
|
+
// Check if data flows from source to sink
|
|
84
|
+
if (dataFlowsFromTo(source, sink, content, lines)) {
|
|
85
|
+
// Check if data is sanitized
|
|
86
|
+
const sanitized = isSanitizedBetween(source, sink, content);
|
|
87
|
+
if (!sanitized) {
|
|
88
|
+
findings.push({
|
|
89
|
+
id: `dataflow-${source.type}-${sink.type}-${filePath}-${sink.line}`,
|
|
90
|
+
severity: getSeverity(sink.type),
|
|
91
|
+
category: getCategory(sink.type),
|
|
92
|
+
title: `${sink.type.toUpperCase()} Injection via Data Flow`,
|
|
93
|
+
description: `User input from ${source.name} flows to ${sink.name} without sanitization`,
|
|
94
|
+
file: filePath,
|
|
95
|
+
line: sink.line,
|
|
96
|
+
evidence: lines[sink.line - 1]?.trim() || '',
|
|
97
|
+
dataFlow: {
|
|
98
|
+
source: source.name,
|
|
99
|
+
sourceLine: source.line,
|
|
100
|
+
sink: sink.name,
|
|
101
|
+
sinkLine: sink.line,
|
|
102
|
+
sanitized: false,
|
|
103
|
+
path: [source.name, sink.name],
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
112
|
+
function findInputSources(content, lines) {
|
|
113
|
+
const sources = [];
|
|
114
|
+
for (const pattern of INPUT_SOURCES) {
|
|
115
|
+
const matches = content.matchAll(pattern);
|
|
116
|
+
for (const match of matches) {
|
|
117
|
+
if (!match.index)
|
|
118
|
+
continue;
|
|
119
|
+
const beforeMatch = content.slice(0, match.index);
|
|
120
|
+
const lineNumber = beforeMatch.split('\n').length;
|
|
121
|
+
// Try to extract variable name
|
|
122
|
+
const line = lines[lineNumber - 1] || '';
|
|
123
|
+
const varMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=/);
|
|
124
|
+
sources.push({
|
|
125
|
+
name: match[0],
|
|
126
|
+
line: lineNumber,
|
|
127
|
+
type: 'user-input',
|
|
128
|
+
variable: varMatch?.[1],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return sources;
|
|
133
|
+
}
|
|
134
|
+
function findDangerousSinks(content, lines) {
|
|
135
|
+
const sinks = [];
|
|
136
|
+
for (const [type, patterns] of Object.entries(DANGEROUS_SINKS)) {
|
|
137
|
+
for (const pattern of patterns) {
|
|
138
|
+
const matches = content.matchAll(pattern);
|
|
139
|
+
for (const match of matches) {
|
|
140
|
+
if (!match.index)
|
|
141
|
+
continue;
|
|
142
|
+
const beforeMatch = content.slice(0, match.index);
|
|
143
|
+
const lineNumber = beforeMatch.split('\n').length;
|
|
144
|
+
sinks.push({
|
|
145
|
+
name: match[0],
|
|
146
|
+
line: lineNumber,
|
|
147
|
+
type: type,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return sinks;
|
|
153
|
+
}
|
|
154
|
+
function dataFlowsFromTo(source, sink, content, lines) {
|
|
155
|
+
// Source must come before sink
|
|
156
|
+
if (source.line >= sink.line)
|
|
157
|
+
return false;
|
|
158
|
+
// If we have a variable name, check if it's used in the sink line
|
|
159
|
+
if (source.variable) {
|
|
160
|
+
const sinkLine = lines[sink.line - 1] || '';
|
|
161
|
+
if (sinkLine.includes(source.variable)) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Check if req.body/query/params is used directly in sink
|
|
166
|
+
const sinkLine = lines[sink.line - 1] || '';
|
|
167
|
+
if (/req\.(body|query|params|headers|cookies)/.test(sinkLine)) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
function isSanitizedBetween(source, sink, content) {
|
|
173
|
+
const lines = content.split('\n');
|
|
174
|
+
const betweenLines = lines.slice(source.line, sink.line).join('\n');
|
|
175
|
+
// Check if any sanitization function is called
|
|
176
|
+
for (const sanitizer of SANITIZERS) {
|
|
177
|
+
if (sanitizer.test(betweenLines)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
function getSeverity(sinkType) {
|
|
184
|
+
switch (sinkType) {
|
|
185
|
+
case 'sql':
|
|
186
|
+
case 'command':
|
|
187
|
+
return 'critical';
|
|
188
|
+
case 'path':
|
|
189
|
+
case 'xss':
|
|
190
|
+
return 'high';
|
|
191
|
+
default:
|
|
192
|
+
return 'medium';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function getCategory(sinkType) {
|
|
196
|
+
return 'security';
|
|
197
|
+
}
|