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,291 @@
1
+ /**
2
+ * Insecure Deserialization Detection Rules (SEC-DES-001 through SEC-DES-010)
3
+ *
4
+ * Detects patterns where untrusted data is deserialized in ways that
5
+ * can lead to remote code execution, prototype pollution, or DoS.
6
+ */
7
+
8
+ const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
9
+ const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
10
+ const isPy = (f) => f.endsWith('.py');
11
+
12
+ const SKIP_PATH = /[/\\](test|tests|__tests__|__mocks__|mocks|fixtures|__fixtures__|spec|node_modules|vendor|dist|build)[/\\]/i;
13
+ const COMMENT_LINE = /^\s*(\/\/|#|\/\*|\*)/;
14
+
15
+ function scanLines(content, regex, file, rule) {
16
+ const findings = [];
17
+ const lines = content.split('\n');
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const line = lines[i];
20
+ if (COMMENT_LINE.test(line)) continue;
21
+ if (regex.test(line)) {
22
+ findings.push({
23
+ ruleId: rule.id,
24
+ category: rule.category,
25
+ severity: rule.severity,
26
+ title: rule.title,
27
+ description: rule.description,
28
+ confidence: rule.confidence,
29
+ file,
30
+ line: i + 1,
31
+ fix: rule.fix || null,
32
+ });
33
+ }
34
+ }
35
+ return findings;
36
+ }
37
+
38
+ const rules = [
39
+ // SEC-DES-001: eval-based deserialization
40
+ {
41
+ id: 'SEC-DES-001',
42
+ category: 'security',
43
+ severity: 'critical',
44
+ confidence: 'definite',
45
+ title: 'eval() Used for Deserialization',
46
+ description:
47
+ 'Using eval() to parse data (JSON, config, etc.) allows arbitrary code execution. An attacker can inject executable code into the serialized payload.',
48
+ fix: { suggestion: 'Use JSON.parse() for JSON data. Never use eval() for deserialization.' },
49
+ check({ files }) {
50
+ const findings = [];
51
+ const pattern = /eval\s*\(\s*(?:req\.body|req\.query|data|payload|input|content|message|response|text)\b/;
52
+ for (const [path, content] of files) {
53
+ if (SKIP_PATH.test(path)) continue;
54
+ if (isJS(path)) {
55
+ findings.push(...scanLines(content, pattern, path, this));
56
+ }
57
+ }
58
+ return findings;
59
+ },
60
+ },
61
+
62
+ // SEC-DES-002: Function constructor for deserialization
63
+ {
64
+ id: 'SEC-DES-002',
65
+ category: 'security',
66
+ severity: 'critical',
67
+ confidence: 'definite',
68
+ title: 'Function Constructor Used for Deserialization',
69
+ description:
70
+ 'new Function() with untrusted input is equivalent to eval() and allows arbitrary code execution.',
71
+ fix: { suggestion: 'Use JSON.parse() or a safe parsing library. Never construct functions from user data.' },
72
+ check({ files }) {
73
+ const findings = [];
74
+ const pattern = /new\s+Function\s*\(\s*(?:req\.body|req\.query|data|payload|input|content|message|text)\b/;
75
+ for (const [path, content] of files) {
76
+ if (SKIP_PATH.test(path)) continue;
77
+ if (isJS(path)) {
78
+ findings.push(...scanLines(content, pattern, path, this));
79
+ }
80
+ }
81
+ return findings;
82
+ },
83
+ },
84
+
85
+ // SEC-DES-003: YAML unsafe load
86
+ {
87
+ id: 'SEC-DES-003',
88
+ category: 'security',
89
+ severity: 'critical',
90
+ confidence: 'definite',
91
+ title: 'Unsafe YAML Load (Code Execution Risk)',
92
+ description:
93
+ 'yaml.load() without SafeLoader in Python or js-yaml.load() without safe schema can execute arbitrary code via YAML tags like !!python/object.',
94
+ fix: { suggestion: 'Use yaml.safe_load() in Python or yaml.load(data, { schema: yaml.SAFE_SCHEMA }) in js-yaml.' },
95
+ check({ files }) {
96
+ const findings = [];
97
+ const jsPattern = /yaml\.load\s*\([^)]*(?!.*(?:safe|SAFE_SCHEMA|JSON_SCHEMA|FAILSAFE_SCHEMA))/;
98
+ const pyPattern = /yaml\.load\s*\((?![^)]*(?:SafeLoader|safe_load|CSafeLoader))/;
99
+ for (const [path, content] of files) {
100
+ if (SKIP_PATH.test(path)) continue;
101
+ if (isJS(path)) {
102
+ findings.push(...scanLines(content, jsPattern, path, this));
103
+ } else if (isPy(path)) {
104
+ findings.push(...scanLines(content, pyPattern, path, this));
105
+ }
106
+ }
107
+ return findings;
108
+ },
109
+ },
110
+
111
+ // SEC-DES-004: node-serialize / serialize-javascript with untrusted data
112
+ {
113
+ id: 'SEC-DES-004',
114
+ category: 'security',
115
+ severity: 'critical',
116
+ confidence: 'definite',
117
+ title: 'node-serialize Unserialize with Untrusted Data (RCE)',
118
+ description:
119
+ 'node-serialize.unserialize() can execute arbitrary code via IIFE patterns in serialized data (CVE-2017-5941).',
120
+ fix: { suggestion: 'Remove node-serialize. Use JSON.parse() for structured data exchange.' },
121
+ check({ files }) {
122
+ const findings = [];
123
+ const pattern = /(?:serialize\.unserialize|unserialize)\s*\(\s*(?:req\.body|req\.query|data|payload|input|cookie)/;
124
+ for (const [path, content] of files) {
125
+ if (SKIP_PATH.test(path)) continue;
126
+ if (isJS(path)) {
127
+ findings.push(...scanLines(content, pattern, path, this));
128
+ }
129
+ }
130
+ return findings;
131
+ },
132
+ },
133
+
134
+ // SEC-DES-005: JSON.parse of large/untrusted input without size limit
135
+ {
136
+ id: 'SEC-DES-005',
137
+ category: 'security',
138
+ severity: 'medium',
139
+ confidence: 'likely',
140
+ title: 'JSON.parse on Untrusted Input Without Size Limit',
141
+ description:
142
+ 'Parsing large JSON payloads from user input without size limits can cause DoS via memory exhaustion or CPU-intensive parsing.',
143
+ fix: { suggestion: 'Limit request body size (e.g., express.json({ limit: "1mb" })) and validate input structure with a schema.' },
144
+ check({ files }) {
145
+ const findings = [];
146
+ const pattern = /JSON\.parse\s*\(\s*(?:req\.body|req\.query\.\w+|rawBody|body|payload)\b/;
147
+ for (const [path, content] of files) {
148
+ if (SKIP_PATH.test(path)) continue;
149
+ if (isJS(path)) {
150
+ findings.push(...scanLines(content, pattern, path, this));
151
+ }
152
+ }
153
+ return findings;
154
+ },
155
+ },
156
+
157
+ // SEC-DES-006: Python pickle with untrusted data
158
+ {
159
+ id: 'SEC-DES-006',
160
+ category: 'security',
161
+ severity: 'critical',
162
+ confidence: 'definite',
163
+ title: 'Python pickle.loads with Untrusted Data (RCE)',
164
+ description:
165
+ 'pickle.loads() can execute arbitrary Python code during deserialization. Never unpickle data from untrusted sources.',
166
+ fix: { suggestion: 'Use JSON, MessagePack, or Protocol Buffers instead of pickle for untrusted data.' },
167
+ check({ files }) {
168
+ const findings = [];
169
+ const pattern = /pickle\.(?:loads?|Unpickler)\s*\(/;
170
+ for (const [path, content] of files) {
171
+ if (SKIP_PATH.test(path)) continue;
172
+ if (isPy(path)) {
173
+ findings.push(...scanLines(content, pattern, path, this));
174
+ }
175
+ }
176
+ return findings;
177
+ },
178
+ },
179
+
180
+ // SEC-DES-007: vm/vm2 module with untrusted code
181
+ {
182
+ id: 'SEC-DES-007',
183
+ category: 'security',
184
+ severity: 'critical',
185
+ confidence: 'definite',
186
+ title: 'Node.js vm Module with Untrusted Code (Sandbox Escape)',
187
+ description:
188
+ 'The Node.js vm module does not provide a secure sandbox. Untrusted code can escape via prototype chain access. vm2 also has known escapes (CVE-2023-37466).',
189
+ fix: { suggestion: 'Use isolated-vm or run untrusted code in a separate process/container with seccomp/AppArmor.' },
190
+ check({ files }) {
191
+ const findings = [];
192
+ const pattern = /(?:vm\.runInNewContext|vm\.runInContext|vm\.runInThisContext|vm\.createContext|vm\.Script|new\s+VM\s*\()\s*\(/;
193
+ for (const [path, content] of files) {
194
+ if (SKIP_PATH.test(path)) continue;
195
+ if (isJS(path)) {
196
+ findings.push(...scanLines(content, pattern, path, this));
197
+ }
198
+ }
199
+ return findings;
200
+ },
201
+ },
202
+
203
+ // SEC-DES-008: XML external entity processing (XXE)
204
+ {
205
+ id: 'SEC-DES-008',
206
+ category: 'security',
207
+ severity: 'critical',
208
+ confidence: 'likely',
209
+ title: 'XML Parser Without XXE Protection',
210
+ description:
211
+ 'XML parsers that process external entities can be exploited to read local files, perform SSRF, or cause DoS. Disable external entity processing.',
212
+ fix: { suggestion: 'Disable external entities: use libxmljs with noent: false, or xml2js (safe by default). For fast-xml-parser, set processEntities: false.' },
213
+ check({ files }) {
214
+ const findings = [];
215
+ const pattern = /(?:libxmljs\.parseXml|DOMParser|xml2js\.parseString|parseXml|XMLParser)\s*\(/;
216
+ for (const [path, content] of files) {
217
+ if (SKIP_PATH.test(path)) continue;
218
+ if (!isJS(path)) continue;
219
+ const lines = content.split('\n');
220
+ for (let i = 0; i < lines.length; i++) {
221
+ if (COMMENT_LINE.test(lines[i])) continue;
222
+ if (pattern.test(lines[i])) {
223
+ // Check if noent is explicitly set or entities are disabled
224
+ if (/noent\s*:\s*true|processEntities\s*:\s*true|resolveExternals/.test(content)) {
225
+ findings.push({
226
+ ruleId: this.id,
227
+ category: this.category,
228
+ severity: this.severity,
229
+ title: this.title,
230
+ description: this.description,
231
+ confidence: this.confidence,
232
+ file: path,
233
+ line: i + 1,
234
+ fix: this.fix || null,
235
+ });
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return findings;
241
+ },
242
+ },
243
+
244
+ // SEC-DES-009: MessagePack / BSON deserialization of untrusted data
245
+ {
246
+ id: 'SEC-DES-009',
247
+ category: 'security',
248
+ severity: 'medium',
249
+ confidence: 'likely',
250
+ title: 'Binary Format Deserialization of Untrusted Input',
251
+ description:
252
+ 'Deserializing binary formats (MessagePack, BSON, protobuf) from untrusted input without schema validation can lead to type confusion or memory issues.',
253
+ fix: { suggestion: 'Validate deserialized data against a strict schema using Zod, Joi, or Protocol Buffers with defined .proto files.' },
254
+ check({ files }) {
255
+ const findings = [];
256
+ const pattern = /(?:msgpack\.decode|msgpack\.unpack|BSON\.deserialize|bson\.deserialize)\s*\(\s*(?:req\.body|buffer|data|payload|input)\b/;
257
+ for (const [path, content] of files) {
258
+ if (SKIP_PATH.test(path)) continue;
259
+ if (isJS(path)) {
260
+ findings.push(...scanLines(content, pattern, path, this));
261
+ }
262
+ }
263
+ return findings;
264
+ },
265
+ },
266
+
267
+ // SEC-DES-010: Cookie deserialization without signature verification
268
+ {
269
+ id: 'SEC-DES-010',
270
+ category: 'security',
271
+ severity: 'high',
272
+ confidence: 'likely',
273
+ title: 'Cookie Value Deserialized Without Signature Verification',
274
+ description:
275
+ 'Deserializing cookie values (JSON.parse of cookie, unserialize of cookie) without verifying a cryptographic signature (HMAC) allows cookie tampering and injection.',
276
+ fix: { suggestion: 'Use signed cookies (cookie-signature or express signed cookies) and verify the HMAC before parsing.' },
277
+ check({ files }) {
278
+ const findings = [];
279
+ const pattern = /JSON\.parse\s*\(\s*(?:req\.cookies|cookie|cookies)\b/;
280
+ for (const [path, content] of files) {
281
+ if (SKIP_PATH.test(path)) continue;
282
+ if (isJS(path)) {
283
+ findings.push(...scanLines(content, pattern, path, this));
284
+ }
285
+ }
286
+ return findings;
287
+ },
288
+ },
289
+ ];
290
+
291
+ export default rules;
@@ -0,0 +1,187 @@
1
+ const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
2
+ const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
3
+ const isTest = (f) => f.includes('test') || f.includes('spec') || f.includes('mock');
4
+
5
+ const rules = [
6
+ // SEC-UPL-001
7
+ {
8
+ id: 'SEC-UPL-001', category: 'security', severity: 'high', confidence: 'likely',
9
+ title: 'No file type validation on upload',
10
+ check({ files }) {
11
+ const findings = [];
12
+ for (const [fp, content] of files) {
13
+ if (!isJS(fp) || isTest(fp)) continue;
14
+ if (content.includes('multer') || content.includes('formidable') || content.includes('busboy')) {
15
+ if (!content.includes('fileFilter') && !content.includes('mimetype') && !content.includes('mimeType') &&
16
+ !content.includes('allowedTypes') && !content.includes('accept')) {
17
+ findings.push({
18
+ ruleId: 'SEC-UPL-001', category: 'security', severity: 'high',
19
+ title: 'File uploads without MIME type or extension validation',
20
+ description: 'Validate file types to prevent malicious file uploads.',
21
+ file: fp, fix: null,
22
+ });
23
+ }
24
+ }
25
+ }
26
+ return findings;
27
+ },
28
+ },
29
+
30
+ // SEC-UPL-002
31
+ {
32
+ id: 'SEC-UPL-002', category: 'security', severity: 'high', confidence: 'likely',
33
+ title: 'No file size limit on upload',
34
+ check({ files }) {
35
+ const findings = [];
36
+ for (const [fp, content] of files) {
37
+ if (!isJS(fp) || isTest(fp)) continue;
38
+ if (content.includes('multer') || content.includes('formidable') || content.includes('busboy')) {
39
+ if (!content.includes('limits') && !content.includes('maxFileSize') && !content.includes('fileSize') && !content.includes('maxSize')) {
40
+ findings.push({
41
+ ruleId: 'SEC-UPL-002', category: 'security', severity: 'high',
42
+ title: 'File uploads without size limit — DoS via large file uploads',
43
+ file: fp, fix: null,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ return findings;
49
+ },
50
+ },
51
+
52
+ // SEC-UPL-003
53
+ {
54
+ id: 'SEC-UPL-003', category: 'security', severity: 'high', confidence: 'likely',
55
+ title: 'Files stored in web-accessible directory',
56
+ check({ files }) {
57
+ const findings = [];
58
+ for (const [fp, content] of files) {
59
+ if (!isJS(fp) || isTest(fp)) continue;
60
+ if (content.match(/destination\s*:\s*['"](?:\.\/)?(?:public|static|uploads)(?:\/|['"])/)) {
61
+ findings.push({
62
+ ruleId: 'SEC-UPL-003', category: 'security', severity: 'high',
63
+ title: 'Uploaded files stored in publicly accessible directory',
64
+ description: 'Store uploads outside webroot and serve through a controller with auth.',
65
+ file: fp, fix: null,
66
+ });
67
+ }
68
+ }
69
+ return findings;
70
+ },
71
+ },
72
+
73
+ // SEC-UPL-004
74
+ {
75
+ id: 'SEC-UPL-004', category: 'security', severity: 'medium', confidence: 'likely',
76
+ title: 'Original filename used for storage',
77
+ check({ files }) {
78
+ const findings = [];
79
+ for (const [fp, content] of files) {
80
+ if (!isJS(fp) || isTest(fp)) continue;
81
+ if (content.match(/(?:file|req\.file)\.originalname/) || content.match(/(?:file|req\.file)\.name/)) {
82
+ if (!content.includes('sanitize') && !content.includes('uuid') && !content.includes('crypto.random')) {
83
+ findings.push({
84
+ ruleId: 'SEC-UPL-004', category: 'security', severity: 'medium',
85
+ title: 'Using user-provided filename for storage — path traversal risk',
86
+ description: 'Generate a random filename (UUID) instead of using the original.',
87
+ file: fp, fix: null,
88
+ });
89
+ }
90
+ }
91
+ }
92
+ return findings;
93
+ },
94
+ },
95
+
96
+ // SEC-UPL-005
97
+ {
98
+ id: 'SEC-UPL-005', category: 'security', severity: 'critical', confidence: 'definite',
99
+ title: 'Executable file upload allowed',
100
+ check({ files }) {
101
+ const findings = [];
102
+ for (const [fp, content] of files) {
103
+ if (!isJS(fp) || isTest(fp)) continue;
104
+ if (content.includes('multer') || content.includes('upload')) {
105
+ // If there's a fileFilter, check if it blocks executables
106
+ if (content.includes('fileFilter')) {
107
+ if (!content.match(/(?:exe|sh|bat|cmd|php|jsp|asp|py|rb|pl)\b/i)) {
108
+ // File filter exists but doesn't seem to block executables
109
+ }
110
+ }
111
+ // Check for accept-all patterns
112
+ if (content.match(/fileFilter.*return\s+cb\s*\(\s*null\s*,\s*true\s*\)/)) {
113
+ findings.push({
114
+ ruleId: 'SEC-UPL-005', category: 'security', severity: 'critical',
115
+ title: 'File upload accepts all file types including executables',
116
+ file: fp, fix: null,
117
+ });
118
+ }
119
+ }
120
+ }
121
+ return findings;
122
+ },
123
+ },
124
+
125
+ // SEC-UPL-006
126
+ {
127
+ id: 'SEC-UPL-006', category: 'security', severity: 'medium', confidence: 'likely',
128
+ title: 'No antivirus scanning on uploads',
129
+ check({ files, stack }) {
130
+ const findings = [];
131
+ const hasUpload = [...files.values()].some(c =>
132
+ c.includes('multer') || c.includes('formidable') || c.includes('busboy')
133
+ );
134
+ if (!hasUpload) return findings;
135
+
136
+ const hasAV = Object.keys(stack.dependencies || {}).some(d =>
137
+ ['clamav', 'clamscan', 'node-clam', 'virus-scan'].includes(d)
138
+ );
139
+ const hasAVCode = [...files.values()].some(c =>
140
+ c.includes('clamav') || c.includes('clamscan') || c.includes('virusScan')
141
+ );
142
+ // If an explicit MIME type allowlist check is present in the upload handler, skip AV warning
143
+ // Detect: content has MIME type strings + an .includes(mimetype) check (may span lines)
144
+ const hasMimeAllowlist = [...files.values()].some(c =>
145
+ (c.includes('multer') || c.includes('formidable') || c.includes('busboy')) &&
146
+ /['"]\w+\/[\w.+-]+['"]/.test(c) &&
147
+ /\.includes\s*\(\s*(?:file|req\.file|f)\.mimetype|mimetype\s*===\s*['"]/.test(c)
148
+ );
149
+ if (!hasAV && !hasAVCode && !hasMimeAllowlist) {
150
+ findings.push({
151
+ ruleId: 'SEC-UPL-006', category: 'security', severity: 'medium',
152
+ title: 'Uploaded files not scanned for malware — consider ClamAV integration',
153
+ fix: null,
154
+ });
155
+ }
156
+ return findings;
157
+ },
158
+ },
159
+
160
+ // SEC-UPL-007
161
+ {
162
+ id: 'SEC-UPL-007', category: 'security', severity: 'medium', confidence: 'likely',
163
+ title: 'Image upload without re-encoding',
164
+ check({ files, stack }) {
165
+ const findings = [];
166
+ const hasImageUpload = [...files.values()].some(c =>
167
+ c.match(/(?:image|img|photo|avatar|thumbnail).*(?:upload|multer)/i)
168
+ );
169
+ if (!hasImageUpload) return findings;
170
+
171
+ const hasImageProcessing = Object.keys(stack.dependencies || {}).some(d =>
172
+ ['sharp', 'jimp', 'gm', 'imagemagick', 'canvas'].includes(d)
173
+ );
174
+ if (!hasImageProcessing) {
175
+ findings.push({
176
+ ruleId: 'SEC-UPL-007', category: 'security', severity: 'medium',
177
+ title: 'Image uploads served without re-encoding — may contain embedded scripts',
178
+ description: 'Use sharp or similar to re-encode images before serving.',
179
+ fix: null,
180
+ });
181
+ }
182
+ return findings;
183
+ },
184
+ },
185
+ ];
186
+
187
+ export default rules;