33import traceback
44import xml .etree .ElementTree as ET
55from datetime import datetime
6- from json import JSONDecodeError
76from typing import List
87from typing import MutableMapping
98from typing import Optional
109from typing import Set
10+ from urllib .parse import urlparse
1111
1212import requests
1313from dateutil import parser as dateparser
1414from django .utils .dateparse import parse_datetime
1515
16- from vulnerabilities .package_managers import Version as LegacyVersion
1716from vulnerabilities .package_managers import VersionResponse
1817
1918LOGGER = logging .getLogger (__name__ )
2019
2120
2221@dataclasses .dataclass (frozen = True )
23- class Version :
22+ class LegacyVersion :
2423 value : str
2524 release_date : Optional [datetime ] = None
2625
2726
27+ @dataclasses .dataclass
28+ class VersionResponse :
29+ valid_versions : Set [str ] = dataclasses .field (default_factory = set )
30+ newer_versions : Set [str ] = dataclasses .field (default_factory = set )
31+
32+
2833def get_response (url , type = "json" ):
2934 resp = requests .get (url = url )
3035 if not resp .status_code == 200 :
@@ -81,12 +86,14 @@ def fetch(self, pkg):
8186 self .cache [pkg ] = versions
8287 return
8388
84- for version , download_items in response ["releases" ].items ():
89+ for version , download_items in response ["releases" ].items () or {} :
8590 if download_items :
8691 latest_download_item = max (
8792 download_items ,
8893 key = lambda download_item : dateparser .parse (
8994 download_item ["upload_time_iso_8601" ]
95+ if "upload_time_iso_8601" in download_item
96+ else LOGGER .error (f"{ download_item } has no upload_time_iso_8601" )
9097 ),
9198 )
9299 versions .add (
@@ -110,34 +117,11 @@ def fetch(self, pkg):
110117 self .cache [pkg ] = versions
111118 return
112119 for release in response :
113- versions .add (
114- LegacyVersion (
115- value = release ["number" ],
116- release_date = dateparser .parse (release ["created_at" ]),
117- )
118- )
119-
120- self .cache [pkg ] = versions
121-
122-
123- class NpmVersionAPI (VersionAPI ):
124-
125- package_type = "npm"
126-
127- def fetch (self , pkg ):
128- url = f"https://registry.npmjs.org/{ pkg } "
129- versions = set ()
130- response = get_response (url = url )
131- if not response :
132- self .cache [pkg ] = versions
133- return
134- for version in response .get ("versions" , []):
135- release_date = response .get ("time" , {}).get (version )
136- if release_date :
137- release_date = dateparser .parse (release_date )
138- versions .add (LegacyVersion (value = version , release_date = release_date ))
120+ if release ["number" ] and release ["published_at" ]:
121+ release_date = dateparser .parse (release ["published_at" ])
122+ versions .add (LegacyVersion (value = release ["number" ], release_date = release_date ))
139123 else :
140- versions . add ( LegacyVersion ( value = version , release_date = None ) )
124+ LOGGER . error ( f"Failed to parse release { release } " )
141125
142126 self .cache [pkg ] = versions
143127
@@ -213,19 +197,23 @@ def nuget_url(pkg_name: str) -> str:
213197 def extract_versions (resp : dict ) -> Set [LegacyVersion ]:
214198 all_versions = set ()
215199 try :
216- for entry_group in resp ["items" ]:
217- for entry in entry_group ["items" ]:
218- all_versions .add (
219- LegacyVersion (
220- value = entry ["catalogEntry" ]["version" ],
221- release_date = dateparser .parse (entry ["catalogEntry" ]["published" ]),
200+ for entry_group in resp ["items" ] or []:
201+ for entry in entry_group ["items" ] or []:
202+ catalog_entry = entry ["catalogEntry" ] or {}
203+ version = catalog_entry .get ("version" )
204+ release_date = dateparser .parse (catalog_entry .get ("published" ))
205+ if version and release_date :
206+ all_versions .add (
207+ LegacyVersion (
208+ value = version ,
209+ release_date = release_date ,
210+ )
222211 )
223- )
224212 # FIXME: json response for YamlDotNet.Signed triggers this exception.
225213 # Some packages with many versions give a response of a list of endpoints.
226214 # In such cases rather, we should collect data from those endpoints.
227215 except KeyError :
228- pass
216+ LOGGER . error ( f"Failed to parse nuget response for { resp !r } " )
229217
230218 return all_versions
231219
@@ -239,7 +227,7 @@ def __init__(self, cache: MutableMapping[str, Set[LegacyVersion]] = None):
239227 self .module_name_by_package_name = {}
240228
241229 @staticmethod
242- def trim_url_path (url_path : str ) -> Optional [str ]:
230+ def trim_go_url_path (url_path : str ) -> Optional [str ]:
243231 """
244232 Return a trimmed Go `url_path` removing trailing
245233 package references and keeping only the module
@@ -254,17 +242,21 @@ def trim_url_path(url_path: str) -> Optional[str]:
254242 This functions trims the trailing part(s) of a package URL
255243 and returns the remaining the module name.
256244 For example:
257- >>> module = "https:// github.com/xx/a"
258- >>> assert GoproxyVersionAPI.trim_url_path ("https://github.com/xx/a/b") == module
245+ >>> module = "github.com/xx/a"
246+ >>> assert GoproxyVersionAPI.trim_go_url_path ("https://github.com/xx/a/b") == module
259247 """
260248 # some advisories contains this prefix in package name, e.g. https://github.com/advisories/GHSA-7h6j-2268-fhcm
261249 if url_path .startswith ("https://pkg.go.dev/" ):
262- url_path = url_path .removeprefix ("https://pkg.go.dev/" )
263- parts = url_path .split ("/" )
264- if len (parts ) >= 2 :
265- return "/" .join (parts [:- 1 ])
266- else :
250+ url_path = url_path [len ("https://pkg.go.dev/" ) :]
251+ parsed_url_path = urlparse (url_path )
252+ path = parsed_url_path .path
253+ parts = path .split ("/" )
254+ if len (parts ) < 3 :
255+ LOGGER .error (f"Not a valid Go URL path { url_path } trim_go_url_path" )
267256 return None
257+ else :
258+ joined_path = "/" .join (parts [:3 ])
259+ return f"{ parsed_url_path .netloc } { joined_path } "
268260
269261 @staticmethod
270262 def escape_path (path : str ) -> str :
@@ -318,8 +310,8 @@ def fetch(self, pkg: str):
318310 url = f"https://proxy.golang.org/{ escaped_pkg } /@v/list"
319311 resp_text = get_response (url = url , type = "text" )
320312 if not resp_text :
321- escaped_pkg = GoproxyVersionAPI .trim_url_path (escaped_pkg )
322- trimmed_pkg = GoproxyVersionAPI .trim_url_path (trimmed_pkg ) or ""
313+ escaped_pkg = GoproxyVersionAPI .trim_go_url_path (escaped_pkg )
314+ trimmed_pkg = GoproxyVersionAPI .trim_go_url_path (trimmed_pkg ) or ""
323315 continue
324316 break
325317 if resp_text is None or escaped_pkg is None or trimmed_pkg is None :
0 commit comments