2121# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222
2323import asyncio
24+ from collections import namedtuple
25+ from dateutil import parser
2426from json import JSONDecodeError
2527from typing import Mapping
2628from typing import Set
3234from aiohttp .client_exceptions import ServerDisconnectedError
3335
3436
37+ Version = namedtuple ("Version" , field_names = ["value" , "release_date" ])
38+
39+
3540class VersionAPI :
3641 def __init__ (self , cache : Mapping [str , Set [str ]] = None ):
3742 self .cache = cache or {}
3843
39- def get (self , package_name : str ) -> Set [str ]:
40- return self .cache .get (package_name , set ())
44+ def get (self , package_name , until = None ) -> Set [str ]:
45+ versions = {"new" : set (), "valid" : set ()}
46+ for version in self .cache .get (package_name , set ()):
47+ if until and version .release_date and version .release_date > until :
48+ versions ["new" ].add (version .value )
49+ continue
50+ versions ["valid" ].add (version .value )
51+
52+ return versions
4153
4254
4355def client_session ():
@@ -97,7 +109,16 @@ async def fetch(self, pkg, session):
97109 try :
98110 response = await session .request (method = "GET" , url = url )
99111 response = await response .json ()
100- versions = set (response ["releases" ])
112+ for version in response ["releases" ]:
113+ if response ["releases" ][version ]:
114+ versions .add (
115+ Version (
116+ value = version ,
117+ release_date = parser .parse (
118+ response ["releases" ][version ][- 1 ]["upload_time_iso_8601" ]
119+ ),
120+ )
121+ )
101122 except ClientResponseError :
102123 # PYPI removed this package.
103124 # https://www.zdnet.com/article/twelve-malicious-python-libraries-found-and-removed-from-pypi/ # nopep8
@@ -121,7 +142,11 @@ async def fetch(self, pkg, session):
121142 response = await response .json ()
122143 versions = set ()
123144 for version_info in response ["versions" ]:
124- versions .add (version_info ["num" ])
145+ versions .add (
146+ Version (
147+ value = version_info ["num" ], release_date = parser .parse (version_info ["updated_at" ])
148+ )
149+ )
125150
126151 self .cache [pkg ] = versions
127152
@@ -143,7 +168,11 @@ async def fetch(self, pkg, session):
143168 response = await session .request (method = "GET" , url = url )
144169 response = await response .json ()
145170 for release in response :
146- versions .add (release ["number" ])
171+ versions .add (
172+ Version (
173+ value = release ["number" ], release_date = parser .parse (release ["created_at" ])
174+ )
175+ )
147176 except (ClientResponseError , JSONDecodeError ):
148177 pass
149178
@@ -166,7 +195,13 @@ async def fetch(self, pkg, session):
166195 try :
167196 response = await session .request (method = "GET" , url = url )
168197 response = await response .json ()
169- versions = {v for v in response .get ("versions" , [])}
198+ for version in response .get ("versions" , []):
199+ release_date = response .get ("time" , {}).get (version )
200+ if release_date :
201+ release_date = parser .parse (release_date )
202+ versions .add (Version (value = version , release_date = release_date ))
203+ else :
204+ versions .add (Version (value = version , release_date = None ))
170205
171206 except ClientResponseError :
172207 pass
@@ -294,7 +329,12 @@ def extract_versions(resp: dict) -> Set[str]:
294329 try :
295330 for entry_group in resp ["items" ]:
296331 for entry in entry_group ["items" ]:
297- all_versions .add (entry ["catalogEntry" ]["version" ])
332+ all_versions .add (
333+ Version (
334+ value = entry ["catalogEntry" ]["version" ],
335+ release_date = parser .parse (entry ["catalogEntry" ]["published" ]),
336+ )
337+ )
298338 # FIXME: json response for YamlDotNet.Signed triggers this exception.
299339 # Some packages with many versions give a response of a list of endpoints.
300340 # In such cases rather, we should collect data from those endpoints.
@@ -327,15 +367,21 @@ def composer_url(pkg_name: str) -> str:
327367 vendor , name = pkg_name .split ("/" )
328368 except ValueError :
329369 # TODO Log this
330- return None
370+ return
331371 return f"https://repo.packagist.org/p/{ vendor } /{ name } .json"
332372
333373 @staticmethod
334374 def extract_versions (resp : dict , pkg_name : str ) -> Set [str ]:
335- all_versions = resp ["packages" ][pkg_name ].keys ()
336- all_versions = {
337- version .replace ("v" , "" ) for version in all_versions if "dev" not in version
338- }
375+ all_versions = set ()
376+ for version in resp ["packages" ][pkg_name ]:
377+ if "dev" in version :
378+ continue
379+ all_versions .add (
380+ Version (
381+ value = version .replace ("v" , "" ),
382+ release_date = parser .parse (resp ["packages" ][pkg_name ][version ]["time" ]),
383+ )
384+ )
339385 # This if statement ensures, that all_versions contains only released versions
340386 # See https://github.com/composer/composer/blob/44a4429978d1b3c6223277b875762b2930e83e8c/doc/articles/versions.md#tags # nopep8
341387 # for explanation of removing 'v'
0 commit comments