@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 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", "javascriptreact",
86
- "typescript", "typescriptreact",
87
- "vue", "json", "jsonc", "markdown"
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
- 'CallExpression[callee.object.name="document"][callee.property.name=/^write(ln)?$/]',
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', message: 'child_process is not allowed in build config files — supply-chain risk' },
172
- { name: 'node:child_process', message: 'child_process is not allowed in build config files — supply-chain risk' },
173
- { name: 'http', message: 'http is not allowed in build config files — exfiltration risk' },
174
- { name: 'node:http', message: 'http is not allowed in build config files — exfiltration risk' },
175
- { name: 'https', message: 'https is not allowed in build config files — exfiltration risk' },
176
- { name: 'node:https', message: 'https is not allowed in build config files — exfiltration risk' },
177
- { name: 'dns', message: 'dns is not allowed in build config files — DNS tunnelling risk' },
178
- { name: 'node:dns', message: 'dns is not allowed in build config files — DNS tunnelling risk' },
179
- { name: 'net', message: 'net is not allowed in build config files — reverse-shell risk' },
180
- { name: 'node:net', message: 'net is not allowed in build config files — reverse-shell risk' },
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
- // ── Compatibility mode for existing codebases ─────────────────────
206
- // Keep the security-sensitive rules as errors, but avoid blocking
207
- // commits on purely stylistic differences in current components/tools.
208
- 'style/semi': 'off',
209
- 'style/quotes': 'off',
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': ['error', {
243
- singleline: 3,
244
- multiline: 1,
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
  }
@@ -1,11 +1,6 @@
1
1
  const config = {
2
- '*.{js,mjs,cjs,ts,tsx,vue}': [
3
- 'eslint --fix',
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@archpublicwebsite/eslint-config",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "author": "Archipelago Hotels",
5
5
  "description": "Reusable ESLint flat config and git-hook toolkit for Archipelago projects",
6
6
  "type": "module",
@@ -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, '../security/scan.mjs')
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
- run('pnpm lint-staged', { stdio: 'inherit' })
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 { stdin = readFileSync('/dev/stdin', 'utf8') }
58
- catch { return [] }
57
+ try {
58
+ stdin = readFileSync('/dev/stdin', 'utf8')
59
+ } catch {
60
+ return []
61
+ }
59
62
 
60
- return stdin.trim().split('\n').filter(Boolean).map((line) => {
61
- const parts = line.trim().split(/\s+/)
62
- return { localSha: parts[1] ?? '', remoteSha: parts[3] ?? ZERO_SHA }
63
- }).filter(({ localSha }) => localSha && localSha !== ZERO_SHA)
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)) { skipped++; continue }
179
+ if (shouldSkip(filePath)) {
180
+ skipped++
181
+ continue
182
+ }
102
183
 
