getdoorman 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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/bin/doorman.js +444 -0
  4. package/package.json +74 -0
  5. package/src/ai-fixer.js +559 -0
  6. package/src/ast-scanner.js +434 -0
  7. package/src/auth.js +149 -0
  8. package/src/baseline.js +48 -0
  9. package/src/compliance.js +539 -0
  10. package/src/config.js +466 -0
  11. package/src/custom-rules.js +32 -0
  12. package/src/dashboard.js +202 -0
  13. package/src/detector.js +142 -0
  14. package/src/fix-engine.js +48 -0
  15. package/src/fix-registry-extra.js +95 -0
  16. package/src/fix-registry-go-rust.js +77 -0
  17. package/src/fix-registry-java-csharp.js +77 -0
  18. package/src/fix-registry-js.js +99 -0
  19. package/src/fix-registry-mcp-ai.js +57 -0
  20. package/src/fix-registry-python.js +87 -0
  21. package/src/fixer-ruby-php.js +608 -0
  22. package/src/fixer.js +2113 -0
  23. package/src/hooks.js +115 -0
  24. package/src/ignore.js +176 -0
  25. package/src/index.js +384 -0
  26. package/src/metrics.js +126 -0
  27. package/src/monorepo.js +65 -0
  28. package/src/presets.js +54 -0
  29. package/src/reporter.js +975 -0
  30. package/src/rule-worker.js +36 -0
  31. package/src/rules/ast-rules.js +756 -0
  32. package/src/rules/bugs/accessibility.js +235 -0
  33. package/src/rules/bugs/ai-codegen-fixable.js +172 -0
  34. package/src/rules/bugs/ai-codegen.js +365 -0
  35. package/src/rules/bugs/code-smell-bugs.js +247 -0
  36. package/src/rules/bugs/crypto-bugs.js +195 -0
  37. package/src/rules/bugs/docker-bugs.js +158 -0
  38. package/src/rules/bugs/general.js +361 -0
  39. package/src/rules/bugs/go-bugs.js +279 -0
  40. package/src/rules/bugs/index.js +73 -0
  41. package/src/rules/bugs/js-api.js +257 -0
  42. package/src/rules/bugs/js-array-object.js +210 -0
  43. package/src/rules/bugs/js-async-fixable.js +223 -0
  44. package/src/rules/bugs/js-async.js +211 -0
  45. package/src/rules/bugs/js-closure-scope.js +182 -0
  46. package/src/rules/bugs/js-database.js +203 -0
  47. package/src/rules/bugs/js-error-handling.js +148 -0
  48. package/src/rules/bugs/js-logic.js +261 -0
  49. package/src/rules/bugs/js-memory.js +214 -0
  50. package/src/rules/bugs/js-node.js +361 -0
  51. package/src/rules/bugs/js-react.js +373 -0
  52. package/src/rules/bugs/js-regex.js +200 -0
  53. package/src/rules/bugs/js-state.js +272 -0
  54. package/src/rules/bugs/js-type-coercion.js +318 -0
  55. package/src/rules/bugs/nextjs-bugs.js +242 -0
  56. package/src/rules/bugs/nextjs-fixable.js +120 -0
  57. package/src/rules/bugs/node-fixable.js +178 -0
  58. package/src/rules/bugs/python-advanced.js +245 -0
  59. package/src/rules/bugs/python-fixable.js +98 -0
  60. package/src/rules/bugs/python.js +284 -0
  61. package/src/rules/bugs/react-fixable.js +207 -0
  62. package/src/rules/bugs/ruby-bugs.js +182 -0
  63. package/src/rules/bugs/shell-bugs.js +181 -0
  64. package/src/rules/bugs/silent-failures.js +261 -0
  65. package/src/rules/bugs/ts-bugs.js +235 -0
  66. package/src/rules/bugs/unused-vars.js +65 -0
  67. package/src/rules/compliance/accessibility-ext.js +468 -0
  68. package/src/rules/compliance/education.js +322 -0
  69. package/src/rules/compliance/financial.js +421 -0
  70. package/src/rules/compliance/frameworks.js +507 -0
  71. package/src/rules/compliance/healthcare.js +520 -0
  72. package/src/rules/compliance/index.js +2714 -0
  73. package/src/rules/compliance/regional-eu.js +480 -0
  74. package/src/rules/compliance/regional-international.js +903 -0
  75. package/src/rules/cost/index.js +1993 -0
  76. package/src/rules/data/index.js +2503 -0
  77. package/src/rules/dependencies/index.js +1684 -0
  78. package/src/rules/deployment/index.js +2050 -0
  79. package/src/rules/index.js +71 -0
  80. package/src/rules/infrastructure/index.js +3048 -0
  81. package/src/rules/performance/index.js +3455 -0
  82. package/src/rules/quality/index.js +3175 -0
  83. package/src/rules/reliability/index.js +3040 -0
  84. package/src/rules/scope-rules.js +815 -0
  85. package/src/rules/security/ai-api.js +1177 -0
  86. package/src/rules/security/auth.js +1328 -0
  87. package/src/rules/security/cors.js +127 -0
  88. package/src/rules/security/crypto.js +527 -0
  89. package/src/rules/security/csharp.js +862 -0
  90. package/src/rules/security/csrf.js +193 -0
  91. package/src/rules/security/dart.js +835 -0
  92. package/src/rules/security/deserialization.js +291 -0
  93. package/src/rules/security/file-upload.js +187 -0
  94. package/src/rules/security/go.js +850 -0
  95. package/src/rules/security/headers.js +235 -0
  96. package/src/rules/security/index.js +65 -0
  97. package/src/rules/security/injection.js +1639 -0
  98. package/src/rules/security/mcp-server.js +71 -0
  99. package/src/rules/security/misconfiguration.js +660 -0
  100. package/src/rules/security/oauth-jwt.js +329 -0
  101. package/src/rules/security/path-traversal.js +295 -0
  102. package/src/rules/security/php.js +1054 -0
  103. package/src/rules/security/prototype-pollution.js +283 -0
  104. package/src/rules/security/rate-limiting.js +208 -0
  105. package/src/rules/security/ruby.js +1061 -0
  106. package/src/rules/security/rust.js +693 -0
  107. package/src/rules/security/secrets.js +747 -0
  108. package/src/rules/security/shell.js +647 -0
  109. package/src/rules/security/ssrf.js +298 -0
  110. package/src/rules/security/supply-chain-advanced.js +393 -0
  111. package/src/rules/security/supply-chain.js +734 -0
  112. package/src/rules/security/swift.js +835 -0
  113. package/src/rules/security/taint.js +27 -0
  114. package/src/rules/security/xss.js +520 -0
  115. package/src/scan-cache.js +71 -0
  116. package/src/scanner.js +710 -0
  117. package/src/scope-analyzer.js +685 -0
  118. package/src/share.js +88 -0
  119. package/src/taint.js +300 -0
  120. package/src/telemetry.js +183 -0
  121. package/src/tracer.js +190 -0
  122. package/src/upload.js +35 -0
  123. package/src/worker.js +31 -0
