@archpublicwebsite/eslint-config 1.0.15 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # scan-global.sh
4
+ # Scan global node_modules and caches for PolinRider IOC markers.
5
+ # Run once per machine — no need to be inside any repo.
6
+ #
7
+ # Locations scanned:
8
+ # • npm global node_modules (npm root -g)
9
+ # • All nvm node versions (~/.nvm/versions/node/*/lib/node_modules)
10
+ # • pnpm global node_modules (pnpm root -g)
11
+ # • pnpm store (pnpm store path)
12
+ # • npx cache (~/.npm/_npx)
13
+ #
14
+ # Usage:
15
+ # bash scan-global.sh
16
+ # =============================================================================
17
+
18
+ set -euo pipefail
19
+
20
+ GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'
21
+ BLUE='\033[0;34m'; GRAY='\033[0;90m'; NC='\033[0m'
22
+
23
+ step() { echo -e "\n${BLUE}[→]${NC} $1"; }
24
+ ok() { echo -e "${GREEN}[✓]${NC} $1"; }
25
+ warn() { echo -e "${YELLOW}[!]${NC} $1"; }
26
+ skip() { echo -e "${GRAY}[–]${NC} $1"; }
27
+ hr() { echo -e "${BLUE}────────────────────────────────────────────────────${NC}"; }
28
+
29
+ # IOC patterns: payload markers + C2 hosts from the incident report
30
+ IOC_PATTERN="9-0224-2|9-857-1|trongrid\.io|aptoslabs\.com|bsc-dataseed\.binance|publicnode\.com"
31
+
32
+ SCAN_DIRS=() # entries formatted as "label|path"
33
+ TOTAL_HITS=0
34
+
35
+ hr
36
+ echo -e "${BLUE} Global IOC Scan · Post-PolinRider (Jun 2026)${NC}"
37
+ echo -e " Host: $(hostname)"
38
+ hr
39
+
40
+ # ── Collect scan targets ──────────────────────────────────────────────────────
41
+
42
+ # 1. npm global node_modules
43
+ step "Locating npm global node_modules..."
44
+ NPM_GLOBAL=$(npm root -g 2>/dev/null || echo "")
45
+ if [[ -n "$NPM_GLOBAL" && -d "$NPM_GLOBAL" ]]; then
46
+ SCAN_DIRS+=("npm global|$NPM_GLOBAL")
47
+ ok "$NPM_GLOBAL"
48
+ else
49
+ skip "npm global not found"
50
+ fi
51
+
52
+ # 2. All nvm-managed node versions
53
+ step "Locating nvm node_modules..."
54
+ NVM_BASE="${NVM_DIR:-$HOME/.nvm}/versions/node"
55
+ if [[ -d "$NVM_BASE" ]]; then
56
+ while IFS= read -r ver; do
57
+ nm="$ver/lib/node_modules"
58
+ if [[ -d "$nm" ]]; then
59
+ SCAN_DIRS+=("nvm $(basename "$ver")|$nm")
60
+ ok "$nm"
61
+ fi
62
+ done < <(find "$NVM_BASE" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort)
63
+ else
64
+ skip "nvm not found (checked $NVM_BASE)"
65
+ fi
66
+
67
+ # 3. pnpm global node_modules
68
+ step "Locating pnpm global node_modules..."
69
+ if command -v pnpm >/dev/null 2>&1; then
70
+ PNPM_GLOBAL=$(pnpm root -g 2>/dev/null || echo "")
71
+ if [[ -n "$PNPM_GLOBAL" && -d "$PNPM_GLOBAL" ]]; then
72
+ SCAN_DIRS+=("pnpm global|$PNPM_GLOBAL")
73
+ ok "$PNPM_GLOBAL"
74
+ else
75
+ skip "pnpm global node_modules not found"
76
+ fi
77
+ else
78
+ skip "pnpm not installed"
79
+ fi
80
+
81
+ # 4. pnpm store (content-addressable cache)
82
+ step "Locating pnpm store..."
83
+ if command -v pnpm >/dev/null 2>&1; then
84
+ PNPM_STORE=$(pnpm store path 2>/dev/null || echo "")
85
+ if [[ -n "$PNPM_STORE" && -d "$PNPM_STORE" ]]; then
86
+ SCAN_DIRS+=("pnpm store|$PNPM_STORE")
87
+ warn "pnpm store can be large — this may take a moment"
88
+ warn "$PNPM_STORE"
89
+ else
90
+ skip "pnpm store not found"
91
+ fi
92
+ else
93
+ skip "pnpm not installed"
94
+ fi
95
+
96
+ # 5. npx cache
97
+ step "Locating npx cache..."
98
+ NPM_CACHE=$(npm config get cache 2>/dev/null || echo "$HOME/.npm")
99
+ NPX_CACHE="$NPM_CACHE/_npx"
100
+ if [[ -d "$NPX_CACHE" ]]; then
101
+ SCAN_DIRS+=("npx cache|$NPX_CACHE")
102
+ ok "$NPX_CACHE"
103
+ else
104
+ skip "npx cache not found at $NPX_CACHE"
105
+ fi
106
+
107
+ # ── Run scan ──────────────────────────────────────────────────────────────────
108
+
109
+ echo ""
110
+ hr
111
+ echo -e "${BLUE} Scanning ${#SCAN_DIRS[@]} location(s)...${NC}"
112
+ hr
113
+
114
+ for ENTRY in "${SCAN_DIRS[@]}"; do
115
+ LABEL="${ENTRY%%|*}"
116
+ DIR="${ENTRY##*|}"
117
+
118
+ echo -e "\n${BLUE}[→]${NC} ${LABEL}"
119
+ echo -e " ${GRAY}${DIR}${NC}"
120
+
121
+ HITS=$(grep -rl \
122
+ --include="*.js" --include="*.mjs" --include="*.cjs" \
123
+ -E "$IOC_PATTERN" "$DIR" 2>/dev/null || true)
124
+
125
+ if [[ -z "$HITS" ]]; then
126
+ ok "Clean"
127
+ else
128
+ HIT_COUNT=$(echo "$HITS" | grep -c "." || true)
129
+ echo -e "${RED}[✗]${NC} IOC markers found in ${HIT_COUNT} file(s):"
130
+ while IFS= read -r f; do
131
+ echo -e " ${RED}${f}${NC}"
132
+ PREVIEW=$(grep -m1 -oE ".{0,30}(${IOC_PATTERN}).{0,30}" "$f" 2>/dev/null || true)
133
+ [[ -n "$PREVIEW" ]] && echo -e " ${GRAY}→ ${PREVIEW}${NC}"
134
+ done <<< "$HITS"
135
+ TOTAL_HITS=$((TOTAL_HITS + HIT_COUNT))
136
+ fi
137
+ done
138
+
139
+ # ── Summary ───────────────────────────────────────────────────────────────────
140
+
141
+ echo ""
142
+ hr
143
+ if [[ "$TOTAL_HITS" -eq 0 ]]; then
144
+ echo -e "${GREEN} ✓ All ${#SCAN_DIRS[@]} location(s) clean — machine is clear${NC}"
145
+ else
146
+ echo -e "${RED} ✗ IOC markers found in ${TOTAL_HITS} file(s) across global locations${NC}"
147
+ echo ""
148
+ echo -e "${YELLOW} Recommended next steps:${NC}"
149
+ echo -e "${YELLOW} 1. npm cache clean --force${NC}"
150
+ echo -e "${YELLOW} 2. pnpm store prune (if pnpm is installed)${NC}"
151
+ echo -e "${YELLOW} 3. Identify and remove the flagged packages globally${NC}"
152
+ echo -e "${YELLOW} 4. See: Security-Remediation-Runbook-2026-06.md${NC}"
153
+ fi
154
+ hr
155
+ echo ""
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @archipelago/security-scan
4
+ *
5
+ * Pre-commit scanner inspired by socket.dev.
6
+ * Detects malicious code in staged files and supply-chain risks in
7
+ * newly added packages — blocking zero-day attacks before they reach
8
+ * CI or production.
9
+ *
10
+ * ┌─────────────────────────────────────────────────────┐
11
+ * │ What is checked │
12
+ * │ 1. Staged .js/.ts/.vue files — eval, obfuscation, │
13
+ * │ base64 payloads, net/dns/child_process in │
14
+ * │ build-config files, prototype pollution │
15
+ * │ 2. Newly added npm packages — install scripts │
16
+ * │ with curl/wget, base64-exec, credential theft │
17
+ * │ 3. Typosquatting — package names within edit- │
18
+ * │ distance 1 of popular npm packages │
19
+ * └─────────────────────────────────────────────────────┘
20
+ *
21
+ * Environment flags:
22
+ * SKIP_SECURITY_SCAN=1 Bypass all checks (logged loudly to stderr)
23
+ * SECURITY_AUDIT=1 Also run `pnpm audit` and surface known CVEs
24
+ */
25
+
26
+ import { existsSync, readFileSync } from 'node:fs'
27
+ import { extname, join } from 'node:path'
28
+ import {
29
+ CONFIG_FILE_PATTERNS,
30
+ FILE_PATTERNS,
31
+ INSTALL_SCRIPT_PATTERNS,
32
+ POPULAR_PACKAGES,
33
+ SCAN_EXTENSIONS,
34
+ } from './patterns.mjs'
35
+ import {
36
+ getRepoRoot,
37
+ getStagedNameStatus,
38
+ runSafe,
39
+ } from '../git-hooks/shared.mjs'
40
+
41
+ // ─── ANSI colours ──────────────────────────────────────────────────────────────
42
+ const R = '\x1b[0m'
43
+ const RED = '\x1b[31m'
44
+ const YLW = '\x1b[33m'
45
+ const CYN = '\x1b[36m'
46
+ const GRN = '\x1b[32m'
47
+ const BOLD = '\x1b[1m'
48
+ const DIM = '\x1b[2m'
49
+ const HR = `${DIM}${'─'.repeat(62)}${R}`
50
+
51
+ function badge(severity) {
52
+ switch (severity) {
53
+ case 'critical': return `${RED}${BOLD}[CRITICAL]${R}`
54
+ case 'high': return `${RED}[HIGH] ${R}`
55
+ case 'medium': return `${YLW}[MEDIUM] ${R}`
56
+ default: return `${CYN}[LOW] ${R}`
57
+ }
58
+ }
59
+
60
+ function isBlocking(severity) {
61
+ return severity === 'critical' || severity === 'high'
62
+ }
63
+
64
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
65
+
66
+ function isConfigFile(filePath) {
67
+ return CONFIG_FILE_PATTERNS.some(p => p.test(filePath))
68
+ }
69
+
70
+ // Files that intentionally contain dangerous-pattern literals for lint/security
71
+ // rule definitions and scanner fixture tests. Scanning them with FILE_PATTERNS
72
+ // causes self-referential false positives and blocks commits.
73
+ const SELF_REFERENTIAL_SCAN_EXCLUSIONS = [
74
+ 'packages/eslint-config/eslint.config.mjs',
75
+ 'packages/eslint-config/tools/security/patterns.mjs',
76
+ 'packages/eslint-config/tools/security/test-patterns.mjs',
77
+ ]
78
+
79
+ function shouldSkipSelfReferentialFile(filePath) {
80
+ const normalizedPath = filePath.replaceAll('\\', '/')
81
+ return SELF_REFERENTIAL_SCAN_EXCLUSIONS.some(excluded => normalizedPath.endsWith(excluded))
82
+ }
83
+
84
+ function collectPatternFindings(lines, isConfig, filePath) {
85
+ const findings = []
86
+
87
+ for (const pattern of FILE_PATTERNS) {
88
+ if (pattern.configOnly && !isConfig) continue
89
+
90
+ for (let i = 0; i < lines.length; i++) {
91
+ if (pattern.regex.test(lines[i])) {
92
+ findings.push({
93
+ file: filePath,
94
+ line: i + 1,
95
+ lineContent: lines[i].trim().slice(0, 120),
96
+ patternId: pattern.id,
97
+ severity: pattern.severity,
98
+ message: pattern.message,
99
+ })
100
+ }
101
+ }
102
+ }
103
+
104
+ return findings
105
+ }
106
+
107
+ function collectObfuscatedLineFindings(lines, filePath) {
108
+ const findings = []
109
+
110
+ for (let i = 0; i < lines.length; i++) {
111
+ const line = lines[i]
112
+ if (line.length <= 800) continue
113
+
114
+ // Encoded payload lines tend to have unusually high symbol density.
115
+ const nonWord = (line.match(/[^a-zA-Z0-9\s.,;:(){}[\]=<>+\-*/'"_]/g) || []).length
116
+ const density = nonWord / line.length
117
+ if (density <= 0.18) continue
118
+
119
+ findings.push({
120
+ file: filePath,
121
+ line: i + 1,
122
+ lineContent: `[${line.length} chars] ${line.trim().slice(0, 80)}…`,
123
+ patternId: 'long-obfuscated-line',
124
+ severity: 'critical',
125
+ message: `Line is ${line.length} chars with high symbol density (${(density * 100).toFixed(0)}%) — likely an embedded payload (dropper pattern)`,
126
+ })
127
+ }
128
+
129
+ return findings
130
+ }
131
+
132
+ /**
133
+ * Read the staged (index) version of a file so we scan exactly what will be
134
+ * committed — not any unsaved working-tree changes.
135
+ */
136
+ function readStagedContent(filePath) {
137
+ return runSafe(`git show ":${filePath}"`)
138
+ }
139
+
140
+ /**
141
+ * Levenshtein distance between two strings.
142
+ * Used to detect typosquatted package names.
143
+ * @param {string} a
144
+ * @param {string} b
145
+ * @returns {number}
146
+ */
147
+ function levenshtein(a, b) {
148
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i])
149
+ for (let j = 0; j <= b.length; j++) dp[0][j] = j
150
+ for (let i = 1; i <= a.length; i++) {
151
+ for (let j = 1; j <= b.length; j++) {
152
+ dp[i][j] = a[i - 1] === b[j - 1]
153
+ ? dp[i - 1][j - 1]
154
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
155
+ }
156
+ }
157
+ return dp[a.length][b.length]
158
+ }
159
+
160
+ /**
161
+ * Returns the popular package name that the given name is likely a typosquat
162
+ * of (edit distance === 1), or null if no match.
163
+ * @param {string} pkgName
164
+ * @returns {string|null}
165
+ */
166
+ function detectTyposquat(pkgName) {
167
+ // Strip npm scope for comparison
168
+ const bare = pkgName.startsWith('@') ? (pkgName.split('/')[1] ?? pkgName) : pkgName
169
+ if (bare.length < 3) return null
170
+
171
+ for (const popular of POPULAR_PACKAGES) {
172
+ if (bare === popular) return null // exact match is fine
173
+ if (levenshtein(bare.toLowerCase(), popular.toLowerCase()) === 1) return popular
174
+ }
175
+ return null
176
+ }
177
+
178
+ // ─── Stage-file scanner ────────────────────────────────────────────────────────
179
+
180
+ /**
181
+ * @typedef {{ file: string, line: number, lineContent: string,
182
+ * patternId: string, severity: string, message: string }} FileFinding
183
+ */
184
+
185
+ /**
186
+ * Scan every staged JS/TS/Vue file against FILE_PATTERNS.
187
+ * @param {string} _repoRoot (unused here but kept for symmetry)
188
+ * @returns {FileFinding[]}
189
+ */
190
+ function scanStagedFiles(_repoRoot) {
191
+ const staged = getStagedNameStatus()
192
+ const toScan = staged.filter(({ status, path }) =>
193
+ ['A', 'M'].includes(status[0]) && SCAN_EXTENSIONS.has(extname(path)),
194
+ )
195
+
196
+ if (toScan.length === 0) return []
197
+
198
+ process.stdout.write(`\nScanning ${toScan.length} staged source file(s)...\n`)
199
+
200
+ const findings = []
201
+ let skipped = 0
202
+
203
+ for (const { path: filePath } of toScan) {
204
+ if (shouldSkipSelfReferentialFile(filePath)) {
205
+ skipped++
206
+ continue
207
+ }
208
+
209
+ const content = readStagedContent(filePath)
210
+ if (!content) continue
211
+
212
+ const lines = content.split('\n')
213
+ const isConfig = isConfigFile(filePath)
214
+
215
+ findings.push(
216
+ ...collectPatternFindings(lines, isConfig, filePath),
217
+ ...collectObfuscatedLineFindings(lines, filePath),
218
+ )
219
+ }
220
+
221
+ if (skipped > 0) {
222
+ process.stdout.write(`Skipping ${skipped} self-referential security definition/test file(s).\n`)
223
+ }
224
+
225
+ return findings
226
+ }
227
+
228
+ // ─── Supply-chain scanner ──────────────────────────────────────────────────────
229
+
230
+ /**
231
+ * Compare staged vs committed package.json files to find packages that were
232
+ * newly added in this commit (across all dependency fields).
233
+ * @returns {string[]} Array of package names
234
+ */
235
+ function getNewlyAddedPackages() {
236
+ const staged = getStagedNameStatus()
237
+ const pkgFiles = staged.filter(({ status, path }) =>
238
+ ['A', 'M'].includes(status[0])
239
+ && path.endsWith('package.json')
240
+ && !path.includes('node_modules'),
241
+ )
242
+
243
+ if (pkgFiles.length === 0) return []
244
+
245
+ const depFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
246
+
247
+ function extractDepNames(jsonContent) {
248
+ if (!jsonContent) return new Set()
249
+ try {
250
+ const pkg = JSON.parse(jsonContent)
251
+ const all = new Set()
252
+ for (const field of depFields) {
253
+ if (pkg[field] && typeof pkg[field] === 'object') {
254
+ for (const name of Object.keys(pkg[field])) all.add(name)
255
+ }
256
+ }
257
+ return all
258
+ }
259
+ catch {
260
+ return new Set()
261
+ }
262
+ }
263
+
264
+ const added = new Set()
265
+
266
+ for (const { path: pkgFile } of pkgFiles) {
267
+ const newContent = readStagedContent(pkgFile)
268
+ const oldContent = runSafe(`git show "HEAD:${pkgFile}"`)
269
+ const newDeps = extractDepNames(newContent)
270
+ const oldDeps = extractDepNames(oldContent)
271
+ for (const name of newDeps) {
272
+ if (!oldDeps.has(name)) added.add(name)
273
+ }
274
+ }
275
+
276
+ return [...added]
277
+ }
278
+
279
+ /**
280
+ * @typedef {{ pkg: string, scriptName?: string, patternId: string,
281
+ * severity: string, message: string, lineContent: string }} SupplyFinding
282
+ */
283
+
284
+ /**
285
+ * Inspect a single package's install scripts in node_modules.
286
+ * @param {string} pkgName
287
+ * @param {string} repoRoot
288
+ * @returns {SupplyFinding[]}
289
+ */
290
+ function checkInstallScripts(pkgName, repoRoot) {
291
+ const pkgJsonPath = join(repoRoot, 'node_modules', pkgName, 'package.json')
292
+ if (!existsSync(pkgJsonPath)) return []
293
+
294
+ let pkg
295
+ try {
296
+ pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
297
+ }
298
+ catch {
299
+ return []
300
+ }
301
+
302
+ const findings = []
303
+ const scriptFields = ['preinstall', 'install', 'postinstall']
304
+
305
+ for (const scriptName of scriptFields) {
306
+ const script = pkg.scripts?.[scriptName]
307
+ if (!script) continue
308
+
309
+ // Always surface that an install script exists (medium — for awareness)
310
+ findings.push({
311
+ pkg: pkgName,
312
+ scriptName,
313
+ patternId: 'install-script-present',
314
+ severity: 'medium',
315
+ message: `Package has a ${scriptName} script — verify it is legitimate`,
316
+ lineContent: script.slice(0, 120),
317
+ })
318
+
319
+ // Check for specific dangerous patterns (high/critical)
320
+ for (const pattern of INSTALL_SCRIPT_PATTERNS) {
321
+ if (pattern.regex.test(script)) {
322
+ findings.push({
323
+ pkg: pkgName,
324
+ scriptName,
325
+ patternId: pattern.id,
326
+ severity: pattern.severity,
327
+ message: pattern.message,
328
+ lineContent: script.slice(0, 120),
329
+ })
330
+ }
331
+ }
332
+ }
333
+
334
+ return findings
335
+ }
336
+
337
+ /**
338
+ * Run the full supply-chain scan: typosquatting + install-script analysis
339
+ * for every newly added package.
340
+ * @param {string} repoRoot
341
+ * @returns {SupplyFinding[]}
342
+ */
343
+ function scanNewDependencies(repoRoot) {
344
+ const newPkgs = getNewlyAddedPackages()
345
+ if (newPkgs.length === 0) return []
346
+
347
+ process.stdout.write(`\nSupply-chain scan: ${newPkgs.length} newly added package(s)...\n`)
348
+
349
+ const findings = []
350
+
351
+ for (const pkgName of newPkgs) {
352
+ // 1. Typosquatting
353
+ const similar = detectTyposquat(pkgName)
354
+ if (similar) {
355
+ findings.push({
356
+ pkg: pkgName,
357
+ patternId: 'typosquat',
358
+ severity: 'high',
359
+ message: `Possible typosquat of "${similar}" — verify the package name is correct`,
360
+ lineContent: `"${pkgName}" is 1 edit away from "${similar}"`,
361
+ })
362
+ }
363
+
364
+ // 2. Install scripts
365
+ const scriptFindings = checkInstallScripts(pkgName, repoRoot)
366
+ findings.push(...scriptFindings)
367
+ }
368
+
369
+ return findings
370
+ }
371
+
372
+ // ─── Optional: pnpm/npm audit ─────────────────────────────────────────────────
373
+
374
+ /**
375
+ * Run `pnpm audit --audit-level=high` when SECURITY_AUDIT=1 is set.
376
+ * Non-fatal: output is printed but does not block the commit so it doesn't
377
+ * slow down normal developer workflows.
378
+ */
379
+ function runAudit(repoRoot) {
380
+ if (process.env.SECURITY_AUDIT !== '1') return
381
+
382
+ process.stdout.write(`\nRunning pnpm audit (SECURITY_AUDIT=1)...\n`)
383
+ const result = runSafe('pnpm audit --audit-level=high 2>&1')
384
+ if (result) {
385
+ process.stdout.write(`${DIM}${result}${R}\n`)
386
+ }
387
+ }
388
+
389
+ // ─── Output helpers ────────────────────────────────────────────────────────────
390
+
391
+ function printFileFinding(f) {
392
+ console.log(`\n ${badge(f.severity)} ${DIM}${f.file}:${f.line}${R}`)
393
+ console.log(` ${BOLD}${f.message}${R}`)
394
+ console.log(` ${DIM}│${R} ${f.lineContent}`)
395
+ }
396
+
397
+ function printSupplyFinding(f) {
398
+ if (f.patternId === 'install-script-present') {
399
+ console.log(` ${DIM}⚑${R} ${BOLD}${f.pkg}${R} — ${f.scriptName}: ${DIM}${f.lineContent}${R}`)
400
+ return
401
+ }
402
+ const scriptLabel = f.scriptName ? ` (${DIM}${f.scriptName}${R})` : ''
403
+ console.log(`\n ${badge(f.severity)} ${BOLD}${f.pkg}${R}${scriptLabel}`)
404
+ console.log(` ${f.message}`)
405
+ console.log(` ${DIM}│${R} ${f.lineContent}`)
406
+ }
407
+
408
+ // ─── Entry point ───────────────────────────────────────────────────────────────
409
+
410
+ function main() {
411
+ if (process.env.SKIP_SECURITY_SCAN === '1') {
412
+ process.stderr.write(
413
+ `\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — all security checks bypassed.${R}\n`
414
+ + `${YLW} This bypass is intentional and has been logged.${R}\n\n`,
415
+ )
416
+ return
417
+ }
418
+
419
+ const repoRoot = getRepoRoot()
420
+
421
+ console.log(`\n${HR}`)
422
+ console.log(` ${BOLD}@archipelago/security-scan${R} ${DIM}socket.dev-style pre-commit guard${R}`)
423
+ console.log(HR)
424
+
425
+ const fileFindings = scanStagedFiles(repoRoot)
426
+ const supplyFindings = scanNewDependencies(repoRoot)
427
+
428
+ runAudit(repoRoot)
429
+
430
+ const allFindings = [...fileFindings, ...supplyFindings]
431
+
432
+ if (fileFindings.length > 0) {
433
+ console.log(`\n${BOLD}Source-code findings:${R}`)
434
+ fileFindings.forEach(printFileFinding)
435
+ }
436
+
437
+ if (supplyFindings.length > 0) {
438
+ console.log(`\n${BOLD}Supply-chain findings:${R}`)
439
+ supplyFindings.forEach(printSupplyFinding)
440
+ }
441
+
442
+ const blocking = allFindings.filter(f => isBlocking(f.severity))
443
+ const warnings = allFindings.filter(f => !isBlocking(f.severity))
444
+
445
+ console.log(`\n${HR}`)
446
+
447
+ if (blocking.length === 0) {
448
+ const warnNote = warnings.length > 0 ? ` ${DIM}(${warnings.length} warning(s) — review recommended)${R}` : ''
449
+ console.log(` ${GRN}${BOLD}✔ Security scan passed${R}${warnNote}`)
450
+ console.log(HR)
451
+ console.log()
452
+ return
453
+ }
454
+
455
+ console.log(
456
+ ` ${RED}${BOLD}✖ COMMIT BLOCKED${R} — ${blocking.length} critical/high security issue(s) found`,
457
+ )
458
+ console.log(` ${DIM}To bypass (emergencies only): SKIP_SECURITY_SCAN=1 git commit …${R}`)
459
+ console.log(HR)
460
+ console.log()
461
+
462
+ process.exit(1)
463
+ }
464
+
465
+ main()