@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/README.md
CHANGED
|
@@ -120,6 +120,16 @@ If you are extending or regenerating this package, keep the workflow explicit:
|
|
|
120
120
|
- Re-run the package setup flow when changing VS Code or hook behavior.
|
|
121
121
|
- Prefer explicit examples that show what the consumer project should add.
|
|
122
122
|
|
|
123
|
+
## Refactor/New File Rules
|
|
124
|
+
|
|
125
|
+
When refactoring or creating component files, keep every package aligned to the shared eslint-config contract:
|
|
126
|
+
|
|
127
|
+
1. Keep each package `eslint.config.mjs` on `createArchipelagoConfig(...)` from `@archpublicwebsite/eslint-config`.
|
|
128
|
+
2. Do not add package-local parser stacks that diverge from the shared config unless there is an approved exception.
|
|
129
|
+
3. Run `pnpm lint:check` and `pnpm typecheck` before commit.
|
|
130
|
+
4. If a package needs custom lint behavior, add it as a small override block in that package config while preserving shared base rules.
|
|
131
|
+
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.
|
|
132
|
+
|
|
123
133
|
## Manual setup
|
|
124
134
|
|
|
125
135
|
If you need to rerun the bootstrap manually:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
rules: {
|
|
3
|
+
'type-enum': [
|
|
4
|
+
2,
|
|
5
|
+
'always',
|
|
6
|
+
[
|
|
7
|
+
'feat',
|
|
8
|
+
'fix',
|
|
9
|
+
'docs',
|
|
10
|
+
'refactor',
|
|
11
|
+
'perf',
|
|
12
|
+
'test',
|
|
13
|
+
'build',
|
|
14
|
+
'ci',
|
|
15
|
+
'chore',
|
|
16
|
+
'style',
|
|
17
|
+
'types',
|
|
18
|
+
'workflow',
|
|
19
|
+
'release',
|
|
20
|
+
'deps',
|
|
21
|
+
'revert',
|
|
22
|
+
],
|
|
23
|
+
],
|
|
24
|
+
'type-case': [2, 'always', 'lower-case'],
|
|
25
|
+
'type-empty': [2, 'never'],
|
|
26
|
+
'scope-case': [2, 'always', ['lower-case', 'kebab-case']],
|
|
27
|
+
'subject-empty': [2, 'never'],
|
|
28
|
+
'subject-case': [0],
|
|
29
|
+
'header-max-length': [2, 'always', 100],
|
|
30
|
+
},
|
|
31
|
+
}
|
package/eslint.config.mjs
CHANGED
|
@@ -29,8 +29,12 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
29
29
|
'**/coverage/**',
|
|
30
30
|
'**/.next/**',
|
|
31
31
|
'**/public/**',
|
|
32
|
+
'**/*.md',
|
|
32
33
|
'**/*.d.ts',
|
|
33
34
|
],
|
|
35
|
+
linterOptions: {
|
|
36
|
+
reportUnusedDisableDirectives: 'off',
|
|
37
|
+
},
|
|
34
38
|
},
|
|
35
39
|
|
|
36
40
|
// ── Tailwind ──────────────────────────────────────────────────────────────
|
|
@@ -83,10 +87,8 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
83
87
|
},
|
|
84
88
|
// document.write / document.writeln
|
|
85
89
|
{
|
|
86
|
-
selector:
|
|
87
|
-
|
|
88
|
-
message:
|
|
89
|
-
'[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',
|
|
90
92
|
},
|
|
91
93
|
// Object.__proto__ assignment (belt-and-suspenders with no-prototype-builtins)
|
|
92
94
|
{
|
|
@@ -164,16 +166,19 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
164
166
|
rules: {
|
|
165
167
|
'no-restricted-imports': [
|
|
166
168
|
'error',
|
|
167
|
-
{ name: 'child_process',
|
|
168
|
-
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
{ name: '
|
|
173
|
-
{ name: '
|
|
174
|
-
{ name: '
|
|
175
|
-
{ name: '
|
|
176
|
-
{ 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' },
|
|
177
182
|
],
|
|
178
183
|
},
|
|
179
184
|
},
|
|
@@ -198,6 +203,29 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
198
203
|
'antfu/if-newline': 'off',
|
|
199
204
|
'antfu/no-import-dist': 'off',
|
|
200
205
|
|
|
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' }],
|
|
212
|
+
'style/quote-props': 'off',
|
|
213
|
+
'style/no-multi-spaces': 'off',
|
|
214
|
+
'style/key-spacing': 'off',
|
|
215
|
+
'style/member-delimiter-style': 'off',
|
|
216
|
+
'style/max-statements-per-line': 'off',
|
|
217
|
+
'style/brace-style': 'off',
|
|
218
|
+
'jsdoc/require-returns-description': 'off',
|
|
219
|
+
'regexp/use-ignore-case': 'off',
|
|
220
|
+
'regexp/prefer-w': 'off',
|
|
221
|
+
'unicorn/escape-case': 'off',
|
|
222
|
+
|
|
223
|
+
// Allow progressive tightening later; for now do not block on these.
|
|
224
|
+
'no-unused-vars': 'off',
|
|
225
|
+
'unused-imports/no-unused-imports': 'off',
|
|
226
|
+
'unused-imports/no-unused-vars': 'off',
|
|
227
|
+
'ts/no-unused-vars': 'off',
|
|
228
|
+
|
|
201
229
|
// ── Vue ──────────────────────────────────────────────────────────────
|
|
202
230
|
'vue/multi-word-component-names': 'off',
|
|
203
231
|
'vue/no-required-prop-with-default': 'off',
|
|
@@ -212,26 +240,21 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
212
240
|
'vue/prefer-separate-static-class': 'off',
|
|
213
241
|
'vue/attribute-hyphenation': 'off',
|
|
214
242
|
'vue/define-macros-order': 'off',
|
|
215
|
-
'vue/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
ignoreTemplateLiterals: true,
|
|
225
|
-
ignoreRegExpLiterals: true,
|
|
226
|
-
ignoreHTMLAttributeValues: true,
|
|
227
|
-
ignoreHTMLTextContents: true,
|
|
228
|
-
}],
|
|
243
|
+
'vue/no-v-html': 'off',
|
|
244
|
+
'vue/max-attributes-per-line': [
|
|
245
|
+
'error',
|
|
246
|
+
{
|
|
247
|
+
singleline: 3,
|
|
248
|
+
multiline: 1,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
'vue/max-len': 'off',
|
|
229
252
|
|
|
230
253
|
// ── TypeScript ───────────────────────────────────────────────────────
|
|
231
254
|
'ts/no-use-before-define': 'off',
|
|
232
255
|
'ts/no-empty-object-type': 'off',
|
|
233
|
-
'ts/no-explicit-any': '
|
|
234
|
-
'ts/no-var-requires': '
|
|
256
|
+
'ts/no-explicit-any': 'off',
|
|
257
|
+
'ts/no-var-requires': 'off',
|
|
235
258
|
// Keep disabled globally to avoid parserServices errors on plain JS
|
|
236
259
|
// config files (tailwind/postcss/vite) during lint-staged runs.
|
|
237
260
|
'ts/consistent-type-imports': 'off',
|
|
@@ -290,6 +313,6 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
290
313
|
},
|
|
291
314
|
},
|
|
292
315
|
|
|
293
|
-
...overrides
|
|
316
|
+
...overrides
|
|
294
317
|
)
|
|
295
318
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archpublicwebsite/eslint-config",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"author": "Archipelago Hotels",
|
|
5
5
|
"description": "Reusable ESLint flat config and git-hook toolkit for Archipelago projects",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"import": "./eslint.config.mjs",
|
|
12
12
|
"default": "./eslint.config.mjs"
|
|
13
13
|
},
|
|
14
|
+
"./prettier": "./prettier.config.mjs",
|
|
15
|
+
"./commitlint": "./commitlint.config.cjs",
|
|
16
|
+
"./lint-staged": "./lint-staged.config.mjs",
|
|
14
17
|
"./tools/*": "./tools/*"
|
|
15
18
|
},
|
|
16
19
|
"repository": {
|
|
@@ -20,6 +23,9 @@
|
|
|
20
23
|
},
|
|
21
24
|
"files": [
|
|
22
25
|
"eslint.config.mjs",
|
|
26
|
+
"prettier.config.mjs",
|
|
27
|
+
"commitlint.config.cjs",
|
|
28
|
+
"lint-staged.config.mjs",
|
|
23
29
|
"tools",
|
|
24
30
|
"README.md"
|
|
25
31
|
],
|
|
@@ -4,7 +4,7 @@ import { existsSync } from 'node:fs'
|
|
|
4
4
|
import { hasCommand, run } 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)) {
|
|
@@ -28,13 +28,17 @@ run(`node "${SCAN_SCRIPT}"`, { stdio: 'inherit' })
|
|
|
28
28
|
runIfExists('./safe-reinstall.sh', 'bash ./safe-reinstall.sh --check-only')
|
|
29
29
|
if (process.env.SKIP_GLOBAL_SCAN === '1') {
|
|
30
30
|
console.log('\nSkipping global IOC scan (SKIP_GLOBAL_SCAN=1).')
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
31
|
+
} else {
|
|
33
32
|
runIfExists('./scan-global.sh', 'bash ./scan-global.sh')
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
// ── 3. Lint-staged — auto-fix & format staged files ───────────────────────────
|
|
37
36
|
console.log('\nRunning lint-staged (auto-fix staged files)...')
|
|
38
|
-
|
|
37
|
+
if (existsSync('./lint-staged.config.mjs')) {
|
|
38
|
+
run('pnpm exec lint-staged --config lint-staged.config.mjs', { stdio: 'inherit' })
|
|
39
|
+
} else {
|
|
40
|
+
// Fallback to package.json "lint-staged" block when config file is missing.
|
|
41
|
+
run('pnpm exec lint-staged', { stdio: 'inherit' })
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
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 ───────────────────────────────────────────────
|
|
@@ -98,10 +106,14 @@ function scanFiles(files, repoRoot) {
|
|
|
98
106
|
let skipped = 0
|
|
99
107
|
|
|
100
108
|
for (const filePath of files) {
|
|
101
|
-
if (shouldSkip(filePath)) {
|
|
109
|
+
if (shouldSkip(filePath)) {
|
|
110
|
+
skipped++
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
102
113
|
|
|
103
|
-
const content =
|
|
104
|
-
|
|
114
|
+
const content =
|
|
115
|
+
runSafe(`git show "HEAD:${filePath}"`) ||
|
|
116
|
+
(() => {
|
|
105
117
|
const abs = join(repoRoot, filePath)
|
|
106
118
|
return existsSync(abs) ? readFileSync(abs, 'utf8') : ''
|
|
107
119
|
})()
|
|
@@ -111,7 +123,7 @@ function scanFiles(files, repoRoot) {
|
|
|
111
123
|
const lines = content.split('\n')
|
|
112
124
|
findings.push(
|
|
113
125
|
...collectPatternFindings(lines, isConfigFile(filePath), filePath),
|
|
114
|
-
...collectObfuscatedLineFindings(lines, filePath)
|
|
126
|
+
...collectObfuscatedLineFindings(lines, filePath)
|
|
115
127
|
)
|
|
116
128
|
}
|
|
117
129
|
|
|
@@ -133,24 +145,29 @@ function scanDependencies(repoRoot) {
|
|
|
133
145
|
if (!existsSync(pkgJsonPath)) return []
|
|
134
146
|
|
|
135
147
|
let pkg
|
|
136
|
-
try {
|
|
137
|
-
|
|
148
|
+
try {
|
|
149
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
|
|
150
|
+
} catch {
|
|
151
|
+
return []
|
|
152
|
+
}
|
|
138
153
|
|
|
139
154
|
const depFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
140
|
-
return depFields.flatMap(
|
|
155
|
+
return depFields.flatMap(field => {
|
|
141
156
|
const deps = pkg[field]
|
|
142
157
|
if (!deps || typeof deps !== 'object') return []
|
|
143
|
-
return Object.keys(deps).flatMap(
|
|
158
|
+
return Object.keys(deps).flatMap(name => {
|
|
144
159
|
const similar = detectTyposquat(name)
|
|
145
160
|
return similar
|
|
146
|
-
? [
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
? [
|
|
162
|
+
{
|
|
163
|
+
pkg: name,
|
|
164
|
+
patternId: 'typosquat',
|
|
165
|
+
severity: 'high',
|
|
166
|
+
message: `Possible typosquat of "${similar}" in ${field} — verify the package name`,
|
|
167
|
+
lineContent: `"${name}" is 1 edit away from "${similar}"`,
|
|
168
|
+
category: 'supply-chain',
|
|
169
|
+
},
|
|
170
|
+
]
|
|
154
171
|
: []
|
|
155
172
|
})
|
|
156
173
|
})
|
|
@@ -166,7 +183,7 @@ function runAudit() {
|
|
|
166
183
|
return []
|
|
167
184
|
}
|
|
168
185
|
|
|
169
|
-
process.stdout.write(
|
|
186
|
+
process.stdout.write('\nRunning pnpm audit --audit-level=high...\n')
|
|
170
187
|
const result = runSafe('pnpm audit --audit-level=high 2>&1')
|
|
171
188
|
if (!result) return []
|
|
172
189
|
|
|
@@ -176,14 +193,16 @@ function runAudit() {
|
|
|
176
193
|
if (hasVulns) {
|
|
177
194
|
console.log(`\n${BOLD}pnpm audit output:${R}`)
|
|
178
195
|
console.log(`${DIM}${result}${R}`)
|
|
179
|
-
return [
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
196
|
+
return [
|
|
197
|
+
{
|
|
198
|
+
pkg: 'pnpm-audit',
|
|
199
|
+
patternId: 'known-cve',
|
|
200
|
+
severity: 'high',
|
|
201
|
+
message: "pnpm audit detected known CVEs — run 'pnpm audit --fix' or pin safe versions",
|
|
202
|
+
lineContent: lines.find(l => /vulnerabilit/i.test(l)) ?? result.slice(0, 120),
|
|
203
|
+
category: 'supply-chain',
|
|
204
|
+
},
|
|
205
|
+
]
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
process.stdout.write(`${GRN}pnpm audit: no known vulnerabilities found.${R}\n`)
|
|
@@ -195,8 +214,8 @@ function runAudit() {
|
|
|
195
214
|
function main() {
|
|
196
215
|
if (process.env.SKIP_SECURITY_SCAN === '1') {
|
|
197
216
|
process.stderr.write(
|
|
198
|
-
`\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — pre-push security checks bypassed.${R}\n`
|
|
199
|
-
|
|
217
|
+
`\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — pre-push security checks bypassed.${R}\n` +
|
|
218
|
+
`${YLW} This bypass is intentional and has been logged.${R}\n\n`
|
|
200
219
|
)
|
|
201
220
|
return
|
|
202
221
|
}
|
|
@@ -206,14 +225,15 @@ function main() {
|
|
|
206
225
|
|
|
207
226
|
printScanHeader('@archipelago/pre-push', 'full-branch security gate')
|
|
208
227
|
|
|
209
|
-
const files =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
228
|
+
const files =
|
|
229
|
+
refs.length > 0
|
|
230
|
+
? collectPushFiles(refs)
|
|
231
|
+
: (runSafe('git diff-tree --no-commit-id --name-only -r HEAD') || '')
|
|
232
|
+
.split('\n')
|
|
233
|
+
.filter(f => SCAN_EXTENSIONS.has(extname(f)))
|
|
214
234
|
|
|
215
|
-
const fileFindings
|
|
216
|
-
const depsFindings
|
|
235
|
+
const fileFindings = scanFiles(files, repoRoot)
|
|
236
|
+
const depsFindings = scanDependencies(repoRoot)
|
|
217
237
|
const auditFindings = runAudit()
|
|
218
238
|
|
|
219
239
|
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
|
]
|