@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.
@@ -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)
@@ -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