@@ -43,7 +43,6 @@ func getVersion(packageManager string, pkg RegistryRequest) (*http.Response, err
4343func getRecommendedVersions (npmResponse NPMResponse , currentVersion string ) ([]string , error ) {
4444 var versions [][]string
4545
46- // Extract and filter versions from NPMResponse
4746 for _ , obj := range npmResponse .Versions {
4847 // skip release candidates
4948 if strings .Contains (obj .Version , "-" ) {
@@ -53,15 +52,18 @@ func getRecommendedVersions(npmResponse NPMResponse, currentVersion string) ([]s
5352 versions = append (versions , versionParts )
5453 }
5554
56- // Filter by major version and sort
5755 var currentMajor , currentMinor , currentPatch int
58- fmt .Sscanf (currentVersion , "%d.%d.%d" , & currentMajor , & currentMinor , & currentPatch )
56+ if _ , err := fmt .Sscanf (currentVersion , "%d.%d.%d" , & currentMajor , & currentMinor , & currentPatch ); err != nil {
57+ return nil , fmt .Errorf ("invalid current version format: %s" , currentVersion )
58+ }
5959
6060 var recommended []string
6161 for _ , version := range versions {
6262 var major , minor , patch int
6363 versionStr := strings .Join (version , "." )
64- fmt .Sscanf (versionStr , "%d.%d.%d" , & major , & minor , & patch )
64+ if _ , err := fmt .Sscanf (versionStr , "%d.%d.%d" , & major , & minor , & patch ); err != nil {
65+ continue
66+ }
6567
6668 if major == currentMajor {
6769 if minor >= currentMinor {
@@ -90,7 +92,9 @@ func getRecommendedVersions(npmResponse NPMResponse, currentVersion string) ([]s
9092
9193func parseVersion (version string ) [3 ]int {
9294 var result [3 ]int
93- fmt .Sscanf (version , "%d.%d.%d" , & result [0 ], & result [1 ], & result [2 ])
95+ if _ , err := fmt .Sscanf (version , "%d.%d.%d" , & result [0 ], & result [1 ], & result [2 ]); err != nil {
96+ return [3 ]int {0 , 0 , 0 }
97+ }
9498 return result
9599}
96100
@@ -108,87 +112,41 @@ func IsValidSemver(version string) bool {
108112 return matched
109113}
110114
111- func processDependencies (depMap map [string ]string , depName string , depVersion string , visited map [string ]bool , vulnerablePackage string , vulnerableVersion string , node * DependencyNode ) {
112- for depKey , depVal := range depMap {
113- // remove ^, ~, quotes
114- normalizedDepVal := strings .Trim (depVal , "^~\" " )
115-
116- // Skip non-semver versions
117- if ! IsValidSemver (normalizedDepVal ) {
118- continue
119- }
120-
121- // Check if version exists before fetching
122- if ! VersionExists (depKey , normalizedDepVal ) {
123- fmt .Printf ("Skipping %s@%s: version not found\n " , depKey , normalizedDepVal )
124- continue
125- }
126-
127- depResp , err := GetNPMRegistry (RegistryRequest {Dependency : depKey , Version : depVal })
128- if err != nil {
129- fmt .Printf ("Error fetching %s@%s: %v\n " , depKey , depVal , err )
130- continue
131- }
132-
133- depBody , err := io .ReadAll (depResp .Body )
134- depResp .Body .Close ()
135- if err != nil {
136- fmt .Printf ("Error reading response for %s@%s: %v\n " , depKey , depVal , err )
137- continue
138- }
139-
140- // Recursive call
141- childNode := walkDependencyTree (depBody , depKey , depVal , visited , vulnerablePackage , vulnerableVersion )
142- if childNode != nil {
143- node .Dependencies [depKey ] = childNode
144- }
145- if depKey == vulnerablePackage && depVal == vulnerableVersion {
146- fmt .Printf ("Vulnerable package found: %s@%s\n " , depKey , depVal )
147- }
115+ func parsePurl (purl string ) (string , string , error ) {
116+ // Format: pkg:npm/package-name@version
117+ if ! strings .HasPrefix (purl , "pkg:npm/" ) {
118+ return "" , "" , fmt .Errorf ("invalid purl format: %s" , purl )
148119 }
149- }
150-
151- func walkDependencyTree (npmRegisterResp []byte , depName string , depVersion string , visited map [string ]bool , vulnerablePackage string , vulnerableVersion string ) * DependencyNode {
152- var jsonData NPMResponse
153120
154- if err := json .Unmarshal (npmRegisterResp , & jsonData ); err != nil {
155- return nil
121+ purl = strings .TrimPrefix (purl , "pkg:npm/" )
122+ parts := strings .Split (purl , "@" )
123+ if len (parts ) != 2 {
124+ return "" , "" , fmt .Errorf ("invalid purl format: %s" , purl )
156125 }
157126
158- nodeKey := depName + "@" + depVersion
159- if visited [nodeKey ] {
160- return nil
161- }
162- visited [nodeKey ] = true
163-
164- node := & DependencyNode {
165- Name : depName ,
166- Version : depVersion ,
167- Dependencies : make (map [string ]* DependencyNode ),
168- }
169-
170- if jsonData .Dependencies == nil && jsonData .DevDependencies == nil && jsonData .PeerDependencies == nil && jsonData .OptionalDependencies == nil {
171- return node
172- }
173-
174- // Process all dependency types using the same logic
175- processDependencies (jsonData .Dependencies , depName , depVersion , visited , vulnerablePackage , vulnerableVersion , node )
176- processDependencies (jsonData .OptionalDependencies , depName , depVersion , visited , vulnerablePackage , vulnerableVersion , node )
177- processDependencies (jsonData .DevDependencies , depName , depVersion , visited , vulnerablePackage , vulnerableVersion , node )
127+ return parts [0 ], parts [1 ], nil
128+ }
178129
179- return node
130+ func normalizeVersion (version string ) string {
131+ return strings .Trim (version , "^~\" " )
180132}
181133
182- func printDependencyTree (node * DependencyNode , indent string ) {
183- if node == nil {
184- return
134+ func getAllDependencyMaps (depMeta * NPMResponse ) []map [string ]string {
135+ return []map [string ]string {
136+ depMeta .Dependencies ,
137+ depMeta .PeerDependencies ,
138+ depMeta .OptionalDependencies ,
139+ depMeta .DevDependencies ,
185140 }
141+ }
186142
187- fmt .Printf ("%s%s@%s\n " , indent , node .Name , node .Version )
188-
189- for _ , dep := range node .Dependencies {
190- printDependencyTree (dep , indent + " " )
143+ func findDependencyVersionInMeta (depMeta * NPMResponse , pkgName string ) string {
144+ for _ , depType := range getAllDependencyMaps (depMeta ) {
145+ if version , ok := depType [pkgName ]; ok {
146+ return version
147+ }
191148 }
149+ return ""
192150}
193151
194152func findDependencyVersion (npmResp NPMResponse , depName string ) string {
@@ -208,97 +166,133 @@ func findDependencyVersion(npmResp NPMResponse, depName string) string {
208166 return ""
209167}
210168
211- func fetchPackageMetadata (pkgManager string , dep string , version string ) (* NPMResponse , error ) {
212- resp , err := getVersion (pkgManager , RegistryRequest {Dependency : dep , Version : version })
213- if err != nil {
214- return nil , fmt .Errorf ("error fetching %s@%s: %w" , dep , version , err )
215- }
169+ func checkVulnerabilityFixChain (purls []string , fixedVersion string ) (bool , error ) {
216170
217- body , err := io .ReadAll (resp .Body )
218- resp .Body .Close ()
219- if err != nil {
220- return nil , fmt .Errorf ("error reading response for %s@%s: %w" , dep , version , err )
221- }
171+ packages := make ([]struct {
172+ name string
173+ version string
174+ }, len (purls ))
222175
223- var npmResp NPMResponse
224- if err := json .Unmarshal (body , & npmResp ); err != nil {
225- return nil , fmt .Errorf ("error unmarshalling JSON for %s@%s: %w" , dep , version , err )
176+ for i , purl := range purls {
177+ name , version , err := parsePurl (purl )
178+ if err != nil {
179+ return false , err
180+ }
181+ packages [i ].name = name
182+ packages [i ].version = version
226183 }
227184
228- return & npmResp , nil
229- }
185+ for i := 0 ; i < len (packages )- 1 ; i ++ {
186+ pkgName := packages [i ].name
187+ currentVersion := packages [i ].version
230188
231- func checkVersionAvailability (versions []string , currentVersion string ) (string , error ) {
232- if len (versions ) == 0 {
233- return "" , fmt .Errorf ("no versions available" )
234- }
189+ // fetch all version
190+ allVersionsMeta , err := fetchPackageMetadata (getPackageManager ("npm" ), pkgName , "" )
191+ if err != nil {
192+ return false , fmt .Errorf ("failed to fetch all versions for %s: %w" , pkgName , err )
193+ }
235194
236- if versions [0 ] == currentVersion {
237- return "" , fmt .Errorf ("no new version available (current: %s)" , currentVersion )
238- }
195+ // get major versions and sort
196+ versions , err := getRecommendedVersions (* allVersionsMeta , currentVersion )
197+ if err != nil {
198+ return false , fmt .Errorf ("failed to get recommended versions for %s: %w" , pkgName , err )
199+ }
239200
240- return versions [0 ], nil
241- }
201+ if len (versions ) == 0 {
202+ return false , fmt .Errorf ("no newer version available for %s@%s in the same major band" , pkgName , currentVersion )
203+ }
242204
243- func checkVulnerabilityStatus (latestMeta * NPMResponse , vulnPkg string , vulnVer string ) (bool , string ) {
244- latestVulnVer := findDependencyVersion (* latestMeta , vulnPkg )
205+ latestVersion := versions [0 ]
206+ if latestVersion == currentVersion {
207+ return false , fmt .Errorf ("no new version available for %s (current: %s)" , pkgName , currentVersion )
208+ }
245209
246- if latestVulnVer == vulnVer {
247- return false , latestVulnVer
248- }
210+ fmt .Printf ("Found newer version for %s: %s to %s\n " , pkgName , currentVersion , latestVersion )
249211
250- if latestVulnVer != "" && latestVulnVer != vulnVer {
251- return true , latestVulnVer
252- }
212+ // Second: check latest version
213+ latestMeta , err := fetchPackageMetadata (getPackageManager ("npm" ), pkgName , latestVersion )
214+ if err != nil {
215+ return false , fmt .Errorf ("failed to fetch latest metadata for %s@%s: %w" , pkgName , latestVersion , err )
216+ }
253217
254- return false , latestVulnVer
255- }
218+ nextPkgName := packages [i + 1 ].name
256219
257- func checkVulnerabilityFix (directDep string , currentVer string , vulnPkg string , vulnVer string ) error {
220+ nextVersionInLatest := findDependencyVersion (* latestMeta , nextPkgName )
221+ if nextVersionInLatest == "" {
222+ return false , fmt .Errorf ("package %s not found in %s@%s dependencies" , nextPkgName , pkgName , latestVersion )
223+ }
258224
259- npmMeta , err := fetchPackageMetadata (getPackageManager ("npm" ), directDep , "" )
260- if err != nil {
261- return fmt .Errorf ("failed to fetch package metadata: %w" , err )
225+ normalizedNextVersion := normalizeVersion (nextVersionInLatest )
226+ fmt .Printf (" %s found in %s@%s dependencies: %s\n " , nextPkgName , pkgName , latestVersion , normalizedNextVersion )
227+
228+ packages [i + 1 ].version = normalizedNextVersion
262229 }
263230
264- versions , err := getRecommendedVersions (* npmMeta , currentVer )
265- if err != nil {
266- return fmt .Errorf ("failed to filter versions: %w" , err )
231+ vulnPkgName := packages [len (packages )- 1 ].name
232+ vulnVersion := packages [len (packages )- 1 ].version
233+
234+ if ! IsValidSemver (vulnVersion ) {
235+ return false , fmt .Errorf ("vulnerable package has invalid semver: %s@%s" , vulnPkgName , vulnVersion )
267236 }
268237
269- latestVer , err := checkVersionAvailability (versions , currentVer )
270- if err != nil {
271- fmt .Println (err )
272- return err
238+ // Parse versions to compare
239+ vulnParts := parseVersion (vulnVersion )
240+ fixedParts := parseVersion (fixedVersion )
241+
242+ isFixed := false
243+ if vulnParts [0 ] > fixedParts [0 ] {
244+ isFixed = true
245+ } else if vulnParts [0 ] == fixedParts [0 ] {
246+ if vulnParts [1 ] > fixedParts [1 ] {
247+ isFixed = true
248+ } else if vulnParts [1 ] == fixedParts [1 ] {
249+ if vulnParts [2 ] >= fixedParts [2 ] {
250+ isFixed = true
251+ }
252+ }
253+ }
254+
255+ if isFixed {
256+ fmt .Printf ("Fix verified: %s@%s is >= %s (fixed version)\n " , vulnPkgName , vulnVersion , fixedVersion )
257+ return true , nil
273258 }
274259
275- fmt .Printf ("New versions available for %s: %s -> %s\n " , directDep , currentVer , latestVer )
260+ fmt .Printf ("Fix not verified: %s@%s is < %s\n " , vulnPkgName , vulnVersion , fixedVersion )
261+ return false , nil
262+ }
276263
277- latestMeta , err := fetchPackageMetadata (getPackageManager ("npm" ), directDep , "latest" )
264+ func fetchPackageMetadata (pkgManager string , dep string , version string ) (* NPMResponse , error ) {
265+ resp , err := getVersion (pkgManager , RegistryRequest {Dependency : dep , Version : version })
278266 if err != nil {
279- return fmt .Errorf ("failed to fetch latest version metadata : %w" , err )
267+ return nil , fmt .Errorf ("error fetching %s@%s : %w" , dep , version , err )
280268 }
281269
282- isFixed , newVer := checkVulnerabilityStatus (latestMeta , vulnPkg , vulnVer )
270+ body , err := io .ReadAll (resp .Body )
271+ resp .Body .Close ()
272+ if err != nil {
273+ return nil , fmt .Errorf ("error reading response for %s@%s: %w" , dep , version , err )
274+ }
283275
284- if ! isFixed {
285- fmt . Printf ( "Vulnerability NOT fixed in latest %s (still uses %s@%s) \n " , directDep , vulnPkg , vulnVer )
286- return nil
276+ var npmResp NPMResponse
277+ if err := json . Unmarshal ( body , & npmResp ); err != nil {
278+ return nil , fmt . Errorf ( "error unmarshalling JSON for %s@%s: %w" , dep , version , err )
287279 }
288280
289- fmt .Printf ("✓ Vulnerability FIXED in latest (uses %s@%s instead of %s)\n " , vulnPkg , newVer , vulnVer )
290- return nil
281+ return & npmResp , nil
291282}
292283
293284func main () {
294- directDependency := "playwright"
295- currentVersion := "1.50.1"
296- directVulnerablePackage := "fsevents"
297- directVulnerableVersion := "2.3.2"
298- // transitiveVulnerablePackage := "ip"
299- // transitiveVulnerableVersion := "1.1.5"
300-
301- if err := checkVulnerabilityFix (directDependency , currentVersion , directVulnerablePackage , directVulnerableVersion ); err != nil {
285+ purls := []string {
286+ "pkg:npm/react-markdown@9.0.1" ,
287+ "pkg:npm/remark-rehype@11.1.1" ,
288+ "pkg:npm/mdast-util-to-hast@13.2.0" ,
289+ }
290+ fixedVersion := "13.2.1"
291+
292+ isFixed , err := checkVulnerabilityFixChain (purls , fixedVersion )
293+ if err != nil {
302294 fmt .Println ("Error:" , err )
295+ return
303296 }
297+ fmt .Println (isFixed )
304298}
0 commit comments