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,127 @@
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-CORS-001
7
+ {
8
+ id: 'SEC-CORS-001', category: 'security', severity: 'high', confidence: 'likely',
9
+ title: 'CORS allows all origins (wildcard *)',
10
+ check({ files }) {
11
+ const findings = [];
12
+ for (const [fp, content] of files) {
13
+ if (!isJS(fp) || isTest(fp)) continue;
14
+ const lines = content.split('\n');
15
+ for (let i = 0; i < lines.length; i++) {
16
+ if (lines[i].trim().startsWith('//')) continue;
17
+ if (lines[i].match(/(?:Access-Control-Allow-Origin|origin)\s*[:=]\s*['"`]\*['"`]/i) ||
18
+ lines[i].match(/cors\(\s*\)/) // cors() with no config = allow all
19
+ ) {
20
+ findings.push({
21
+ ruleId: 'SEC-CORS-001', category: 'security', severity: 'high',
22
+ title: 'CORS allows all origins — any website can make requests to your API',
23
+ description: 'Restrict CORS to specific trusted domains.',
24
+ file: fp, line: i + 1, fix: null,
25
+ });
26
+ }
27
+ }
28
+ }
29
+ return findings;
30
+ },
31
+ },
32
+
33
+ // SEC-CORS-002
34
+ {
35
+ id: 'SEC-CORS-002', category: 'security', severity: 'critical', confidence: 'definite',
36
+ title: 'CORS reflects Origin header',
37
+ check({ files }) {
38
+ const findings = [];
39
+ for (const [fp, content] of files) {
40
+ if (!isJS(fp) || isTest(fp)) continue;
41
+ const lines = content.split('\n');
42
+ for (let i = 0; i < lines.length; i++) {
43
+ if (lines[i].match(/origin\s*[:=]\s*(?:req|request)\.headers\.origin/i) ||
44
+ lines[i].match(/Access-Control-Allow-Origin.*req\.headers\.origin/i)) {
45
+ findings.push({
46
+ ruleId: 'SEC-CORS-002', category: 'security', severity: 'critical',
47
+ title: 'CORS reflects any Origin header — effectively allows all origins with credentials',
48
+ file: fp, line: i + 1, fix: null,
49
+ });
50
+ }
51
+ }
52
+ }
53
+ return findings;
54
+ },
55
+ },
56
+
57
+ // SEC-CORS-003
58
+ {
59
+ id: 'SEC-CORS-003', category: 'security', severity: 'critical', confidence: 'definite',
60
+ title: 'CORS allows credentials with wildcard origin',
61
+ check({ files }) {
62
+ const findings = [];
63
+ for (const [fp, content] of files) {
64
+ if (!isJS(fp) || isTest(fp)) continue;
65
+ if (content.includes('credentials: true') || content.includes('Access-Control-Allow-Credentials')) {
66
+ if (content.match(/origin\s*[:=]\s*['"`]\*['"`]/) || content.match(/cors\(\s*\)/)) {
67
+ findings.push({
68
+ ruleId: 'SEC-CORS-003', category: 'security', severity: 'critical',
69
+ title: 'CORS allows credentials with wildcard origin — critical security vulnerability',
70
+ file: fp, fix: null,
71
+ });
72
+ }
73
+ }
74
+ }
75
+ return findings;
76
+ },
77
+ },
78
+
79
+ // SEC-CORS-004
80
+ {
81
+ id: 'SEC-CORS-004', category: 'security', severity: 'medium', confidence: 'likely',
82
+ title: 'CORS exposes sensitive headers',
83
+ check({ files }) {
84
+ const findings = [];
85
+ for (const [fp, content] of files) {
86
+ if (!isJS(fp) || isTest(fp)) continue;
87
+ const lines = content.split('\n');
88
+ for (let i = 0; i < lines.length; i++) {
89
+ if (lines[i].match(/exposedHeaders.*(?:Authorization|X-Api-Key|Set-Cookie)/i)) {
90
+ findings.push({
91
+ ruleId: 'SEC-CORS-004', category: 'security', severity: 'medium',
92
+ title: 'CORS exposes sensitive headers (Authorization, API keys)',
93
+ file: fp, line: i + 1, fix: null,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ return findings;
99
+ },
100
+ },
101
+
102
+ // SEC-CORS-005
103
+ {
104
+ id: 'SEC-CORS-005', category: 'security', severity: 'medium', confidence: 'likely',
105
+ title: 'Overly permissive CORS methods',
106
+ check({ files }) {
107
+ const findings = [];
108
+ for (const [fp, content] of files) {
109
+ if (!isJS(fp) || isTest(fp)) continue;
110
+ const lines = content.split('\n');
111
+ for (let i = 0; i < lines.length; i++) {
112
+ if (lines[i].match(/methods\s*[:=]\s*['"`]\*['"`]/) ||
113
+ lines[i].match(/Access-Control-Allow-Methods.*\*/)) {
114
+ findings.push({
115
+ ruleId: 'SEC-CORS-005', category: 'security', severity: 'medium',
116
+ title: 'CORS allows all HTTP methods — restrict to only needed methods',
117
+ file: fp, line: i + 1, fix: null,
118
+ });
119
+ }
120
+ }
121
+ }
122
+ return findings;
123
+ },
124
+ },
125
+ ];
126
+
127
+ export default rules;
@@ -0,0 +1,527 @@
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-CRY-001
7
+ {
8
+ id: 'SEC-CRY-001', category: 'security', severity: 'high', confidence: 'likely',
9
+ title: 'Using MD5 for hashing',
10
+ check({ files }) {
11
+ const findings = [];
12
+ for (const [fp, content] of files) {
13
+ if (!isJS(fp) || isTest(fp)) continue;
14
+ const lines = content.split('\n');
15
+ for (let i = 0; i < lines.length; i++) {
16
+ if (lines[i].trim().startsWith('//')) continue;
17
+ if (lines[i].match(/(?:createHash|crypto\.hash)\s*\(\s*['"]md5['"]/i) ||
18
+ lines[i].match(/md5\s*\(/)) {
19
+ findings.push({
20
+ ruleId: 'SEC-CRY-001', category: 'security', severity: 'high',
21
+ title: 'MD5 hashing detected — cryptographically broken, use SHA-256+ or bcrypt',
22
+ confidence: 'definite',
23
+ file: fp, line: i + 1, fix: null,
24
+ });
25
+ }
26
+ }
27
+ }
28
+ return findings;
29
+ },
30
+ },
31
+
32
+ // SEC-CRY-002
33
+ {
34
+ id: 'SEC-CRY-002', category: 'security', severity: 'high', confidence: 'likely',
35
+ title: 'Using SHA1 for hashing',
36
+ check({ files }) {
37
+ const findings = [];
38
+ for (const [fp, content] of files) {
39
+ if (!isJS(fp) || isTest(fp)) continue;
40
+ const lines = content.split('\n');
41
+ for (let i = 0; i < lines.length; i++) {
42
+ if (lines[i].trim().startsWith('//')) continue;
43
+ if (lines[i].match(/createHash\s*\(\s*['"]sha1['"]/i)) {
44
+ findings.push({
45
+ ruleId: 'SEC-CRY-002', category: 'security', severity: 'high',
46
+ title: 'SHA1 hashing detected — deprecated for security purposes, use SHA-256+',
47
+ confidence: 'definite',
48
+ file: fp, line: i + 1, fix: null,
49
+ });
50
+ }
51
+ }
52
+ }
53
+ return findings;
54
+ },
55
+ },
56
+
57
+ // SEC-CRY-003
58
+ {
59
+ id: 'SEC-CRY-003', category: 'security', severity: 'high', confidence: 'likely',
60
+ title: 'Weak random number generation',
61
+ check({ files }) {
62
+ const findings = [];
63
+ for (const [fp, content] of files) {
64
+ if (!isJS(fp) || isTest(fp)) continue;
65
+ const lines = content.split('\n');
66
+ for (let i = 0; i < lines.length; i++) {
67
+ if (lines[i].trim().startsWith('//')) continue;
68
+ // Math.random() used in security context
69
+ if (lines[i].includes('Math.random()') &&
70
+ (content.includes('token') || content.includes('secret') || content.includes('password') ||
71
+ content.includes('session') || content.includes('nonce') || content.includes('salt'))) {
72
+ findings.push({
73
+ ruleId: 'SEC-CRY-003', category: 'security', severity: 'high',
74
+ title: 'Math.random() used in security context — not cryptographically secure',
75
+ description: 'Use crypto.randomBytes() or crypto.randomUUID() instead.',
76
+ confidence: 'definite',
77
+ file: fp, line: i + 1, fix: null,
78
+ });
79
+ break; // One per file is enough
80
+ }
81
+ }
82
+ }
83
+ return findings;
84
+ },
85
+ },
86
+
87
+ // SEC-CRY-004
88
+ {
89
+ id: 'SEC-CRY-004', category: 'security', severity: 'high', confidence: 'likely',
90
+ title: 'Hardcoded initialization vector (IV)',
91
+ check({ files }) {
92
+ const findings = [];
93
+ for (const [fp, content] of files) {
94
+ if (!isJS(fp) || isTest(fp)) continue;
95
+ const lines = content.split('\n');
96
+ for (let i = 0; i < lines.length; i++) {
97
+ if (lines[i].trim().startsWith('//')) continue;
98
+ if (lines[i].match(/(?:iv|nonce|initializationVector)\s*[:=]\s*(?:Buffer\.from\s*\(\s*)?['"][a-fA-F0-9]{16,}['"]/i)) {
99
+ findings.push({
100
+ ruleId: 'SEC-CRY-004', category: 'security', severity: 'high',
101
+ title: 'Hardcoded IV/nonce — must be randomly generated for each encryption',
102
+ confidence: 'definite',
103
+ file: fp, line: i + 1, fix: null,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ return findings;
109
+ },
110
+ },
111
+
112
+ // SEC-CRY-005
113
+ {
114
+ id: 'SEC-CRY-005', category: 'security', severity: 'high', confidence: 'likely',
115
+ title: 'ECB mode encryption',
116
+ check({ files }) {
117
+ const findings = [];
118
+ for (const [fp, content] of files) {
119
+ if (!isJS(fp) || isTest(fp)) continue;
120
+ const lines = content.split('\n');
121
+ for (let i = 0; i < lines.length; i++) {
122
+ if (lines[i].match(/aes.*ecb/i) || lines[i].match(/['"]aes-\d+-ecb['"]/i)) {
123
+ findings.push({
124
+ ruleId: 'SEC-CRY-005', category: 'security', severity: 'high',
125
+ title: 'AES-ECB mode used — patterns visible in ciphertext, use CBC or GCM',
126
+ confidence: 'definite',
127
+ file: fp, line: i + 1, fix: null,
128
+ });
129
+ }
130
+ }
131
+ }
132
+ return findings;
133
+ },
134
+ },
135
+
136
+ // SEC-CRY-006
137
+ {
138
+ id: 'SEC-CRY-006', category: 'security', severity: 'high', confidence: 'likely',
139
+ title: 'Missing HTTPS enforcement',
140
+ check({ files, stack }) {
141
+ const findings = [];
142
+ if (stack.runtime !== 'node') return findings;
143
+ const hasHttpsRedirect = [...files.values()].some(c =>
144
+ c.includes('https://') && c.includes('redirect') ||
145
+ c.includes('SECURE_SSL_REDIRECT') ||
146
+ c.includes('forceSSL') ||
147
+ c.includes('requireHTTPS') ||
148
+ c.includes("protocol === 'https'") ||
149
+ c.includes('Strict-Transport-Security')
150
+ );
151
+ const hasHelmet = 'helmet' in (stack.dependencies || {});
152
+ if (!hasHttpsRedirect && !hasHelmet) {
153
+ const hasServer = [...files.values()].some(c => c.includes('.listen(') || c.includes('createServer'));
154
+ if (hasServer) {
155
+ findings.push({
156
+ ruleId: 'SEC-CRY-006', category: 'security', severity: 'high',
157
+ title: 'No HTTPS enforcement detected — traffic may be unencrypted',
158
+ confidence: 'suggestion',
159
+ fix: null,
160
+ });
161
+ }
162
+ }
163
+ return findings;
164
+ },
165
+ },
166
+
167
+ // SEC-CRY-007
168
+ {
169
+ id: 'SEC-CRY-007', category: 'security', severity: 'medium', confidence: 'likely',
170
+ title: 'Self-signed certificate in production',
171
+ check({ files }) {
172
+ const findings = [];
173
+ for (const [fp, content] of files) {
174
+ if (fp.includes('test') || fp.includes('dev')) continue;
175
+ if (content.includes('rejectUnauthorized: false') || content.includes('NODE_TLS_REJECT_UNAUTHORIZED')) {
176
+ findings.push({
177
+ ruleId: 'SEC-CRY-007', category: 'security', severity: 'medium',
178
+ title: 'TLS certificate verification disabled — man-in-the-middle attacks possible',
179
+ confidence: 'likely',
180
+ file: fp, fix: null,
181
+ });
182
+ }
183
+ }
184
+ return findings;
185
+ },
186
+ },
187
+
188
+ // SEC-CRY-008
189
+ {
190
+ id: 'SEC-CRY-008', category: 'security', severity: 'high', confidence: 'likely',
191
+ title: 'Deprecated TLS version allowed',
192
+ check({ files }) {
193
+ const findings = [];
194
+ for (const [fp, content] of files) {
195
+ if (fp.includes('test')) continue;
196
+ const lines = content.split('\n');
197
+ for (let i = 0; i < lines.length; i++) {
198
+ if (lines[i].match(/(?:minVersion|secureProtocol).*(?:TLSv1(?:\.0)?|TLSv1\.1|SSLv3)/i)) {
199
+ findings.push({
200
+ ruleId: 'SEC-CRY-008', category: 'security', severity: 'high',
201
+ title: 'TLS 1.0 or 1.1 allowed — use TLS 1.2+ minimum',
202
+ confidence: 'definite',
203
+ file: fp, line: i + 1, fix: null,
204
+ });
205
+ }
206
+ }
207
+ }
208
+ return findings;
209
+ },
210
+ },
211
+
212
+ // SEC-CRY-009
213
+ {
214
+ id: 'SEC-CRY-009', category: 'security', severity: 'medium', confidence: 'likely',
215
+ title: 'Weak cipher suites allowed',
216
+ check({ files }) {
217
+ const findings = [];
218
+ for (const [fp, content] of files) {
219
+ if (fp.includes('test')) continue;
220
+ if (content.match(/(?:ciphers|cipher).*(?:RC4|DES|3DES|RC2|SEED|IDEA)/i)) {
221
+ findings.push({
222
+ ruleId: 'SEC-CRY-009', category: 'security', severity: 'medium',
223
+ title: 'Weak cipher suites detected (RC4, DES, 3DES) — use modern ciphers',
224
+ confidence: 'definite',
225
+ file: fp, fix: null,
226
+ });
227
+ }
228
+ }
229
+ return findings;
230
+ },
231
+ },
232
+
233
+ // SEC-CRY-010
234
+ {
235
+ id: 'SEC-CRY-010', category: 'security', severity: 'high', confidence: 'likely',
236
+ title: 'Using deprecated createCipher',
237
+ check({ files }) {
238
+ const findings = [];
239
+ for (const [fp, content] of files) {
240
+ if (!isJS(fp) || isTest(fp)) continue;
241
+ const lines = content.split('\n');
242
+ for (let i = 0; i < lines.length; i++) {
243
+ if (lines[i].match(/crypto\.createCipher\s*\(/) && !lines[i].includes('createCipheriv')) {
244
+ findings.push({
245
+ ruleId: 'SEC-CRY-010', category: 'security', severity: 'high',
246
+ title: 'Using deprecated crypto.createCipher — use createCipheriv with a random IV',
247
+ confidence: 'definite',
248
+ file: fp, line: i + 1, fix: null,
249
+ });
250
+ }
251
+ }
252
+ }
253
+ return findings;
254
+ },
255
+ },
256
+ ];
257
+
258
+ export default rules;
259
+
260
+ // SEC-CRY-011: Hardcoded encryption key
261
+ rules.push({
262
+ id: 'SEC-CRY-011', category: 'security', severity: 'critical', confidence: 'definite',
263
+ title: 'Hardcoded encryption key or IV',
264
+ check({ files }) {
265
+ const findings = [];
266
+ const p = /(?:key|iv|secret|password)\s*[:=]\s*['"][A-Za-z0-9+/=]{16,}['"]/i;
267
+ const cryptoCtx = /createCipher|createDecipher|encrypt|decrypt|AES|DES|RSA/i;
268
+ for (const [fp, c] of files) {
269
+ if (!isJS(fp) || isTest(fp)) continue;
270
+ const lines = c.split('\n');
271
+ for (let i = 0; i < lines.length; i++) {
272
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
273
+ if (p.test(lines[i]) && !/process\.env/.test(lines[i])) {
274
+ const ctx = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 5)).join('\n');
275
+ if (cryptoCtx.test(ctx)) findings.push({ ruleId: 'SEC-CRY-011', category: 'security', severity: 'critical', title: 'Hardcoded encryption key — rotate immediately', description: 'Encryption keys hardcoded in source are compromised when code is exposed. Load keys from a secrets manager or environment variable.', file: fp, line: i + 1, fix: null });
276
+ }
277
+ }
278
+ }
279
+ return findings;
280
+ },
281
+ });
282
+
283
+ // SEC-CRY-012: Using DES or 3DES
284
+ rules.push({
285
+ id: 'SEC-CRY-012', category: 'security', severity: 'high', confidence: 'definite',
286
+ title: 'DES or 3DES encryption used — broken algorithm',
287
+ check({ files }) {
288
+ const findings = [];
289
+ const p = /(?:des|3des|des3|triple.?des)/i;
290
+ for (const [fp, c] of files) {
291
+ if (!isJS(fp) || isTest(fp)) continue;
292
+ const lines = c.split('\n');
293
+ for (let i = 0; i < lines.length; i++) {
294
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
295
+ if (p.test(lines[i]) && /createCipher|cipher|encrypt/i.test(lines[i])) findings.push({ ruleId: 'SEC-CRY-012', category: 'security', severity: 'high', title: 'DES/3DES detected — use AES-256-GCM instead', description: 'DES is broken (56-bit key). 3DES is deprecated and vulnerable to Sweet32 attack. Migrate to AES-256-GCM.', file: fp, line: i + 1, fix: null });
296
+ }
297
+ }
298
+ return findings;
299
+ },
300
+ });
301
+
302
+ // SEC-CRY-013: Insecure random bytes for IV/nonce
303
+ rules.push({
304
+ id: 'SEC-CRY-013', category: 'security', severity: 'high', confidence: 'likely',
305
+ title: 'Math.random() used for cryptographic IV or nonce',
306
+ check({ files }) {
307
+ const findings = [];
308
+ for (const [fp, c] of files) {
309
+ if (!isJS(fp) || isTest(fp)) continue;
310
+ const lines = c.split('\n');
311
+ for (let i = 0; i < lines.length; i++) {
312
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
313
+ if (/Math\.random\s*\(/.test(lines[i])) {
314
+ const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 3)).join('\n');
315
+ if (/iv|nonce|salt|key|cipher|encrypt/i.test(ctx)) findings.push({ ruleId: 'SEC-CRY-013', category: 'security', severity: 'high', title: 'Math.random() for IV/nonce — not cryptographically secure', description: 'Use crypto.randomBytes(16) to generate cryptographically random IVs and nonces.', file: fp, line: i + 1, fix: null });
316
+ }
317
+ }
318
+ }
319
+ return findings;
320
+ },
321
+ });
322
+
323
+ // SEC-CRY-014: CBC mode without authentication
324
+ rules.push({
325
+ id: 'SEC-CRY-014', category: 'security', severity: 'high', confidence: 'likely',
326
+ title: 'AES-CBC without HMAC authentication — padding oracle risk',
327
+ check({ files }) {
328
+ const findings = [];
329
+ const p = /aes-(?:128|192|256)-cbc|AES.*CBC/i;
330
+ const hmacPat = /hmac|createHmac|authenticate|tag|GCM/i;
331
+ for (const [fp, c] of files) {
332
+ if (!isJS(fp) || isTest(fp)) continue;
333
+ const lines = c.split('\n');
334
+ for (let i = 0; i < lines.length; i++) {
335
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
336
+ if (p.test(lines[i]) && !hmacPat.test(lines[i])) {
337
+ const ctx = lines.slice(Math.max(0, i - 10), Math.min(lines.length, i + 10)).join('\n');
338
+ if (!hmacPat.test(ctx)) findings.push({ ruleId: 'SEC-CRY-014', category: 'security', severity: 'high', title: 'AES-CBC without HMAC — padding oracle attack possible', description: 'AES-CBC without message authentication is vulnerable to padding oracle attacks. Use AES-256-GCM which provides authenticated encryption.', file: fp, line: i + 1, fix: null });
339
+ }
340
+ }
341
+ }
342
+ return findings;
343
+ },
344
+ });
345
+
346
+ // SEC-CRY-015: Weak RSA key size
347
+ rules.push({
348
+ id: 'SEC-CRY-015', category: 'security', severity: 'high', confidence: 'definite',
349
+ title: 'RSA key size less than 2048 bits',
350
+ check({ files }) {
351
+ const findings = [];
352
+ const p = /(?:modulusLength|keySize)\s*[:=]\s*(?:512|768|1024)\b/i;
353
+ for (const [fp, c] of files) {
354
+ if (!isJS(fp) || isTest(fp)) continue;
355
+ const lines = c.split('\n');
356
+ for (let i = 0; i < lines.length; i++) {
357
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
358
+ if (p.test(lines[i])) findings.push({ ruleId: 'SEC-CRY-015', category: 'security', severity: 'high', title: 'RSA key size < 2048 bits — insufficient security', description: 'RSA keys under 2048 bits can be factored. Use at least 2048 bits; prefer 4096 for long-lived keys.', file: fp, line: i + 1, fix: null });
359
+ }
360
+ }
361
+ return findings;
362
+ },
363
+ });
364
+
365
+ // SEC-CRY-016: TLS minimum version not set
366
+ rules.push({
367
+ id: 'SEC-CRY-016', category: 'security', severity: 'medium', confidence: 'suggestion',
368
+ title: 'TLS server created without minimum version requirement',
369
+ check({ files }) {
370
+ const findings = [];
371
+ const p = /createServer|https\.createServer|tls\.createServer/i;
372
+ const minVersion = /minVersion|secureProtocol|TLSv1_2|TLSv1_3/i;
373
+ for (const [fp, c] of files) {
374
+ if (!isJS(fp) || isTest(fp)) continue;
375
+ const lines = c.split('\n');
376
+ for (let i = 0; i < lines.length; i++) {
377
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
378
+ if (p.test(lines[i]) && /tls|https/i.test(lines[i])) {
379
+ const ctx = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 10)).join('\n');
380
+ if (!minVersion.test(ctx)) findings.push({ ruleId: 'SEC-CRY-016', category: 'security', severity: 'medium', title: 'TLS server without minVersion — may accept TLS 1.0/1.1', description: 'Set minVersion: "TLSv1.2" or "TLSv1.3" to prevent downgrade attacks to deprecated TLS versions.', file: fp, line: i + 1, fix: null });
381
+ }
382
+ }
383
+ }
384
+ return findings;
385
+ },
386
+ });
387
+
388
+ // SEC-CRY-017: bcrypt with weak rounds
389
+ rules.push({
390
+ id: 'SEC-CRY-017', category: 'security', severity: 'medium', confidence: 'definite',
391
+ title: 'bcrypt used with too few rounds (< 10)',
392
+ check({ files }) {
393
+ const findings = [];
394
+ const p = /bcrypt\s*\.\s*(?:hash|genSalt)\s*\([^,]+,\s*([1-9])\b/;
395
+ for (const [fp, c] of files) {
396
+ if (!isJS(fp) || isTest(fp)) continue;
397
+ const lines = c.split('\n');
398
+ for (let i = 0; i < lines.length; i++) {
399
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
400
+ const m = lines[i].match(/bcrypt\s*\.\s*(?:hash|genSalt)\s*\(\s*\w+\s*,\s*(\d+)/);
401
+ if (m && parseInt(m[1], 10) < 10) findings.push({ ruleId: 'SEC-CRY-017', category: 'security', severity: 'medium', title: `bcrypt rounds=${m[1]} — too low, use at least 12`, description: 'Low bcrypt cost factor allows faster offline cracking. Use at least 10 rounds; 12+ is recommended.', file: fp, line: i + 1, fix: null });
402
+ }
403
+ }
404
+ return findings;
405
+ },
406
+ });
407
+
408
+ // SEC-CRY-018: Sensitive data in URL (query string)
409
+ rules.push({
410
+ id: 'SEC-CRY-018', category: 'security', severity: 'high', confidence: 'likely',
411
+ title: 'Sensitive data passed in URL query string',
412
+ check({ files }) {
413
+ const findings = [];
414
+ const p = /[?&](?:password|passwd|pwd|token|api_key|apikey|secret|auth)[=]/i;
415
+ for (const [fp, c] of files) {
416
+ if (!isJS(fp) || isTest(fp)) continue;
417
+ const lines = c.split('\n');
418
+ for (let i = 0; i < lines.length; i++) {
419
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
420
+ if (p.test(lines[i])) findings.push({ ruleId: 'SEC-CRY-018', category: 'security', severity: 'high', title: 'Credential in URL query string — logged in server access logs', description: 'Passwords and tokens in URLs appear in server logs, browser history, and Referer headers. Use POST body or Authorization header instead.', file: fp, line: i + 1, fix: null });
421
+ }
422
+ }
423
+ return findings;
424
+ },
425
+ });
426
+
427
+ // SEC-CRY-019 through SEC-CRY-030: Additional crypto rules
428
+
429
+ // SEC-CRY-019: Password reset token not time-limited
430
+ rules.push({
431
+ id: 'SEC-CRY-019', category: 'security', severity: 'high', confidence: 'suggestion',
432
+ title: 'Password reset token without expiry',
433
+ check({ files }) {
434
+ const findings = [];
435
+ for (const [fp, c] of files) {
436
+ if (!isJS(fp) || isTest(fp)) continue;
437
+ if (/resetToken|reset_token|passwordReset|forgotPassword/i.test(c)) {
438
+ if (!/expires|expiry|expiresAt|ttl|timeout/i.test(c)) {
439
+ findings.push({ ruleId: 'SEC-CRY-019', category: 'security', severity: 'high', title: 'Password reset token without expiry — valid indefinitely', description: 'Password reset tokens must expire (typically 15-60 minutes). Tokens valid indefinitely can be used long after a breach.', file: fp, fix: null });
440
+ }
441
+ }
442
+ }
443
+ return findings;
444
+ },
445
+ });
446
+
447
+ // SEC-CRY-020: Random token generation with low entropy
448
+ rules.push({
449
+ id: 'SEC-CRY-020', category: 'security', severity: 'high', confidence: 'likely',
450
+ title: 'Low entropy token generation (< 128 bits)',
451
+ check({ files }) {
452
+ const findings = [];
453
+ for (const [fp, c] of files) {
454
+ if (!isJS(fp) || isTest(fp)) continue;
455
+ const lines = c.split('\n');
456
+ for (let i = 0; i < lines.length; i++) {
457
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
458
+ const m = lines[i].match(/crypto\.randomBytes\s*\(\s*(\d+)\s*\)/);
459
+ if (m && parseInt(m[1]) < 16) {
460
+ findings.push({ ruleId: 'SEC-CRY-020', category: 'security', severity: 'high', title: `crypto.randomBytes(${m[1]}) — only ${parseInt(m[1]) * 8} bits, use at least 128 bits (16 bytes)`, description: 'Security tokens need at least 128 bits (16 bytes) of entropy. Use crypto.randomBytes(32) for session tokens and API keys.', file: fp, line: i + 1, fix: null });
461
+ }
462
+ }
463
+ }
464
+ return findings;
465
+ },
466
+ });
467
+
468
+ // SEC-CRY-021: Hardcoded salt
469
+ rules.push({
470
+ id: 'SEC-CRY-021', category: 'security', severity: 'critical', confidence: 'definite',
471
+ title: 'Hardcoded salt for password hashing',
472
+ check({ files }) {
473
+ const findings = [];
474
+ for (const [fp, c] of files) {
475
+ if (!isJS(fp) || isTest(fp)) continue;
476
+ const lines = c.split('\n');
477
+ for (let i = 0; i < lines.length; i++) {
478
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
479
+ if (/salt\s*[:=]\s*['"][A-Za-z0-9+/=]{8,}['"]/.test(lines[i]) && !/process\.env/.test(lines[i])) {
480
+ const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 3)).join('\n');
481
+ if (/hash|bcrypt|pbkdf2|scrypt|crypto/i.test(ctx)) {
482
+ findings.push({ ruleId: 'SEC-CRY-021', category: 'security', severity: 'critical', title: 'Hardcoded salt used in hashing — salt must be random per-hash', description: 'Salts must be randomly generated per password using crypto.randomBytes(). A hardcoded salt turns it into a pepper at best, and allows rainbow table attacks.', file: fp, line: i + 1, fix: null });
483
+ }
484
+ }
485
+ }
486
+ }
487
+ return findings;
488
+ },
489
+ });
490
+
491
+ // SEC-CRY-022: Missing HSTS header
492
+ rules.push({
493
+ id: 'SEC-CRY-022', category: 'security', severity: 'medium', confidence: 'suggestion',
494
+ title: 'No HTTP Strict Transport Security (HSTS) header configured',
495
+ check({ files, stack }) {
496
+ const findings = [];
497
+ const deps = { ...stack.dependencies, ...stack.devDependencies };
498
+ // helmet enables HSTS by default
499
+ if (deps.helmet) return findings;
500
+ const hasExpress = [...files.values()].some(c => /require.*express|from.*express/i.test(c));
501
+ const hasHSTS = [...files.values()].some(c => /Strict-Transport-Security|hsts|maxAge.*includeSubDomains/i.test(c));
502
+ if (hasExpress && !hasHSTS) {
503
+ findings.push({ ruleId: 'SEC-CRY-022', category: 'security', severity: 'medium', title: 'No HSTS header — browsers may connect over HTTP first', description: 'Set Strict-Transport-Security: max-age=31536000; includeSubDomains to force HTTPS. Use helmet.hsts() or set the header manually.', fix: null });
504
+ }
505
+ return findings;
506
+ },
507
+ });
508
+
509
+ // SEC-CRY-023: Secret comparison with early exit
510
+ rules.push({
511
+ id: 'SEC-CRY-023', category: 'security', severity: 'high', confidence: 'likely',
512
+ title: 'HMAC or signature compared character-by-character — timing oracle',
513
+ check({ files }) {
514
+ const findings = [];
515
+ for (const [fp, c] of files) {
516
+ if (!isJS(fp) || isTest(fp)) continue;
517
+ const lines = c.split('\n');
518
+ for (let i = 0; i < lines.length; i++) {
519
+ if (/^\s*(\/\/|\/\*|\*)/.test(lines[i])) continue;
520
+ if (/(?:hmac|signature|digest)\s*(?:!==|===|==|!=)\s*\w|(?:signature|hmac).*\.startsWith\s*\(/.test(lines[i]) && !/timingSafeEqual/i.test(lines[i])) {
521
+ findings.push({ ruleId: 'SEC-CRY-023', category: 'security', severity: 'high', title: 'HMAC/signature compared with === — timing attack leaks secret', description: 'Use crypto.timingSafeEqual() for all HMAC and signature comparisons to prevent timing attacks.', file: fp, line: i + 1, fix: null });
522
+ }
523
+ }
524
+ }
525
+ return findings;
526
+ },
527
+ });