Skip to content

Commit 5b6bb67

Browse files
committed
add tests for get_package_version
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 2318841 commit 5b6bb67

4 files changed

Lines changed: 11284 additions & 102 deletions

File tree

vulnerabilities/importers/github.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
# VulnerableCode is a free software from nexB Inc. and others.
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222

23-
import json
2423
import logging
2524
import os
2625
from datetime import datetime
@@ -48,13 +47,13 @@
4847
from vulnerabilities.improver import Improver
4948
from vulnerabilities.improver import Inference
5049
from vulnerabilities.models import Advisory
51-
from vulnerabilities.package_managers import Version as PackageVersion
5250
from vulnerabilities.package_managers_2 import ComposerVersionAPI
5351
from vulnerabilities.package_managers_2 import GoproxyVersionAPI
5452
from vulnerabilities.package_managers_2 import MavenVersionAPI
5553
from vulnerabilities.package_managers_2 import NugetVersionAPI
5654
from vulnerabilities.package_managers_2 import PypiVersionAPI
5755
from vulnerabilities.package_managers_2 import RubyVersionAPI
56+
from vulnerabilities.package_managers_2 import Version as PackageVersion
5857
from vulnerabilities.package_managers_2 import VersionAPI
5958
from vulnerabilities.severity_systems import SCORING_SYSTEMS
6059

@@ -434,24 +433,23 @@ def interesting_advisories(self) -> QuerySet:
434433
return Advisory.objects.filter(created_by=GitHubAPIImporter.qualified_name)
435434

436435
def get_package_versions(
437-
self, package_url: PackageURL, until: Optional[datetime]
436+
self, package_url: PackageURL, until: Optional[datetime] = None
438437
) -> List[PackageVersion]:
439438
"""
440439
Return a list of `PackageVersion` for the `package_url`
441440
"""
442-
purl = str(package_url)
443-
version_api = self.version_api_by_purl_type.get(purl.type)
441+
api_name = get_api_package_name(package_url)
442+
if not api_name:
443+
LOGGER.error(f"Could not get versions for {package_url!r}")
444+
return []
445+
version_api = self.version_api_by_purl_type.get(package_url.type)
444446
if not version_api:
445-
version_api: VersionAPI = VERSION_API_CLASSES_BY_PACKAGE_TYPE(package_url.type)
446-
api_name = get_api_package_name(package_url)
447-
if not api_name:
448-
LOGGER.error(f"Could not get versions for {package_url!r}")
449-
return []
450-
451-
version_api.load_api([api_name])
452-
self.version_api_by_purl_type[purl.type] = version_api()
453-
454-
return version_api.get(package_name=api_name, until=until).valid_versions
447+
version_api: VersionAPI = VERSION_API_CLASSES_BY_PACKAGE_TYPE[package_url.type]
448+
self.version_api_by_purl_type[package_url.type] = version_api()
449+
api_object = self.version_api_by_purl_type[package_url.type]
450+
api_object.load_api([api_name])
451+
self.version_api_by_purl_type[package_url.type] = api_object
452+
return api_object.get(package_name=api_name, until=until).valid_versions
455453

