Skip to content

Commit e8e3606

Browse files
Optimize CVE download script (#15)
* fix cve download command * Fix isPostinstallConfigured to handle non-string postinstall values Handle edge cases where package.json scripts.postinstall is null, an object, or an array instead of a string. Previously this would throw "currentScript.includes is not a function". 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Bump version to 0.2.1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent a35b009 commit e8e3606

File tree

4 files changed

+94
-5
lines changed

4 files changed

+94
-5
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@socketsecurity/socket-patch",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"packageManager": "[email protected]",
55
"description": "CLI tool for applying security patches to dependencies",
66
"main": "dist/index.js",

src/commands/download.ts

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,65 @@ const GHSA_PATTERN = /^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/i
2727

2828
type IdentifierType = 'uuid' | 'cve' | 'ghsa' | 'purl' | 'package'
2929

30+
/**
31+
* Parse a PURL to extract the package directory path and version
32+
* @example parsePurl('pkg:npm/[email protected]') => { packageDir: 'lodash', version: '4.17.21' }
33+
* @example parsePurl('pkg:npm/@types/[email protected]') => { packageDir: '@types/node', version: '20.0.0' }
34+
*/
35+
function parsePurl(purl: string): { packageDir: string; version: string } | null {
36+
const match = purl.match(/^pkg:npm\/(.+)@([^@]+)$/)
37+
if (!match) return null
38+
return { packageDir: match[1], version: match[2] }
39+
}
40+
41+
/**
42+
* Check which PURLs from search results are actually installed in node_modules.
43+
* This is O(n) where n = number of unique packages in search results,
44+
* NOT O(m) where m = total packages in node_modules.
45+
*/
46+
async function findInstalledPurls(
47+
cwd: string,
48+
purls: string[],
49+
): Promise<Set<string>> {
50+
const nodeModulesPath = path.join(cwd, 'node_modules')
51+
const installedPurls = new Set<string>()
52+
53+
// Group PURLs by package directory to handle multiple versions of same package
54+
const packageVersions = new Map<string, Set<string>>()
55+
const purlLookup = new Map<string, string>() // "packageDir@version" -> original purl
56+
57+
for (const purl of purls) {
58+
const parsed = parsePurl(purl)
59+
if (!parsed) continue
60+
61+
if (!packageVersions.has(parsed.packageDir)) {
62+
packageVersions.set(parsed.packageDir, new Set())
63+
}
64+
packageVersions.get(parsed.packageDir)!.add(parsed.version)
65+
purlLookup.set(`${parsed.packageDir}@${parsed.version}`, purl)
66+
}
67+
68+
// Check only the specific packages we need - O(n) filesystem operations
69+
for (const [packageDir, versions] of packageVersions) {
70+
const pkgJsonPath = path.join(nodeModulesPath, packageDir, 'package.json')
71+
try {
72+
const content = await fs.readFile(pkgJsonPath, 'utf-8')
73+
const pkg = JSON.parse(content)
74+
if (pkg.version && versions.has(pkg.version)) {
75+
const key = `${packageDir}@${pkg.version}`
76+
const originalPurl = purlLookup.get(key)
77+
if (originalPurl) {
78+
installedPurls.add(originalPurl)
79+
}
80+
}
81+
} catch {
82+
// Package not found or invalid package.json - skip
83+
}
84+
}
85+
86+
return installedPurls
87+
}
88+
3089
interface DownloadArgs {
3190
identifier: string
3291
org?: string
@@ -390,14 +449,41 @@ async function downloadPatches(args: DownloadArgs): Promise<boolean> {
390449
return true
391450
}
392451

452+
// For CVE/GHSA searches, filter to only show patches for installed packages
453+
// Uses O(n) filesystem operations where n = unique packages in results,
454+
// NOT O(m) where m = all packages in node_modules
455+
let filteredResults = searchResults
456+
let notInstalledCount = 0
457+
458+
if (idType === 'cve' || idType === 'ghsa') {
459+
console.log(`Checking which packages are installed...`)
460+
const searchPurls = searchResults.map(patch => patch.purl)
461+
const installedPurls = await findInstalledPurls(cwd, searchPurls)
462+
463+
filteredResults = searchResults.filter(patch => installedPurls.has(patch.purl))
464+
notInstalledCount = searchResults.length - filteredResults.length
465+
466+
if (filteredResults.length === 0) {
467+
console.log(`No patches found for installed packages.`)
468+
if (notInstalledCount > 0) {
469+
console.log(` (${notInstalledCount} patch(es) exist for packages not installed in this project)`)
470+
}
471+
return true
472+
}
473+
}
474+
393475
// Filter patches based on tier access
394-
const accessiblePatches = searchResults.filter(
476+
const accessiblePatches = filteredResults.filter(
395477
patch => patch.tier === 'free' || canAccessPaidPatches,
396478
)
397-
const inaccessibleCount = searchResults.length - accessiblePatches.length
479+
const inaccessibleCount = filteredResults.length - accessiblePatches.length
398480

399481
// Display search results
400-
displaySearchResults(searchResults, canAccessPaidPatches)
482+
displaySearchResults(filteredResults, canAccessPaidPatches)
483+
484+
if (notInstalledCount > 0) {
485+
console.log(`Note: ${notInstalledCount} patch(es) for packages not installed in this project were hidden.`)
486+
}
401487

402488
if (inaccessibleCount > 0) {
403489
console.log(

src/package-json/detect.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export function isPostinstallConfigured(
3333
packageJson = packageJsonContent
3434
}
3535

36-
const currentScript = packageJson.scripts?.postinstall || ''
36+
const rawPostinstall = packageJson.scripts?.postinstall
37+
// Handle non-string values (null, object, array) by treating as empty string
38+
const currentScript = typeof rawPostinstall === 'string' ? rawPostinstall : ''
3739

3840
// Check if socket-patch apply is already present
3941
const configured = currentScript.includes('socket-patch apply')

src/utils/api-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export class APIClient {
9191

9292
const headers: Record<string, string> = {
9393
Accept: 'application/json',
94+
'User-Agent': 'SocketPatchCLI/1.0',
9495
}
9596

9697
// Only add auth header if we have a token (not using public proxy)

0 commit comments

Comments
 (0)