103
- const content = runSafe(`git show "HEAD:${filePath}"`)
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 { pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) }
137
- catch { return [] }
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((field) => {
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((name) => {
228
+ return Object.keys(deps).flatMap(name => {
144
229
  const similar = detectTyposquat(name)
145
230
  return similar
146
- ? [{
147
- pkg: name,
148
- patternId: 'typosquat',
149
- severity: 'high',
150
- message: `Possible typosquat of "${similar}" in ${field} — verify the package name`,
151
- lineContent: `"${name}" is 1 edit away from "${similar}"`,
152
- category: 'supply-chain',
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(`\nRunning pnpm audit --audit-level=high...\n`)
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
- pkg: 'pnpm-audit',
181
- patternId: 'known-cve',
182
- severity: 'high',
183
- message: `pnpm audit detected known CVEs — run 'pnpm audit --fix' or pin safe versions`,
184
- lineContent: lines.find(l => /vulnerabilit/i.test(l)) ?? result.slice(0, 120),
185
- category: 'supply-chain',
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
- + `${YLW} This bypass is intentional and has been logged.${R}\n\n`,
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 = refs.length > 0
210
- ? collectPushFiles(refs)
211
- : (runSafe('git diff-tree --no-commit-id --name-only -r HEAD') || '')
212
- .split('\n')
213
- .filter(f => SCAN_EXTENSIONS.has(extname(f)))
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 = scanFiles(files, repoRoot)
216
- const depsFindings = scanDependencies(repoRoot)
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: 'Shuffle-cipher IIFE — RC4-variant string decoder used to hide payload strings (seen in event-stream attack)',
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: 'Function constructor accessed via decoded array — used to run hidden payload without calling new Function() directly',
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: 'v-html directive detected — ensure content is sanitised (e.g. DOMPurify) and never bind unsanitised user input',
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
- severity: 'critical',
310
- message: 'Literal path traversal sequence (../) in source — remove or validate user-supplied path segments',
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', 'vue', 'svelte', 'angular', 'next', 'nuxt', 'remix', 'astro',
471
+ 'react',
472
+ 'vue',
473
+ 'svelte',
474
+ 'angular',
475
+ 'next',
476
+ 'nuxt',
477
+ 'remix',
478
+ 'astro',
474
479
  // Build tools
475
- 'vite', 'webpack', 'rollup', 'parcel', 'esbuild', 'turbopack',
480
+ 'vite',
481
+ 'webpack',
482
+ 'rollup',
483
+ 'parcel',
484
+ 'esbuild',
485
+ 'turbopack',
476
486
  // Styling
477
- 'tailwindcss', 'postcss', 'autoprefixer', 'sass', 'less',
487
+ 'tailwindcss',
488
+ 'postcss',
489
+ 'autoprefixer',
490
+ 'sass',
491
+ 'less',
478
492
  // Utility libraries
479
- 'lodash', 'axios', 'dayjs', 'moment', 'date-fns', 'zod', 'yup',
493
+ 'lodash',
494
+ 'axios',
495
+ 'dayjs',
496
+ 'moment',
497
+ 'date-fns',
498
+ 'zod',
499
+ 'yup',
480
500
  // State management
481
- 'pinia', 'vuex', 'redux', 'mobx', 'zustand', 'jotai',
501
+ 'pinia',
502
+ 'vuex',
503
+ 'redux',
504
+ 'mobx',
505
+ 'zustand',
506
+ 'jotai',
482
507
  // Routing
483
- 'react-router', 'vue-router',
508
+ 'react-router',
509
+ 'vue-router',
484
510
  // Testing
485
- 'vitest', 'jest', 'mocha', 'chai', 'playwright', 'cypress',
511
+ 'vitest',
512
+ 'jest',
513
+ 'mocha',
514
+ 'chai',
515
+ 'playwright',
516
+ 'cypress',
486
517
  // TypeScript
487
- 'typescript', 'ts-node', 'tsx',
518
+ 'typescript',
519
+ 'ts-node',
520
+ 'tsx',
488
521
  // Linting
489
- 'eslint', 'prettier', 'stylelint',
522
+ 'eslint',
523
+ 'prettier',
524
+ 'stylelint',
490
525
  // Security / auth
491
- 'bcrypt', 'jsonwebtoken', 'passport', 'helmet', 'cors',
526
+ 'bcrypt',
527
+ 'jsonwebtoken',
528
+ 'passport',
529
+ 'helmet',
530
+ 'cors',
492
531
  // Database
493
- 'prisma', 'sequelize', 'mongoose', 'typeorm', 'knex', 'drizzle-orm',
532
+ 'prisma',
533
+ 'sequelize',
534
+ 'mongoose',
535
+ 'typeorm',
536
+ 'knex',
537
+ 'drizzle-orm',
494
538
  // Node utilities
495
- 'express', 'fastify', 'koa', 'hapi', 'socket.io', 'ws', 'nodemailer',
539
+ 'express',
540
+ 'fastify',
541
+ 'koa',
542
+ 'hapi',
543
+ 'socket.io',
544
+ 'ws',
545
+ 'nodemailer',
496
546
  // Package tooling
497
- 'pnpm', 'yarn', 'npm',
547
+ 'pnpm',
548
+ 'yarn',
549
+ 'npm',
498
550
  ]