@@ -13,14 +13,19 @@ import {
1313 cleanupUnusedBlobs ,
1414 formatCleanupResult ,
1515} from '../utils/cleanup-blobs.js'
16+ import {
17+ enumerateNodeModules ,
18+ type EnumeratedPackage ,
19+ } from '../utils/enumerate-packages.js'
20+ import { fuzzyMatchPackages , isPurl } from '../utils/fuzzy-match.js'
1621
1722// Identifier type patterns
1823const UUID_PATTERN =
1924 / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / i
2025const CVE_PATTERN = / ^ C V E - \d { 4 } - \d + $ / i
2126const GHSA_PATTERN = / ^ G H S A - [ a - z 0 - 9 ] { 4 } - [ a - z 0 - 9 ] { 4 } - [ a - z 0 - 9 ] { 4 } $ / i
2227
23- type IdentifierType = 'uuid' | 'cve' | 'ghsa'
28+ type IdentifierType = 'uuid' | 'cve' | 'ghsa' | 'purl' | 'package'
2429
2530interface DownloadArgs {
2631 identifier : string
@@ -29,6 +34,7 @@ interface DownloadArgs {
2934 id ?: boolean
3035 cve ?: boolean
3136 ghsa ?: boolean
37+ package ?: boolean
3238 yes ?: boolean
3339 'api-url' ?: string
3440 'api-token' ?: string
@@ -47,6 +53,9 @@ function detectIdentifierType(identifier: string): IdentifierType | null {
4753 if ( GHSA_PATTERN . test ( identifier ) ) {
4854 return 'ghsa'
4955 }
56+ if ( isPurl ( identifier ) ) {
57+ return 'purl'
58+ }
5059 return null
5160}
5261
@@ -67,6 +76,44 @@ async function promptConfirmation(message: string): Promise<boolean> {
6776 } )
6877}
6978
79+ /**
80+ * Display enumerated packages and prompt user to select one
81+ */
82+ async function promptSelectPackage (
83+ packages : EnumeratedPackage [ ] ,
84+ ) : Promise < EnumeratedPackage | null > {
85+ console . log ( '\nMatching packages found:\n' )
86+
87+ for ( let i = 0 ; i < packages . length ; i ++ ) {
88+ const pkg = packages [ i ]
89+ const displayName = pkg . namespace
90+ ? `${ pkg . namespace } /${ pkg . name } `
91+ : pkg . name
92+ console . log ( ` ${ i + 1 } . ${ displayName } @${ pkg . version } ` )
93+ console . log ( ` PURL: ${ pkg . purl } ` )
94+ }
95+
96+ const rl = readline . createInterface ( {
97+ input : process . stdin ,
98+ output : process . stdout ,
99+ } )
100+
101+ return new Promise ( resolve => {
102+ rl . question (
103+ `\nSelect a package (1-${ packages . length } ) or 0 to cancel: ` ,
104+ answer => {
105+ rl . close ( )
106+ const selection = parseInt ( answer , 10 )
107+ if ( isNaN ( selection ) || selection < 1 || selection > packages . length ) {
108+ resolve ( null )
109+ } else {
110+ resolve ( packages [ selection - 1 ] )
111+ }
112+ } ,
113+ )
114+ } )
115+ }
116+
70117/**
71118 * Display search results to the user
72119 */
@@ -163,6 +210,7 @@ async function downloadPatches(args: DownloadArgs): Promise<boolean> {
163210 id : forceId ,
164211 cve : forceCve ,
165212 ghsa : forceGhsa ,
213+ package : forcePackage ,
166214 yes : skipConfirmation ,
167215 'api-url' : apiUrl ,
168216 'api-token' : apiToken ,
@@ -197,15 +245,19 @@ async function downloadPatches(args: DownloadArgs): Promise<boolean> {
197245 idType = 'cve'
198246 } else if ( forceGhsa ) {
199247 idType = 'ghsa'
248+ } else if ( forcePackage ) {
249+ // --package flag forces package search (fuzzy match against node_modules)
250+ idType = 'package'
200251 } else {
201252 const detectedType = detectIdentifierType ( identifier )
202253 if ( ! detectedType ) {
203- throw new Error (
204- `Unrecognized identifier format: ${ identifier } . Expected UUID, CVE ID (CVE-YYYY-NNNNN), or GHSA ID (GHSA-xxxx-xxxx-xxxx).` ,
205- )
254+ // If not recognized as UUID/CVE/GHSA/PURL, assume it's a package name search
255+ idType = 'package'
256+ console . log ( `Treating "${ identifier } " as a package name search` )
257+ } else {
258+ idType = detectedType
259+ console . log ( `Detected identifier type: ${ idType } ` )
206260 }
207- idType = detectedType
208- console . log ( `Detected identifier type: ${ idType } ` )
209261 }
210262
211263 // For UUID, directly fetch and download the patch
@@ -251,7 +303,7 @@ async function downloadPatches(args: DownloadArgs): Promise<boolean> {
251303 return true
252304 }
253305
254- // For CVE/GHSA/package, first search then download
306+ // For CVE/GHSA/PURL/ package, first search then download
255307 let searchResponse : SearchResponse
256308
257309 switch ( idType ) {
@@ -265,6 +317,61 @@ async function downloadPatches(args: DownloadArgs): Promise<boolean> {
265317 searchResponse = await apiClient . searchPatchesByGHSA ( effectiveOrgSlug , identifier )
266318 break
267319 }
320+ case 'purl' : {
321+ console . log ( `Searching patches for PURL: ${ identifier } ` )
322+ searchResponse = await apiClient . searchPatchesByPackage ( effectiveOrgSlug , identifier )
323+ break
324+ }
325+ case 'package' : {
326+ // Enumerate packages from node_modules and fuzzy match
327+ console . log ( `Enumerating packages in ${ cwd } ...` )
328+ const packages = await enumerateNodeModules ( cwd )
329+
330+ if ( packages . length === 0 ) {
331+ console . log ( 'No packages found in node_modules. Run npm/yarn/pnpm install first.' )
332+ return true
333+ }
334+
335+ console . log ( `Found ${ packages . length } packages in node_modules` )
336+
337+ // Fuzzy match against the identifier
338+ const matches = fuzzyMatchPackages ( identifier , packages )
339+
340+ if ( matches . length === 0 ) {
341+ console . log ( `No packages matching "${ identifier } " found in node_modules.` )
342+ return true
343+ }
344+
345+ let selectedPackage : EnumeratedPackage
346+
347+ if ( matches . length === 1 ) {
348+ // Single match, use it directly
349+ selectedPackage = matches [ 0 ]
350+ console . log ( `Found exact match: ${ selectedPackage . purl } ` )
351+ } else {
352+ // Multiple matches, prompt user to select
353+ if ( skipConfirmation ) {
354+ // With --yes, use the best match (first result)
355+ selectedPackage = matches [ 0 ]
356+ console . log ( `Using best match: ${ selectedPackage . purl } ` )
357+ } else {
358+ const selected = await promptSelectPackage ( matches )
359+ if ( ! selected ) {
360+ console . log ( 'No package selected. Download cancelled.' )
361+ return true
362+ }
363+ selectedPackage = selected
364+ }
365+ }
366+
367+ // Search for patches using the selected package's PURL
368+ console . log ( `Searching patches for package: ${ selectedPackage . purl } ` )
369+ searchResponse = await apiClient . searchPatchesByPackage (
370+ effectiveOrgSlug ,
371+ selectedPackage . purl ,
372+ )
373+ break
374+ }
268375 default :
269376 throw new Error ( `Unknown identifier type: ${ idType } ` )
270377 }
@@ -381,7 +488,7 @@ export const downloadCommand: CommandModule<{}, DownloadArgs> = {
381488 return yargs
382489 . positional ( 'identifier' , {
383490 describe :
384- 'Patch identifier (UUID, CVE ID, or GHSA ID)' ,
491+ 'Patch identifier (UUID, CVE ID, GHSA ID, PURL, or package name )' ,
385492 type : 'string' ,
386493 demandOption : true ,
387494 } )
@@ -405,6 +512,12 @@ export const downloadCommand: CommandModule<{}, DownloadArgs> = {
405512 type : 'boolean' ,
406513 default : false ,
407514 } )
515+ . option ( 'package' , {
516+ alias : 'p' ,
517+ describe : 'Force identifier to be treated as a package name (fuzzy matches against node_modules)' ,
518+ type : 'boolean' ,
519+ default : false ,
520+ } )
408521 . option ( 'yes' , {
409522 alias : 'y' ,
410523 describe : 'Skip confirmation prompt for multiple patches' ,
@@ -432,6 +545,14 @@ export const downloadCommand: CommandModule<{}, DownloadArgs> = {
432545 '$0 download GHSA-jfhm-5ghh-2f97' ,
433546 'Download free patches for a GHSA (no auth required)' ,
434547 )
548+ . example (
549+ '$0 download pkg:npm/[email protected] ' , 550+ 'Download patches for a specific package version by PURL' ,
551+ )
552+ . example (
553+ '$0 download lodash --package' ,
554+ 'Search for patches by package name (fuzzy matches node_modules)' ,
555+ )
435556 . example (
436557 '$0 download 12345678-1234-1234-1234-123456789abc --org myorg' ,
437558 'Download a patch by UUID (requires SOCKET_API_TOKEN)' ,
@@ -442,12 +563,12 @@ export const downloadCommand: CommandModule<{}, DownloadArgs> = {
442563 )
443564 . check ( argv => {
444565 // Ensure only one type flag is set
445- const typeFlags = [ argv . id , argv . cve , argv . ghsa ] . filter (
566+ const typeFlags = [ argv . id , argv . cve , argv . ghsa , argv . package ] . filter (
446567 Boolean ,
447568 )
448569 if ( typeFlags . length > 1 ) {
449570 throw new Error (
450- 'Only one of --id, --cve, or --ghsa can be specified' ,
571+ 'Only one of --id, --cve, --ghsa, or --package can be specified' ,
451572 )
452573 }
453574 return true
0 commit comments