@@ -0,0 +1,756 @@
1
+ /**
2
+ * AST-based security rules (AST-SQL-001 through AST-ERR-002)
3
+ *
4
+ * These rules leverage tree-sitter AST analysis for significantly more precise
5
+ * detection than regex-based rules. They can follow variable indirection, ignore
6
+ * comments and strings, and reason about scope and control flow.
7
+ *
8
+ * Each rule receives a context object with { files, stack, astParse } where
9
+ * astParse is a function (content, language) => ASTContext | null.
10
+ */
11
+
12
+ import { parseFile, detectLanguage, isASTAvailable } from '../ast-scanner.js';
13
+
14
+ const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
15
+ const isJS = (f) => JS_EXT.some((ext) => f.endsWith(ext));
16
+
17
+ const SKIP_PATH =
18
+ /[/\\](test|tests|__tests__|__mocks__|mocks|fixtures|__fixtures__|spec|__snapshots__|node_modules|vendor|dist|build)[/\\]/i;
19
+
20
+ /**
21
+ * Helper: run an AST-based check on every applicable file.
22
+ * Returns findings array.
23
+ */
24
+ async function forEachAST(files, rule, checkFn) {
25
+ if (!isASTAvailable()) return [];
26
+
27
+ const findings = [];
28
+
29
+ for (const [filePath, content] of files) {
30
+ if (SKIP_PATH.test(filePath)) continue;
31
+
32
+ const lang = detectLanguage(filePath);
33
+ if (!lang) continue;
34
+
35
+ const ctx = await parseFile(content, lang);
36
+ if (!ctx) continue;
37
+
38
+ const fileFindings = checkFn(ctx, filePath, content);
39
+ for (const f of fileFindings) {
40
+ findings.push({
41
+ ruleId: rule.id,
42
+ category: rule.category,
43
+ severity: rule.severity,
44
+ title: rule.title,
45
+ description: rule.description,
46
+ confidence: rule.confidence,
47
+ file: filePath,
48
+ line: f.line,
49
+ fix: rule.fix || null,
50
+ astBased: true,
51
+ });
52
+ }
53
+ }
54
+
55
+ return findings;
56
+ }
57
+
58
+ function makeFinding(ctx, node) {
59
+ return { line: ctx.nodeLocation(node).line };
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Query/DB call helpers
64
+ // ---------------------------------------------------------------------------
65
+ const DB_CALL_NAMES = ['query', 'execute', 'exec', '$queryRaw', '$executeRaw', 'raw'];
66
+ const SECRET_VAR_NAMES = /^(password|secret|apiKey|api_key|token|auth_token|private_key|privateKey|secretKey|secret_key|accessToken|access_token)$/i;
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // The 20 AST rules
70
+ // ---------------------------------------------------------------------------
71
+
72
+ const astRules = [
73
+ // -----------------------------------------------------------------------
74
+ // AST-SQL-001: SQL injection via string concatenation in db calls
75
+ // -----------------------------------------------------------------------
76
+ {
77
+ id: 'AST-SQL-001',
78
+ category: 'security',
79
+ severity: 'critical',
80
+ confidence: 'firm',
81
+ title: 'SQL Injection via String Concatenation (AST)',
82
+ description:
83
+ 'Database query call uses string concatenation with non-literal values. An attacker may inject SQL.',
84
+ fix: { suggestion: 'Use parameterized queries instead of string concatenation.' },
85
+ check({ files }) {
86
+ return forEachAST(files, this, (ctx, filePath) => {
87
+ const results = [];
88
+ for (const name of DB_CALL_NAMES) {
89
+ for (const call of ctx.findCalls(name)) {
90
+ const args = ctx.getArguments(call);
91
+ if (args.length > 0 && ctx.containsConcatenation(args[0])) {
92
+ results.push(makeFinding(ctx, call));
93
+ }
94
+ }
95
+ }
96
+ return results;
97
+ });
98
+ },
99
+ },
100
+
101
+ // -----------------------------------------------------------------------
102
+ // AST-SQL-002: SQL injection via template literals in query calls
103
+ // -----------------------------------------------------------------------
104
+ {
105
+ id: 'AST-SQL-002',
106
+ category: 'security',
107
+ severity: 'critical',
108
+ confidence: 'firm',
109
+ title: 'SQL Injection via Template Literal (AST)',
110
+ description:
111
+ 'Database query call uses a template literal with embedded expressions, allowing SQL injection.',
112
+ fix: { suggestion: 'Use parameterized queries with placeholders ($1, ?) instead of template literals.' },
113
+ check({ files }) {
114
+ return forEachAST(files, this, (ctx) => {
115
+ const results = [];
116
+ for (const name of DB_CALL_NAMES) {
117
+ for (const call of ctx.findCalls(name)) {
118
+ const args = ctx.getArguments(call);
119
+ if (args.length > 0 && args[0].type === 'template_string') {
120
+ // Check that template has substitutions (not a plain string)
121
+ let hasSubs = false;
122
+ for (let i = 0; i < args[0].childCount; i++) {
123
+ if (args[0].child(i).type === 'template_substitution') {
124
+ hasSubs = true;
125
+ break;
126
+ }
127
+ }
128
+ if (hasSubs) {
129
+ results.push(makeFinding(ctx, call));
130
+ }
131
+ }
132
+ }
133
+ }
134
+ return results;
135
+ });
136
+ },
137
+ },
138
+
139
+ // -----------------------------------------------------------------------
140
+ // AST-EVAL-001: eval() with non-literal argument
141
+ // -----------------------------------------------------------------------
142
+ {
143
+ id: 'AST-EVAL-001',
144
+ category: 'security',
145
+ severity: 'critical',
146
+ confidence: 'firm',
147
+ title: 'eval() with Non-Literal Argument (AST)',
148
+ description:
149
+ 'eval() is called with a non-literal argument, allowing arbitrary code execution.',
150
+ fix: { suggestion: 'Avoid eval(). Use JSON.parse() for data, or a sandboxed VM for code execution.' },
151
+ check({ files }) {
152
+ return forEachAST(files, this, (ctx) => {
153
+ const results = [];
154
+ for (const call of ctx.findCalls('eval')) {
155
+ const args = ctx.getArguments(call);
156
+ if (args.length > 0 && ctx.isNonLiteral(args[0])) {
157
+ results.push(makeFinding(ctx, call));
158
+ }
159
+ }
160
+ return results;
161
+ });
162
+ },
163
+ },
164
+
165
+ // -----------------------------------------------------------------------
166
+ // AST-EVAL-002: Function constructor with non-literal body
167
+ // -----------------------------------------------------------------------
168
+ {
169
+ id: 'AST-EVAL-002',
170
+ category: 'security',
171
+ severity: 'critical',
172
+ confidence: 'firm',
173
+ title: 'Function Constructor with Non-Literal Body (AST)',
174
+ description:
175
+ 'new Function() with a non-literal body string is equivalent to eval() and allows code injection.',
176
+ fix: { suggestion: 'Avoid the Function constructor with dynamic arguments.' },
177
+ check({ files }) {
178
+ return forEachAST(files, this, (ctx) => {
179
+ const results = [];
180
+ const newExprs = ctx.findNodes('new_expression');
181
+ for (const node of newExprs) {
182
+ const constructor = node.childForFieldName('constructor') || node.child(1);
183
+ if (constructor && constructor.text === 'Function') {
184
+ // Check if any argument is non-literal
185
+ const args = ctx.getArguments(node);
186
+ const hasNonLiteral = args.some((a) => ctx.isNonLiteral(a));
187
+ if (hasNonLiteral) {
188
+ results.push(makeFinding(ctx, node));
189
+ }
190
+ }
191
+ }
192
+ return results;
193
+ });
194
+ },
195
+ },
196
+
197
+ // -----------------------------------------------------------------------
198
+ // AST-EXEC-001: child_process.exec with non-literal command
199
+ // -----------------------------------------------------------------------
200
+ {
201
+ id: 'AST-EXEC-001',
202
+ category: 'security',
203
+ severity: 'critical',
204
+ confidence: 'firm',
205
+ title: 'Command Injection via child_process.exec (AST)',
206
+ description:
207
+ 'child_process.exec/execSync is called with a non-literal command string, enabling command injection.',
208
+ fix: { suggestion: 'Use child_process.execFile or spawn with an argument array instead of exec.' },
209
+ check({ files }) {
210
+ return forEachAST(files, this, (ctx) => {
211
+ const results = [];
212
+ for (const name of ['exec', 'execSync']) {
213
+ for (const call of ctx.findCalls(name)) {
214
+ const args = ctx.getArguments(call);
215
+ if (args.length > 0 && ctx.isNonLiteral(args[0])) {
216
+ results.push(makeFinding(ctx, call));
217
+ }
218
+ }
219
+ }
220
+ return results;
221
+ });
222
+ },
223
+ },
224
+
225
+ // -----------------------------------------------------------------------
226
+ // AST-XSS-001: res.send() with user input argument
227
+ // -----------------------------------------------------------------------
228
+ {
229
+ id: 'AST-XSS-001',
230
+ category: 'security',
231
+ severity: 'high',
232
+ confidence: 'firm',
233
+ title: 'Reflected XSS via res.send (AST)',
234
+ description:
235
+ 'res.send() or res.write() is called with user-controlled input (req.query/body/params), enabling reflected XSS.',
236
+ fix: { suggestion: 'Sanitize or encode user input before sending it in the response.' },
237
+ check({ files }) {
238
+ return forEachAST(files, this, (ctx) => {
239
+ const results = [];
240
+ for (const name of ['send', 'write', 'end']) {
241
+ for (const call of ctx.findCalls(name)) {
242
+ const callee = call.child(0);
243
+ // Ensure it's res.send, not arbitrary .send
244
+ if (callee && callee.type === 'member_expression') {
245
+ const obj = callee.childForFieldName('object');
246
+ if (obj && (obj.text === 'res' || obj.text === 'response')) {
247
+ const args = ctx.getArguments(call);
248
+ if (args.length > 0 && ctx.containsUserInput(args[0])) {
249
+ results.push(makeFinding(ctx, call));
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+ return results;
256
+ });
257
+ },
258
+ },
259
+
260
+ // -----------------------------------------------------------------------
261
+ // AST-XSS-002: dangerouslySetInnerHTML with non-literal value
262
+ // -----------------------------------------------------------------------
263
+ {
264
+ id: 'AST-XSS-002',
265
+ category: 'security',
266
+ severity: 'high',
267
+ confidence: 'likely',
268
+ title: 'dangerouslySetInnerHTML with Non-Literal Value (AST)',
269
+ description:
270
+ 'React dangerouslySetInnerHTML is set with a non-literal value, which may lead to XSS.',
271
+ fix: { suggestion: 'Sanitize HTML with DOMPurify before using dangerouslySetInnerHTML.' },
272
+ check({ files }) {
273
+ return forEachAST(files, this, (ctx) => {
274
+ const results = [];
275
+ // Look for JSX attributes named dangerouslySetInnerHTML
276
+ for (const node of ctx.findNodes('jsx_attribute')) {
277
+ const name = node.childForFieldName('name') || node.child(0);
278
+ if (name && name.text === 'dangerouslySetInnerHTML') {
279
+ // Check if the value contains non-literal content
280
+ const value = node.childForFieldName('value') || node.child(node.childCount - 1);
281
+ if (value && ctx.isNonLiteral(value)) {
282
+ results.push(makeFinding(ctx, node));
283
+ }
284
+ }
285
+ }
286
+ return results;
287
+ });
288
+ },
289
+ },
290
+
291
+ // -----------------------------------------------------------------------
292
+ // AST-AUTH-001: Express route without auth middleware
293
+ // -----------------------------------------------------------------------
294
+ {
295
+ id: 'AST-AUTH-001',
296
+ category: 'security',
297
+ severity: 'medium',
298
+ confidence: 'likely',
299
+ title: 'Express Route Without Auth Middleware (AST)',
300
+ description:
301
+ 'Express route handler has no authentication middleware. Sensitive routes may be publicly accessible.',
302
+ fix: { suggestion: 'Add authentication middleware (e.g., requireAuth) before the route handler.' },
303
+ check({ files }) {
304
+ return forEachAST(files, this, (ctx) => {
305
+ const results = [];
306
+ const sensitivePatterns = /\/(admin|user|account|profile|dashboard|api\/v\d+)/;
307
+ const authMiddlewareNames = /auth|authenticate|requireAuth|isAuthenticated|protect|verify|jwt|token/i;
308
+
309
+ for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
310
+ for (const call of ctx.findCalls(method)) {
311
+ const callee = call.child(0);
312
+ // Must be router.get / app.post etc
313
+ if (!callee || callee.type !== 'member_expression') continue;
314
+
315
+ const args = ctx.getArguments(call);
316
+ if (args.length < 2) continue;
317
+
318
+ // First arg should be a route string
319
+ const routeArg = args[0];
320
+ if (!ctx.isStringLiteral(routeArg)) continue;
321
+ if (!sensitivePatterns.test(routeArg.text)) continue;
322
+
323
+ // Check if any middle argument (between route and last handler) is auth middleware
324
+ const middlewareArgs = args.slice(1, -1);
325
+ const hasAuth = middlewareArgs.some((a) => authMiddlewareNames.test(a.text));
326
+ if (!hasAuth) {
327
+ results.push(makeFinding(ctx, call));
328
+ }
329
+ }
330
+ }
331
+ return results;
332
+ });
333
+ },
334
+ },
335
+
336
+ // -----------------------------------------------------------------------
337
+ // AST-AUTH-002: Missing rate limiting on auth routes
338
+ // -----------------------------------------------------------------------
339
+ {
340
+ id: 'AST-AUTH-002',
341
+ category: 'security',
342
+ severity: 'medium',
343
+ confidence: 'likely',
344
+ title: 'Missing Rate Limiting on Auth Route (AST)',
345
+ description:
346
+ 'Authentication route (login, register, password) has no rate-limiting middleware, enabling brute-force attacks.',
347
+ fix: { suggestion: 'Add rate-limiting middleware (e.g., express-rate-limit) to authentication endpoints.' },
348
+ check({ files }) {
349
+ return forEachAST(files, this, (ctx) => {
350
+ const results = [];
351
+ const authRoutes = /\/(login|signin|sign-in|register|signup|sign-up|forgot-password|reset-password|auth)/;
352
+ const rateLimitNames = /rateLimit|rateLimiter|limiter|throttle|slowDown/i;
353
+
354
+ for (const method of ['post', 'get', 'put']) {
355
+ for (const call of ctx.findCalls(method)) {
356
+ const callee = call.child(0);
357
+ if (!callee || callee.type !== 'member_expression') continue;
358
+
359
+ const args = ctx.getArguments(call);
360
+ if (args.length < 2) continue;
361
+
362
+ const routeArg = args[0];
363
+ if (!ctx.isStringLiteral(routeArg)) continue;
364
+ if (!authRoutes.test(routeArg.text)) continue;
365
+
366
+ const middlewareArgs = args.slice(1, -1);
367
+ const hasRateLimit = middlewareArgs.some((a) => rateLimitNames.test(a.text));
368
+ if (!hasRateLimit) {
369
+ results.push(makeFinding(ctx, call));
370
+ }
371
+ }
372
+ }
373
+ return results;
374
+ });
375
+ },
376
+ },
377
+
378
+ // -----------------------------------------------------------------------
379
+ // AST-CRYPTO-001: crypto.createCipher (deprecated)
380
+ // -----------------------------------------------------------------------
381
+ {
382
+ id: 'AST-CRYPTO-001',
383
+ category: 'security',
384
+ severity: 'high',
385
+ confidence: 'firm',
386
+ title: 'Deprecated crypto.createCipher Usage (AST)',
387
+ description:
388
+ 'crypto.createCipher is deprecated and uses a weak key derivation. Use crypto.createCipheriv instead.',
389
+ fix: { suggestion: 'Replace createCipher with createCipheriv and provide an explicit IV.' },
390
+ check({ files }) {
391
+ return forEachAST(files, this, (ctx) => {
392
+ const results = [];
393
+ for (const call of ctx.findCalls('createCipher')) {
394
+ // Confirm it's crypto.createCipher, not createCipheriv
395
+ const callee = call.child(0);
396
+ if (callee && callee.text && !callee.text.includes('createCipheriv')) {
397
+ results.push(makeFinding(ctx, call));
398
+ }
399
+ }
400
+ return results;
401
+ });
402
+ },
403
+ },
404
+
405
+ // -----------------------------------------------------------------------
406
+ // AST-CRYPTO-002: Math.random() in security context
407
+ // -----------------------------------------------------------------------
408
+ {
409
+ id: 'AST-CRYPTO-002',
410
+ category: 'security',
411
+ severity: 'high',
412
+ confidence: 'likely',
413
+ title: 'Math.random() Used in Security Context (AST)',
414
+ description:
415
+ 'Math.random() is not cryptographically secure. It should not be used for tokens, passwords, or keys.',
416
+ fix: { suggestion: 'Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive randomness.' },
417
+ check({ files }) {
418
+ return forEachAST(files, this, (ctx) => {
419
+ const results = [];
420
+ const securityContext = /token|secret|password|key|salt|nonce|iv|hash|session|csrf|otp|code/i;
421
+
422
+ for (const call of ctx.findCalls('Math.random')) {
423
+ // Check if the result is assigned to or used in a security context
424
+ const parent = call.parent;
425
+ if (!parent) {
426
+ results.push(makeFinding(ctx, call));
427
+ continue;
428
+ }
429
+
430
+ // Check variable name in assignment
431
+ if (parent.type === 'variable_declarator') {
432
+ const name = parent.childForFieldName('name');
433
+ if (name && securityContext.test(name.text)) {
434
+ results.push(makeFinding(ctx, call));
435
+ continue;
436
+ }
437
+ }
438
+
439
+ // Check enclosing function name
440
+ const fn = ctx.getParentFunction(call);
441
+ if (fn) {
442
+ const fnName = fn.childForFieldName('name');
443
+ if (fnName && securityContext.test(fnName.text)) {
444
+ results.push(makeFinding(ctx, call));
445
+ }
446
+ }
447
+ }
448
+ return results;
449
+ });
450
+ },
451
+ },
452
+
453
+ // -----------------------------------------------------------------------
454
+ // AST-PATH-001: fs operations with user-controlled path
455
+ // -----------------------------------------------------------------------
456
+ {
457
+ id: 'AST-PATH-001',
458
+ category: 'security',
459
+ severity: 'high',
460
+ confidence: 'firm',
461
+ title: 'Path Traversal via fs Operations (AST)',
462
+ description:
463
+ 'File system operation (readFile, writeFile) uses a path derived from user input, enabling path traversal.',
464
+ fix: { suggestion: 'Validate and sanitize file paths. Use path.resolve() and check that the result is within an allowed directory.' },
465
+ check({ files }) {
466
+ return forEachAST(files, this, (ctx) => {
467
+ const results = [];
468
+ const fsOps = ['readFile', 'readFileSync', 'writeFile', 'writeFileSync',
469
+ 'readdir', 'readdirSync', 'unlink', 'unlinkSync', 'stat', 'statSync',
470
+ 'createReadStream', 'createWriteStream'];
471
+
472
+ for (const name of fsOps) {
473
+ for (const call of ctx.findCalls(name)) {
474
+ const args = ctx.getArguments(call);
475
+ if (args.length > 0 && ctx.containsUserInput(args[0])) {
476
+ results.push(makeFinding(ctx, call));
477
+ }
478
+ }
479
+ }
480
+ return results;
481
+ });
482
+ },
483
+ },
484
+
485
+ // -----------------------------------------------------------------------
486
+ // AST-PATH-002: path.join with user-controlled segment
487
+ // -----------------------------------------------------------------------
488
+ {
489
+ id: 'AST-PATH-002',
490
+ category: 'security',
491
+ severity: 'high',
492
+ confidence: 'firm',
493
+ title: 'Path Traversal via path.join (AST)',
494
+ description:
495
+ 'path.join() includes a user-controlled segment, which may allow directory traversal (../../etc/passwd).',
496
+ fix: { suggestion: 'Sanitize user input to remove ".." sequences, or validate the resolved path is within an allowed directory.' },
497
+ check({ files }) {
498
+ return forEachAST(files, this, (ctx) => {
499
+ const results = [];
500
+ for (const name of ['join', 'resolve']) {
501
+ for (const call of ctx.findCalls(name)) {
502
+ const callee = call.child(0);
503
+ if (callee && callee.type === 'member_expression') {
504
+ const obj = callee.childForFieldName('object');
505
+ if (!obj || obj.text !== 'path') continue;
506
+ }
507
+ const args = ctx.getArguments(call);
508
+ const hasUserInput = args.some((a) => ctx.containsUserInput(a));
509
+ if (hasUserInput) {
510
+ results.push(makeFinding(ctx, call));
511
+ }
512
+ }
513
+ }
514
+ return results;
515
+ });
516
+ },
517
+ },
518
+
519
+ // -----------------------------------------------------------------------
520
+ // AST-SSRF-001: HTTP request with user-controlled URL
521
+ // -----------------------------------------------------------------------
522
+ {
523
+ id: 'AST-SSRF-001',
524
+ category: 'security',
525
+ severity: 'high',
526
+ confidence: 'firm',
527
+ title: 'Server-Side Request Forgery (SSRF) (AST)',
528
+ description:
529
+ 'HTTP request (fetch, axios) uses a URL constructed from user input, enabling SSRF attacks.',
530
+ fix: { suggestion: 'Validate URLs against an allowlist of domains. Do not allow user input to control the full URL.' },
531
+ check({ files }) {
532
+ return forEachAST(files, this, (ctx) => {
533
+ const results = [];
534
+ for (const name of ['fetch', 'get', 'post', 'put', 'delete', 'request']) {
535
+ for (const call of ctx.findCalls(name)) {
536
+ const callee = call.child(0);
537
+ // For get/post/put/delete, require axios.get or http.get
538
+ if (['get', 'post', 'put', 'delete', 'request'].includes(name)) {
539
+ if (!callee || callee.type !== 'member_expression') continue;
540
+ const obj = callee.childForFieldName('object');
541
+ if (!obj) continue;
542
+ const objName = obj.text;
543
+ if (!/(axios|http|https|got|superagent|request|fetch)/i.test(objName)) continue;
544
+ }
545
+
546
+ const args = ctx.getArguments(call);
547
+ if (args.length > 0 && ctx.containsUserInput(args[0])) {
548
+ results.push(makeFinding(ctx, call));
549
+ }
550
+ }
551
+ }
552
+ return results;
553
+ });
554
+ },
555
+ },
556
+
557
+ // -----------------------------------------------------------------------
558
+ // AST-PROTO-001: Object.assign with user-controlled source
559
+ // -----------------------------------------------------------------------
560
+ {
561
+ id: 'AST-PROTO-001',
562
+ category: 'security',
563
+ severity: 'high',
564
+ confidence: 'likely',
565
+ title: 'Prototype Pollution via Object.assign (AST)',
566
+ description:
567
+ 'Object.assign() merges user-controlled input, which may allow __proto__ injection (prototype pollution).',
568
+ fix: { suggestion: 'Sanitize input to strip __proto__, constructor, and prototype keys, or use a safe merge library.' },
569
+ check({ files }) {
570
+ return forEachAST(files, this, (ctx) => {
571
+ const results = [];
572
+ for (const call of ctx.findCalls('Object.assign')) {
573
+ const args = ctx.getArguments(call);
574
+ // Check sources (all args after the first)
575
+ for (let i = 1; i < args.length; i++) {
576
+ if (ctx.containsUserInput(args[i])) {
577
+ results.push(makeFinding(ctx, call));
578
+ break;
579
+ }
580
+ }
581
+ }
582
+ return results;
583
+ });
584
+ },
585
+ },
586
+
587
+ // -----------------------------------------------------------------------
588
+ // AST-DESER-001: JSON.parse of request body without size check
589
+ // -----------------------------------------------------------------------
590
+ {
591
+ id: 'AST-DESER-001',
592
+ category: 'security',
593
+ severity: 'medium',
594
+ confidence: 'likely',
595
+ title: 'Unbounded JSON.parse of Request Body (AST)',
596
+ description:
597
+ 'JSON.parse() is called on user input without prior size validation, which may lead to denial-of-service.',
598
+ fix: { suggestion: 'Enforce a request body size limit via middleware (e.g., express.json({ limit: "100kb" })).' },
599
+ check({ files }) {
600
+ return forEachAST(files, this, (ctx) => {
601
+ const results = [];
602
+ for (const call of ctx.findCalls('JSON.parse')) {
603
+ const args = ctx.getArguments(call);
604
+ if (args.length > 0 && ctx.containsUserInput(args[0])) {
605
+ results.push(makeFinding(ctx, call));
606
+ }
607
+ }
608
+ return results;
609
+ });
610
+ },
611
+ },
612
+
613
+ // -----------------------------------------------------------------------
614
+ // AST-SECRET-001: Hardcoded string assigned to secret variable
615
+ // -----------------------------------------------------------------------
616
+ {
617
+ id: 'AST-SECRET-001',
618
+ category: 'security',
619
+ severity: 'high',
620
+ confidence: 'firm',
621
+ title: 'Hardcoded Secret in Variable Assignment (AST)',
622
+ description:
623
+ 'A string literal is assigned to a variable whose name indicates it holds a secret (password, apiKey, token, etc.).',
624
+ fix: { suggestion: 'Store secrets in environment variables or a secrets manager, not in source code.' },
625
+ check({ files }) {
626
+ return forEachAST(files, this, (ctx) => {
627
+ const results = [];
628
+ for (const node of ctx.findNodes('variable_declarator')) {
629
+ const name = node.childForFieldName('name') || node.child(0);
630
+ const value = node.childForFieldName('value');
631
+ if (!name || !value) continue;
632
+
633
+ if (SECRET_VAR_NAMES.test(name.text) && ctx.isStringLiteral(value)) {
634
+ // Skip empty strings and placeholder patterns
635
+ const text = value.text;
636
+ if (text === '""' || text === "''" || text === '``') continue;
637
+ if (/process\.env|env\.|config\./i.test(text)) continue;
638
+ results.push(makeFinding(ctx, node));
639
+ }
640
+ }
641
+ return results;
642
+ });
643
+ },
644
+ },
645
+
646
+ // -----------------------------------------------------------------------
647
+ // AST-SECRET-002: Hardcoded credentials in object literal
648
+ // -----------------------------------------------------------------------
649
+ {
650
+ id: 'AST-SECRET-002',
651
+ category: 'security',
652
+ severity: 'high',
653
+ confidence: 'firm',
654
+ title: 'Hardcoded Credentials in Object Literal (AST)',
655
+ description:
656
+ 'An object literal has a property named password/secret/apiKey with a hardcoded string value.',
657
+ fix: { suggestion: 'Use environment variables or a secrets manager instead of hardcoded credentials.' },
658
+ check({ files }) {
659
+ return forEachAST(files, this, (ctx) => {
660
+ const results = [];
661
+ for (const node of ctx.findNodes('pair')) {
662
+ const key = node.childForFieldName('key') || node.child(0);
663
+ const value = node.childForFieldName('value') || node.child(node.childCount - 1);
664
+ if (!key || !value) continue;
665
+
666
+ const keyText = key.text.replace(/['"]/g, '');
667
+ if (SECRET_VAR_NAMES.test(keyText) && ctx.isStringLiteral(value)) {
668
+ const text = value.text;
669
+ if (text === '""' || text === "''" || text === '``') continue;
670
+ results.push(makeFinding(ctx, node));
671
+ }
672
+ }
673
+ return results;
674
+ });
675
+ },
676
+ },
677
+
678
+ // -----------------------------------------------------------------------
679
+ // AST-ERR-001: Empty catch block
680
+ // -----------------------------------------------------------------------
681
+ {
682
+ id: 'AST-ERR-001',
683
+ category: 'security',
684
+ severity: 'medium',
685
+ confidence: 'firm',
686
+ title: 'Empty Catch Block (AST)',
687
+ description:
688
+ 'A catch block is empty, silently swallowing errors. This can hide security-critical failures.',
689
+ fix: { suggestion: 'Log or handle errors in catch blocks. At minimum, log the error for debugging.' },
690
+ check({ files }) {
691
+ return forEachAST(files, this, (ctx) => {
692
+ const results = [];
693
+ for (const node of ctx.findNodes('catch_clause')) {
694
+ const body = node.childForFieldName('body');
695
+ if (!body) continue;
696
+ // Count non-whitespace, non-brace children
697
+ let hasStatements = false;
698
+ for (let i = 0; i < body.childCount; i++) {
699
+ const child = body.child(i);
700
+ if (child.type !== '{' && child.type !== '}' && child.type !== 'comment') {
701
+ hasStatements = true;
702
+ break;
703
+ }
704
+ }
705
+ if (!hasStatements) {
706
+ results.push(makeFinding(ctx, node));
707
+ }
708
+ }
709
+ return results;
710
+ });
711
+ },
712
+ },
713
+
714
+ // -----------------------------------------------------------------------
715
+ // AST-ERR-002: console.log in catch block instead of proper error handling
716
+ // -----------------------------------------------------------------------
717
+ {
718
+ id: 'AST-ERR-002',
719
+ category: 'security',
720
+ severity: 'low',
721
+ confidence: 'likely',
722
+ title: 'console.log in Catch Block (AST)',
723
+ description:
724
+ 'A catch block only contains console.log, which may leak sensitive error details in production and is not proper error handling.',
725
+ fix: { suggestion: 'Use a proper logging framework with log levels, and consider re-throwing or returning an error response.' },
726
+ check({ files }) {
727
+ return forEachAST(files, this, (ctx) => {
728
+ const results = [];
729
+ for (const node of ctx.findNodes('catch_clause')) {
730
+ const body = node.childForFieldName('body');
731
+ if (!body) continue;
732
+
733
+ const statements = [];
734
+ for (let i = 0; i < body.childCount; i++) {
735
+ const child = body.child(i);
736
+ if (child.type !== '{' && child.type !== '}' && child.type !== 'comment') {
737
+ statements.push(child);
738
+ }
739
+ }
740
+
741
+ // If the only statement is a console.log call
742
+ if (statements.length === 1) {
743
+ const stmt = statements[0];
744
+ const text = stmt.text;
745
+ if (/console\.(log|warn|info|debug)\s*\(/.test(text)) {
746
+ results.push(makeFinding(ctx, node));
747
+ }
748
+ }
749
+ }
750
+ return results;
751
+ });
752
+ },
753
+ },
754
+ ];
755
+
756
+ export default astRules;