77 "fmt"
88 "io"
99 "net/http"
10+ "regexp"
1011 "sort"
1112 "strings"
1213)
@@ -39,38 +40,25 @@ func getVersion(packageManager string, pkg RegistryRequest) (*http.Response, err
3940 return nil , nil
4041}
4142
42- func generalizeAllVersions (resp []byte ) [][]string {
43- var npmResponseObject NPMResponse
44-
45- err := json .Unmarshal (resp , & npmResponseObject )
46-
47- if err != nil {
48- fmt .Println ("Error unmarshalling JSON:" , err )
49- return nil
50- }
51-
43+ func getRecommendedVersions (npmResponse NPMResponse , currentVersion string ) ([]string , error ) {
5244 var versions [][]string
53- for _ , Obj := range npmResponseObject .Versions {
54- // skip release candidates since recommending alpha version is not a good idea for security updates haha
55- if strings .Contains (Obj .Version , "-" ) {
45+
46+ // Extract and filter versions from NPMResponse
47+ for _ , obj := range npmResponse .Versions {
48+ // skip release candidates
49+ if strings .Contains (obj .Version , "-" ) {
5650 continue
5751 }
58- // split numbers into array to easily compare major versions later
59- versionParts := strings .Split (Obj .Version , "." )
52+ versionParts := strings .Split (obj .Version , "." )
6053 versions = append (versions , versionParts )
61-
6254 }
63- return versions
64- }
6555
66- func filterMajorVersions (versionHistory [][]string , currentVersion string ) ([]string , error ) {
67- // currentParts := strings.Split(currentVersion, ".")
56+ // Filter by major version and sort
6857 var currentMajor , currentMinor , currentPatch int
6958 fmt .Sscanf (currentVersion , "%d.%d.%d" , & currentMajor , & currentMinor , & currentPatch )
7059
7160 var recommended []string
72-
73- for _ , version := range versionHistory {
61+ for _ , version := range versions {
7462 var major , minor , patch int
7563 versionStr := strings .Join (version , "." )
7664 fmt .Sscanf (versionStr , "%d.%d.%d" , & major , & minor , & patch )
@@ -113,112 +101,32 @@ type DependencyNode struct {
113101 Dependencies map [string ]* DependencyNode
114102}
115103
116- func caretHandler (version []string ) string {
117- //version range detection:
118- // example
119- /*
120- | Dependency | Actual Range |
121- | ---------------------------------------|---------------- |
122- | socks-proxy-agent@^7.0.0 | >=7.0.0 <8.0.0 |
123- | debug@^4.3.3 | >=4.3.3 <5.0.0 |
124- | ip@^1.1.5 |>=1.1.5 <2.0.0 |
125- | test@^0.2.3 | >= 0.2.3 < 0.3.0|
126- caret applies to the most left non-zero digit in the version
127- Range detection:
128-
129-
130- */
131- //todo detect minRange maxRange
132- // minRange := "7.0.0"
133- // maxRange := "8.0.0"
134-
135- // for _, version := range versionHistory {
136- // if version[0] == currentParts[0] {
137- // if version[1] >= currentParts[1] {
138- // if version[2] >= currentParts[2] {
139- // // fmt.Println(strings.Join(version, "."))
140- // recommended = append(recommended, strings.Join(version, "."))
141- // }
142- // }
143- // }
144- // return ""
145- // }
146- return ""
147- }
148- func suggestVersion (currentVersion string , vulnerableVersion string ) string {
149- // alright so
150- return ""
151- }
152-
153- func walkDependencyTree (npmRegisterResp []byte , depName string , depVersion string , visited map [string ]bool , vulnerablePackage string , vulnerableVersion string ) * DependencyNode {
154- var jsonData NPMResponse
104+ func IsValidSemver (version string ) bool {
155105
156- if err := json .Unmarshal (npmRegisterResp , & jsonData ); err != nil {
157- // fmt.Println("Error unmarshalling JSON:", err)
158- return nil
159- }
160-
161- nodeKey := depName + "@" + depVersion
162- if visited [nodeKey ] {
163- return nil
164- }
165- visited [nodeKey ] = true
166-
167- node := & DependencyNode {
168- Name : depName ,
169- Version : depVersion ,
170- Dependencies : make (map [string ]* DependencyNode ),
171- }
172-
173- if jsonData .Dependencies == nil && jsonData .DevDependencies == nil && jsonData .PeerDependencies == nil && jsonData .OptionalDependencies == nil {
174- //fmt.Printf("No dependencies found for %s@%s\n", depName, depVersion)
175- return node
176- }
177-
178- for depKey , depVal := range jsonData .Dependencies {
179- //fmt.Printf("Fetching dependency: %s@%s\n", depKey, depVal)
180-
181- // Check if version exists before fetching
182- if ! VersionExists (depKey , depVal ) {
183- fmt .Printf ("Skipping %s@%s: version not found\n " , depKey , depVal )
184- continue
185- }
106+ pattern := `^\d+\.\d+\.\d+$`
107+ matched , _ := regexp .MatchString (pattern , version )
108+ return matched
109+ }
186110
187- depResp , err := GetNPMRegistry (RegistryRequest {Dependency : depKey , Version : depVal })
188- if err != nil {
189- fmt .Printf ("Error fetching %s@%s: %v\n " , depKey , depVal , err )
190- continue
191- }
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 , "^~\" " )
192115
193- depBody , err := io .ReadAll (depResp .Body )
194- depResp .Body .Close ()
195- if err != nil {
196- fmt .Printf ("Error reading response for %s@%s: %v\n " , depKey , depVal , err )
116+ // Skip non-semver versions
117+ if ! IsValidSemver (normalizedDepVal ) {
197118 continue
198119 }
199120
200- // Recursive call
201- childNode := walkDependencyTree (depBody , depKey , depVal , visited , vulnerablePackage , vulnerableVersion )
202- if childNode != nil {
203- node .Dependencies [depKey ] = childNode
204- }
205- if depKey == vulnerablePackage && depVal == vulnerableVersion {
206- fmt .Printf ("Vulnerable package found: %s@%s\n " , depKey , depVal )
207- }
208- }
209-
210- for depKey , depVal := range jsonData .OptionalDependencies {
211- //fmt.Printf("Fetching optional dependency: %s@%s\n", depKey, depVal)
212-
213121 // Check if version exists before fetching
214- if ! VersionExists (depKey , depVal ) {
215- fmt .Printf ("Skipping %s@%s: version not found\n " , depKey , depVal )
122+ if ! VersionExists (depKey , normalizedDepVal ) {
123+ fmt .Printf ("Skipping %s@%s: version not found\n " , depKey , normalizedDepVal )
216124 continue
217125 }
218126
219127 depResp , err := GetNPMRegistry (RegistryRequest {Dependency : depKey , Version : depVal })
220128 if err != nil {
221- fmt .Printf ("Error fetching %s@%s: %v\n " , depKey , node . Version , err )
129+ fmt .Printf ("Error fetching %s@%s: %v\n " , depKey , depVal , err )
222130 continue
223131 }
224132
@@ -238,39 +146,36 @@ func walkDependencyTree(npmRegisterResp []byte, depName string, depVersion strin
238146 fmt .Printf ("Vulnerable package found: %s@%s\n " , depKey , depVal )
239147 }
240148 }
149+ }
241150
242- for depKey , depVal := range jsonData . DevDependencies {
243- //fmt.Printf("Fetching dev dependency: %s@%s\n", depKey, depVal)
151+ func walkDependencyTree ( npmRegisterResp [] byte , depName string , depVersion string , visited map [ string ] bool , vulnerablePackage string , vulnerableVersion string ) * DependencyNode {
152+ var jsonData NPMResponse
244153
245- // Check if version exists before fetching
246- if ! VersionExists (depKey , depVal ) {
247- fmt .Printf ("Skipping %s@%s: version not found\n " , depKey , depVal )
248- continue
249- }
154+ if err := json .Unmarshal (npmRegisterResp , & jsonData ); err != nil {
155+ return nil
156+ }
250157
251- depResp , err := GetNPMRegistry ( RegistryRequest { Dependency : depKey , Version : depVal })
252- if err != nil {
253- fmt . Printf ( "Error fetching %s@%s: %v \n " , depKey , depVal , err )
254- continue
255- }
158+ nodeKey := depName + "@" + depVersion
159+ if visited [ nodeKey ] {
160+ return nil
161+ }
162+ visited [ nodeKey ] = true
256163
257- depBody , err := io .ReadAll (depResp .Body )
258- depResp .Body .Close ()
259- if err != nil {
260- fmt .Printf ("Error reading response for %s@%s: %v\n " , depKey , depVal , err )
261- continue
262- }
164+ node := & DependencyNode {
165+ Name : depName ,
166+ Version : depVersion ,
167+ Dependencies : make (map [string ]* DependencyNode ),
168+ }
263169
264- // Recursive call
265- childNode := walkDependencyTree (depBody , depKey , depVal , visited , vulnerablePackage , vulnerableVersion )
266- if childNode != nil {
267- node .Dependencies [depKey ] = childNode
268- }
269- if depKey == vulnerablePackage && depVal == vulnerableVersion {
270- fmt .Printf ("Vulnerable package found: %s@%s\n " , depKey , depVal )
271- }
170+ if jsonData .Dependencies == nil && jsonData .DevDependencies == nil && jsonData .PeerDependencies == nil && jsonData .OptionalDependencies == nil {
171+ return node
272172 }
273173
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 )
178+
274179 return node
275180}
276181
@@ -286,51 +191,114 @@ func printDependencyTree(node *DependencyNode, indent string) {
286191 }
287192}
288193
289- func main () {
290- DirectDependency := "playwright"
291- currentVersion := "1.50.1"
292- vulnerablePackage := "ip"
293- vulnerableVersion := "1.1.5"
294- resp , err := getVersion (getPackageManager ("npm" ), RegistryRequest {Dependency : DirectDependency })
194+ func findDependencyVersion (npmResp NPMResponse , depName string ) string {
195+ // Check all dependency types
196+ if version , ok := npmResp .Dependencies [depName ]; ok {
197+ return version
198+ }
199+ if version , ok := npmResp .OptionalDependencies [depName ]; ok {
200+ return version
201+ }
202+ if version , ok := npmResp .DevDependencies [depName ]; ok {
203+ return version
204+ }
205+ if version , ok := npmResp .PeerDependencies [depName ]; ok {
206+ return version
207+ }
208+ return ""
209+ }
210+
211+ func fetchPackageMetadata (pkgManager string , dep string , version string ) (* NPMResponse , error ) {
212+ resp , err := getVersion (pkgManager , RegistryRequest {Dependency : dep , Version : version })
295213 if err != nil {
296- fmt .Println ("Error:" , err )
297- return
214+ return nil , fmt .Errorf ("error fetching %s@%s: %w" , dep , version , err )
298215 }
216+
299217 body , err := io .ReadAll (resp .Body )
218+ resp .Body .Close ()
300219 if err != nil {
301- fmt .Println ("Error reading body:" , err )
302- return
220+ return nil , fmt .Errorf ("error reading response for %s@%s: %w" , dep , version , err )
221+ }
222+
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 )
303226 }
304- defer resp .Body .Close ()
305227
306- versions , err := filterMajorVersions (generalizeAllVersions (body ), currentVersion )
228+ return & npmResp , nil
229+ }
230+
231+ func checkVersionAvailability (versions []string , currentVersion string ) (string , error ) {
232+ if len (versions ) == 0 {
233+ return "" , fmt .Errorf ("no versions available" )
234+ }
235+
236+ if versions [0 ] == currentVersion {
237+ return "" , fmt .Errorf ("no new version available (current: %s)" , currentVersion )
238+ }
239+
240+ return versions [0 ], nil
241+ }
242+
243+ func checkVulnerabilityStatus (latestMeta * NPMResponse , vulnPkg string , vulnVer string ) (bool , string ) {
244+ latestVulnVer := findDependencyVersion (* latestMeta , vulnPkg )
245+
246+ if latestVulnVer == vulnVer {
247+ return false , latestVulnVer
248+ }
249+
250+ if latestVulnVer != "" && latestVulnVer != vulnVer {
251+ return true , latestVulnVer
252+ }
253+
254+ return false , latestVulnVer
255+ }
256+
257+ func checkVulnerabilityFix (directDep string , currentVer string , vulnPkg string , vulnVer string ) error {
258+
259+ npmMeta , err := fetchPackageMetadata (getPackageManager ("npm" ), directDep , "" )
307260 if err != nil {
308- fmt .Println ("Error filtering versions:" , err )
309- return
261+ return fmt .Errorf ("failed to fetch package metadata: %w" , err )
310262 }
311263
312- for _ , version := range versions {
313- npmResponse , err := GetNPMRegistry (RegistryRequest {Dependency : DirectDependency , Version : version })
314- if err != nil {
315- fmt .Println ("Error fetching version details:" , err )
316- continue
317- }
264+ versions , err := getRecommendedVersions (* npmMeta , currentVer )
265+ if err != nil {
266+ return fmt .Errorf ("failed to filter versions: %w" , err )
267+ }
318268
319- response , err := io .ReadAll (npmResponse .Body )
320- npmResponse .Body .Close ()
321- if err != nil {
322- fmt .Println ("Error reading response:" , err )
323- continue
324- }
269+ latestVer , err := checkVersionAvailability (versions , currentVer )
270+ if err != nil {
271+ fmt .Println (err )
272+ return err
273+ }
325274
326- // Build dependency tree recursively
327- visited := make (map [string ]bool )
328- tree := walkDependencyTree (response , DirectDependency , version , visited , vulnerablePackage , vulnerableVersion )
275+ fmt .Printf ("New versions available for %s: %s -> %s\n " , directDep , currentVer , latestVer )
329276
330- // fmt.Println(tree)
331- for _ , dep := range tree .Dependencies {
332- fmt .Println (dep )
333- }
334- printDependencyTree (tree , "" )
277+ latestMeta , err := fetchPackageMetadata (getPackageManager ("npm" ), directDep , "latest" )
278+ if err != nil {
279+ return fmt .Errorf ("failed to fetch latest version metadata: %w" , err )
280+ }
281+
282+ isFixed , newVer := checkVulnerabilityStatus (latestMeta , vulnPkg , vulnVer )
283+
284+ if ! isFixed {
285+ fmt .Printf ("Vulnerability NOT fixed in latest %s (still uses %s@%s)\n " , directDep , vulnPkg , vulnVer )
286+ return nil
287+ }
288+
289+ fmt .Printf ("✓ Vulnerability FIXED in latest (uses %s@%s instead of %s)\n " , vulnPkg , newVer , vulnVer )
290+ return nil
291+ }
292+
293+ func 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 {
302+ fmt .Println ("Error:" , err )
335303 }
336304}
0 commit comments