@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/README.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
Reusable ESLint flat config and git-hook toolkit for Archipelago projects.
|
|
4
4
|
|
|
5
|
+
## Update 1 Bulan Terakhir (Mei-Jun 2026)
|
|
6
|
+
|
|
7
|
+
Ringkasan perubahan terbaru yang paling penting:
|
|
8
|
+
|
|
9
|
+
- Installer sekarang bisa membuat file setup utama otomatis saat belum ada.
|
|
10
|
+
- Hook `pre-commit` lebih tahan error saat config belum lengkap.
|
|
11
|
+
- Security scanner lebih akurat dan mengurangi false positive.
|
|
12
|
+
- Ada opsi validasi akun/signature ketat lewat `REQUIRE_VERIFIED_ACCOUNT=1`.
|
|
13
|
+
|
|
14
|
+
Jika `REQUIRE_VERIFIED_ACCOUNT=1` aktif:
|
|
15
|
+
|
|
16
|
+
- `pre-commit` akan cek `user.name`, `user.email`, dan `commit.gpgsign=true`.
|
|
17
|
+
- `pre-push` akan menolak commit yang status signature-nya bukan `G`.
|
|
18
|
+
|
|
19
|
+
Cara pakai cepat:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm install
|
|
23
|
+
node node_modules/@archpublicwebsite/eslint-config/tools/setup/install.mjs
|
|
24
|
+
pnpm lint:check
|
|
25
|
+
pnpm typecheck
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Aktifkan strict verification hanya saat dibutuhkan:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
REQUIRE_VERIFIED_ACCOUNT=1 git commit
|
|
32
|
+
REQUIRE_VERIFIED_ACCOUNT=1 git push
|
|
33
|
+
```
|
|
34
|
+
|
|
5
35
|
## What this package includes
|
|
6
36
|
|
|
7
37
|
This package ships a ready-to-use flat config plus setup scripts for local project automation:
|
|
@@ -82,9 +112,14 @@ The setup merges ESLint-related settings into `.vscode/settings.json`:
|
|
|
82
112
|
{
|
|
83
113
|
"eslint.useFlatConfig": true,
|
|
84
114
|
"eslint.validate": [
|
|
85
|
-
"javascript",
|
|
86
|
-
"
|
|
87
|
-
"
|
|
115
|
+
"javascript",
|
|
116
|
+
"javascriptreact",
|
|
117
|
+
"typescript",
|
|
118
|
+
"typescriptreact",
|
|
119
|
+
"vue",
|
|
120
|
+
"json",
|
|
121
|
+
"jsonc",
|
|
122
|
+
"markdown"
|
|
88
123
|
],
|
|
89
124
|
"editor.codeActionsOnSave": {
|
|
90
125
|
"source.fixAll.eslint": "explicit",
|
|
@@ -120,6 +155,16 @@ If you are extending or regenerating this package, keep the workflow explicit:
|
|
|
120
155
|
- Re-run the package setup flow when changing VS Code or hook behavior.
|
|
121
156
|
- Prefer explicit examples that show what the consumer project should add.
|
|
122
157
|
|
|
158
|
+
## Refactor/New File Rules
|
|
159
|
+
|
|
160
|
+
When refactoring or creating component files, keep every package aligned to the shared eslint-config contract:
|
|
161
|
+
|
|
162
|
+
1. Keep each package `eslint.config.mjs` on `createArchipelagoConfig(...)` from `@archpublicwebsite/eslint-config`.
|
|
163
|
+
2. Do not add package-local parser stacks that diverge from the shared config unless there is an approved exception.
|
|
164
|
+
3. Run `pnpm lint:check` and `pnpm typecheck` before commit.
|
|
165
|
+
4. If a package needs custom lint behavior, add it as a small override block in that package config while preserving shared base rules.
|
|
166
|
+
5. For new projects or clones, run the setup script so required files (`eslint.config.mjs`, `lint-staged.config.mjs`, hooks, VSCode settings) are auto-created when missing.
|
|
167
|
+
|
|
123
168
|
## Manual setup
|
|
124
169
|
|
|
125
170
|
If you need to rerun the bootstrap manually:
|
package/eslint.config.mjs
CHANGED
|
@@ -87,10 +87,8 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
87
87
|
},
|
|
88
88
|
// document.write / document.writeln
|
|
89
89
|
{
|
|
90
|
-
selector:
|
|
91
|
-
|
|
92
|
-
message:
|
|
93
|
-
'[security/xss] document.write() is deprecated and a direct XSS sink — remove it',
|
|
90
|
+
selector: 'CallExpression[callee.object.name="document"][callee.property.name=/^write(ln)?$/]',
|
|
91
|
+
message: '[security/xss] document.write() is deprecated and a direct XSS sink — remove it',
|
|
94
92
|
},
|
|
95
93
|
// Object.__proto__ assignment (belt-and-suspenders with no-prototype-builtins)
|
|
96
94
|
{
|
|
@@ -168,16 +166,19 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
168
166
|
rules: {
|
|
169
167
|
'no-restricted-imports': [
|
|
170
168
|
'error',
|
|
171
|
-
{ name: 'child_process',
|
|
172
|
-
{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
{ name: '
|
|
177
|
-
{ name: '
|
|
178
|
-
{ name: '
|
|
179
|
-
{ name: '
|
|
180
|
-
{ name: '
|
|
169
|
+
{ name: 'child_process', message: 'child_process is not allowed in build config files — supply-chain risk' },
|
|
170
|
+
{
|
|
171
|
+
name: 'node:child_process',
|
|
172
|
+
message: 'child_process is not allowed in build config files — supply-chain risk',
|
|
173
|
+
},
|
|
174
|
+
{ name: 'http', message: 'http is not allowed in build config files — exfiltration risk' },
|
|
175
|
+
{ name: 'node:http', message: 'http is not allowed in build config files — exfiltration risk' },
|
|
176
|
+
{ name: 'https', message: 'https is not allowed in build config files — exfiltration risk' },
|
|
177
|
+
{ name: 'node:https', message: 'https is not allowed in build config files — exfiltration risk' },
|
|
178
|
+
{ name: 'dns', message: 'dns is not allowed in build config files — DNS tunnelling risk' },
|
|
179
|
+
{ name: 'node:dns', message: 'dns is not allowed in build config files — DNS tunnelling risk' },
|
|
180
|
+
{ name: 'net', message: 'net is not allowed in build config files — reverse-shell risk' },
|
|
181
|
+
{ name: 'node:net', message: 'net is not allowed in build config files — reverse-shell risk' },
|
|
181
182
|
],
|
|
182
183
|
},
|
|
183
184
|
},
|
|
@@ -202,11 +203,12 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
202
203
|
'antfu/if-newline': 'off',
|
|
203
204
|
'antfu/no-import-dist': 'off',
|
|
204
205
|
|
|
205
|
-
// ──
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
'style/semi': '
|
|
209
|
-
|
|
206
|
+
// ── Canonical style enforcement ─────────────────────────────────────
|
|
207
|
+
// No semicolons — matches every component in the codebase.
|
|
208
|
+
// Adding a `;` in any .ts / .vue file will surface as a lint error.
|
|
209
|
+
'style/semi': ['error', 'never'],
|
|
210
|
+
// Single quotes — consistent with all current component files.
|
|
211
|
+
'style/quotes': ['error', 'single', { avoidEscape: true, allowTemplateLiterals: 'never' }],
|
|
210
212
|
'style/quote-props': 'off',
|
|
211
213
|
'style/no-multi-spaces': 'off',
|
|
212
214
|
'style/key-spacing': 'off',
|
|
@@ -239,10 +241,13 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
239
241
|
'vue/attribute-hyphenation': 'off',
|
|
240
242
|
'vue/define-macros-order': 'off',
|
|
241
243
|
'vue/no-v-html': 'off',
|
|
242
|
-
'vue/max-attributes-per-line': [
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
'vue/max-attributes-per-line': [
|
|
245
|
+
'error',
|
|
246
|
+
{
|
|
247
|
+
singleline: 3,
|
|
248
|
+
multiline: 1,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
246
251
|
'vue/max-len': 'off',
|
|
247
252
|
|
|
248
253
|
// ── TypeScript ───────────────────────────────────────────────────────
|
|
@@ -308,6 +313,6 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
308
313
|
},
|
|
309
314
|
},
|
|
310
315
|
|
|
311
|
-
...overrides
|
|
316
|
+
...overrides
|
|
312
317
|
)
|
|
313
318
|
}
|
package/lint-staged.config.mjs
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
const config = {
|
|
2
|
-
'*.{js,mjs,cjs,ts,tsx,vue}': [
|
|
3
|
-
|
|
4
|
-
'prettier --write',
|
|
5
|
-
],
|
|
6
|
-
'*.{json,md,yml,yaml,css,scss,html}': [
|
|
7
|
-
'prettier --write',
|
|
8
|
-
],
|
|
2
|
+
'*.{js,mjs,cjs,ts,tsx,vue}': ['eslint --fix', 'prettier --write'],
|
|
3
|
+
'*.{json,md,yml,yaml,css,scss,html}': ['prettier --write'],
|
|
9
4
|
}
|
|
10
5
|
|
|
11
6
|
export default config
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
|
-
import { hasCommand, run } from './shared.mjs'
|
|
4
|
+
import { hasCommand, run, runSafe } from './shared.mjs'
|
|
5
5
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
const SCAN_SCRIPT = join(__dirname, '
|
|
7
|
+
const SCAN_SCRIPT = join(__dirname, '..', 'security', 'scan.mjs')
|
|
8
8
|
|
|
9
9
|
function runIfExists(scriptPath, command) {
|
|
10
10
|
if (!existsSync(scriptPath)) {
|
|
@@ -20,6 +20,31 @@ if (!hasCommand('pnpm')) {
|
|
|
20
20
|
process.exit(1)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function enforceVerifiedAccountIfEnabled() {
|
|
24
|
+
if (process.env.REQUIRE_VERIFIED_ACCOUNT !== '1') {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const userEmail = runSafe('git config --get user.email')
|
|
29
|
+
const userName = runSafe('git config --get user.name')
|
|
30
|
+
const commitSign = runSafe('git config --get commit.gpgsign').toLowerCase()
|
|
31
|
+
|
|
32
|
+
if (!userEmail || !userName) {
|
|
33
|
+
console.error('\nCOMMIT FAILED: REQUIRE_VERIFIED_ACCOUNT=1 but git user identity is incomplete.\n')
|
|
34
|
+
console.error('Set both user.name and user.email first.')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (commitSign !== 'true') {
|
|
39
|
+
console.error('\nCOMMIT FAILED: REQUIRE_VERIFIED_ACCOUNT=1 but commit signing is disabled.\n')
|
|
40
|
+
console.error('Run: git config commit.gpgsign true')
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── 0. Optional strict verified-account gate ─────────────────────────────────
|
|
46
|
+
enforceVerifiedAccountIfEnabled()
|
|
47
|
+
|
|
23
48
|
// ── 1. Security scan — blocks on critical/high findings ───────────────────────
|
|
24
49
|
run(`node "${SCAN_SCRIPT}"`, { stdio: 'inherit' })
|
|
25
50
|
|
|
@@ -28,13 +53,17 @@ run(`node "${SCAN_SCRIPT}"`, { stdio: 'inherit' })
|
|
|
28
53
|
runIfExists('./safe-reinstall.sh', 'bash ./safe-reinstall.sh --check-only')
|
|
29
54
|
if (process.env.SKIP_GLOBAL_SCAN === '1') {
|
|
30
55
|
console.log('\nSkipping global IOC scan (SKIP_GLOBAL_SCAN=1).')
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
56
|
+
} else {
|
|
33
57
|
runIfExists('./scan-global.sh', 'bash ./scan-global.sh')
|
|
34
58
|
}
|
|
35
59
|
|
|
36
60
|
// ── 3. Lint-staged — auto-fix & format staged files ───────────────────────────
|
|
37
61
|
console.log('\nRunning lint-staged (auto-fix staged files)...')
|
|
38
|
-
|
|
62
|
+
if (existsSync('./lint-staged.config.mjs')) {
|
|
63
|
+
run('pnpm exec lint-staged --config lint-staged.config.mjs', { stdio: 'inherit' })
|
|
64
|
+
} else {
|
|
65
|
+
// Fallback to package.json "lint-staged" block when config file is missing.
|
|
66
|
+
run('pnpm exec lint-staged', { stdio: 'inherit' })
|
|
67
|
+
}
|
|
39
68
|
|
|
40
69
|
console.log('\nCOMMIT CHECKS PASSED\n')
|
|
@@ -54,13 +54,21 @@ const ZERO_SHA = '0000000000000000000000000000000000000000'
|
|
|
54
54
|
*/
|
|
55
55
|
function parsePushRefs() {
|
|
56
56
|
let stdin = ''
|
|
57
|
-
try {
|
|
58
|
-
|
|
57
|
+
try {
|
|
58
|
+
stdin = readFileSync('/dev/stdin', 'utf8')
|
|
59
|
+
} catch {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
59
62
|
|
|
60
|
-
return stdin
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
return stdin
|
|
64
|
+
.trim()
|
|
65
|
+
.split('\n')
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.map(line => {
|
|
68
|
+
const parts = line.trim().split(/\s+/)
|
|
69
|
+
return { localSha: parts[1] ?? '', remoteSha: parts[3] ?? ZERO_SHA }
|
|
70
|
+
})
|
|
71
|
+
.filter(({ localSha }) => localSha && localSha !== ZERO_SHA)
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
// ─── Collect files in push range ───────────────────────────────────────────────
|
|
@@ -82,6 +90,76 @@ function collectPushFiles(refs) {
|
|
|
82
90
|
return [...seen]
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Collect commits in push range(s).
|
|
95
|
+
* @param {{ localSha: string, remoteSha: string }[]} refs
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
*/
|
|
98
|
+
function collectPushCommits(refs) {
|
|
99
|
+
const seen = new Set()
|
|
100
|
+
for (const { localSha, remoteSha } of refs) {
|
|
101
|
+
const range = remoteSha === ZERO_SHA ? localSha : `${remoteSha}..${localSha}`
|
|
102
|
+
const output = runSafe(`git rev-list ${range}`)
|
|
103
|
+
if (!output) continue
|
|
104
|
+
for (const sha of output.split('\n').filter(Boolean)) {
|
|
105
|
+
seen.add(sha)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [...seen]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Enforce signed + verified commits in push range when REQUIRE_VERIFIED_ACCOUNT=1.
|
|
113
|
+
* Git signature status reference (%G?):
|
|
114
|
+
* G = good signature, N = no signature, B = bad signature, etc.
|
|
115
|
+
* @param {string[]} commits
|
|
116
|
+
*/
|
|
117
|
+
function enforceVerifiedAccountIfEnabled(commits) {
|
|
118
|
+
if (process.env.REQUIRE_VERIFIED_ACCOUNT !== '1') {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const userEmail = runSafe('git config --get user.email')
|
|
123
|
+
const userName = runSafe('git config --get user.name')
|
|
124
|
+
const commitSign = runSafe('git config --get commit.gpgsign').toLowerCase()
|
|
125
|
+
|
|
126
|
+
if (!userEmail || !userName) {
|
|
127
|
+
process.stderr.write(
|
|
128
|
+
`\n${YLW}${BOLD}✖ PUSH BLOCKED${R} — REQUIRE_VERIFIED_ACCOUNT=1 but git user identity is incomplete.\n`
|
|
129
|
+
)
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (commitSign !== 'true') {
|
|
134
|
+
process.stderr.write(
|
|
135
|
+
`\n${YLW}${BOLD}✖ PUSH BLOCKED${R} — REQUIRE_VERIFIED_ACCOUNT=1 but commit signing is disabled.\n`
|
|
136
|
+
)
|
|
137
|
+
process.stderr.write(`${DIM}Run: git config commit.gpgsign true${R}\n\n`)
|
|
138
|
+
process.exit(1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const invalid = []
|
|
142
|
+
for (const sha of commits) {
|
|
143
|
+
const status = runSafe(`git log -1 --format=%G? ${sha}`)
|
|
144
|
+
if (status !== 'G') {
|
|
145
|
+
const subject = runSafe(`git log -1 --format=%s ${sha}`)
|
|
146
|
+
invalid.push({ sha, status: status || '?', subject })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (invalid.length > 0) {
|
|
151
|
+
process.stderr.write(`\n${YLW}${BOLD}✖ PUSH BLOCKED${R} — found unverified commit signature(s).\n`)
|
|
152
|
+
invalid.slice(0, 10).forEach(({ sha, status, subject }) => {
|
|
153
|
+
process.stderr.write(` - ${sha.slice(0, 10)} status=${status} ${subject}\n`)
|
|
154
|
+
})
|
|
155
|
+
if (invalid.length > 10) {
|
|
156
|
+
process.stderr.write(` ...and ${invalid.length - 10} more\n`)
|
|
157
|
+
}
|
|
158
|
+
process.stderr.write(`${DIM}Set REQUIRE_VERIFIED_ACCOUNT=0 to disable this gate.${R}\n\n`)
|
|
159
|
+
process.exit(1)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
85
163
|
// ─── Scan committed files ──────────────────────────────────────────────────────
|
|
86
164
|
|
|
87
165
|
/**
|
|
@@ -98,10 +176,14 @@ function scanFiles(files, repoRoot) {
|
|
|
98
176
|
let skipped = 0
|
|
99
177
|
|
|
100
178
|
for (const filePath of files) {
|
|
101
|
-
if (shouldSkip(filePath)) {
|
|
179
|
+
if (shouldSkip(filePath)) {
|
|
180
|
+
skipped++
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
102
183
|
|
|
103
|
-
const content =
|
|
104
|
-
|
|
184
|
+
const content =
|
|
185
|
+
runSafe(`git show "HEAD:${filePath}"`) ||
|
|
186
|
+
(() => {
|
|
105
187
|
const abs = join(repoRoot, filePath)
|
|
106
188
|
return existsSync(abs) ? readFileSync(abs, 'utf8') : ''
|
|
107
189
|
})()
|
|
@@ -111,7 +193,7 @@ function scanFiles(files, repoRoot) {
|
|
|
111
193
|
const lines = content.split('\n')
|
|
112
194
|
findings.push(
|
|
113
195
|
...collectPatternFindings(lines, isConfigFile(filePath), filePath),
|
|
114
|
-
...collectObfuscatedLineFindings(lines, filePath)
|
|
196
|
+
...collectObfuscatedLineFindings(lines, filePath)
|
|
115
197
|
)
|
|
116
198
|
}
|
|
117
199
|
|
|
@@ -133,24 +215,29 @@ function scanDependencies(repoRoot) {
|
|
|
133
215
|
if (!existsSync(pkgJsonPath)) return []
|
|
134
216
|
|
|
135
217
|
let pkg
|
|
136
|
-
try {
|
|
137
|
-
|
|
218
|
+
try {
|
|
219
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
|
|
220
|
+
} catch {
|
|
221
|
+
return []
|
|
222
|
+
}
|
|
138
223
|
|
|
139
224
|
const depFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
140
|
-
return depFields.flatMap(
|
|
225
|
+
return depFields.flatMap(field => {
|
|
141
226
|
const deps = pkg[field]
|
|
142
227
|
if (!deps || typeof deps !== 'object') return []
|
|
143
|
-
return Object.keys(deps).flatMap(
|
|
228
|
+
return Object.keys(deps).flatMap(name => {
|
|
144
229
|
const similar = detectTyposquat(name)
|
|
145
230
|
return similar
|
|
146
|
-
? [
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
231
|
+
? [
|
|
232
|
+
{
|
|
233
|
+
pkg: name,
|
|
234
|
+
patternId: 'typosquat',
|
|
235
|
+
severity: 'high',
|
|
236
|
+
message: `Possible typosquat of "${similar}" in ${field} — verify the package name`,
|
|
237
|
+
lineContent: `"${name}" is 1 edit away from "${similar}"`,
|
|
238
|
+
category: 'supply-chain',
|
|
239
|
+
},
|
|
240
|
+
]
|
|
154
241
|
: []
|
|
155
242
|
})
|
|
156
243
|
})
|
|
@@ -166,7 +253,7 @@ function runAudit() {
|
|
|
166
253
|
return []
|
|
167
254
|
}
|
|
168
255
|
|
|
169
|
-
process.stdout.write(
|
|
256
|
+
process.stdout.write('\nRunning pnpm audit --audit-level=high...\n')
|
|
170
257
|
const result = runSafe('pnpm audit --audit-level=high 2>&1')
|
|
171
258
|
if (!result) return []
|
|
172
259
|
|
|
@@ -176,14 +263,16 @@ function runAudit() {
|
|
|
176
263
|
if (hasVulns) {
|
|
177
264
|
console.log(`\n${BOLD}pnpm audit output:${R}`)
|
|
178
265
|
console.log(`${DIM}${result}${R}`)
|
|
179
|
-
return [
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
266
|
+
return [
|
|
267
|
+
{
|
|
268
|
+
pkg: 'pnpm-audit',
|
|
269
|
+
patternId: 'known-cve',
|
|
270
|
+
severity: 'high',
|
|
271
|
+
message: "pnpm audit detected known CVEs — run 'pnpm audit --fix' or pin safe versions",
|
|
272
|
+
lineContent: lines.find(l => /vulnerabilit/i.test(l)) ?? result.slice(0, 120),
|
|
273
|
+
category: 'supply-chain',
|
|
274
|
+
},
|
|
275
|
+
]
|
|
187
276
|
}
|
|
188
277
|
|
|
189
278
|
process.stdout.write(`${GRN}pnpm audit: no known vulnerabilities found.${R}\n`)
|
|
@@ -195,25 +284,30 @@ function runAudit() {
|
|
|
195
284
|
function main() {
|
|
196
285
|
if (process.env.SKIP_SECURITY_SCAN === '1') {
|
|
197
286
|
process.stderr.write(
|
|
198
|
-
`\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — pre-push security checks bypassed.${R}\n`
|
|
199
|
-
|
|
287
|
+
`\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — pre-push security checks bypassed.${R}\n` +
|
|
288
|
+
`${YLW} This bypass is intentional and has been logged.${R}\n\n`
|
|
200
289
|
)
|
|
201
290
|
return
|
|
202
291
|
}
|
|
203
292
|
|
|
204
293
|
const repoRoot = getRepoRoot()
|
|
205
294
|
const refs = parsePushRefs()
|
|
295
|
+
const commits = refs.length > 0 ? collectPushCommits(refs) : [runSafe('git rev-parse HEAD')].filter(Boolean)
|
|
296
|
+
|
|
297
|
+
// ── 0. Optional strict verified-account gate ───────────────────────────────
|
|
298
|
+
enforceVerifiedAccountIfEnabled(commits)
|
|
206
299
|
|
|
207
300
|
printScanHeader('@archipelago/pre-push', 'full-branch security gate')
|
|
208
301
|
|
|
209
|
-
const files =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
302
|
+
const files =
|
|
303
|
+
refs.length > 0
|
|
304
|
+
? collectPushFiles(refs)
|
|
305
|
+
: (runSafe('git diff-tree --no-commit-id --name-only -r HEAD') || '')
|
|
306
|
+
.split('\n')
|
|
307
|
+
.filter(f => SCAN_EXTENSIONS.has(extname(f)))
|
|
214
308
|
|
|
215
|
-
const fileFindings
|
|
216
|
-
const depsFindings
|
|
309
|
+
const fileFindings = scanFiles(files, repoRoot)
|
|
310
|
+
const depsFindings = scanDependencies(repoRoot)
|
|
217
311
|
const auditFindings = runAudit()
|
|
218
312
|
|
|
219
313
|
const allFindings = sortFindings([...fileFindings, ...depsFindings, ...auditFindings])
|
|
@@ -147,7 +147,8 @@ export const FILE_PATTERNS = [
|
|
|
147
147
|
{
|
|
148
148
|
id: 'shuffle-cipher-iife',
|
|
149
149
|
severity: 'critical',
|
|
150
|
-
message:
|
|
150
|
+
message:
|
|
151
|
+
'Shuffle-cipher IIFE — RC4-variant string decoder used to hide payload strings (seen in event-stream attack)',
|
|
151
152
|
// Matches: (function(l,e){ ... })("...", number) — the typical 2-arg encoded-string bootstrapper
|
|
152
153
|
// [\s\S] instead of [^}] so the body can contain nested {}
|
|
153
154
|
regex: /\(function\s*\(\w\s*,\s*\w\)[\s\S]{20,}?\}\)\s*\(\s*["'][^"']{4,}["']\s*,\s*\d{4,}\s*\)/,
|
|
@@ -178,7 +179,8 @@ export const FILE_PATTERNS = [
|
|
|
178
179
|
{
|
|
179
180
|
id: 'function-constructor-via-array',
|
|
180
181
|
severity: 'critical',
|
|
181
|
-
message:
|
|
182
|
+
message:
|
|
183
|
+
'Function constructor accessed via decoded array — used to run hidden payload without calling new Function() directly',
|
|
182
184
|
// Matches: var x = sfL[EKc] where the array was built from a string decoder
|
|
183
185
|
// Generalised: identifier assigned from identifier[short-identifier] then called as a function
|
|
184
186
|
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*\)/,
|
|
@@ -234,7 +236,8 @@ export const FILE_PATTERNS = [
|
|
|
234
236
|
{
|
|
235
237
|
id: 'vue-v-html',
|
|
236
238
|
severity: 'medium',
|
|
237
|
-
message:
|
|
239
|
+
message:
|
|
240
|
+
'v-html directive detected — ensure content is sanitised (e.g. DOMPurify) and never bind unsanitised user input',
|
|
238
241
|
// Matches both :v-html and v-html= in .vue template strings captured in JS
|
|
239
242
|
regex: /v-html\s*=/,
|
|
240
243
|
category: 'xss',
|
|
@@ -306,8 +309,11 @@ export const FILE_PATTERNS = [
|
|
|
306
309
|
},
|
|
307
310
|
{
|
|
308
311
|
id: 'path-traversal-dotdot',
|
|
309
|
-
|
|
310
|
-
|
|
312
|
+
// Downgraded to medium: internal tooling uses join(__dirname, '..', 'security', ...)
|
|
313
|
+
// which is safe fixed-path navigation, not user-supplied input. Only upgrade back
|
|
314
|
+
// to 'critical' if you start accepting external path segments at runtime.
|
|
315
|
+
severity: 'medium',
|
|
316
|
+
message: 'Literal path traversal sequence (../) in source — verify this is not user-supplied input',
|
|
311
317
|
regex: /['"]\.\.[/\\]/,
|
|
312
318
|
category: 'path-traversal',
|
|
313
319
|
},
|
|
@@ -452,15 +458,7 @@ export const CONFIG_FILE_PATTERNS = [
|
|
|
452
458
|
// ─── Extension allow-list ──────────────────────────────────────────────────────
|
|
453
459
|
|
|
454
460
|
/** Source file extensions to include in the scan. */
|
|
455
|
-
export const SCAN_EXTENSIONS = new Set([
|
|
456
|
-
'.js',
|
|
457
|
-
'.mjs',
|
|
458
|
-
'.cjs',
|
|
459
|
-
'.ts',
|
|
460
|
-
'.mts',
|
|
461
|
-
'.cts',
|
|
462
|
-
'.vue',
|
|
463
|
-
])
|
|
461
|
+
export const SCAN_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.vue'])
|
|
464
462
|
|
|
465
463
|
// ─── Typosquatting reference list ──────────────────────────────────────────────
|
|
466
464
|
|
|
@@ -470,29 +468,83 @@ export const SCAN_EXTENSIONS = new Set([
|
|
|
470
468
|
*/
|
|
471
469
|
export const POPULAR_PACKAGES = [
|
|
472
470
|
// Frameworks & meta-frameworks
|
|
473
|
-
'react',
|
|
471
|
+
'react',
|
|
472
|
+
'vue',
|
|
473
|
+
'svelte',
|
|
474
|
+
'angular',
|
|
475
|
+
'next',
|
|
476
|
+
'nuxt',
|
|
477
|
+
'remix',
|
|
478
|
+
'astro',
|
|
474
479
|
// Build tools
|
|
475
|
-
'vite',
|
|
480
|
+
'vite',
|
|
481
|
+
'webpack',
|
|
482
|
+
'rollup',
|
|
483
|
+
'parcel',
|
|
484
|
+
'esbuild',
|
|
485
|
+
'turbopack',
|
|
476
486
|
// Styling
|
|
477
|
-
'tailwindcss',
|
|
487
|
+
'tailwindcss',
|
|
488
|
+
'postcss',
|
|
489
|
+
'autoprefixer',
|
|
490
|
+
'sass',
|
|
491
|
+
'less',
|
|
478
492
|
// Utility libraries
|
|
479
|
-
'lodash',
|
|
493
|
+
'lodash',
|
|
494
|
+
'axios',
|
|
495
|
+
'dayjs',
|
|
496
|
+
'moment',
|
|
497
|
+
'date-fns',
|
|
498
|
+
'zod',
|
|
499
|
+
'yup',
|
|
480
500
|
// State management
|
|
481
|
-
'pinia',
|
|
501
|
+
'pinia',
|
|
502
|
+
'vuex',
|
|
503
|
+
'redux',
|
|
504
|
+
'mobx',
|
|
505
|
+
'zustand',
|
|
506
|
+
'jotai',
|
|
482
507
|
// Routing
|
|
483
|
-
'react-router',
|
|
508
|
+
'react-router',
|
|
509
|
+
'vue-router',
|
|
484
510
|
// Testing
|
|
485
|
-
'vitest',
|
|
511
|
+
'vitest',
|
|
512
|
+
'jest',
|
|
513
|
+
'mocha',
|
|
514
|
+
'chai',
|
|
515
|
+
'playwright',
|
|
516
|
+
'cypress',
|
|
486
517
|
// TypeScript
|
|
487
|
-
'typescript',
|
|
518
|
+
'typescript',
|
|
519
|
+
'ts-node',
|
|
520
|
+
'tsx',
|
|
488
521
|
// Linting
|
|
489
|
-
'eslint',
|
|
522
|
+
'eslint',
|
|
523
|
+
'prettier',
|
|
524
|
+
'stylelint',
|
|
490
525
|
// Security / auth
|
|
491
|
-
'bcrypt',
|
|
526
|
+
'bcrypt',
|
|
527
|
+
'jsonwebtoken',
|
|
528
|
+
'passport',
|
|
529
|
+
'helmet',
|
|
530
|
+
'cors',
|
|
492
531
|
// Database
|
|
493
|
-
'prisma',
|
|
532
|
+
'prisma',
|
|
533
|
+
'sequelize',
|
|
534
|
+
'mongoose',
|
|
535
|
+
'typeorm',
|
|
536
|
+
'knex',
|
|
537
|
+
'drizzle-orm',
|
|
494
538
|
// Node utilities
|
|
495
|
-
'express',
|
|
539
|
+
'express',
|
|
540
|
+
'fastify',
|
|
541
|
+
'koa',
|
|
542
|
+
'hapi',
|
|
543
|
+
'socket.io',
|
|
544
|
+
'ws',
|
|
545
|
+
'nodemailer',
|
|
496
546
|
// Package tooling
|
|
497
|
-
'pnpm',
|
|
547
|
+
'pnpm',
|
|
548
|
+
'yarn',
|
|
549
|
+
'npm',
|
|
498
550
|
]
|