456454
def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
457455
"""

vulnerabilities/package_managers_2.py

Lines changed: 99 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,43 @@
1+
import dataclasses
2+
import logging
13
import traceback
24
import xml.etree.ElementTree as ET
5+
from datetime import datetime
36
from json import JSONDecodeError
47
from typing import List
58
from typing import MutableMapping
69
from typing import Optional
710
from typing import Set
811

9-
from aiohttp import ClientResponseError
10-
from aiohttp import ClientSession
11-
from aiohttp.web_exceptions import HTTPGone
12+
import requests
1213
from dateutil import parser as dateparser
1314
from django.utils.dateparse import parse_datetime
1415

1516
from vulnerabilities.package_managers import Version as LegacyVersion
1617
from vulnerabilities.package_managers import VersionResponse
1718

19+
LOGGER = logging.getLogger(__name__)
20+
21+
22+
@dataclasses.dataclass(frozen=True)
23+
class Version:
24+
value: str
25+
release_date: Optional[datetime] = None
26+
27+
28+
def get_response(url, type="json"):
29+
resp = requests.get(url=url)
30+
type_of_response = {
31+
"json": resp.json(),
32+
"text": resp.text,
33+
"read": resp.content,
34+
}
35+
if resp.status_code == 200:
36+
return type_of_response[type]
37+
else:
38+
LOGGER.error(f"Error while fetching {url}: {resp.status_code}")
39+
return None
40+
1841

1942
class VersionAPI:
2043
def __init__(self, cache: MutableMapping[str, Set[LegacyVersion]] = None):
@@ -35,13 +58,12 @@ def load_api(self, pkg_set):
3558
"""
3659
Populate the cache with the versions of the packages in pkg_set
3760
"""
38-
with ClientSession() as session:
39-
for pkg in pkg_set:
40-
if pkg in self.cache:
41-
continue
42-
self.fetch(pkg, session)
61+
for pkg in pkg_set:
62+
if pkg in self.cache:
63+
continue
64+
self.fetch(pkg)
4365

44-
def fetch(self, pkg, session):
66+
def fetch(self, pkg):
4567
"""
4668
Override this method to fetch the pkg's version in the cache
4769
"""
@@ -52,54 +74,50 @@ class PypiVersionAPI(VersionAPI):
5274

5375
package_type = "pypi"
5476

55-
def fetch(self, pkg, session):
77+
def fetch(self, pkg):
5678
url = f"https://pypi.org/pypi/{pkg}/json"
5779
versions = set()
58-
try:
59-
response = session.request(method="GET", url=url)
60-
response = response.json()
61-
for version, download_items in response["releases"].items():
62-
if download_items:
63-
latest_download_item = max(
64-
download_items,
65-
key=lambda download_item: dateparser.parse(
66-
download_item["upload_time_iso_8601"]
67-
),
68-
)
69-
versions.add(
70-
LegacyVersion(
71-
value=version,
72-
release_date=dateparser.parse(
73-
latest_download_item["upload_time_iso_8601"]
74-
),
75-
)
80+
response = get_response(url=url)
81+
82+
if not response:
83+
self.cache[pkg] = versions
84+
return
85+
86+
for version, download_items in response["releases"].items():
87+
if download_items:
88+
latest_download_item = max(
89+
download_items,
90+
key=lambda download_item: dateparser.parse(
91+
download_item["upload_time_iso_8601"]
92+
),
93+
)
94+
versions.add(
95+
LegacyVersion(
96+
value=version,
97+
release_date=dateparser.parse(latest_download_item["upload_time_iso_8601"]),
7698
)
77-
except ClientResponseError:
78-
# PYPI removed this package.
79-
# https://www.zdnet.com/article/twelve-malicious-python-libraries-found-and-removed-from-pypi/ # nopep8
80-
pass
99+
)
81100
self.cache[pkg] = versions
82101

83102

84103
class RubyVersionAPI(VersionAPI):
85104

86105
package_type = "gem"
87106

88-
def fetch(self, pkg, session):
107+
def fetch(self, pkg):
89108
url = f"https://rubygems.org/api/v1/versions/{pkg}.json"
90109
versions = set()
91-
try:
92-
response = session.request(method="GET", url=url)
93-
response = response.json()
94-
for release in response:
95-
versions.add(
96-
LegacyVersion(
97-
value=release["number"],
98-
release_date=dateparser.parse(release["created_at"]),
99-
)
110+
response = get_response(url=url)
111+
if not response:
112+
self.cache[pkg] = versions
113+
return
114+
for release in response:
115+
versions.add(
116+
LegacyVersion(
117+
value=release["number"],
118+
release_date=dateparser.parse(release["created_at"]),
100119
)
101-
except (ClientResponseError, JSONDecodeError):
102-
pass
120+
)
103121

104122
self.cache[pkg] = versions
105123

@@ -108,22 +126,20 @@ class NpmVersionAPI(VersionAPI):
108126

109127
package_type = "npm"
110128

111-
def fetch(self, pkg, session):
129+
def fetch(self, pkg):
112130
url = f"https://registry.npmjs.org/{pkg}"
113131
versions = set()
114-
try:
115-
response = session.request(method="GET", url=url)
116-
response = response.json()
117-
for version in response.get("versions", []):
118-
release_date = response.get("time", {}).get(version)
119-
if release_date:
120-
release_date = dateparser.parse(release_date)
121-
versions.add(LegacyVersion(value=version, release_date=release_date))
122-
else:
123-
versions.add(LegacyVersion(value=version, release_date=None))
124-
125-
except ClientResponseError:
126-
pass
132+
response = get_response(url=url)
133+
if not response:
134+
self.cache[pkg] = versions
135+
return
136+
for version in response.get("versions", []):
137+
release_date = response.get("time", {}).get(version)
138+
if release_date:
139+
release_date = dateparser.parse(release_date)
140+
versions.add(LegacyVersion(value=version, release_date=release_date))
141+
else:
142+
versions.add(LegacyVersion(value=version, release_date=None))
127143

128144
self.cache[pkg] = versions
129145

@@ -132,14 +148,13 @@ class MavenVersionAPI(VersionAPI):
132148

133149
package_type = "maven"
134150

135-
def fetch(self, pkg, session) -> None:
151+
def fetch(self, pkg) -> None:
136152
artifact_comps = pkg.split(":")
137153
endpoint = self.artifact_url(artifact_comps)
138-
try:
139-
resp = session.request(method="GET", url=endpoint)
140-
resp = resp.read()
141154

142-
except ClientResponseError:
155+
resp = get_response(url=endpoint, type="read")
156+
157+
if not resp:
143158
self.cache[pkg] = set()
144159
return
145160

@@ -182,10 +197,12 @@ class NugetVersionAPI(VersionAPI):
182197

183198
package_type = "nuget"
184199

185-
def fetch(self, pkg, session) -> None:
200+
def fetch(self, pkg) -> None:
186201
endpoint = self.nuget_url(pkg)
187-
resp = session.request(method="GET", url=endpoint)
188-
resp = resp.json()
202+
resp = get_response(url=endpoint)
203+
if not resp:
204+
self.cache[pkg] = set()
205+
return
189206
self.cache[pkg] = self.extract_versions(resp)
190207

191208
@staticmethod
@@ -271,9 +288,7 @@ def escape_path(path: str) -> str:
271288
return escaped_path
272289

273290
@staticmethod
274-
def parse_version_info(
275-
version_info: str, escaped_pkg: str, session: ClientSession
276-
) -> Optional[LegacyVersion]:
291+
def parse_version_info(version_info: str, escaped_pkg: str) -> Optional[LegacyVersion]:
277292
v = version_info.split()
278293
if not v:
279294
return None
@@ -283,33 +298,28 @@ def parse_version_info(
283298
release_date = parse_datetime(v[1])
284299
else:
285300
escaped_ver = GoproxyVersionAPI.escape_path(value)
286-
try:
287-
response = session.request(
288-
method="GET",
289-
url=f"https://proxy.golang.org/{escaped_pkg}/@v/{escaped_ver}.info",
290-
)
291-
resp_json = response.json()
292-
release_date = parse_datetime(resp_json.get("Time", ""))
293-
except:
301+
resp_json = get_response(
302+
url=f"https://proxy.golang.org/{escaped_pkg}/@v/{escaped_ver}.info"
303+
)
304+
if not resp_json:
294305
traceback.print_exc()
295306
print(
296307
f"error while fetching version info for {escaped_pkg}/{escaped_ver} from goproxy"
297308
)
298-
release_date = None
309+
release_date = parse_datetime(resp_json.get("Time", "")) if resp_json else None
310+
299311
return LegacyVersion(value=value, release_date=release_date)
300312

301-
def fetch(self, pkg: str, session: ClientSession):
313+
def fetch(self, pkg: str):
302314
# escape uppercase in module path
303315
escaped_pkg = GoproxyVersionAPI.escape_path(pkg)
304316
trimmed_pkg = pkg
305317
resp_text = None
306318
# resolve module name from package name, see https://go.dev/ref/mod#resolve-pkg-mod
307319
while escaped_pkg is not None:
308320
url = f"https://proxy.golang.org/{escaped_pkg}/@v/list"
309-
try:
310-
response = session.request(method="GET", url=url)
311-
resp_text = response.text()
312-
except HTTPGone:
321+
resp_text = get_response(url=url, type="text")
322+
if not resp_text:
313323
escaped_pkg = GoproxyVersionAPI.trim_url_path(escaped_pkg)
314324
trimmed_pkg = GoproxyVersionAPI.trim_url_path(trimmed_pkg) or ""
315325
continue
@@ -320,7 +330,7 @@ def fetch(self, pkg: str, session: ClientSession):
320330
self.module_name_by_package_name[pkg] = trimmed_pkg
321331
versions = set()
322332
for version_info in resp_text.split("\n"):
323-
version = GoproxyVersionAPI.parse_version_info(version_info, escaped_pkg, session)
333+
version = GoproxyVersionAPI.parse_version_info(version_info, escaped_pkg)
324334
if version is not None:
325335
versions.add(version)
326336
self.cache[pkg] = versions
@@ -330,11 +340,13 @@ class ComposerVersionAPI(VersionAPI):
330340

331341
package_type = "composer"
332342

333-
def fetch(self, pkg, session) -> None:
343+
def fetch(self, pkg) -> None:
334344
endpoint = self.composer_url(pkg)
335345
if endpoint:
336-
resp = session.request(method="GET", url=endpoint)
337-
resp = resp.json()
346+
resp = get_response(url=endpoint)
347+
if not resp:
348+
self.cache[pkg] = set()
349+
return
338350
self.cache[pkg] = self.extract_versions(resp, pkg)
339351

340352
@staticmethod

0 commit comments

Comments
 (0)