@vibecheck-ai/mcp 24.6.9 → 24.6.12
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/README.md +1 -1
- package/dist/APITruthEngine-IZRR3NT5-LPFUOMLD.js +9 -0
- package/dist/CredentialsEngine-B66ANCBB-HY5ZQTSX.js +9 -0
- package/dist/EnvVarEngine-ZFNW2XKP-6HRTZULP.js +9 -0
- package/dist/ErrorHandlingEngine-FG65SFRB-4NEANCMF.js +11 -0
- package/dist/FrameworkPackEngine-RRBJW4MC-KH7WRXXS.js +12 -0
- package/dist/GhostRouteEngine-UMYBCOCL-MSZOPVZY.js +9 -0
- package/dist/LogicGapEngine-OK5UKZQ5-YGXZDERB.js +11 -0
- package/dist/PhantomDepEngine-5O7Z7MDE-4A37GGL4.js +10 -0
- package/dist/SecurityEngine-MVMRPKLH-BNP7IC46.js +9 -0
- package/dist/VersionHallucinationEngine-673DJ26J-BD4SK6JX.js +9 -0
- package/dist/chokidar-CI5VJY5M.js +2414 -0
- package/dist/chunk-43XAAYST.js +863 -0
- package/dist/chunk-5DADZJ3D.js +650 -0
- package/dist/chunk-DDTUTWRY.js +605 -0
- package/dist/chunk-DGNNNAVK.js +304 -0
- package/dist/chunk-F34MHA6A.js +772 -0
- package/dist/chunk-FGMVY5QW.js +42 -0
- package/dist/chunk-FMRX5OVJ.js +1968 -0
- package/dist/chunk-FRK2XZX5.js +213309 -0
- package/dist/chunk-J52EUKKW.js +196 -0
- package/dist/chunk-JZSHXEYP.js +915 -0
- package/dist/chunk-LQSBUKYZ.js +551 -0
- package/dist/chunk-MUP4JXOF.js +219 -0
- package/dist/chunk-NR36RTVO.js +152 -0
- package/dist/chunk-QGPX6H6L.js +3044 -0
- package/dist/chunk-QYXENOVK.js +499 -0
- package/dist/chunk-RR5ETBSV.js +66 -0
- package/dist/chunk-WUHPSW7M.js +11130 -0
- package/dist/chunk-YWUMPN4Z.js +53 -0
- package/dist/dist-HFMJ3GIR.js +1091 -0
- package/dist/dist-JUOVMQEA.js +9 -0
- package/dist/dist-NXITTS32-O3XLWR6T.js +386 -0
- package/dist/dist-Y2Z46SBD.js +22 -0
- package/dist/fingerprint-NOJ7TDB6-K6SB7LCZ.js +9 -0
- package/dist/index.js +5462 -4676
- package/dist/semantic-WW6XVII4.js +8544 -0
- package/dist/transformers.node-K4WKH4PR.js +45809 -0
- package/dist/tree-sitter-AGICL65I.js +1412 -0
- package/dist/tree-sitter-H5E7LKR4-MKO3NNLJ.js +9 -0
- package/package.json +7 -6
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { dirname } from 'path';
|
|
5
|
+
|
|
6
|
+
createRequire(import.meta.url);
|
|
7
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
8
|
+
dirname(__filename$1);
|
|
9
|
+
function deterministicId(uri, line, ruleId, patternName) {
|
|
10
|
+
const input = `sec:${uri}::${line}::${ruleId}::${patternName}`;
|
|
11
|
+
let hash = 2166136261;
|
|
12
|
+
for (let i = 0; i < input.length; i++) {
|
|
13
|
+
hash ^= input.charCodeAt(i);
|
|
14
|
+
hash = hash * 16777619 >>> 0;
|
|
15
|
+
}
|
|
16
|
+
return `sec-${hash.toString(16).padStart(8, "0")}`;
|
|
17
|
+
}
|
|
18
|
+
function inferSecLang(delta) {
|
|
19
|
+
const lid = delta.documentLanguage.toLowerCase();
|
|
20
|
+
if (lid.includes("python")) return "py";
|
|
21
|
+
if (lid.includes("go")) return "go";
|
|
22
|
+
const uriPath = delta.documentUri.replace(/^file:\/\//, "");
|
|
23
|
+
const ext = path.extname(uriPath).toLowerCase();
|
|
24
|
+
const base = path.basename(uriPath).toLowerCase();
|
|
25
|
+
if (ext === ".py" || ext === ".pyi") return "py";
|
|
26
|
+
if (ext === ".go" || base === "go.mod" || base === "go.work") return "go";
|
|
27
|
+
return "js";
|
|
28
|
+
}
|
|
29
|
+
function isTestFile(uri) {
|
|
30
|
+
const u = uri.replace(/^file:\/\//, "");
|
|
31
|
+
if (/(?:^|[\\/])test_[\w.-]+\.py$/i.test(u) || /(?:^|[\\/])[\w.-]+_test\.py$/i.test(u) || /(?:^|[\\/])conftest\.py$/i.test(u)) return true;
|
|
32
|
+
if (/[^\\/]+_test\.go$/i.test(u)) return true;
|
|
33
|
+
return /\.(test|spec)\.(ts|tsx|js|jsx)$/i.test(u) || /(?:^|[\\/])(?:__tests__|__mocks__|tests?|fixtures?|e2e|spec|cypress|playwright|__snapshots__|stubs?|mocks?)[\\/]/i.test(u);
|
|
34
|
+
}
|
|
35
|
+
function isCriticalPath(uri) {
|
|
36
|
+
const u = uri.replace(/^file:\/\//, "");
|
|
37
|
+
return /[\\/]api[\\/]|[\\/]auth[\\/]|[\\/]payment|[\\/]billing|[\\/]admin|[\\/]checkout|[\\/]webhook|[\\/]security/.test(u) || /middleware\.(ts|js|go|py)$/.test(u) || /[\\/]lib[\\/]auth|[\\/]lib[\\/]db/.test(u);
|
|
38
|
+
}
|
|
39
|
+
function isScriptFile(uri) {
|
|
40
|
+
return /(?:^|\/)scripts?\//i.test(uri);
|
|
41
|
+
}
|
|
42
|
+
function isBuildOrUtilityFile(uri) {
|
|
43
|
+
const basename2 = uri.split("/").pop()?.toLowerCase() ?? "";
|
|
44
|
+
return /(?:cleanup|clean|build|generate|deploy|migrate|setup|teardown|seed|reset|purge|prune)/.test(basename2);
|
|
45
|
+
}
|
|
46
|
+
function isVibeCheckFile(uri) {
|
|
47
|
+
return /vibecheck/i.test(uri);
|
|
48
|
+
}
|
|
49
|
+
function hasUserInputIndicators(line) {
|
|
50
|
+
return /(?:req\.|params\.|query\.|body\.|args\.|argv\.|request\.|searchParams|formData|request\.args|request\.form|request\.json|c\.(?:Query|Param|PostForm)|ctx\.(?:Query|PostForm))/i.test(
|
|
51
|
+
line
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
function escalate(severity, critical) {
|
|
55
|
+
if (!critical) return severity;
|
|
56
|
+
const up = {
|
|
57
|
+
low: "medium",
|
|
58
|
+
medium: "high",
|
|
59
|
+
high: "critical",
|
|
60
|
+
critical: "critical"
|
|
61
|
+
};
|
|
62
|
+
return up[severity] ?? severity;
|
|
63
|
+
}
|
|
64
|
+
function appliesToLang(pattern, lang) {
|
|
65
|
+
if (pattern.langs === void 0) return lang === "js";
|
|
66
|
+
return pattern.langs.includes(lang);
|
|
67
|
+
}
|
|
68
|
+
var PATTERNS = [
|
|
69
|
+
// ── SQL Injection ──
|
|
70
|
+
{
|
|
71
|
+
name: "sql-injection-template",
|
|
72
|
+
ruleId: "SEC001",
|
|
73
|
+
regex: /(?:query|execute|raw)\s*\(\s*[`'"](?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER)\s[^`'"]*\$\{/i,
|
|
74
|
+
severity: "critical",
|
|
75
|
+
message: "SQL injection: template literal in query",
|
|
76
|
+
suggestion: 'Use parameterized queries: db.query("SELECT * FROM users WHERE id = $1", [userId])',
|
|
77
|
+
confidence: 92,
|
|
78
|
+
excludeInTests: true,
|
|
79
|
+
langs: ["js"]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "sql-injection-concat",
|
|
83
|
+
ruleId: "SEC001",
|
|
84
|
+
regex: /(?:query|execute|raw)\s*\(\s*['"](?:SELECT|INSERT|UPDATE|DELETE)\s[^'"]*['"]\s*\+/i,
|
|
85
|
+
severity: "critical",
|
|
86
|
+
message: "SQL injection: string concatenation in query",
|
|
87
|
+
suggestion: "Use parameterized queries. Never concatenate user input into SQL.",
|
|
88
|
+
confidence: 90,
|
|
89
|
+
excludeInTests: true,
|
|
90
|
+
langs: ["js", "py", "go"]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "unfiltered-delete",
|
|
94
|
+
ruleId: "SEC011",
|
|
95
|
+
regex: /DELETE\s+FROM\s+\w+\s*;?\s*$/im,
|
|
96
|
+
severity: "critical",
|
|
97
|
+
message: "Unfiltered DELETE statement \u2014 no WHERE clause",
|
|
98
|
+
suggestion: "Add a WHERE clause. Add confirmation logic for destructive operations.",
|
|
99
|
+
confidence: 85,
|
|
100
|
+
excludeInTests: true,
|
|
101
|
+
langs: ["js", "py", "go"]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "drop-table",
|
|
105
|
+
ruleId: "SEC011",
|
|
106
|
+
regex: /DROP\s+TABLE/i,
|
|
107
|
+
severity: "critical",
|
|
108
|
+
message: "DROP TABLE in application code",
|
|
109
|
+
suggestion: "Use schema migrations. Never DROP tables in application logic.",
|
|
110
|
+
confidence: 88,
|
|
111
|
+
excludeInTests: true,
|
|
112
|
+
langs: ["js", "py", "go"]
|
|
113
|
+
},
|
|
114
|
+
// ── Command Injection ──
|
|
115
|
+
{
|
|
116
|
+
name: "exec-template",
|
|
117
|
+
ruleId: "SEC002",
|
|
118
|
+
regex: /(?:exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/,
|
|
119
|
+
severity: "critical",
|
|
120
|
+
message: "Command injection: template literal in shell command",
|
|
121
|
+
suggestion: 'Use execFile() with args array: execFile("cmd", [arg1, arg2])',
|
|
122
|
+
confidence: 95,
|
|
123
|
+
excludeInTests: true,
|
|
124
|
+
validate: (line) => !/(?:which\s+\$|where\s+\$|--version|--help|\$\{(?:mgr|tool|bin|cmd)(?:\.name)?\}\s+--|taskkill.*\/pid\s+\$|lsof\s+-i|netstat|ps\s+-p)/i.test(line)
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "exec-concat",
|
|
128
|
+
ruleId: "SEC002",
|
|
129
|
+
regex: /(?:exec|execSync)\s*\(\s*['"][^'"]*['"]\s*\+/,
|
|
130
|
+
severity: "critical",
|
|
131
|
+
message: "Command injection: string concatenation in shell command",
|
|
132
|
+
suggestion: "Use execFile() with args array.",
|
|
133
|
+
confidence: 93,
|
|
134
|
+
excludeInTests: true
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "spawn-shell-true",
|
|
138
|
+
ruleId: "SEC012",
|
|
139
|
+
regex: /spawn\s*\(.*shell\s*:\s*true/,
|
|
140
|
+
severity: "critical",
|
|
141
|
+
message: "spawn() with { shell: true } \u2014 enables shell injection",
|
|
142
|
+
suggestion: 'Remove shell: true and pass arguments as an array: spawn("cmd", [arg1, arg2])',
|
|
143
|
+
confidence: 92,
|
|
144
|
+
excludeInTests: true
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "execsync-interpolation",
|
|
148
|
+
ruleId: "SEC012",
|
|
149
|
+
regex: /execSync\s*\(\s*`[^`]*\$\{/,
|
|
150
|
+
severity: "critical",
|
|
151
|
+
message: "execSync() with string interpolation \u2014 shell injection risk",
|
|
152
|
+
suggestion: "Use execFileSync() with args array.",
|
|
153
|
+
confidence: 94,
|
|
154
|
+
excludeInTests: true,
|
|
155
|
+
validate: (line) => !/(?:which\s+\$|--version|--help)/i.test(line)
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "child-process-exec",
|
|
159
|
+
ruleId: "SEC002",
|
|
160
|
+
regex: /child_process\s*\.\s*exec\s*\(/,
|
|
161
|
+
severity: "high",
|
|
162
|
+
message: "child_process.exec() can run arbitrary shell commands",
|
|
163
|
+
suggestion: "Prefer execFile() or spawn() with an explicit args array.",
|
|
164
|
+
confidence: 80,
|
|
165
|
+
excludeInTests: true
|
|
166
|
+
},
|
|
167
|
+
// ── XSS ──
|
|
168
|
+
{
|
|
169
|
+
name: "innerhtml-assignment",
|
|
170
|
+
ruleId: "SEC003",
|
|
171
|
+
regex: /\.innerHTML\s*=/,
|
|
172
|
+
severity: "high",
|
|
173
|
+
message: "innerHTML assignment \u2014 XSS risk if content is user-controlled",
|
|
174
|
+
suggestion: "Use textContent for text. Use DOMPurify.sanitize() if HTML is required.",
|
|
175
|
+
confidence: 75,
|
|
176
|
+
excludeInTests: true,
|
|
177
|
+
validate: (line, lines, index) => {
|
|
178
|
+
if (/\.innerHTML\s*=\s*['"`]/.test(line)) return false;
|
|
179
|
+
if (/(?:sanitize|purify|escape|DOMPurify|sanitizeHtml|xss|marked|markdown|highlight|hljs)\s*\(/i.test(line)) return false;
|
|
180
|
+
if (/\.innerHTML\s*=\s*(?:sanitized|safe|escaped|purified|content|html|rendered|compiled)/i.test(line)) return false;
|
|
181
|
+
for (let j = Math.max(0, index - 5); j <= Math.min(lines.length - 1, index + 5); j++) {
|
|
182
|
+
if (/(?:DOMPurify|sanitize|purify|sanitizeHtml|xss)\s*[\.(]/i.test(lines[j])) return false;
|
|
183
|
+
}
|
|
184
|
+
if (/\.innerHTML\s*=\s*['"`]['"`]/.test(line)) return false;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "dangerously-set-innerhtml",
|
|
190
|
+
ruleId: "SEC003",
|
|
191
|
+
regex: /dangerouslySetInnerHTML\s*=\s*\{/,
|
|
192
|
+
severity: "info",
|
|
193
|
+
message: "dangerouslySetInnerHTML \u2014 ensure content is sanitized before use",
|
|
194
|
+
suggestion: "Sanitize with DOMPurify before passing to dangerouslySetInnerHTML.",
|
|
195
|
+
confidence: 70,
|
|
196
|
+
excludeInTests: true,
|
|
197
|
+
validate: (line, lines, index) => {
|
|
198
|
+
if (/<style[\s>]/i.test(line)) return false;
|
|
199
|
+
for (let j = Math.max(0, index - 3); j <= Math.min(lines.length - 1, index + 3); j++) {
|
|
200
|
+
if (/<style[\s>]/i.test(lines[j])) return false;
|
|
201
|
+
}
|
|
202
|
+
if (/__html\s*:\s*['"`]/.test(line)) return false;
|
|
203
|
+
if (/(?:sanitize|purify|escape|DOMPurify|sanitizeHtml|xss|marked|markdown|rehype|remark|serialize|render|compile|highlight|hljs|prism)\s*\(/i.test(line)) return false;
|
|
204
|
+
for (let j = Math.max(0, index - 8); j <= Math.min(lines.length - 1, index + 8); j++) {
|
|
205
|
+
if (/(?:DOMPurify|sanitize|purify|sanitizeHtml|xss|marked|markdown|rehype|remark|serialize|highlight|hljs|prismjs|showdown|turndown|unified|mdx|contentlayer|cms|richtext|PortableText|BlockContent)\s*[\.(]/i.test(lines[j])) return false;
|
|
206
|
+
}
|
|
207
|
+
if (/__html\s*:\s*(?:sanitized|safe|escaped|purified|content|html|markup|rendered|processed|parsed|compiled)/i.test(line)) return false;
|
|
208
|
+
if (/__html\s*:\s*\w*(?:content|html|body|markup|rendered|compiled|processed|parsed|article|post|page|blog|description|summary|excerpt|bio|text|message|richText|mdx)/i.test(line)) return false;
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "document-write",
|
|
214
|
+
ruleId: "SEC003",
|
|
215
|
+
regex: /document\.write\s*\(/,
|
|
216
|
+
severity: "high",
|
|
217
|
+
message: "document.write() can overwrite the entire page with unescaped content",
|
|
218
|
+
suggestion: "Use DOM manipulation methods (createElement, appendChild) instead.",
|
|
219
|
+
confidence: 80,
|
|
220
|
+
excludeInTests: true
|
|
221
|
+
},
|
|
222
|
+
// ── Path Traversal ──
|
|
223
|
+
{
|
|
224
|
+
name: "path-traversal-join",
|
|
225
|
+
ruleId: "SEC004",
|
|
226
|
+
regex: /(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/,
|
|
227
|
+
severity: "high",
|
|
228
|
+
message: "User input in file path \u2014 potential path traversal",
|
|
229
|
+
suggestion: "Validate the resolved path starts with the allowed base directory.",
|
|
230
|
+
confidence: 82,
|
|
231
|
+
excludeInTests: true
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "fs-read-user-input",
|
|
235
|
+
ruleId: "SEC004",
|
|
236
|
+
regex: /fs\.(?:readFile|readFileSync|createReadStream)\s*\([^)]*(?:req\.|params\.|query\.)/,
|
|
237
|
+
severity: "high",
|
|
238
|
+
message: "User input passed directly to file read",
|
|
239
|
+
suggestion: "Sanitize path and verify it is within the allowed directory.",
|
|
240
|
+
confidence: 85,
|
|
241
|
+
excludeInTests: true
|
|
242
|
+
},
|
|
243
|
+
// ── SSRF ──
|
|
244
|
+
{
|
|
245
|
+
name: "ssrf-fetch",
|
|
246
|
+
ruleId: "SEC005",
|
|
247
|
+
regex: /fetch\s*\(\s*(?:req\.query|req\.params|req\.body|request\.url|searchParams|formData)/,
|
|
248
|
+
severity: "high",
|
|
249
|
+
message: "User-controlled URL in server-side fetch \u2014 SSRF risk",
|
|
250
|
+
suggestion: "Validate URL against an allowlist. Block private IPs (10.x, 172.16-31.x, 192.168.x, 169.254.x).",
|
|
251
|
+
confidence: 78,
|
|
252
|
+
excludeInTests: true
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "ssrf-axios",
|
|
256
|
+
ruleId: "SEC005",
|
|
257
|
+
regex: /axios\s*(?:\.get|\.post|\.put|\.delete|\.request)\s*\(\s*(?:req\.|params\.|query\.|body\.)/,
|
|
258
|
+
severity: "high",
|
|
259
|
+
message: "User-controlled URL in axios request \u2014 SSRF risk",
|
|
260
|
+
suggestion: "Validate URL against an allowlist of allowed domains.",
|
|
261
|
+
confidence: 78,
|
|
262
|
+
excludeInTests: true
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "cors-wildcard",
|
|
266
|
+
ruleId: "SEC005",
|
|
267
|
+
regex: /(?:(?:Access-Control-Allow-Origin|origin)\s*[:=]\s*['"`]\*['"`]|['"]Access-Control-Allow-Origin['"]\s*,\s*['"`]\*['"`])/,
|
|
268
|
+
severity: "high",
|
|
269
|
+
message: "CORS wildcard \u2014 allows requests from any origin",
|
|
270
|
+
suggestion: "Restrict Access-Control-Allow-Origin to specific trusted origins.",
|
|
271
|
+
confidence: 80,
|
|
272
|
+
excludeInTests: true,
|
|
273
|
+
langs: ["js", "py", "go"]
|
|
274
|
+
},
|
|
275
|
+
// ── Code Execution ──
|
|
276
|
+
{
|
|
277
|
+
name: "eval-usage",
|
|
278
|
+
ruleId: "SEC007",
|
|
279
|
+
regex: /\beval\s*\(/,
|
|
280
|
+
severity: "critical",
|
|
281
|
+
message: "eval() executes arbitrary code",
|
|
282
|
+
suggestion: "Use JSON.parse() for data. Use a sandboxed interpreter for expressions.",
|
|
283
|
+
confidence: 88,
|
|
284
|
+
excludeInTests: true,
|
|
285
|
+
langs: ["js", "py", "go"],
|
|
286
|
+
validate: (line) => !/\/\/.*eval|['"][^'"]*eval[^'"]*['"]/.test(line)
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "new-function",
|
|
290
|
+
ruleId: "SEC007",
|
|
291
|
+
regex: /\bnew\s+Function\s*\(/,
|
|
292
|
+
severity: "critical",
|
|
293
|
+
message: "new Function() executes arbitrary code",
|
|
294
|
+
suggestion: "Avoid dynamic code generation. Use a template engine or parser.",
|
|
295
|
+
confidence: 90,
|
|
296
|
+
excludeInTests: true,
|
|
297
|
+
validate: (line) => !/['"].*new\s+Function.*['"]/.test(line) && !/\/\/.*new\s+Function/.test(line)
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "dynamic-require",
|
|
301
|
+
ruleId: "SEC007",
|
|
302
|
+
regex: /require\s*\(\s*[^'"][a-zA-Z_$]/,
|
|
303
|
+
severity: "info",
|
|
304
|
+
message: "Dynamic require() \u2014 path injection risk if path comes from user input",
|
|
305
|
+
suggestion: "Use static imports. Validate the path against an allowlist if dynamic loading is needed.",
|
|
306
|
+
confidence: 55,
|
|
307
|
+
excludeInTests: true,
|
|
308
|
+
validate: (line) => {
|
|
309
|
+
if (/require\s*\(\s*path\.\w+\s*\(\s*__dirname/.test(line)) return false;
|
|
310
|
+
if (/require\s*\(\s*path\.\w+\s*\(\s*['"`]/.test(line)) return false;
|
|
311
|
+
if (/require\s*\(\s*(?:config|options|settings|modulePath|pluginPath|resolvedPath)\b/.test(line)) return false;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
// ── Prototype Pollution ──
|
|
316
|
+
{
|
|
317
|
+
name: "proto-access",
|
|
318
|
+
ruleId: "SEC006",
|
|
319
|
+
regex: /__proto__|prototype\s*\[/,
|
|
320
|
+
severity: "high",
|
|
321
|
+
message: "Prototype pollution risk \u2014 __proto__ or prototype[] access",
|
|
322
|
+
suggestion: "Use Object.create(null) for lookup objects. Validate property names before access.",
|
|
323
|
+
confidence: 82,
|
|
324
|
+
excludeInTests: true
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "mass-assignment",
|
|
328
|
+
ruleId: "SEC008",
|
|
329
|
+
regex: /Object\.assign\s*\([^,]+,\s*req\.body/,
|
|
330
|
+
severity: "high",
|
|
331
|
+
message: "Mass assignment \u2014 spreading request body into object",
|
|
332
|
+
suggestion: "Whitelist allowed fields: const { name, email } = req.body",
|
|
333
|
+
confidence: 85,
|
|
334
|
+
excludeInTests: true
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "spread-req-body",
|
|
338
|
+
ruleId: "SEC008",
|
|
339
|
+
regex: /\{\s*\.\.\.req\.body\s*\}/,
|
|
340
|
+
severity: "high",
|
|
341
|
+
message: "Mass assignment \u2014 spreading request body directly",
|
|
342
|
+
suggestion: "Destructure only the fields you need from req.body.",
|
|
343
|
+
confidence: 88,
|
|
344
|
+
excludeInTests: true
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "mongoose-mass-assign",
|
|
348
|
+
ruleId: "SEC015",
|
|
349
|
+
regex: /find(?:ById|One)And(?:Update|Replace)\s*\([^)]*req\.body/,
|
|
350
|
+
severity: "high",
|
|
351
|
+
message: "Mongoose update with raw req.body \u2014 mass assignment risk",
|
|
352
|
+
suggestion: "Use { $set: pick(req.body, allowedFields) } with schema validation.",
|
|
353
|
+
confidence: 86,
|
|
354
|
+
excludeInTests: true
|
|
355
|
+
},
|
|
356
|
+
// ── Insecure Randomness ──
|
|
357
|
+
{
|
|
358
|
+
name: "math-random-token",
|
|
359
|
+
ruleId: "SEC009",
|
|
360
|
+
regex: /Math\.random\s*\(\s*\).*(?:token|secret|password|nonce|salt|session|auth|csrf)/i,
|
|
361
|
+
severity: "high",
|
|
362
|
+
message: "Math.random() used for security-sensitive value \u2014 not cryptographically secure",
|
|
363
|
+
suggestion: "Use crypto.randomUUID() or crypto.randomBytes()",
|
|
364
|
+
confidence: 85,
|
|
365
|
+
excludeInTests: true
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "math-random-hex",
|
|
369
|
+
ruleId: "SEC009",
|
|
370
|
+
regex: /Math\.random\(\)\.toString\((?:16|36)\)/,
|
|
371
|
+
severity: "info",
|
|
372
|
+
message: "Math.random() for ID generation \u2014 not cryptographically secure",
|
|
373
|
+
suggestion: "Use crypto.randomUUID() for unique IDs.",
|
|
374
|
+
confidence: 60,
|
|
375
|
+
excludeInTests: true,
|
|
376
|
+
validate: (line) => {
|
|
377
|
+
if (/(?:id|key|className|style|toast|tab|index|suffix|prefix|label|color|placeholder)\s*[:=]/i.test(line)) return false;
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
// ── Weak Crypto ──
|
|
382
|
+
{
|
|
383
|
+
name: "md5-usage",
|
|
384
|
+
ruleId: "SEC009",
|
|
385
|
+
regex: /createHash\s*\(\s*['"`]md5['"`]\s*\)/,
|
|
386
|
+
severity: "info",
|
|
387
|
+
message: "MD5 hash \u2014 not suitable for security use (fine for checksums)",
|
|
388
|
+
suggestion: 'Use SHA-256 for security: crypto.createHash("sha256"). MD5 is OK for non-security checksums.',
|
|
389
|
+
confidence: 60,
|
|
390
|
+
excludeInTests: true,
|
|
391
|
+
validate: (line, lines, index) => {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: "sha1-usage",
|
|
397
|
+
ruleId: "SEC009",
|
|
398
|
+
regex: /createHash\s*\(\s*['"`]sha1['"`]\s*\)/,
|
|
399
|
+
severity: "medium",
|
|
400
|
+
message: "SHA-1 hash \u2014 deprecated for security use",
|
|
401
|
+
suggestion: 'Use SHA-256: crypto.createHash("sha256")',
|
|
402
|
+
confidence: 85,
|
|
403
|
+
excludeInTests: true
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "weak-cipher",
|
|
407
|
+
ruleId: "SEC013",
|
|
408
|
+
regex: /createCipher(?:iv)?\s*\(\s*['"]?(?:des|rc4|blowfish|rc2|seed)/i,
|
|
409
|
+
severity: "high",
|
|
410
|
+
message: "Weak cipher algorithm \u2014 vulnerable to known attacks",
|
|
411
|
+
suggestion: 'Use aes-256-gcm: crypto.createCipheriv("aes-256-gcm", key, iv)',
|
|
412
|
+
confidence: 93,
|
|
413
|
+
excludeInTests: true
|
|
414
|
+
},
|
|
415
|
+
// ── NoSQL Injection ──
|
|
416
|
+
{
|
|
417
|
+
name: "nosql-injection",
|
|
418
|
+
ruleId: "SEC001",
|
|
419
|
+
regex: /\$(?:where|regex|gt|lt|ne|or|and)\s*[:=]\s*(?:req\.|params\.|query\.|body\.)/,
|
|
420
|
+
severity: "high",
|
|
421
|
+
message: "NoSQL injection \u2014 MongoDB operator built from user input",
|
|
422
|
+
suggestion: "Sanitize input. Use mongoose-sanitize or explicit field selection.",
|
|
423
|
+
confidence: 82,
|
|
424
|
+
excludeInTests: true
|
|
425
|
+
},
|
|
426
|
+
// ── Destructive Ops ──
|
|
427
|
+
{
|
|
428
|
+
name: "rm-rf-root",
|
|
429
|
+
ruleId: "SEC002",
|
|
430
|
+
regex: /rm\s+-rf\s+\//,
|
|
431
|
+
severity: "critical",
|
|
432
|
+
message: "Recursive delete from root directory",
|
|
433
|
+
suggestion: "Use an explicit, validated path. Never delete from root.",
|
|
434
|
+
confidence: 98,
|
|
435
|
+
excludeInTests: false,
|
|
436
|
+
langs: ["js", "py", "go"]
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "rm-rf-wildcard",
|
|
440
|
+
ruleId: "SEC002",
|
|
441
|
+
regex: /rm\s+-rf\s+\*/,
|
|
442
|
+
severity: "critical",
|
|
443
|
+
message: "Recursive delete with wildcard",
|
|
444
|
+
suggestion: "Use an explicit, validated path.",
|
|
445
|
+
confidence: 95,
|
|
446
|
+
excludeInTests: false,
|
|
447
|
+
langs: ["js", "py", "go"]
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: "fs-unlink-unvalidated",
|
|
451
|
+
ruleId: "SEC002",
|
|
452
|
+
regex: /fs\.(?:unlink|rmdir|rm)(?:Sync)?\s*\(/,
|
|
453
|
+
severity: "medium",
|
|
454
|
+
message: "File/directory deletion \u2014 ensure path is validated",
|
|
455
|
+
suggestion: "Validate path is within the allowed directory before deletion.",
|
|
456
|
+
confidence: 65,
|
|
457
|
+
excludeInTests: true,
|
|
458
|
+
validate: (line) => {
|
|
459
|
+
if (/(?:temp|tmp|cache|dist|build|output|\.cache|node_modules)/i.test(line)) return false;
|
|
460
|
+
if (/(?:unlink|rmdir|rm)(?:Sync)?\s*\(\s*['"`]/.test(line)) return false;
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
// ── ReDoS ──
|
|
465
|
+
{
|
|
466
|
+
name: "redos-req-input",
|
|
467
|
+
ruleId: "SEC014",
|
|
468
|
+
regex: /new\s+RegExp\s*\([^)]*req\.(?:body|query|params|headers)/,
|
|
469
|
+
severity: "critical",
|
|
470
|
+
message: "User input in new RegExp() \u2014 ReDoS attack vector",
|
|
471
|
+
suggestion: "Never construct regex from user input. Validate against a fixed pattern instead.",
|
|
472
|
+
confidence: 91,
|
|
473
|
+
excludeInTests: true
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "redos-var-input",
|
|
477
|
+
ruleId: "SEC014",
|
|
478
|
+
regex: /new\s+RegExp\s*\([^)]*\binput\b/,
|
|
479
|
+
severity: "critical",
|
|
480
|
+
message: 'Variable "input" in new RegExp() \u2014 potential ReDoS',
|
|
481
|
+
suggestion: "Avoid building regex from external input. Use escapeRegExp() to sanitize.",
|
|
482
|
+
confidence: 78,
|
|
483
|
+
excludeInTests: true,
|
|
484
|
+
validate: (line) => !/['"].*input.*['"]/.test(line)
|
|
485
|
+
},
|
|
486
|
+
// ── JWT Vulnerabilities ──
|
|
487
|
+
{
|
|
488
|
+
name: "jwt-no-verify",
|
|
489
|
+
ruleId: "SEC010",
|
|
490
|
+
regex: /jwt\.decode\s*\(/,
|
|
491
|
+
severity: "high",
|
|
492
|
+
message: "jwt.decode() does NOT verify the signature \u2014 tokens can be forged",
|
|
493
|
+
suggestion: "Use jwt.verify(token, secret) to validate the signature before trusting the payload.",
|
|
494
|
+
confidence: 88,
|
|
495
|
+
excludeInTests: true,
|
|
496
|
+
validate: (line, lines, index) => {
|
|
497
|
+
for (let j = Math.max(0, index - 5); j <= Math.min(lines.length - 1, index + 5); j++) {
|
|
498
|
+
if (/jwt\.verify\s*\(/.test(lines[j])) return false;
|
|
499
|
+
}
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: "jwt-algorithm-none",
|
|
505
|
+
ruleId: "SEC010",
|
|
506
|
+
regex: /algorithms?\s*:\s*\[?\s*['"`]none['"`]/i,
|
|
507
|
+
severity: "critical",
|
|
508
|
+
message: 'JWT with "none" algorithm \u2014 accepts unsigned tokens',
|
|
509
|
+
suggestion: 'Never allow "none" algorithm. Use: algorithms: ["HS256"] or ["RS256"]',
|
|
510
|
+
confidence: 95,
|
|
511
|
+
excludeInTests: true,
|
|
512
|
+
langs: ["js", "py", "go"]
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: "jwt-no-expiry",
|
|
516
|
+
ruleId: "SEC010",
|
|
517
|
+
regex: /jwt\.sign\s*\([^)]*\)\s*(?:;|\n)/,
|
|
518
|
+
severity: "medium",
|
|
519
|
+
message: "JWT signed without expiration \u2014 tokens are valid forever",
|
|
520
|
+
suggestion: 'Add expiresIn: jwt.sign(payload, secret, { expiresIn: "1h" })',
|
|
521
|
+
confidence: 72,
|
|
522
|
+
excludeInTests: true,
|
|
523
|
+
validate: (line) => {
|
|
524
|
+
if (/(?:expiresIn|exp\s*:)/.test(line)) return false;
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
// ── Open Redirect ──
|
|
529
|
+
{
|
|
530
|
+
name: "open-redirect-param",
|
|
531
|
+
ruleId: "SEC016",
|
|
532
|
+
regex: /(?:res\.redirect|redirect|location\.href|window\.location)\s*(?:=|\()\s*(?:req\.query|req\.params|req\.body|searchParams\.get)/,
|
|
533
|
+
severity: "high",
|
|
534
|
+
message: "Open redirect \u2014 user-controlled redirect destination",
|
|
535
|
+
suggestion: "Validate redirect URL against an allowlist of trusted domains. Use new URL() to parse and check the hostname.",
|
|
536
|
+
confidence: 88,
|
|
537
|
+
excludeInTests: true
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: "open-redirect-url-param",
|
|
541
|
+
ruleId: "SEC016",
|
|
542
|
+
regex: /(?:redirect|callback|return|next|goto|url|returnTo|redirect_uri)\s*=\s*(?:req\.|params\.|query\.|searchParams)/,
|
|
543
|
+
severity: "medium",
|
|
544
|
+
message: "URL parameter used for redirect \u2014 potential open redirect",
|
|
545
|
+
suggestion: "Validate the redirect target is a relative path or a trusted domain.",
|
|
546
|
+
confidence: 75,
|
|
547
|
+
excludeInTests: true,
|
|
548
|
+
validate: (line) => {
|
|
549
|
+
if (/(?:new\s+URL|isValidUrl|allowedHosts|trustedDomains|startsWith\s*\(\s*['"`]\/)/i.test(line)) return false;
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
// ── Insecure Cookie Settings ──
|
|
554
|
+
{
|
|
555
|
+
name: "cookie-no-httponly",
|
|
556
|
+
ruleId: "SEC017",
|
|
557
|
+
regex: /(?:cookie|setCookie|set-cookie|cookies\.set)\s*\([^)]*(?:session|auth|token|jwt|csrf)/i,
|
|
558
|
+
severity: "medium",
|
|
559
|
+
message: "Security-sensitive cookie \u2014 verify httpOnly, secure, and sameSite are set",
|
|
560
|
+
suggestion: 'Set { httpOnly: true, secure: true, sameSite: "strict" } for auth cookies.',
|
|
561
|
+
confidence: 70,
|
|
562
|
+
excludeInTests: true,
|
|
563
|
+
validate: (line, lines, index) => {
|
|
564
|
+
const context = lines.slice(Math.max(0, index - 3), Math.min(lines.length, index + 5)).join(" ");
|
|
565
|
+
if (/httpOnly\s*:\s*true/i.test(context)) return false;
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "cookie-secure-false",
|
|
571
|
+
ruleId: "SEC017",
|
|
572
|
+
regex: /secure\s*:\s*false/,
|
|
573
|
+
severity: "high",
|
|
574
|
+
message: "Cookie with secure: false \u2014 will be sent over HTTP (plaintext)",
|
|
575
|
+
suggestion: 'Set secure: true. Use secure: process.env.NODE_ENV === "production" if needed for dev.',
|
|
576
|
+
confidence: 88,
|
|
577
|
+
excludeInTests: true
|
|
578
|
+
},
|
|
579
|
+
// ── Insecure Deserialization ──
|
|
580
|
+
{
|
|
581
|
+
name: "unsafe-json-parse-req",
|
|
582
|
+
ruleId: "SEC018",
|
|
583
|
+
regex: /JSON\.parse\s*\(\s*(?:req\.body|req\.query|req\.params|request\.body)/,
|
|
584
|
+
severity: "medium",
|
|
585
|
+
message: "JSON.parse on raw request input \u2014 use schema validation",
|
|
586
|
+
suggestion: "Validate input with zod, joi, or ajv before parsing. Framework body parsers handle this automatically.",
|
|
587
|
+
confidence: 72,
|
|
588
|
+
excludeInTests: true,
|
|
589
|
+
validate: (line, lines, index) => {
|
|
590
|
+
for (let j = Math.max(0, index - 5); j <= Math.min(lines.length - 1, index + 8); j++) {
|
|
591
|
+
if (/(?:\.parse\(|\.validate\(|\.safeParse\(|ajv\.compile|Joi\.object|z\.object)/i.test(lines[j])) return false;
|
|
592
|
+
}
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
name: "unsafe-yaml-load",
|
|
598
|
+
ruleId: "SEC018",
|
|
599
|
+
regex: /yaml\.load\s*\(/,
|
|
600
|
+
severity: "high",
|
|
601
|
+
message: "yaml.load() allows arbitrary code execution via YAML tags",
|
|
602
|
+
suggestion: 'Use yaml.safeLoad() or the "json" schema: yaml.load(str, { schema: FAILSAFE_SCHEMA })',
|
|
603
|
+
confidence: 85,
|
|
604
|
+
excludeInTests: true,
|
|
605
|
+
langs: ["js", "py", "go"],
|
|
606
|
+
validate: (line) => {
|
|
607
|
+
if (/(?:safeLoad|FAILSAFE_SCHEMA|JSON_SCHEMA|safe:\s*true|safe_load|CSafeLoader|SafeLoader)/i.test(line)) return false;
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
// ── Hardcoded Config Secrets ──
|
|
612
|
+
{
|
|
613
|
+
name: "hardcoded-jwt-secret",
|
|
614
|
+
ruleId: "SEC019",
|
|
615
|
+
regex: /(?:jwt|JWT)(?:_SECRET|Secret|_secret)\s*[:=]\s*['"`][^'"`]{4,}['"`]/,
|
|
616
|
+
severity: "critical",
|
|
617
|
+
message: "JWT secret hardcoded in source \u2014 anyone with code access can forge tokens",
|
|
618
|
+
suggestion: "Use environment variable: process.env.JWT_SECRET",
|
|
619
|
+
confidence: 92,
|
|
620
|
+
excludeInTests: true,
|
|
621
|
+
langs: ["js", "py", "go"],
|
|
622
|
+
validate: (line) => {
|
|
623
|
+
if (/process\.env|import\.meta\.env|Deno\.env|getenv|os\.environ|os\.getenv/i.test(line)) return false;
|
|
624
|
+
if (/(?:type|interface|declare)\s/.test(line)) return false;
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "hardcoded-db-password",
|
|
630
|
+
ruleId: "SEC019",
|
|
631
|
+
regex: /(?:password|passwd|db_pass|DB_PASSWORD)\s*[:=]\s*['"`](?!process\.env|import\.meta)[^'"`]{4,}['"`]/i,
|
|
632
|
+
severity: "high",
|
|
633
|
+
message: "Database password hardcoded in source code",
|
|
634
|
+
suggestion: "Use environment variable: process.env.DB_PASSWORD",
|
|
635
|
+
confidence: 78,
|
|
636
|
+
excludeInTests: true,
|
|
637
|
+
langs: ["js", "py", "go"],
|
|
638
|
+
validate: (line) => {
|
|
639
|
+
if (/process\.env|import\.meta\.env|getenv|os\.environ|os\.getenv|placeholder|example|changeme|your[-_]?password/i.test(line)) return false;
|
|
640
|
+
if (/(?:type|interface|declare|@param|@type)\s/.test(line)) return false;
|
|
641
|
+
if (/(?:z\.|Joi\.|yup\.|validate|schema|min|max)\b/.test(line)) return false;
|
|
642
|
+
if (/(?:label|name|placeholder|aria-label|autocomplete|type\s*=\s*['"]password)\b/i.test(line)) return false;
|
|
643
|
+
if (/(?:function|=>|param|\()\s*.*password/i.test(line) && !/[:=]\s*['"`][^'"`]{4,}['"`]/.test(line)) return false;
|
|
644
|
+
if (/password\s*(?:===|!==|==|!=|\.length|\.trim|\.match|\.test)/i.test(line)) return false;
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
// ── HTTP Header Injection ──
|
|
649
|
+
{
|
|
650
|
+
name: "header-injection",
|
|
651
|
+
ruleId: "SEC021",
|
|
652
|
+
regex: /(?:res\.setHeader|res\.header|response\.headers\.set)\s*\(\s*[^,]+,\s*(?:req\.|params\.|query\.|body\.)/,
|
|
653
|
+
severity: "high",
|
|
654
|
+
message: "User input in HTTP response header \u2014 header injection / response splitting risk",
|
|
655
|
+
suggestion: "Sanitize header values. Remove newlines (\\r\\n) from user input before setting headers.",
|
|
656
|
+
confidence: 82,
|
|
657
|
+
excludeInTests: true
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: "location-header-user-input",
|
|
661
|
+
ruleId: "SEC021",
|
|
662
|
+
regex: /(?:res\.setHeader|response\.headers\.set)\s*\(\s*['"`](?:Location|location)['"`]\s*,\s*(?:req\.|params\.|query\.)/,
|
|
663
|
+
severity: "high",
|
|
664
|
+
message: "User input in Location header \u2014 open redirect via header injection",
|
|
665
|
+
suggestion: "Validate redirect URLs against an allowlist of trusted domains.",
|
|
666
|
+
confidence: 88,
|
|
667
|
+
excludeInTests: true
|
|
668
|
+
},
|
|
669
|
+
// ── Timing Attack ──
|
|
670
|
+
{
|
|
671
|
+
name: "string-comparison-secret",
|
|
672
|
+
ruleId: "SEC009",
|
|
673
|
+
regex: /(?:secret|token|apiKey|password|hash|signature|hmac|digest)\s*(?:===|!==|==|!=)\s*(?:req\.|params\.|query\.|body\.|input|provided|submitted)/i,
|
|
674
|
+
severity: "medium",
|
|
675
|
+
message: "String comparison for secret \u2014 vulnerable to timing attacks",
|
|
676
|
+
suggestion: "Use crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)) for constant-time comparison.",
|
|
677
|
+
confidence: 78,
|
|
678
|
+
excludeInTests: true,
|
|
679
|
+
validate: (line) => {
|
|
680
|
+
if (/timingSafeEqual/.test(line)) return false;
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
// ── Insecure TLS ──
|
|
685
|
+
{
|
|
686
|
+
name: "tls-reject-unauthorized",
|
|
687
|
+
ruleId: "SEC020",
|
|
688
|
+
regex: /(?:rejectUnauthorized|NODE_TLS_REJECT_UNAUTHORIZED)\s*[:=]\s*(?:false|0|'0'|"0")/,
|
|
689
|
+
severity: "critical",
|
|
690
|
+
message: "TLS certificate verification disabled \u2014 MITM attack possible",
|
|
691
|
+
suggestion: "Never disable certificate verification in production. Fix the certificate chain instead.",
|
|
692
|
+
confidence: 95,
|
|
693
|
+
excludeInTests: false,
|
|
694
|
+
langs: ["js", "py", "go"],
|
|
695
|
+
validate: (line) => {
|
|
696
|
+
if (/(?:development|dev|test|local|NODE_ENV)/i.test(line)) return false;
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
// ── Python / Go (explicit langs) ──
|
|
701
|
+
{
|
|
702
|
+
name: "py-subprocess-shell-true",
|
|
703
|
+
ruleId: "SEC002",
|
|
704
|
+
regex: /subprocess\.(?:run|Popen|call|check_output)\s*\([^)]*\bshell\s*=\s*True\b/,
|
|
705
|
+
severity: "critical",
|
|
706
|
+
message: "subprocess with shell=True \u2014 shell injection risk",
|
|
707
|
+
suggestion: "Use shell=False and pass argv as a list. Prefer APIs over shelling out.",
|
|
708
|
+
confidence: 94,
|
|
709
|
+
excludeInTests: true,
|
|
710
|
+
langs: ["py"]
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
name: "py-os-system",
|
|
714
|
+
ruleId: "SEC002",
|
|
715
|
+
regex: /\bos\.system\s*\(/,
|
|
716
|
+
severity: "high",
|
|
717
|
+
message: "os.system() runs a shell command \u2014 injection risk if input is tainted",
|
|
718
|
+
suggestion: "Use subprocess.run([...], shell=False) with a fixed argv list.",
|
|
719
|
+
confidence: 88,
|
|
720
|
+
excludeInTests: true,
|
|
721
|
+
langs: ["py"]
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
name: "py-sql-fstring-query",
|
|
725
|
+
ruleId: "SEC001",
|
|
726
|
+
regex: /\.execute\s*\(\s*f['"`](?:[^'"`]|\\.)*(?:SELECT|INSERT|UPDATE|DELETE|DROP)\s[^'"`]*\{/i,
|
|
727
|
+
severity: "critical",
|
|
728
|
+
message: "SQL built with f-string \u2014 interpolation allows SQL injection",
|
|
729
|
+
suggestion: 'Use parameterized queries: cursor.execute("SELECT * FROM t WHERE id = %s", (id,))',
|
|
730
|
+
confidence: 90,
|
|
731
|
+
excludeInTests: true,
|
|
732
|
+
langs: ["py"]
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: "py-pickle-loads",
|
|
736
|
+
ruleId: "SEC018",
|
|
737
|
+
regex: /pickle\.loads?\s*\(/,
|
|
738
|
+
severity: "critical",
|
|
739
|
+
message: "pickle can execute arbitrary code when loading untrusted data",
|
|
740
|
+
suggestion: "Use JSON or protobuf. Never unpickle untrusted bytes.",
|
|
741
|
+
confidence: 92,
|
|
742
|
+
excludeInTests: true,
|
|
743
|
+
langs: ["py"]
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: "go-exec-command-fmt",
|
|
747
|
+
ruleId: "SEC002",
|
|
748
|
+
regex: /exec\.Command(?:Context)?\s*\([^)]*fmt\.Sprintf\s*\(/,
|
|
749
|
+
severity: "high",
|
|
750
|
+
message: "exec.Command with fmt.Sprintf \u2014 argv may embed unsanitized input",
|
|
751
|
+
suggestion: "Pass argv as separate strings with validated values, not one formatted shell string.",
|
|
752
|
+
confidence: 85,
|
|
753
|
+
excludeInTests: true,
|
|
754
|
+
langs: ["go"]
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
name: "go-sql-string-concat",
|
|
758
|
+
ruleId: "SEC001",
|
|
759
|
+
regex: /(?:QueryRow|Query|Exec)(?:Context)?\s*\(\s*["'](?:SELECT|INSERT|UPDATE|DELETE|DROP)\s[^"']*["']\s*\+\s*/i,
|
|
760
|
+
severity: "critical",
|
|
761
|
+
message: "SQL string concatenation in database call \u2014 SQL injection risk",
|
|
762
|
+
suggestion: 'Use parameterized queries: db.Query("SELECT ... WHERE id = ?", id) or argument list APIs.',
|
|
763
|
+
confidence: 88,
|
|
764
|
+
excludeInTests: true,
|
|
765
|
+
langs: ["go"]
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: "py-sql-string-modulo",
|
|
769
|
+
ruleId: "SEC001",
|
|
770
|
+
regex: /["'](?:SELECT|INSERT|UPDATE|DELETE|DROP)\s[^"']*%[^"']*["']\s*%\s*\w+/i,
|
|
771
|
+
severity: "critical",
|
|
772
|
+
message: "SQL built with % string formatting \u2014 use parameterized queries",
|
|
773
|
+
suggestion: 'Use cursor.execute("SELECT ... WHERE id = %s", (id,)) with a tuple of parameters.',
|
|
774
|
+
confidence: 86,
|
|
775
|
+
excludeInTests: true,
|
|
776
|
+
langs: ["py"]
|
|
777
|
+
}
|
|
778
|
+
];
|
|
779
|
+
var SecurityEngine = class {
|
|
780
|
+
id = "security";
|
|
781
|
+
async scan(delta, signal) {
|
|
782
|
+
if (signal?.aborted) return [];
|
|
783
|
+
const uri = delta.documentUri;
|
|
784
|
+
const source = delta.fullText;
|
|
785
|
+
if (!/(?:query|exec|innerHTML|dangerously|eval|Function|spawn|redirect|cookie|crypto|md5|sha1|jwt|\.verify|\.sign|req\.|params\.|body\.|unlink|rmSync|document\.write|__proto__|prototype\[|Math\.random|\.delete\s*\(|require\s*\(|Access-Control|origin\s*[=:]|yaml\.load|password|NODE_TLS|new\s+RegExp|DELETE\s+FROM|DROP\s+TABLE|TRUNCATE|rejectUnauthorized|subprocess|pickle\.|os\.system|shell\s*=\s*True|fmt\.Sprintf|exec\.Command|\bQuery(?:Context|Row)?\s*\(|SELECT\s+\*)/i.test(
|
|
786
|
+
source
|
|
787
|
+
)) {
|
|
788
|
+
return [];
|
|
789
|
+
}
|
|
790
|
+
const secLang = inferSecLang(delta);
|
|
791
|
+
const isTest = isTestFile(uri);
|
|
792
|
+
const critical = isCriticalPath(uri);
|
|
793
|
+
const isScript = isScriptFile(uri);
|
|
794
|
+
const isBuildUtil = isBuildOrUtilityFile(uri);
|
|
795
|
+
const isVibecheckSelf = isVibeCheckFile(uri);
|
|
796
|
+
const findings = [];
|
|
797
|
+
const lines = delta.lines ?? source.split("\n");
|
|
798
|
+
const applicablePatterns = PATTERNS.filter(
|
|
799
|
+
(p) => appliesToLang(p, secLang) && (!isTest || !p.excludeInTests)
|
|
800
|
+
);
|
|
801
|
+
for (let i = 0; i < lines.length; i++) {
|
|
802
|
+
if (signal?.aborted) break;
|
|
803
|
+
const line = lines[i];
|
|
804
|
+
const trimmed = line.trim();
|
|
805
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.startsWith("#")) continue;
|
|
806
|
+
if (/^\s*(?:regex|pattern|re|PATTERN)\s*[:=]\s*(?:\/|new\s+RegExp)/.test(line)) continue;
|
|
807
|
+
if (/^\s*['"`:]\//.test(trimmed)) continue;
|
|
808
|
+
for (const pattern of applicablePatterns) {
|
|
809
|
+
const match = pattern.regex.exec(line);
|
|
810
|
+
if (!match) continue;
|
|
811
|
+
if (pattern.validate && !pattern.validate(line, lines, i)) continue;
|
|
812
|
+
const ruleId = pattern.ruleId;
|
|
813
|
+
if (ruleId === "SEC002" && pattern.name === "fs-unlink-unvalidated") {
|
|
814
|
+
if (isScript || isBuildUtil) continue;
|
|
815
|
+
}
|
|
816
|
+
if (ruleId === "SEC003" && isVibecheckSelf) continue;
|
|
817
|
+
if (ruleId === "SEC005" && pattern.name === "cors-wildcard") {
|
|
818
|
+
if (/(?:\.config\.|development|dev|local)/i.test(uri)) continue;
|
|
819
|
+
}
|
|
820
|
+
if (ruleId === "SEC006" && /['"`].*__proto__.*['"`]/.test(line)) continue;
|
|
821
|
+
if (ruleId === "SEC009" && pattern.name === "math-random-hex" && /(?:key|className|style|color|animation)/i.test(line)) continue;
|
|
822
|
+
if (ruleId === "SEC007" && pattern.name === "dynamic-require" && isScript) continue;
|
|
823
|
+
if (ruleId === "SEC012" && (isScript || isBuildUtil)) continue;
|
|
824
|
+
let severity = escalate(pattern.severity, critical);
|
|
825
|
+
if (ruleId === "SEC002" && pattern.name === "fs-unlink-unvalidated") {
|
|
826
|
+
if (hasUserInputIndicators(line)) {
|
|
827
|
+
severity = escalate("high", critical);
|
|
828
|
+
} else {
|
|
829
|
+
severity = "info";
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (ruleId === "SEC007" && pattern.name === "dynamic-require") {
|
|
833
|
+
if (hasUserInputIndicators(line)) {
|
|
834
|
+
severity = escalate("medium", critical);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (ruleId === "SEC009" && pattern.name === "md5-usage") {
|
|
838
|
+
if (/(?:password|passwd|auth|credential|secret|token)/i.test(line)) {
|
|
839
|
+
severity = escalate("medium", critical);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
findings.push({
|
|
843
|
+
id: deterministicId(uri, i + 1, ruleId, pattern.name),
|
|
844
|
+
engine: "security",
|
|
845
|
+
severity,
|
|
846
|
+
category: "security",
|
|
847
|
+
file: uri,
|
|
848
|
+
line: i + 1,
|
|
849
|
+
column: match.index ?? 0,
|
|
850
|
+
message: pattern.message,
|
|
851
|
+
evidence: trimmed,
|
|
852
|
+
suggestion: pattern.suggestion,
|
|
853
|
+
confidence: pattern.confidence / 100,
|
|
854
|
+
autoFixable: false,
|
|
855
|
+
ruleId
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return findings;
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
export { SecurityEngine };
|