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.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Side Request Forgery (SSRF) Detection Rules (SEC-SSRF-001 through SEC-SSRF-010)
|
|
3
|
+
*
|
|
4
|
+
* Detects patterns where user-controlled input is used to construct
|
|
5
|
+
* URLs for server-side HTTP requests, potentially allowing access
|
|
6
|
+
* to internal services, cloud metadata, or localhost.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
10
|
+
const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
|
|
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-SSRF-001: fetch/axios/http.get with user-controlled URL
|
|
40
|
+
{
|
|
41
|
+
id: 'SEC-SSRF-001',
|
|
42
|
+
category: 'security',
|
|
43
|
+
severity: 'critical',
|
|
44
|
+
confidence: 'likely',
|
|
45
|
+
title: 'HTTP Request with User-Controlled URL (SSRF)',
|
|
46
|
+
description:
|
|
47
|
+
'Server-side HTTP request (fetch/axios/http) using user-controlled URL allows SSRF attacks to access internal services, cloud metadata endpoints, or localhost.',
|
|
48
|
+
fix: { suggestion: 'Validate URLs against an allowlist of domains. Block private IP ranges and metadata endpoints (169.254.169.254).' },
|
|
49
|
+
check({ files }) {
|
|
50
|
+
const findings = [];
|
|
51
|
+
const pattern = /(?:fetch|axios\.get|axios\.post|axios\.put|axios\.delete|axios\.request|axios\(|http\.get|http\.request|https\.get|https\.request|got\(|got\.get|request\(|needle\(|superagent\.get)\s*\(\s*(?:req\.body|req\.query|req\.params|userUrl|url|targetUrl|input)\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-SSRF-002: URL construction with template literal from user input
|
|
63
|
+
{
|
|
64
|
+
id: 'SEC-SSRF-002',
|
|
65
|
+
category: 'security',
|
|
66
|
+
severity: 'high',
|
|
67
|
+
confidence: 'likely',
|
|
68
|
+
title: 'URL Constructed from User Input via Template Literal',
|
|
69
|
+
description:
|
|
70
|
+
'Building request URLs with template literals containing user input enables SSRF. Attackers can inject full URLs or path traversal to reach internal endpoints.',
|
|
71
|
+
fix: { suggestion: 'Use URL constructor to parse and validate the resulting URL. Block private and reserved IP ranges.' },
|
|
72
|
+
check({ files }) {
|
|
73
|
+
const findings = [];
|
|
74
|
+
const pattern = /(?:fetch|axios|http\.get|https\.get|got|request|needle)\s*\(\s*`[^`]*\$\{(?:req\.(?:body|query|params)|input|url|host|domain|target)/;
|
|
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-SSRF-003: Cloud metadata endpoint access not blocked
|
|
86
|
+
{
|
|
87
|
+
id: 'SEC-SSRF-003',
|
|
88
|
+
category: 'security',
|
|
89
|
+
severity: 'critical',
|
|
90
|
+
confidence: 'definite',
|
|
91
|
+
title: 'Cloud Metadata Endpoint URL in Code',
|
|
92
|
+
description:
|
|
93
|
+
'Reference to cloud metadata endpoint (169.254.169.254) found. Ensure SSRF protections block access to this endpoint to prevent credential theft.',
|
|
94
|
+
fix: { suggestion: 'Block requests to 169.254.169.254 and fd00:ec2::254. Use IMDSv2 on AWS (requires token header).' },
|
|
95
|
+
check({ files }) {
|
|
96
|
+
const findings = [];
|
|
97
|
+
const pattern = /169\.254\.169\.254|metadata\.google\.internal|metadata\.azure\.com/;
|
|
98
|
+
for (const [path, content] of files) {
|
|
99
|
+
if (SKIP_PATH.test(path)) continue;
|
|
100
|
+
if (isJS(path)) {
|
|
101
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return findings;
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// SEC-SSRF-004: URL string concatenation with user input
|
|
109
|
+
{
|
|
110
|
+
id: 'SEC-SSRF-004',
|
|
111
|
+
category: 'security',
|
|
112
|
+
severity: 'high',
|
|
113
|
+
confidence: 'likely',
|
|
114
|
+
title: 'URL Built via String Concatenation with User Input',
|
|
115
|
+
description:
|
|
116
|
+
'HTTP request URL built by concatenating user input allows SSRF. An attacker can override the protocol, host, or path.',
|
|
117
|
+
fix: { suggestion: 'Use the URL constructor to safely build URLs and validate the resulting hostname against an allowlist.' },
|
|
118
|
+
check({ files }) {
|
|
119
|
+
const findings = [];
|
|
120
|
+
const pattern = /(?:fetch|axios|http\.get|https\.get|got|request)\s*\(\s*(?:['"][^'"]*['"]\s*\+\s*(?:req\.|input|url|host|target)|(?:req\.|input|url|host|target)\w*\s*\+)/;
|
|
121
|
+
for (const [path, content] of files) {
|
|
122
|
+
if (SKIP_PATH.test(path)) continue;
|
|
123
|
+
if (isJS(path)) {
|
|
124
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return findings;
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// SEC-SSRF-005: No URL validation before HTTP request
|
|
132
|
+
{
|
|
133
|
+
id: 'SEC-SSRF-005',
|
|
134
|
+
category: 'security',
|
|
135
|
+
severity: 'high',
|
|
136
|
+
confidence: 'likely',
|
|
137
|
+
title: 'HTTP Request with Dynamic URL Without Validation',
|
|
138
|
+
description:
|
|
139
|
+
'A variable URL is passed to an HTTP client without visible URL validation or allowlist check, creating SSRF risk.',
|
|
140
|
+
fix: { suggestion: 'Parse the URL with new URL(), validate the protocol (https only), and check the hostname against an allowlist.' },
|
|
141
|
+
check({ files }) {
|
|
142
|
+
const findings = [];
|
|
143
|
+
const pattern = /(?:fetch|axios|got|request|needle|superagent)\s*\(\s*(?:redirectUrl|callbackUrl|webhookUrl|proxyUrl|imageUrl|fileUrl|downloadUrl)\b/;
|
|
144
|
+
for (const [path, content] of files) {
|
|
145
|
+
if (SKIP_PATH.test(path)) continue;
|
|
146
|
+
if (isJS(path)) {
|
|
147
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// SEC-SSRF-006: DNS rebinding vulnerability (no IP validation after DNS resolution)
|
|
155
|
+
{
|
|
156
|
+
id: 'SEC-SSRF-006',
|
|
157
|
+
category: 'security',
|
|
158
|
+
severity: 'high',
|
|
159
|
+
confidence: 'likely',
|
|
160
|
+
title: 'DNS Lookup Without IP Validation (DNS Rebinding Risk)',
|
|
161
|
+
description:
|
|
162
|
+
'Using dns.lookup or dns.resolve without validating the resolved IP against blocked ranges enables DNS rebinding attacks to bypass SSRF protections.',
|
|
163
|
+
fix: { suggestion: 'After DNS resolution, verify the IP is not in private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16).' },
|
|
164
|
+
check({ files }) {
|
|
165
|
+
const findings = [];
|
|
166
|
+
const pattern = /dns\.(?:lookup|resolve|resolve4|resolve6)\s*\(/;
|
|
167
|
+
for (const [path, content] of files) {
|
|
168
|
+
if (SKIP_PATH.test(path)) continue;
|
|
169
|
+
if (!isJS(path)) continue;
|
|
170
|
+
if (pattern.test(content)) {
|
|
171
|
+
// Check if there is IP validation after DNS resolution
|
|
172
|
+
if (!/(?:isPrivate|isInternal|isReserved|blockList|ipRangeCheck|private.*ip|127\.|10\.|172\.16|192\.168)/.test(content)) {
|
|
173
|
+
findings.push({
|
|
174
|
+
ruleId: this.id,
|
|
175
|
+
category: this.category,
|
|
176
|
+
severity: this.severity,
|
|
177
|
+
title: this.title,
|
|
178
|
+
description: this.description,
|
|
179
|
+
confidence: this.confidence,
|
|
180
|
+
file: path,
|
|
181
|
+
fix: this.fix || null,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return findings;
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// SEC-SSRF-007: HTTP redirect following enabled
|
|
191
|
+
{
|
|
192
|
+
id: 'SEC-SSRF-007',
|
|
193
|
+
category: 'security',
|
|
194
|
+
severity: 'medium',
|
|
195
|
+
confidence: 'likely',
|
|
196
|
+
title: 'HTTP Client Follows Redirects (SSRF Bypass)',
|
|
197
|
+
description:
|
|
198
|
+
'HTTP clients that follow redirects can be tricked into accessing internal resources via an open redirect. An attacker-controlled server can redirect to 127.0.0.1.',
|
|
199
|
+
fix: { suggestion: 'Disable automatic redirect following (redirect: "manual" for fetch, maxRedirects: 0 for axios) and validate redirect targets.' },
|
|
200
|
+
check({ files }) {
|
|
201
|
+
const findings = [];
|
|
202
|
+
const pattern = /(?:follow|maxRedirects|followRedirect)\s*:\s*(?:true|[1-9]\d*)/;
|
|
203
|
+
for (const [path, content] of files) {
|
|
204
|
+
if (SKIP_PATH.test(path)) continue;
|
|
205
|
+
if (!isJS(path)) continue;
|
|
206
|
+
// Only flag if user-controlled URL is present in the same file
|
|
207
|
+
if (/(?:req\.body|req\.query|req\.params|userUrl|targetUrl|webhookUrl|callbackUrl)/.test(content)) {
|
|
208
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return findings;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// SEC-SSRF-008: Internal IP address in allowlist or bypass
|
|
216
|
+
{
|
|
217
|
+
id: 'SEC-SSRF-008',
|
|
218
|
+
category: 'security',
|
|
219
|
+
severity: 'high',
|
|
220
|
+
confidence: 'likely',
|
|
221
|
+
title: 'Localhost or Internal IP Allowed in URL Validation',
|
|
222
|
+
description:
|
|
223
|
+
'URL validation allows localhost, 127.0.0.1, or 0.0.0.0 which can be used for SSRF to access internal services.',
|
|
224
|
+
fix: { suggestion: 'Block all private and loopback addresses in URL validation: 127.0.0.0/8, 0.0.0.0, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16.' },
|
|
225
|
+
check({ files }) {
|
|
226
|
+
const findings = [];
|
|
227
|
+
const pattern = /(?:allowedHosts|whitelist|allowlist|trustedHosts|validHosts).*(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)/;
|
|
228
|
+
for (const [path, content] of files) {
|
|
229
|
+
if (SKIP_PATH.test(path)) continue;
|
|
230
|
+
if (isJS(path)) {
|
|
231
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return findings;
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
// SEC-SSRF-009: Image/file proxy without URL validation
|
|
239
|
+
{
|
|
240
|
+
id: 'SEC-SSRF-009',
|
|
241
|
+
category: 'security',
|
|
242
|
+
severity: 'high',
|
|
243
|
+
confidence: 'likely',
|
|
244
|
+
title: 'Image/File Proxy Endpoint Without URL Validation',
|
|
245
|
+
description:
|
|
246
|
+
'Proxy endpoints that fetch remote images or files based on user-provided URLs are common SSRF vectors.',
|
|
247
|
+
fix: { suggestion: 'Validate and restrict proxy target URLs to allowed domains and public IP ranges only.' },
|
|
248
|
+
check({ files }) {
|
|
249
|
+
const findings = [];
|
|
250
|
+
const routePattern = /(?:app|router)\.(?:get|post)\s*\(\s*['"].*(?:proxy|image|fetch|download|thumbnail|preview)/;
|
|
251
|
+
const fetchPattern = /(?:fetch|axios|got|request|http\.get|https\.get)\s*\(/;
|
|
252
|
+
for (const [path, content] of files) {
|
|
253
|
+
if (SKIP_PATH.test(path)) continue;
|
|
254
|
+
if (!isJS(path)) continue;
|
|
255
|
+
if (routePattern.test(content) && fetchPattern.test(content)) {
|
|
256
|
+
if (!/(?:allowlist|whitelist|allowedDomains|validateUrl|isAllowed|blockPrivate)/.test(content)) {
|
|
257
|
+
findings.push({
|
|
258
|
+
ruleId: this.id,
|
|
259
|
+
category: this.category,
|
|
260
|
+
severity: this.severity,
|
|
261
|
+
title: this.title,
|
|
262
|
+
description: this.description,
|
|
263
|
+
confidence: this.confidence,
|
|
264
|
+
file: path,
|
|
265
|
+
fix: this.fix || null,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return findings;
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// SEC-SSRF-010: Webhook URL not validated
|
|
275
|
+
{
|
|
276
|
+
id: 'SEC-SSRF-010',
|
|
277
|
+
category: 'security',
|
|
278
|
+
severity: 'high',
|
|
279
|
+
confidence: 'likely',
|
|
280
|
+
title: 'Webhook URL Sent Without Validation',
|
|
281
|
+
description:
|
|
282
|
+
'Webhook endpoints that accept user-provided callback URLs and make server-side requests to them are SSRF vectors.',
|
|
283
|
+
fix: { suggestion: 'Validate webhook URLs: require HTTPS, check against domain allowlist, block private IPs, and resolve DNS before making the request.' },
|
|
284
|
+
check({ files }) {
|
|
285
|
+
const findings = [];
|
|
286
|
+
const pattern = /(?:webhook|callback)(?:Url|URL|_url|Uri)\s*=\s*(?:req\.body|req\.query|req\.params)\b/;
|
|
287
|
+
for (const [path, content] of files) {
|
|
288
|
+
if (SKIP_PATH.test(path)) continue;
|
|
289
|
+
if (isJS(path)) {
|
|
290
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return findings;
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
export default rules;
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Supply Chain Security Rules (SEC-SCA-001 through SEC-SCA-010)
|
|
3
|
+
*
|
|
4
|
+
* Detects additional supply chain attack vectors including postinstall
|
|
5
|
+
* binary execution, dependency confusion, install hook abuse, and
|
|
6
|
+
* suspicious package patterns.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
10
|
+
const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
|
|
11
|
+
|
|
12
|
+
const SKIP_PATH = /[/\\](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-SCA-001: postinstall script executes binary/shell
|
|
40
|
+
{
|
|
41
|
+
id: 'SEC-SCA-001',
|
|
42
|
+
category: 'security',
|
|
43
|
+
severity: 'critical',
|
|
44
|
+
confidence: 'definite',
|
|
45
|
+
title: 'Postinstall Script Executes Binary or Shell Command',
|
|
46
|
+
description:
|
|
47
|
+
'A postinstall script that runs a binary, shell script, or node command is the primary attack vector in supply chain attacks (event-stream, node-ipc). Audit carefully.',
|
|
48
|
+
fix: { suggestion: 'Remove postinstall scripts that execute binaries. Use prepare for build steps and document manual setup steps.' },
|
|
49
|
+
check({ files }) {
|
|
50
|
+
const findings = [];
|
|
51
|
+
const pkg = files.get('package.json');
|
|
52
|
+
if (!pkg) return findings;
|
|
53
|
+
try {
|
|
54
|
+
const json = JSON.parse(pkg);
|
|
55
|
+
const scripts = json.scripts || {};
|
|
56
|
+
for (const hook of ['postinstall', 'preinstall', 'install']) {
|
|
57
|
+
const cmd = scripts[hook];
|
|
58
|
+
if (cmd && /(?:node\s|sh\s|bash\s|\.\/|\.exe|chmod|curl|wget|python|ruby)/.test(cmd)) {
|
|
59
|
+
findings.push({
|
|
60
|
+
ruleId: this.id,
|
|
61
|
+
category: this.category,
|
|
62
|
+
severity: this.severity,
|
|
63
|
+
title: `"${hook}" script runs binary/shell: "${cmd.substring(0, 80)}"`,
|
|
64
|
+
description: this.description,
|
|
65
|
+
confidence: this.confidence,
|
|
66
|
+
file: 'package.json',
|
|
67
|
+
fix: this.fix || null,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
return findings;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// SEC-SCA-002: Install script downloads and executes remote code
|
|
77
|
+
{
|
|
78
|
+
id: 'SEC-SCA-002',
|
|
79
|
+
category: 'security',
|
|
80
|
+
severity: 'critical',
|
|
81
|
+
confidence: 'definite',
|
|
82
|
+
title: 'Install Hook Downloads and Executes Remote Code',
|
|
83
|
+
description:
|
|
84
|
+
'An install lifecycle script that downloads and executes code from the internet is a critical supply chain risk.',
|
|
85
|
+
fix: { suggestion: 'Remove remote code execution from install hooks. Bundle required binaries or use optional dependencies.' },
|
|
86
|
+
check({ files }) {
|
|
87
|
+
const findings = [];
|
|
88
|
+
const pkg = files.get('package.json');
|
|
89
|
+
if (!pkg) return findings;
|
|
90
|
+
try {
|
|
91
|
+
const json = JSON.parse(pkg);
|
|
92
|
+
const scripts = json.scripts || {};
|
|
93
|
+
for (const hook of ['postinstall', 'preinstall', 'install', 'prepare']) {
|
|
94
|
+
const cmd = scripts[hook];
|
|
95
|
+
if (cmd && /(?:curl|wget|fetch|http).*(?:\|\s*(?:bash|sh|node)|>\s*\w+\.(?:js|sh))/.test(cmd)) {
|
|
96
|
+
findings.push({
|
|
97
|
+
ruleId: this.id,
|
|
98
|
+
category: this.category,
|
|
99
|
+
severity: this.severity,
|
|
100
|
+
title: `"${hook}" script downloads and executes remote code`,
|
|
101
|
+
description: this.description,
|
|
102
|
+
confidence: this.confidence,
|
|
103
|
+
file: 'package.json',
|
|
104
|
+
fix: this.fix || null,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
return findings;
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// SEC-SCA-003: Dependency version uses dist-tag instead of semver
|
|
114
|
+
{
|
|
115
|
+
id: 'SEC-SCA-003',
|
|
116
|
+
category: 'security',
|
|
117
|
+
severity: 'high',
|
|
118
|
+
confidence: 'definite',
|
|
119
|
+
title: 'Dependency Uses Dist-Tag Instead of Semver Range',
|
|
120
|
+
description:
|
|
121
|
+
'Using dist-tags like "latest", "next", or "canary" means the resolved version can change at any time, including to a compromised version.',
|
|
122
|
+
fix: { suggestion: 'Pin dependencies to specific semver ranges and use a lockfile for reproducible installs.' },
|
|
123
|
+
check({ files }) {
|
|
124
|
+
const findings = [];
|
|
125
|
+
const pkg = files.get('package.json');
|
|
126
|
+
if (!pkg) return findings;
|
|
127
|
+
try {
|
|
128
|
+
const json = JSON.parse(pkg);
|
|
129
|
+
const allDeps = { ...json.dependencies, ...json.devDependencies };
|
|
130
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
131
|
+
if (typeof version === 'string' && /^(latest|next|canary|beta|alpha|rc|dev|nightly|experimental|\*)$/.test(version)) {
|
|
132
|
+
findings.push({
|
|
133
|
+
ruleId: this.id,
|
|
134
|
+
category: this.category,
|
|
135
|
+
severity: this.severity,
|
|
136
|
+
title: `Package "${name}" pinned to dist-tag "${version}" — version can change unpredictably`,
|
|
137
|
+
description: this.description,
|
|
138
|
+
confidence: this.confidence,
|
|
139
|
+
file: 'package.json',
|
|
140
|
+
fix: this.fix || null,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {}
|
|
145
|
+
return findings;
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// SEC-SCA-004: Dependency with HTTP (non-HTTPS) registry URL
|
|
150
|
+
{
|
|
151
|
+
id: 'SEC-SCA-004',
|
|
152
|
+
category: 'security',
|
|
153
|
+
severity: 'high',
|
|
154
|
+
confidence: 'definite',
|
|
155
|
+
title: 'Dependency from HTTP (Non-HTTPS) Registry',
|
|
156
|
+
description:
|
|
157
|
+
'Using HTTP instead of HTTPS for package registries allows man-in-the-middle attacks to inject malicious packages.',
|
|
158
|
+
fix: { suggestion: 'Always use HTTPS URLs for package registries in .npmrc and package.json.' },
|
|
159
|
+
check({ files }) {
|
|
160
|
+
const findings = [];
|
|
161
|
+
for (const [fp, c] of files) {
|
|
162
|
+
if (!fp.endsWith('.npmrc') && !fp.endsWith('package.json') && !fp.endsWith('.yarnrc') && !fp.endsWith('.yarnrc.yml')) continue;
|
|
163
|
+
const lines = c.split('\n');
|
|
164
|
+
for (let i = 0; i < lines.length; i++) {
|
|
165
|
+
if (/registry\s*=?\s*['"]?http:\/\//.test(lines[i])) {
|
|
166
|
+
findings.push({
|
|
167
|
+
ruleId: this.id,
|
|
168
|
+
category: this.category,
|
|
169
|
+
severity: this.severity,
|
|
170
|
+
title: 'Package registry configured with HTTP instead of HTTPS',
|
|
171
|
+
description: this.description,
|
|
172
|
+
confidence: this.confidence,
|
|
173
|
+
file: fp,
|
|
174
|
+
line: i + 1,
|
|
175
|
+
fix: this.fix || null,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return findings;
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// SEC-SCA-005: exec/spawn in postinstall-referenced script
|
|
185
|
+
{
|
|
186
|
+
id: 'SEC-SCA-005',
|
|
187
|
+
category: 'security',
|
|
188
|
+
severity: 'high',
|
|
189
|
+
confidence: 'likely',
|
|
190
|
+
title: 'child_process Usage in Install Script File',
|
|
191
|
+
description:
|
|
192
|
+
'A file referenced by install hooks that uses child_process (exec, spawn, execSync) can execute arbitrary system commands during npm install.',
|
|
193
|
+
fix: { suggestion: 'Audit all files executed during install hooks. Avoid child_process in install-time scripts.' },
|
|
194
|
+
check({ files }) {
|
|
195
|
+
const findings = [];
|
|
196
|
+
const pkg = files.get('package.json');
|
|
197
|
+
if (!pkg) return findings;
|
|
198
|
+
try {
|
|
199
|
+
const json = JSON.parse(pkg);
|
|
200
|
+
const scripts = json.scripts || {};
|
|
201
|
+
const installScripts = ['postinstall', 'preinstall', 'install', 'prepare']
|
|
202
|
+
.map(h => scripts[h])
|
|
203
|
+
.filter(Boolean);
|
|
204
|
+
// Find referenced JS files
|
|
205
|
+
const referencedFiles = [];
|
|
206
|
+
for (const cmd of installScripts) {
|
|
207
|
+
const match = cmd.match(/node\s+(\S+\.js)/);
|
|
208
|
+
if (match) referencedFiles.push(match[1]);
|
|
209
|
+
}
|
|
210
|
+
for (const [fp, c] of files) {
|
|
211
|
+
if (!referencedFiles.some(ref => fp.endsWith(ref))) continue;
|
|
212
|
+
if (/(?:child_process|execSync|spawnSync|exec\s*\(|spawn\s*\()/.test(c)) {
|
|
213
|
+
findings.push({
|
|
214
|
+
ruleId: this.id,
|
|
215
|
+
category: this.category,
|
|
216
|
+
severity: this.severity,
|
|
217
|
+
title: `Install script "${fp}" uses child_process — can execute system commands`,
|
|
218
|
+
description: this.description,
|
|
219
|
+
confidence: this.confidence,
|
|
220
|
+
file: fp,
|
|
221
|
+
fix: this.fix || null,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch {}
|
|
226
|
+
return findings;
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// SEC-SCA-006: Dependency confusion — publishConfig missing scope registry
|
|
231
|
+
{
|
|
232
|
+
id: 'SEC-SCA-006',
|
|
233
|
+
category: 'security',
|
|
234
|
+
severity: 'high',
|
|
235
|
+
confidence: 'likely',
|
|
236
|
+
title: 'Scoped Package Without publishConfig Registry',
|
|
237
|
+
description:
|
|
238
|
+
'A scoped package without publishConfig.registry may accidentally publish to the public npm registry, leaking private code.',
|
|
239
|
+
fix: { suggestion: 'Add publishConfig.registry pointing to your private registry, or set "private": true.' },
|
|
240
|
+
check({ files }) {
|
|
241
|
+
const findings = [];
|
|
242
|
+
const pkg = files.get('package.json');
|
|
243
|
+
if (!pkg) return findings;
|
|
244
|
+
try {
|
|
245
|
+
const json = JSON.parse(pkg);
|
|
246
|
+
if (json.name && json.name.startsWith('@') && !json.private) {
|
|
247
|
+
if (!json.publishConfig || !json.publishConfig.registry) {
|
|
248
|
+
findings.push({
|
|
249
|
+
ruleId: this.id,
|
|
250
|
+
category: this.category,
|
|
251
|
+
severity: this.severity,
|
|
252
|
+
title: `Scoped package "${json.name}" without publishConfig.registry`,
|
|
253
|
+
description: this.description,
|
|
254
|
+
confidence: this.confidence,
|
|
255
|
+
file: 'package.json',
|
|
256
|
+
fix: this.fix || null,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch {}
|
|
261
|
+
return findings;
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// SEC-SCA-007: Package with too many transitive dependencies
|
|
266
|
+
{
|
|
267
|
+
id: 'SEC-SCA-007',
|
|
268
|
+
category: 'security',
|
|
269
|
+
severity: 'medium',
|
|
270
|
+
confidence: 'likely',
|
|
271
|
+
title: 'Excessive Dependency Count Increases Attack Surface',
|
|
272
|
+
description:
|
|
273
|
+
'Projects with a very large number of direct dependencies have a larger supply chain attack surface. Each dependency is a potential compromise vector.',
|
|
274
|
+
fix: { suggestion: 'Audit and reduce dependencies. Replace large utility packages with native implementations where possible.' },
|
|
275
|
+
check({ files }) {
|
|
276
|
+
const findings = [];
|
|
277
|
+
const pkg = files.get('package.json');
|
|
278
|
+
if (!pkg) return findings;
|
|
279
|
+
try {
|
|
280
|
+
const json = JSON.parse(pkg);
|
|
281
|
+
const depCount = Object.keys(json.dependencies || {}).length;
|
|
282
|
+
if (depCount > 50) {
|
|
283
|
+
findings.push({
|
|
284
|
+
ruleId: this.id,
|
|
285
|
+
category: this.category,
|
|
286
|
+
severity: this.severity,
|
|
287
|
+
title: `${depCount} direct dependencies — large attack surface`,
|
|
288
|
+
description: this.description,
|
|
289
|
+
confidence: this.confidence,
|
|
290
|
+
file: 'package.json',
|
|
291
|
+
fix: this.fix || null,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
} catch {}
|
|
295
|
+
return findings;
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
// SEC-SCA-008: Dynamic require/import with variable
|
|
300
|
+
{
|
|
301
|
+
id: 'SEC-SCA-008',
|
|
302
|
+
category: 'security',
|
|
303
|
+
severity: 'high',
|
|
304
|
+
confidence: 'likely',
|
|
305
|
+
title: 'Dynamic require/import with Variable Path',
|
|
306
|
+
description:
|
|
307
|
+
'Dynamic require() or import() with a variable path can load arbitrary modules, enabling code injection if the path is user-controlled.',
|
|
308
|
+
fix: { suggestion: 'Use a static allowlist of module names and map user input to the allowlist.' },
|
|
309
|
+
check({ files }) {
|
|
310
|
+
const findings = [];
|
|
311
|
+
const pattern = /(?:require|import)\s*\(\s*(?:req\.body|req\.query|req\.params|moduleName|pluginName|input|userModule)\b/;
|
|
312
|
+
for (const [path, content] of files) {
|
|
313
|
+
if (SKIP_PATH.test(path)) continue;
|
|
314
|
+
if (isJS(path)) {
|
|
315
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return findings;
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
// SEC-SCA-009: Overridden package resolution (resolutions/overrides)
|
|
323
|
+
{
|
|
324
|
+
id: 'SEC-SCA-009',
|
|
325
|
+
category: 'security',
|
|
326
|
+
severity: 'medium',
|
|
327
|
+
confidence: 'likely',
|
|
328
|
+
title: 'Package Resolution Override May Mask Vulnerabilities',
|
|
329
|
+
description:
|
|
330
|
+
'Using npm overrides or yarn resolutions to force package versions can mask known vulnerabilities or introduce incompatible versions.',
|
|
331
|
+
fix: { suggestion: 'Document why each override exists and periodically review if they are still needed.' },
|
|
332
|
+
check({ files }) {
|
|
333
|
+
const findings = [];
|
|
334
|
+
const pkg = files.get('package.json');
|
|
335
|
+
if (!pkg) return findings;
|
|
336
|
+
try {
|
|
337
|
+
const json = JSON.parse(pkg);
|
|
338
|
+
if (json.overrides || json.resolutions) {
|
|
339
|
+
const overrideCount = Object.keys(json.overrides || json.resolutions || {}).length;
|
|
340
|
+
if (overrideCount > 0) {
|
|
341
|
+
findings.push({
|
|
342
|
+
ruleId: this.id,
|
|
343
|
+
category: this.category,
|
|
344
|
+
severity: this.severity,
|
|
345
|
+
title: `${overrideCount} package resolution override(s) found — may mask vulnerabilities`,
|
|
346
|
+
description: this.description,
|
|
347
|
+
confidence: this.confidence,
|
|
348
|
+
file: 'package.json',
|
|
349
|
+
fix: this.fix || null,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch {}
|
|
354
|
+
return findings;
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
// SEC-SCA-010: Bundled dependencies with no integrity check
|
|
359
|
+
{
|
|
360
|
+
id: 'SEC-SCA-010',
|
|
361
|
+
category: 'security',
|
|
362
|
+
severity: 'medium',
|
|
363
|
+
confidence: 'likely',
|
|
364
|
+
title: 'Bundled Dependencies Without Integrity Verification',
|
|
365
|
+
description:
|
|
366
|
+
'bundledDependencies or bundleDependencies include packages in the published tarball. These bypass registry integrity checks and can be tampered with.',
|
|
367
|
+
fix: { suggestion: 'Prefer normal dependencies over bundled ones. If bundled, verify their integrity in CI.' },
|
|
368
|
+
check({ files }) {
|
|
369
|
+
const findings = [];
|
|
370
|
+
const pkg = files.get('package.json');
|
|
371
|
+
if (!pkg) return findings;
|
|
372
|
+
try {
|
|
373
|
+
const json = JSON.parse(pkg);
|
|
374
|
+
const bundled = json.bundledDependencies || json.bundleDependencies;
|
|
375
|
+
if (bundled && Array.isArray(bundled) && bundled.length > 0) {
|
|
376
|
+
findings.push({
|
|
377
|
+
ruleId: this.id,
|
|
378
|
+
category: this.category,
|
|
379
|
+
severity: this.severity,
|
|
380
|
+
title: `${bundled.length} bundled dependencies bypass registry integrity checks`,
|
|
381
|
+
description: this.description,
|
|
382
|
+
confidence: this.confidence,
|
|
383
|
+
file: 'package.json',
|
|
384
|
+
fix: this.fix || null,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
} catch {}
|
|
388
|
+
return findings;
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
export default rules;
|