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,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
+ }