diff --git a/.claude/hooks/check-new-deps/README.md b/.claude/hooks/check-new-deps/README.md index 5be7f3a6..25fb1128 100644 --- a/.claude/hooks/check-new-deps/README.md +++ b/.claude/hooks/check-new-deps/README.md @@ -8,10 +8,9 @@ When Claude edits a file like `package.json`, `requirements.txt`, `Cargo.toml`, 1. **Detects the file type** and extracts dependency names from the content 2. **Diffs against the old content** (for edits) so only *newly added* deps are checked -3. **Queries the Socket.dev API** to check for malware and critical security alerts -4. **Blocks the edit** (exit code 2) if malware or critical alerts are found -5. **Warns** (but allows) if a package has a low quality score -6. **Allows** (exit code 0) if everything is clean or the file isn't a manifest +3. **Queries the Socket.dev API** to check for malware +4. **Blocks the edit** (exit code 2) if malware is detected +5. **Allows** (exit code 0) if everything is clean or the file isn't a manifest ## How it works @@ -30,11 +29,8 @@ Build Package URLs (PURLs) for each dep │ ▼ Call sdk.checkMalware(components) - - ≤5 deps: parallel firewall API (fast, full data) - - >5 deps: batch PURL API (efficient) │ - ├── Malware/critical alert → EXIT 2 (blocked) - ├── Low score → warn, EXIT 0 (allowed) + ├── Malware detected → EXIT 2 (blocked) └── Clean → EXIT 0 (allowed) ``` diff --git a/.claude/hooks/check-new-deps/index.mts b/.claude/hooks/check-new-deps/index.mts index 335868cd..5cae1a52 100644 --- a/.claude/hooks/check-new-deps/index.mts +++ b/.claude/hooks/check-new-deps/index.mts @@ -15,7 +15,7 @@ // // Exit codes: // 0 = allow (no new deps, all clean, or non-dep file) -// 2 = block (malware or critical alert from Socket.dev) +// 2 = block (malware detected by Socket.dev) import { parseNpmSpecifier, @@ -36,8 +36,6 @@ const logger = getDefaultLogger() // Per-request timeout (ms) to avoid blocking the hook on slow responses. const API_TIMEOUT = 5_000 -// Deps scoring below this threshold trigger a warning (not a block). -const LOW_SCORE_THRESHOLD = 0.5 // Max PURLs per batch request (API limit is 1024). const MAX_BATCH_SIZE = 1024 // How long (ms) to cache a successful API response (5 minutes). @@ -75,12 +73,9 @@ interface HookInput { interface CheckResult { purl: string blocked?: boolean - warned?: boolean reason?: string - score?: number } - // A cached API lookup result with expiration timestamp. interface CacheEntry { result: CheckResult | undefined @@ -324,14 +319,8 @@ async function check(hook: HookInput): Promise { if (deps.length === 0) return 0 // Check all deps via SDK checkMalware(). - const { blocked, warned } = await checkDepsBatch(deps) + const blocked = await checkDepsBatch(deps) - if (warned.length > 0) { - logger.warn('Socket: low-scoring dependencies (not blocked):') - for (const w of warned) { - logger.warn(` ${w.purl}: overall score ${w.score}`) - } - } if (blocked.length > 0) { logger.error(`Socket: blocked ${blocked.length} dep(s):`) for (const b of blocked) { @@ -343,14 +332,11 @@ async function check(hook: HookInput): Promise { } // Check deps against Socket.dev using SDK v4 checkMalware(). -// The SDK automatically routes small sets (<=5) to parallel firewall -// requests and larger sets to the batch PURL API. // Deps already in cache are skipped; results are cached after lookup. async function checkDepsBatch( deps: Dep[], -): Promise<{ blocked: CheckResult[]; warned: CheckResult[] }> { +): Promise { const blocked: CheckResult[] = [] - const warned: CheckResult[] = [] // Partition deps into cached vs uncached. const uncached: Array<{ dep: Dep; purl: string }> = [] @@ -359,13 +345,12 @@ async function checkDepsBatch( const cached = cacheGet(purl) if (cached) { if (cached.result?.blocked) blocked.push(cached.result) - else if (cached.result?.warned) warned.push(cached.result) continue } uncached.push({ dep, purl }) } - if (!uncached.length) return { blocked, warned } + if (!uncached.length) return blocked try { // Process in chunks to respect API batch size limit. @@ -379,7 +364,7 @@ async function checkDepsBatch( logger.warn( `Socket: API returned ${result.status}, allowing all` ) - return { blocked, warned } + return blocked } // Build lookup keyed by full PURL (includes namespace + version). @@ -395,37 +380,22 @@ async function checkDepsBatch( const purl = purlByKey.get(key) if (!purl) continue - // Check for malware or critical-severity alerts. - const critical = pkg.alerts.find( + // Check for malware alerts. + const malware = pkg.alerts.find( a => a.severity === 'critical' || a.type === 'malware' ) - if (critical) { + if (malware) { const cr: CheckResult = { purl, blocked: true, - reason: `${critical.type} — ${critical.severity ?? 'critical'}`, + reason: `${malware.type} — ${malware.severity ?? 'critical'}`, } cacheSet(purl, cr) blocked.push(cr) continue } - // Warn on low quality score. - if ( - pkg.score?.overall !== undefined - && pkg.score.overall < LOW_SCORE_THRESHOLD - ) { - const wr: CheckResult = { - purl, - warned: true, - score: pkg.score.overall, - } - cacheSet(purl, wr) - warned.push(wr) - continue - } - - // No blocking alerts — clean dep. + // No malware alerts — clean dep. cacheSet(purl, undefined) } } @@ -437,7 +407,7 @@ async function checkDepsBatch( ) } - return { blocked, warned } + return blocked } // Return deps in `newDeps` that don't appear in `oldDeps` (by PURL). diff --git a/.claude/hooks/check-new-deps/package-lock.json b/.claude/hooks/check-new-deps/package-lock.json new file mode 100644 index 00000000..498e9420 --- /dev/null +++ b/.claude/hooks/check-new-deps/package-lock.json @@ -0,0 +1,73 @@ +{ + "name": "@socketsecurity/hook-check-new-deps", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@socketsecurity/hook-check-new-deps", + "dependencies": { + "@socketregistry/packageurl-js": "1.4.2", + "@socketsecurity/lib": "5.18.2", + "@socketsecurity/sdk": "4.0.1" + }, + "devDependencies": { + "@types/node": "24.9.2" + } + }, + "node_modules/@socketregistry/packageurl-js": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@socketregistry/packageurl-js/-/packageurl-js-1.4.2.tgz", + "integrity": "sha512-yt9UfUzD02wZ7kwb67oe4jxG2D9JtgPqjrK/ans2BovFyeie0w8hvRR0MuOWM4mUt2371oFPp7NB6O5ZjYJmlw==", + "license": "MIT", + "engines": { + "node": ">=18.20.8", + "pnpm": ">=11.0.0-rc.0" + } + }, + "node_modules/@socketsecurity/lib": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/@socketsecurity/lib/-/lib-5.18.2.tgz", + "integrity": "sha512-h6aGfphQ9jdVjUMGIKJcsIvT6BmzBo0OD20HzeK+6KQJi2HupfCUzIH26vDPxf+aYVmrX0/hKJDYI5sXfTGx9A==", + "license": "MIT", + "engines": { + "node": ">=22", + "pnpm": ">=11.0.0-rc.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@socketsecurity/sdk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@socketsecurity/sdk/-/sdk-4.0.1.tgz", + "integrity": "sha512-fe3DQp2dFwhc0G6Za36GIMSV+QaPAP5L96K3ZOtywt9nhbwxc9IQwqzdOVztdn5Rbez3t9EHU9Esj24/hWdP0g==", + "license": "MIT", + "engines": { + "node": ">=18.20.8", + "pnpm": ">=11.0.0-rc.0" + } + }, + "node_modules/@types/node": { + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +}