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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taint analysis rules — wraps src/taint.js into the standard rule format.
|
|
3
|
+
*
|
|
4
|
+
* These rules detect INDIRECT injection paths: user input flows through one
|
|
5
|
+
* or more variable assignments before reaching a dangerous sink. Direct paths
|
|
6
|
+
* (e.g. exec(req.query.x)) are already caught by the pattern-matching rules.
|
|
7
|
+
*
|
|
8
|
+
* Rule IDs: TAINT-CMD-001, TAINT-SQL-001, TAINT-EVAL-001, TAINT-XSS-001,
|
|
9
|
+
* TAINT-PATH-001, TAINT-REDIR-001, TAINT-REQUIRE-001
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { runTaintAnalysis } from '../../taint.js';
|
|
13
|
+
|
|
14
|
+
const rules = [
|
|
15
|
+
{
|
|
16
|
+
id: 'TAINT-001',
|
|
17
|
+
category: 'security',
|
|
18
|
+
severity: 'critical',
|
|
19
|
+
confidence: 'likely',
|
|
20
|
+
title: 'Data Flow / Taint Analysis — Indirect Injection',
|
|
21
|
+
check({ files }) {
|
|
22
|
+
return runTaintAnalysis(files);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export default rules;
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
2
|
+
const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
|
|
3
|
+
|
|
4
|
+
function shouldSkip(filepath, line) {
|
|
5
|
+
if (/[/\\](test|mock|fixture|__test__|__mock__|__fixture__|\.test\.|\.spec\.|\.mock\.)/.test(filepath)) return true;
|
|
6
|
+
if (line.trimStart().startsWith('//') || line.trimStart().startsWith('*') || line.trimStart().startsWith('/*')) return true;
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const rules = [
|
|
11
|
+
// SEC-XSS-001: innerHTML/outerHTML/insertAdjacentHTML usage
|
|
12
|
+
{
|
|
13
|
+
id: 'SEC-XSS-001',
|
|
14
|
+
category: 'security',
|
|
15
|
+
severity: 'high',
|
|
16
|
+
confidence: 'likely',
|
|
17
|
+
title: 'innerHTML/outerHTML/insertAdjacentHTML usage (XSS risk)',
|
|
18
|
+
check({ files }) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
const pattern = /\.(innerHTML|outerHTML)\s*=|\.insertAdjacentHTML\s*\(/;
|
|
21
|
+
// Only flag when the RHS looks like a variable/expression, not a static string literal
|
|
22
|
+
const staticRhsPattern = /\.(innerHTML|outerHTML)\s*=\s*'[^']*'|\.innerHTML\s*=\s*"[^"]*"|\.innerHTML\s*=\s*`[^`$]*`/;
|
|
23
|
+
|
|
24
|
+
for (const [filepath, content] of files) {
|
|
25
|
+
if (!isJS(filepath)) continue;
|
|
26
|
+
const lines = content.split('\n');
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
if (shouldSkip(filepath, lines[i])) continue;
|
|
29
|
+
if (pattern.test(lines[i]) && !staticRhsPattern.test(lines[i])) {
|
|
30
|
+
findings.push({
|
|
31
|
+
ruleId: 'SEC-XSS-001',
|
|
32
|
+
category: 'security',
|
|
33
|
+
severity: 'high',
|
|
34
|
+
confidence: 'likely',
|
|
35
|
+
title: 'Direct DOM HTML injection via innerHTML/outerHTML/insertAdjacentHTML',
|
|
36
|
+
description: 'Assigning unsanitized content to innerHTML, outerHTML, or insertAdjacentHTML can lead to XSS. Use textContent or a sanitization library like DOMPurify.',
|
|
37
|
+
file: filepath,
|
|
38
|
+
line: i + 1,
|
|
39
|
+
fix: null,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return findings;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// SEC-XSS-002: React dangerouslySetInnerHTML
|
|
49
|
+
{
|
|
50
|
+
id: 'SEC-XSS-002',
|
|
51
|
+
category: 'security',
|
|
52
|
+
severity: 'high',
|
|
53
|
+
confidence: 'likely',
|
|
54
|
+
title: 'React dangerouslySetInnerHTML usage',
|
|
55
|
+
check({ files }) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
const pattern = /dangerouslySetInnerHTML\s*=\s*\{\s*\{/;
|
|
58
|
+
|
|
59
|
+
for (const [filepath, content] of files) {
|
|
60
|
+
if (!isJS(filepath)) continue;
|
|
61
|
+
const lines = content.split('\n');
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
if (shouldSkip(filepath, lines[i])) continue;
|
|
64
|
+
if ((pattern.test(lines[i]) || /dangerouslySetInnerHTML/.test(lines[i])) &&
|
|
65
|
+
!/DOMPurify\.sanitize|sanitizeHtml|xss\s*\(|escape\s*\(/.test(lines[i])) {
|
|
66
|
+
findings.push({
|
|
67
|
+
ruleId: 'SEC-XSS-002',
|
|
68
|
+
category: 'security',
|
|
69
|
+
severity: 'high',
|
|
70
|
+
confidence: 'likely',
|
|
71
|
+
title: 'dangerouslySetInnerHTML renders raw HTML — XSS risk',
|
|
72
|
+
description: 'dangerouslySetInnerHTML bypasses React\'s XSS protections. Sanitize the HTML with DOMPurify before rendering.',
|
|
73
|
+
file: filepath,
|
|
74
|
+
line: i + 1,
|
|
75
|
+
fix: null,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return findings;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// SEC-XSS-003: Vue v-html directive
|
|
85
|
+
{
|
|
86
|
+
id: 'SEC-XSS-003',
|
|
87
|
+
category: 'security',
|
|
88
|
+
severity: 'high',
|
|
89
|
+
confidence: 'likely',
|
|
90
|
+
title: 'Vue v-html directive usage',
|
|
91
|
+
check({ files }) {
|
|
92
|
+
const findings = [];
|
|
93
|
+
const pattern = /v-html\s*=/;
|
|
94
|
+
|
|
95
|
+
for (const [filepath, content] of files) {
|
|
96
|
+
if (!filepath.endsWith('.vue') && !isJS(filepath)) continue;
|
|
97
|
+
const lines = content.split('\n');
|
|
98
|
+
for (let i = 0; i < lines.length; i++) {
|
|
99
|
+
if (shouldSkip(filepath, lines[i])) continue;
|
|
100
|
+
if (pattern.test(lines[i])) {
|
|
101
|
+
findings.push({
|
|
102
|
+
ruleId: 'SEC-XSS-003',
|
|
103
|
+
category: 'security',
|
|
104
|
+
severity: 'high',
|
|
105
|
+
confidence: 'likely',
|
|
106
|
+
title: 'Vue v-html renders raw HTML — XSS risk',
|
|
107
|
+
description: 'v-html renders unescaped HTML and can lead to XSS. Use v-text or sanitize the content before rendering.',
|
|
108
|
+
file: filepath,
|
|
109
|
+
line: i + 1,
|
|
110
|
+
fix: null,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return findings;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// SEC-XSS-004: Angular bypassSecurityTrust*
|
|
120
|
+
{
|
|
121
|
+
id: 'SEC-XSS-004',
|
|
122
|
+
category: 'security',
|
|
123
|
+
severity: 'high',
|
|
124
|
+
confidence: 'likely',
|
|
125
|
+
title: 'Angular bypassSecurityTrust* usage',
|
|
126
|
+
check({ files }) {
|
|
127
|
+
const findings = [];
|
|
128
|
+
const pattern = /bypassSecurityTrust(Html|Script|Style|Url|ResourceUrl)\s*\(/;
|
|
129
|
+
|
|
130
|
+
for (const [filepath, content] of files) {
|
|
131
|
+
if (!isJS(filepath)) continue;
|
|
132
|
+
const lines = content.split('\n');
|
|
133
|
+
for (let i = 0; i < lines.length; i++) {
|
|
134
|
+
if (shouldSkip(filepath, lines[i])) continue;
|
|
135
|
+
if (pattern.test(lines[i])) {
|
|
136
|
+
findings.push({
|
|
137
|
+
ruleId: 'SEC-XSS-004',
|
|
138
|
+
category: 'security',
|
|
139
|
+
severity: 'high',
|
|
140
|
+
confidence: 'likely',
|
|
141
|
+
title: 'Angular security bypass via bypassSecurityTrust*',
|
|
142
|
+
description: 'bypassSecurityTrust* disables Angular\'s built-in sanitization. Ensure the input is trusted and already sanitized.',
|
|
143
|
+
file: filepath,
|
|
144
|
+
line: i + 1,
|
|
145
|
+
fix: null,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// SEC-XSS-005: Unescaped template output
|
|
155
|
+
{
|
|
156
|
+
id: 'SEC-XSS-005',
|
|
157
|
+
category: 'security',
|
|
158
|
+
severity: 'high',
|
|
159
|
+
confidence: 'likely',
|
|
160
|
+
title: 'Unescaped template output',
|
|
161
|
+
check({ files }) {
|
|
162
|
+
const findings = [];
|
|
163
|
+
const patterns = [
|
|
164
|
+
{ regex: /<%-.+?%>/, name: 'EJS unescaped output (<%-)', },
|
|
165
|
+
{ regex: /\{!!.+?!!\}/, name: 'Blade unescaped output ({!! !!})' },
|
|
166
|
+
{ regex: /\|\s*safe\b/, name: 'Jinja/Nunjucks |safe filter' },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
for (const [filepath, content] of files) {
|
|
170
|
+
const lines = content.split('\n');
|
|
171
|
+
for (let i = 0; i < lines.length; i++) {
|
|
172
|
+
if (shouldSkip(filepath, lines[i])) continue;
|
|
173
|
+
for (const { regex, name } of patterns) {
|
|
174
|
+
if (regex.test(lines[i])) {
|
|
175
|
+
findings.push({
|
|
176
|
+
ruleId: 'SEC-XSS-005',
|
|
177
|
+
category: 'security',
|
|
178
|
+
severity: 'high',
|
|
179
|
+
confidence: 'likely',
|
|
180
|
+
title: `Unescaped template output: ${name}`,
|
|
181
|
+
description: 'Unescaped template output renders raw HTML and can lead to XSS. Use escaped output or sanitize the data.',
|
|
182
|
+
file: filepath,
|
|
183
|
+
line: i + 1,
|
|
184
|
+
fix: null,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return findings;
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// SEC-XSS-006: document.write/document.writeln
|
|
195
|
+
{
|
|
196
|
+
id: 'SEC-XSS-006',
|
|
197
|
+
category: 'security',
|
|
198
|
+
severity: 'high',
|
|
199
|
+
confidence: 'likely',
|
|
200
|
+
title: 'document.write/document.writeln usage',
|
|
201
|
+
check({ files }) {
|
|
202
|
+
const findings = [];
|
|
203
|
+
const pattern = /document\.write(ln)?\s*\(/;
|
|
204
|
+
|
|
205
|
+
for (const [filepath, content] of files) {
|
|
206
|
+
if (!isJS(filepath)) continue;
|
|
207
|
+
const lines = content.split('\n');
|
|
208
|
+
for (let i = 0; i < lines.length; i++) {
|
|
209
|
+
if (shouldSkip(filepath, lines[i])) continue;
|
|
210
|
+
if (pattern.test(lines[i])) {
|
|
211
|
+
findings.push({
|
|
212
|
+
ruleId: 'SEC-XSS-006',
|
|
213
|
+
category: 'security',
|
|
214
|
+
severity: 'high',
|
|
215
|
+
confidence: 'likely',
|
|
216
|
+
title: 'document.write/writeln used — XSS risk',
|
|
217
|
+
description: 'document.write injects raw HTML into the page and is a known XSS vector. Use DOM APIs like createElement instead.',
|
|
218
|
+
file: filepath,
|
|
219
|
+
line: i + 1,
|
|
220
|
+
fix: null,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return findings;
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// SEC-XSS-007: javascript: in href/src attributes
|
|
230
|
+
{
|
|
231
|
+
id: 'SEC-XSS-007',
|
|
232
|
+
category: 'security',
|
|
233
|
+
severity: 'critical',
|
|
234
|
+
confidence: 'definite',
|
|
235
|
+
title: 'javascript: protocol in href/src attributes',
|
|
236
|
+
check({ files }) {
|
|
237
|
+
const findings = [];
|
|
238
|
+
// Exclude javascript:void(0) and javascript:; which are legitimate placeholders
|
|
239
|
+
const pattern = /(?:href|src|action)\s*=\s*['"`]?\s*javascript\s*:(?!void\s*\(0\)|;|void\(0\))/i;
|
|
240
|
+
|
|
241
|
+
for (const [filepath, content] of files) {
|
|
242
|
+
if (!isJS(filepath) && !filepath.endsWith('.html') && !filepath.endsWith('.vue')) continue;
|
|
243
|
+
const lines = content.split('\n');
|
|
244
|
+
for (let i = 0; i < lines.length; i++) {
|
|
245
|
+
if (shouldSkip(filepath, lines[i])) continue;
|
|
246
|
+
if (pattern.test(lines[i])) {
|
|
247
|
+
findings.push({
|
|
248
|
+
ruleId: 'SEC-XSS-007',
|
|
249
|
+
category: 'security',
|
|
250
|
+
severity: 'critical',
|
|
251
|
+
confidence: 'definite',
|
|
252
|
+
title: 'javascript: protocol in href/src — XSS risk',
|
|
253
|
+
description: 'Using javascript: in href or src attributes allows script execution. Validate and sanitize all URLs.',
|
|
254
|
+
file: filepath,
|
|
255
|
+
line: i + 1,
|
|
256
|
+
fix: null,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return findings;
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// SEC-XSS-008: Database content rendered without escaping
|
|
266
|
+
{
|
|
267
|
+
id: 'SEC-XSS-008',
|
|
268
|
+
category: 'security',
|
|
269
|
+
severity: 'medium',
|
|
270
|
+
confidence: 'likely',
|
|
271
|
+
title: 'Database content rendered without escaping',
|
|
272
|
+
check({ files }) {
|
|
273
|
+
const findings = [];
|
|
274
|
+
const pattern = /\{\s*(?:data|record|row|result|item|entry|doc|document|user|post|comment)\.(?:html|content|body|description|bio|message|text|markup|template|richText|htmlContent)\s*\}/;
|
|
275
|
+
|
|
276
|
+
for (const [filepath, content] of files) {
|
|
277
|
+
if (!isJS(filepath) && !filepath.endsWith('.vue')) continue;
|
|
278
|
+
if (shouldSkip(filepath, '')) continue;
|
|
279
|
+
// JSX files (.jsx/.tsx) auto-escape {} expressions — React handles XSS protection
|
|
280
|
+
if (filepath.endsWith('.jsx') || filepath.endsWith('.tsx')) continue;
|
|
281
|
+
const lines = content.split('\n');
|
|
282
|
+
for (let i = 0; i < lines.length; i++) {
|
|
283
|
+
if (lines[i].trimStart().startsWith('//') || lines[i].trimStart().startsWith('*')) continue;
|
|
284
|
+
if (pattern.test(lines[i])) {
|
|
285
|
+
// Skip if it's already inside dangerouslySetInnerHTML (caught by SEC-XSS-002)
|
|
286
|
+
if (/dangerouslySetInnerHTML/.test(lines[i])) continue;
|
|
287
|
+
findings.push({
|
|
288
|
+
ruleId: 'SEC-XSS-008',
|
|
289
|
+
category: 'security',
|
|
290
|
+
severity: 'medium',
|
|
291
|
+
confidence: 'likely',
|
|
292
|
+
title: 'Database content rendered without explicit escaping',
|
|
293
|
+
description: 'Rendering database-sourced HTML content directly in templates may lead to stored XSS. Sanitize with DOMPurify or similar.',
|
|
294
|
+
file: filepath,
|
|
295
|
+
line: i + 1,
|
|
296
|
+
fix: null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return findings;
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
// SEC-XSS-009: SVG file upload without sanitization
|
|
306
|
+
{
|
|
307
|
+
id: 'SEC-XSS-009',
|
|
308
|
+
category: 'security',
|
|
309
|
+
severity: 'medium',
|
|
310
|
+
confidence: 'likely',
|
|
311
|
+
title: 'SVG file upload without sanitization',
|
|
312
|
+
check({ files }) {
|
|
313
|
+
const findings = [];
|
|
314
|
+
const uploadPattern = /(?:upload|multer|formidable|busboy|multipart)/i;
|
|
315
|
+
const svgAllowPattern = /(?:image\/svg|\.svg|svg\+xml|'svg'|"svg")/i;
|
|
316
|
+
|
|
317
|
+
for (const [filepath, content] of files) {
|
|
318
|
+
if (!isJS(filepath)) continue;
|
|
319
|
+
if (shouldSkip(filepath, '')) continue;
|
|
320
|
+
if (!uploadPattern.test(content)) continue;
|
|
321
|
+
if (!svgAllowPattern.test(content)) continue;
|
|
322
|
+
|
|
323
|
+
// Check if there is any SVG sanitization
|
|
324
|
+
const hasSanitization = /(?:DOMPurify|sanitize-svg|svg-sanitizer|sanitizeSvg|sanitize\s*\(|clean\s*\()/i.test(content);
|
|
325
|
+
if (!hasSanitization) {
|
|
326
|
+
findings.push({
|
|
327
|
+
ruleId: 'SEC-XSS-009',
|
|
328
|
+
category: 'security',
|
|
329
|
+
severity: 'medium',
|
|
330
|
+
confidence: 'likely',
|
|
331
|
+
title: 'SVG upload accepted without sanitization',
|
|
332
|
+
description: 'SVG files can contain embedded JavaScript. Sanitize uploaded SVGs to strip script elements and event handlers.',
|
|
333
|
+
file: filepath,
|
|
334
|
+
fix: null,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return findings;
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// SEC-XSS-010: Missing Content-Security-Policy
|
|
343
|
+
{
|
|
344
|
+
id: 'SEC-XSS-010',
|
|
345
|
+
category: 'security',
|
|
346
|
+
severity: 'medium',
|
|
347
|
+
confidence: 'likely',
|
|
348
|
+
title: 'Missing Content-Security-Policy',
|
|
349
|
+
check({ files, stack }) {
|
|
350
|
+
const findings = [];
|
|
351
|
+
const deps = stack.dependencies || {};
|
|
352
|
+
|
|
353
|
+
// Check for helmet usage
|
|
354
|
+
const hasHelmet = 'helmet' in deps;
|
|
355
|
+
if (hasHelmet) return findings;
|
|
356
|
+
|
|
357
|
+
// Check for CSP header set manually in any file
|
|
358
|
+
let hasCsp = false;
|
|
359
|
+
for (const [filepath, content] of files) {
|
|
360
|
+
if (!isJS(filepath)) continue;
|
|
361
|
+
if (/Content-Security-Policy/i.test(content) || /\.contentSecurityPolicy\s*\(/.test(content) || /csp\s*[:=]/i.test(content)) {
|
|
362
|
+
hasCsp = true;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!hasCsp) {
|
|
368
|
+
// Only report if there are server-side files
|
|
369
|
+
const hasServer = [...files.keys()].some(f =>
|
|
370
|
+
isJS(f) && (/server|app|index|middleware|routes/i.test(f))
|
|
371
|
+
);
|
|
372
|
+
if (hasServer) {
|
|
373
|
+
findings.push({
|
|
374
|
+
ruleId: 'SEC-XSS-010',
|
|
375
|
+
category: 'security',
|
|
376
|
+
severity: 'medium',
|
|
377
|
+
confidence: 'likely',
|
|
378
|
+
title: 'No Content-Security-Policy header configured',
|
|
379
|
+
description: 'CSP is a critical defense against XSS attacks. Use the helmet package or set the Content-Security-Policy header manually.',
|
|
380
|
+
fix: null,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return findings;
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
// SEC-XSS-011: window.location/URL params used in DOM
|
|
389
|
+
{
|
|
390
|
+
id: 'SEC-XSS-011',
|
|
391
|
+
category: 'security',
|
|
392
|
+
severity: 'high',
|
|
393
|
+
confidence: 'likely',
|
|
394
|
+
title: 'URL parameters used in DOM manipulation',
|
|
395
|
+
check({ files }) {
|
|
396
|
+
const findings = [];
|
|
397
|
+
const sourcePattern = /(?:window\.location|location\.(?:search|hash|href)|URLSearchParams|url\.searchParams|document\.URL|document\.referrer|document\.documentURI)/;
|
|
398
|
+
const sinkPattern = /(?:innerHTML|outerHTML|document\.write|\.html\s*\(|insertAdjacentHTML|\$\s*\()/;
|
|
399
|
+
|
|
400
|
+
for (const [filepath, content] of files) {
|
|
401
|
+
if (!isJS(filepath)) continue;
|
|
402
|
+
if (shouldSkip(filepath, '')) continue;
|
|
403
|
+
const lines = content.split('\n');
|
|
404
|
+
for (let i = 0; i < lines.length; i++) {
|
|
405
|
+
if (lines[i].trimStart().startsWith('//') || lines[i].trimStart().startsWith('*')) continue;
|
|
406
|
+
if (sourcePattern.test(lines[i]) && sinkPattern.test(lines[i])) {
|
|
407
|
+
findings.push({
|
|
408
|
+
ruleId: 'SEC-XSS-011',
|
|
409
|
+
category: 'security',
|
|
410
|
+
severity: 'high',
|
|
411
|
+
confidence: 'likely',
|
|
412
|
+
title: 'URL/location parameters used directly in DOM sink',
|
|
413
|
+
description: 'User-controlled URL parameters written to innerHTML or similar DOM sinks enable DOM-based XSS. Sanitize all URL-derived data before DOM insertion.',
|
|
414
|
+
file: filepath,
|
|
415
|
+
line: i + 1,
|
|
416
|
+
fix: null,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Also detect across nearby lines (source on one line, sink within 5 lines)
|
|
422
|
+
const lines2 = content.split('\n');
|
|
423
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
424
|
+
if (lines2[i].trimStart().startsWith('//') || lines2[i].trimStart().startsWith('*')) continue;
|
|
425
|
+
if (sourcePattern.test(lines2[i])) {
|
|
426
|
+
for (let j = i + 1; j < Math.min(i + 6, lines2.length); j++) {
|
|
427
|
+
if (sinkPattern.test(lines2[j])) {
|
|
428
|
+
// Avoid duplicate if same line was already caught
|
|
429
|
+
if (j !== i) {
|
|
430
|
+
findings.push({
|
|
431
|
+
ruleId: 'SEC-XSS-011',
|
|
432
|
+
category: 'security',
|
|
433
|
+
severity: 'high',
|
|
434
|
+
confidence: 'likely',
|
|
435
|
+
title: 'URL/location parameters flow into DOM sink nearby',
|
|
436
|
+
description: 'User-controlled URL parameters flow into an unsafe DOM sink within a few lines. Sanitize or encode all URL-derived values.',
|
|
437
|
+
file: filepath,
|
|
438
|
+
line: i + 1,
|
|
439
|
+
fix: null,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return findings;
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
// SEC-XSS-012: postMessage without origin validation
|
|
453
|
+
{
|
|
454
|
+
id: 'SEC-XSS-012',
|
|
455
|
+
category: 'security',
|
|
456
|
+
severity: 'high',
|
|
457
|
+
confidence: 'likely',
|
|
458
|
+
title: 'postMessage without origin validation',
|
|
459
|
+
check({ files }) {
|
|
460
|
+
const findings = [];
|
|
461
|
+
const listenerPattern = /addEventListener\s*\(\s*['"]message['"]/;
|
|
462
|
+
const originCheckPattern = /(?:event|e|evt|msg)\.origin\s*[!=]==?\s*['"`]/;
|
|
463
|
+
|
|
464
|
+
for (const [filepath, content] of files) {
|
|
465
|
+
if (!isJS(filepath)) continue;
|
|
466
|
+
if (shouldSkip(filepath, '')) continue;
|
|
467
|
+
if (!listenerPattern.test(content)) continue;
|
|
468
|
+
|
|
469
|
+
const lines = content.split('\n');
|
|
470
|
+
for (let i = 0; i < lines.length; i++) {
|
|
471
|
+
if (lines[i].trimStart().startsWith('//') || lines[i].trimStart().startsWith('*')) continue;
|
|
472
|
+
if (listenerPattern.test(lines[i])) {
|
|
473
|
+
// Look ahead up to 15 lines for an origin check
|
|
474
|
+
let hasOriginCheck = false;
|
|
475
|
+
for (let j = i; j < Math.min(i + 16, lines.length); j++) {
|
|
476
|
+
if (originCheckPattern.test(lines[j]) || /\.origin/.test(lines[j])) {
|
|
477
|
+
hasOriginCheck = true;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (!hasOriginCheck) {
|
|
482
|
+
findings.push({
|
|
483
|
+
ruleId: 'SEC-XSS-012',
|
|
484
|
+
category: 'security',
|
|
485
|
+
severity: 'high',
|
|
486
|
+
confidence: 'likely',
|
|
487
|
+
title: 'postMessage listener without origin validation',
|
|
488
|
+
description: 'Always validate event.origin in message event handlers to prevent cross-origin attacks. Check origin against a trusted allowlist.',
|
|
489
|
+
file: filepath,
|
|
490
|
+
line: i + 1,
|
|
491
|
+
fix: null,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return findings;
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
// SEC-XSS-013: Server-side reflected XSS via res.send/res.write
|
|
501
|
+
{ id: 'SEC-XSS-013', category: 'security', severity: 'critical', confidence: 'definite', title: 'Server-Side Reflected XSS via res.send()',
|
|
502
|
+
check({ files }) {
|
|
503
|
+
const findings = [];
|
|
504
|
+
for (const [fp, c] of files) {
|
|
505
|
+
if (!['.js', '.ts', '.mjs', '.cjs'].some(e => fp.endsWith(e))) continue;
|
|
506
|
+
const lines = c.split('\n');
|
|
507
|
+
for (let i = 0; i < lines.length; i++) {
|
|
508
|
+
if (lines[i].match(/res\.(send|write)\s*\(/) && !lines[i].match(/\/\//)) {
|
|
509
|
+
if (lines[i].match(/req\.(query|body|params)\.\w+|req\.query\b|req\.body\b|req\.params\b/)) {
|
|
510
|
+
findings.push({ ruleId: 'SEC-XSS-013', category: 'security', severity: 'critical', title: 'Reflected XSS: user input directly sent in HTTP response', description: 'Use res.json() for API responses or sanitize HTML with DOMPurify/sanitize-html. Sending unsanitized user input in res.send() causes reflected XSS.', file: fp, line: i + 1, fix: null });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return findings;
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
export default rules;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const CACHE_DIR = '.doorman';
|
|
5
|
+
const CACHE_FILE = 'last-scan.json';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Save scan results to cache for incremental merging and instant fix.
|
|
9
|
+
*/
|
|
10
|
+
export function saveScanCache(targetPath, findings, stack, score) {
|
|
11
|
+
const dir = join(targetPath, CACHE_DIR);
|
|
12
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const cache = {
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
score,
|
|
17
|
+
stack,
|
|
18
|
+
findings: findings.map(f => ({
|
|
19
|
+
ruleId: f.ruleId,
|
|
20
|
+
category: f.category,
|
|
21
|
+
severity: f.severity,
|
|
22
|
+
title: f.title,
|
|
23
|
+
file: f.file,
|
|
24
|
+
line: f.line,
|
|
25
|
+
confidence: f.confidence,
|
|
26
|
+
fix: f.fix,
|
|
27
|
+
description: f.description,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
writeFileSync(join(dir, CACHE_FILE), JSON.stringify(cache, null, 2));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load cached scan results.
|
|
36
|
+
*/
|
|
37
|
+
export function loadScanCache(targetPath) {
|
|
38
|
+
const cachePath = join(targetPath, CACHE_DIR, CACHE_FILE);
|
|
39
|
+
if (!existsSync(cachePath)) return null;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Merge incremental scan results with cached results.
|
|
49
|
+
* Replaces cached findings for files that were re-scanned,
|
|
50
|
+
* keeps cached findings for files that weren't.
|
|
51
|
+
*/
|
|
52
|
+
export function mergeWithCache(targetPath, newFindings, scannedFiles) {
|
|
53
|
+
const cache = loadScanCache(targetPath);
|
|
54
|
+
if (!cache || !cache.findings) return newFindings;
|
|
55
|
+
|
|
56
|
+
// Files that were scanned — their findings are fresh
|
|
57
|
+
const scannedFileSet = new Set(scannedFiles);
|
|
58
|
+
|
|
59
|
+
// Keep cached findings for files NOT in this scan
|
|
60
|
+
const keptFromCache = cache.findings.filter(f =>
|
|
61
|
+
f.file && !scannedFileSet.has(f.file)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Also keep project-level findings (no file) from cache if no new ones replace them
|
|
65
|
+
const newRuleIds = new Set(newFindings.map(f => f.ruleId));
|
|
66
|
+
const keptProjectLevel = cache.findings.filter(f =>
|
|
67
|
+
!f.file && !newRuleIds.has(f.ruleId)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return [...newFindings, ...keptFromCache, ...keptProjectLevel];
|
|
71
|
+
}
|