@archpublicwebsite/eslint-config 1.0.15 → 1.0.18
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 +19 -1
- package/eslint.config.mjs +60 -4
- package/package.json +7 -1
- package/tools/git-hooks/pre-commit.mjs +30 -0
- package/tools/security/patterns.mjs +311 -0
- package/tools/security/safe-reinstall.sh +179 -0
- package/tools/security/scan-global.sh +155 -0
- package/tools/security/scan.mjs +465 -0
- package/tools/security/test-patterns.mjs +114 -0
- package/tools/setup/install.mjs +70 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Pattern-detection self-test
|
|
4
|
+
* Run: node packages/eslint-config/tools/security/test-patterns.mjs
|
|
5
|
+
*
|
|
6
|
+
* Validates that every attack vector in the wild (including the dropper from
|
|
7
|
+
* the supply-chain report) is caught by FILE_PATTERNS.
|
|
8
|
+
*/
|
|
9
|
+
import { FILE_PATTERNS } from './patterns.mjs'
|
|
10
|
+
|
|
11
|
+
const RED = '\x1b[31m'
|
|
12
|
+
const GRN = '\x1b[32m'
|
|
13
|
+
const YLW = '\x1b[33m'
|
|
14
|
+
const BOLD = '\x1b[1m'
|
|
15
|
+
const R = '\x1b[0m'
|
|
16
|
+
|
|
17
|
+
// ─── Test cases (real-world attack lines) ─────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const testCases = [
|
|
20
|
+
// ── From the exact dropper submitted in the issue ─────────────────────────
|
|
21
|
+
{
|
|
22
|
+
label: 'Dropper — global require hijack',
|
|
23
|
+
expectedId: 'require-global-hijack',
|
|
24
|
+
line: "global[_$_1e42[0]]= require;",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: 'Dropper — global bracket assignment via decoded array',
|
|
28
|
+
expectedId: 'global-bracket-assignment',
|
|
29
|
+
line: "global[_$_1e42[2]]= module",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'Dropper — obfuscated _$_ variable names',
|
|
33
|
+
expectedId: 'obfuscated-variable-names',
|
|
34
|
+
line: "var _$_1e42=(function(l,e){var h=l.length;var g=[];",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: 'Dropper — shuffle-cipher IIFE bootstrap',
|
|
38
|
+
expectedId: 'shuffle-cipher-iife',
|
|
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
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'Dropper — array-based Function call chain (final execution)',
|
|
43
|
+
expectedId: 'array-fn-call-chain',
|
|
44
|
+
line: "var Tgw=jFD(LQI,pYd );Tgw(2509);",
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// ── Classic attack patterns ───────────────────────────────────────────────
|
|
48
|
+
{
|
|
49
|
+
label: 'eval() call',
|
|
50
|
+
expectedId: 'no-eval',
|
|
51
|
+
line: "const x = eval(someVar)",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: 'new Function() call',
|
|
55
|
+
expectedId: 'no-new-func',
|
|
56
|
+
line: "const fn = new Function('a','return a+1')",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: 'Buffer base64 decode',
|
|
60
|
+
expectedId: 'base64-decode',
|
|
61
|
+
line: `const payload = Buffer.from('abc123', 'base64').toString()`,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: 'String.fromCharCode obfuscation',
|
|
65
|
+
expectedId: 'charcode-obfuscation',
|
|
66
|
+
line: "const s = String.fromCharCode(104, 101, 108, 108, 111, 32, 119, 111, 114)",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: 'Hex-escaped string payload',
|
|
70
|
+
expectedId: 'hex-string-obfuscation',
|
|
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
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: 'Prototype pollution',
|
|
75
|
+
expectedId: 'prototype-pollution',
|
|
76
|
+
line: "obj.__proto__ = { admin: true }",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: 'child_process in config',
|
|
80
|
+
expectedId: 'child-process-in-config',
|
|
81
|
+
configOnly: true,
|
|
82
|
+
line: "const { execSync } = require('child_process')",
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
// ─── Run tests ────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
let passed = 0
|
|
89
|
+
let failed = 0
|
|
90
|
+
|
|
91
|
+
for (const tc of testCases) {
|
|
92
|
+
// For configOnly patterns, the scanner applies them; we can test them directly
|
|
93
|
+
const matched = FILE_PATTERNS.filter(p => p.regex.test(tc.line))
|
|
94
|
+
const hit = matched.find(p => p.id === tc.expectedId)
|
|
95
|
+
|
|
96
|
+
if (hit) {
|
|
97
|
+
console.log(`${GRN}✔${R} ${tc.label}`)
|
|
98
|
+
console.log(` ${YLW}→ [${hit.severity.toUpperCase()}] ${hit.id}${R}`)
|
|
99
|
+
passed++
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.log(`${RED}${BOLD}✖ ${tc.label}${R}`)
|
|
103
|
+
console.log(` Expected pattern id: ${tc.expectedId}`)
|
|
104
|
+
if (matched.length > 0)
|
|
105
|
+
console.log(` (other matches: ${matched.map(p => p.id).join(', ')})`)
|
|
106
|
+
else
|
|
107
|
+
console.log(` (no patterns matched this line)`)
|
|
108
|
+
failed++
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(`\n${'─'.repeat(50)}`)
|
|
113
|
+
console.log(`${passed} passed ${failed > 0 ? `${RED}${BOLD}${failed} FAILED${R}` : ''}`)
|
|
114
|
+
if (failed > 0) process.exit(1)
|
package/tools/setup/install.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process'
|
|
2
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { join } from 'node:path'
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
4
5
|
import { ensureVscodeSettings } from './vscode.mjs'
|
|
5
6
|
|
|
6
7
|
const TAG = '[@archpublicwebsite/eslint-config]'
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
9
|
|
|
8
10
|
function log(msg) {
|
|
9
11
|
console.log(`${TAG} ${msg}`)
|
|
@@ -193,6 +195,70 @@ function ensureHooksPath(projectRoot) {
|
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
// ─── security shell scripts ────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
function ensureSecurityScripts(projectRoot) {
|
|
201
|
+
const securityDir = join(__dirname, '../security')
|
|
202
|
+
const scripts = ['safe-reinstall.sh', 'scan-global.sh']
|
|
203
|
+
|
|
204
|
+
let created = false
|
|
205
|
+
|
|
206
|
+
for (const scriptName of scripts) {
|
|
207
|
+
const sourcePath = join(securityDir, scriptName)
|
|
208
|
+
const targetPath = join(projectRoot, scriptName)
|
|
209
|
+
|
|
210
|
+
if (!existsSync(sourcePath) || existsSync(targetPath)) {
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
copyFileSync(sourcePath, targetPath)
|
|
215
|
+
chmodSync(targetPath, 0o755)
|
|
216
|
+
created = true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (created) {
|
|
220
|
+
log('Created security scripts (safe-reinstall.sh, scan-global.sh)')
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function ensurePackageScripts(projectRoot) {
|
|
225
|
+
const packageJsonPath = join(projectRoot, 'package.json')
|
|
226
|
+
if (!existsSync(packageJsonPath)) {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let pkg
|
|
231
|
+
try {
|
|
232
|
+
pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
|
|
239
|
+
const desiredScripts = {
|
|
240
|
+
precommit: 'node node_modules/@archpublicwebsite/eslint-config/tools/git-hooks/pre-commit.mjs',
|
|
241
|
+
'security:global-scan': 'bash ./node_modules/@archpublicwebsite/eslint-config/tools/security/scan-global.sh',
|
|
242
|
+
'security:safe-check': 'bash ./node_modules/@archpublicwebsite/eslint-config/tools/security/safe-reinstall.sh --check-only',
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let updated = false
|
|
246
|
+
for (const [name, value] of Object.entries(desiredScripts)) {
|
|
247
|
+
if (!scripts[name]) {
|
|
248
|
+
scripts[name] = value
|
|
249
|
+
updated = true
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!updated) {
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
pkg.scripts = scripts
|
|
258
|
+
writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
|
|
259
|
+
log('Updated package.json scripts (precommit, security:global-scan, security:safe-check)')
|
|
260
|
+
}
|
|
261
|
+
|
|
196
262
|
// ─── .vscode/extensions.json ────────────────────────────────────────────────
|
|
197
263
|
|
|
198
264
|
function ensureVscodeExtensions(projectRoot) {
|
|
@@ -240,6 +306,8 @@ function main() {
|
|
|
240
306
|
ensurePrettierConfig(projectRoot)
|
|
241
307
|
ensureHooks(projectRoot)
|
|
242
308
|
ensureHooksPath(projectRoot)
|
|
309
|
+
ensureSecurityScripts(projectRoot)
|
|
310
|
+
ensurePackageScripts(projectRoot)
|
|
243
311
|
ensureVscodeSettings(projectRoot)
|
|
244
312
|
ensureVscodeExtensions(projectRoot)
|
|
245
313
|
|