Skip to content

Commit 7f74476

Browse files
committed
Migrate redhat importer
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 554384b commit 7f74476

6 files changed

Lines changed: 2354 additions & 166 deletions

File tree

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
from vulnerabilities.importers import nginx
2525
from vulnerabilities.importers import nvd
2626
from vulnerabilities.importers import openssl
27+
from vulnerabilities.importers import redhat
2728

2829
IMPORTERS_REGISTRY = [
2930
nginx.NginxImporter,
3031
alpine_linux.AlpineImporter,
3132
github.GitHubAPIImporter,
3233
nvd.NVDImporter,
3334
openssl.OpensslImporter,
35+
redhat.RedhatImporter,
3436
]
3537

3638
IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}

vulnerabilities/importers/redhat.py

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

23+
import logging
24+
import re
25+
from collections import namedtuple
26+
from typing import Dict
27+
from typing import Iterable
28+
from typing import List
29+
2330
import requests
2431
from packageurl import PackageURL
32+
from univers.version_range import RpmVersionRange
2533

2634
from vulnerabilities import severity_systems
27-
from vulnerabilities.helpers import nearest_patched_package
35+
from vulnerabilities.helpers import get_item
2836
from vulnerabilities.helpers import requests_with_5xx_retry
2937
from vulnerabilities.importer import AdvisoryData
38+
from vulnerabilities.importer import AffectedPackage
3039
from vulnerabilities.importer import Importer
3140
from vulnerabilities.importer import Reference
3241
from vulnerabilities.importer import VulnerabilitySeverity
3342

34-
35-
class RedhatImporter(Importer):
36-
def __enter__(self):
37-
38-
self.redhat_cves = fetch()
39-
40-
def updated_advisories(self):
41-
processed_advisories = list(map(to_advisory, self.redhat_cves))
42-
return self.batch_advisories(processed_advisories)
43-
43+
logger = logging.getLogger(__name__)
4444

4545
requests_session = requests_with_5xx_retry(max_retries=5, backoff_factor=1)
4646

4747

48-
def fetch():
49-
"""
50-
Return a list of CVE data mappings fetched from the RedHat API.
51-
See:
52-
https://access.redhat.com/documentation/en-us/red_hat_security_data_api/1.0/html/red_hat_security_data_api/index
53-
"""
54-
cves = []
48+
def fetch_list_of_cves() -> Iterable[List[Dict]]:
5549
page_no = 1
56-
url_template = "https://access.redhat.com/hydra/rest/securitydata/cve.json?per_page=10000&page={}" # nopep8
57-
5850
cve_data = None
5951
while True:
60-
current_url = url_template.format(page_no)
52+
current_url = f"https://access.redhat.com/hydra/rest/securitydata/cve.json?per_page=10000&page={page_no}" # nopep8
6153
try:
62-
print(f"Fetching: {current_url}")
6354
response = requests_session.get(current_url)
6455
if response.status_code != requests.codes.ok:
65-
# TODO: log me
66-
print(f"Failed to fetch results from {current_url}")
56+
logger.error(f"Failed to fetch results from {current_url}")
6757
break
6858
cve_data = response.json()
6959
except Exception as e:
70-
# TODO: log me
71-
msg = f"Failed to fetch results from {current_url}:\n{e}"
72-
print(msg)
60+
logger.error(f"Failed to fetch results from {current_url} {e}")
7361
break
74-
7562
if not cve_data:
7663
break
77-
cves.extend(cve_data)
7864
page_no += 1
65+
yield cve_data
66+
67+
68+
class RedhatImporter(Importer):
7969

80-
return cves
70+
spdx_license_expression = "TBD"
71+
72+
def advisory_data(self) -> Iterable[AdvisoryData]:
73+
for list_of_redhat_cves in fetch_list_of_cves():
74+
for redhat_cve in list_of_redhat_cves:
75+
yield to_advisory(redhat_cve)
8176

8277

8378
def to_advisory(advisory_data):
84-
affected_purls = []
85-
if advisory_data.get("affected_packages"):
86-
for rpm in advisory_data["affected_packages"]:
87-
purl = rpm_to_purl(rpm)
88-
if purl:
89-
affected_purls.append(purl)
79+
affected_packages: List[AffectedPackage] = []
80+
for rpm in advisory_data.get("affected_packages") or []:
81+
purl = rpm_to_purl(rpm)
82+
if purl:
83+
affected_packages.append(
84+
AffectedPackage(
85+
package=PackageURL(
86+
type=purl.type,
87+
name=purl.name,
88+
namespace=purl.namespace,
89+
qualifiers=purl.qualifiers,
90+
subpath=purl.subpath,
91+
),
92+
affected_version_range=RpmVersionRange.from_native(f"= {purl.version}"),
93+
fixed_version=None,
94+
)
95+
)
9096

9197
references = []
9298
bugzilla = advisory_data.get("bugzilla")
@@ -114,10 +120,13 @@ def to_advisory(advisory_data):
114120
)
115121
)
116122

117-
for rh_adv in advisory_data["advisories"]:
123+
for rh_adv in advisory_data.get("advisories") or []:
118124
# RH provides 3 types of advisories RHSA, RHBA, RHEA. Only RHSA's contain severity score.
119125
# See https://access.redhat.com/articles/2130961 for more details.
120126

127+
if not isinstance(rh_adv, str):
128+
continue
129+
121130
if "RHSA" in rh_adv.upper():
122131
rhsa_data = requests_session.get(
123132
f"https://access.redhat.com/hydra/rest/securitydata/cvrf/{rh_adv}.json"
@@ -126,7 +135,7 @@ def to_advisory(advisory_data):
126135
rhsa_aggregate_severities = []
127136
if rhsa_data.get("cvrfdoc"):
128137
# not all RHSA errata have a corresponding CVRF document
129-
value = rhsa_data["cvrfdoc"]["aggregate_severity"]
138+
value = get_item(rhsa_data, "cvrfdoc", "aggregate_severity")
130139
rhsa_aggregate_severities.append(
131140
VulnerabilitySeverity(
132141
system=severity_systems.REDHAT_AGGREGATE,
@@ -166,25 +175,89 @@ def to_advisory(advisory_data):
166175

167176
references.append(Reference(severities=redhat_scores, url=advisory_data["resource_url"]))
168177
return AdvisoryData(
169-
vulnerability_id=advisory_data["CVE"],
170-
summary=advisory_data["bugzilla_description"],
171-
affected_packages=nearest_patched_package(affected_purls, []),
178+
aliases=advisory_data.get("CVE") or "",
179+
summary=advisory_data.get("bugzilla_description") or "",
180+
affected_packages=affected_packages,
172181
references=references,
173182
)
174183

175184

185+
# This code has been vendored from scancode.
186+
# https://github.com/nexB/scancode-toolkit/blob/16ae20a343c5332114edac34c7b6fcf2fb6bca74/src/packagedcode/rpm.py#L91
187+
class EVR(namedtuple("EVR", "epoch version release")):
188+
"""
189+
The RPM Epoch, Version, Release tuple.
190+
"""
191+
192+
def __new__(self, version, release=None, epoch=None):
193+
"""
194+
note: the sort order of the named tuple is the sort order.
195+
But for creation we put the rarely used epoch last with a default to None.
196+
"""
197+
if not isinstance(epoch, int):
198+
if epoch and epoch.strip():
199+
raise ValueError("Invalid epoch: must be a number or empty.")
200+
if not version:
201+
raise ValueError("Version is required: {}".format(repr(version)))
202+
203+
return super().__new__(EVR, epoch, version, release)
204+
205+
def __str__(self, *args, **kwargs):
206+
return self.to_string()
207+
208+
def to_string(self):
209+
if self.release:
210+
vr = f"{self.version}-{self.release}"
211+
else:
212+
vr = self.version
213+
214+
if self.epoch:
215+
vr = ":".join([str(self.epoch), vr])
216+
return vr
217+
218+
219+
# This code has been vendored from scancode.
220+
# https://github.com/nexB/scancode-toolkit/blob/16ae20a343c5332114edac34c7b6fcf2fb6bca74/src/packagedcode/nevra.py#L36
221+
def from_name(rpm_string):
222+
"""
223+
Return an (E, N, V, R, A) tuple given a file name, by splitting
224+
[e:]name-version-release.arch into the four possible subcomponents.
225+
Default epoch, version, release and arch to None if not specified.
226+
Accepts RPM names with and without extensions
227+
"""
228+
parse_nevra = re.compile("^" "(.*)" "-" "([^-]*)" "-" "([^-]*)" "\\." "([^.]*)" "$").match
229+
m = parse_nevra(rpm_string)
230+
if not m:
231+
return None
232+
n, v, r, a = m.groups()
233+
if ":" not in v:
234+
return None, n, v, r, a
235+
e, v = v.split(":", 1)
236+
e = int(e)
237+
return (e, n, v, r, a)
238+
239+
176240
def rpm_to_purl(rpm_string):
177241
# FIXME: there is code in scancode to handle RPM conversion AND this should
178242
# be all be part of the packageurl library
179243

180244
# FIXME: the comment below is not correct, this is the Epoch in the RPM version and not redhat specific
181245
# Red Hat uses `-:0` instead of just `-` to separate
182246
# package name and version
183-
components = rpm_string.split("-0:")
184-
if len(components) != 2:
185-
return
186247

187-
name, version = components
248+
# This code has been vendored from scancode.
249+
# https://github.com/nexB/scancode-toolkit/blob/16ae20a343c5332114edac34c7b6fcf2fb6bca74/src/packagedcode/rpm.py#L310
250+
251+
envra = from_name(rpm_string)
188252

189-
if version[0].isdigit():
190-
return PackageURL(namespace="redhat", name=name, type="rpm", version=version)
253+
if not envra:
254+
return None
255+
sepoch, sname, sversion, srel, sarch = envra
256+
src_evr = EVR(sversion, srel, sepoch).to_string()
257+
src_qualifiers = {}
258+
if sarch:
259+
src_qualifiers["arch"] = sarch
260+
261+
return PackageURL(
262+
type="rpm", namespace="redhat", name=sname, version=src_evr, qualifiers=src_qualifiers
263+
)

vulnerabilities/tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ def no_rmtree(monkeypatch):
5757
"test_npm.py",
5858
"test_package_managers.py",
5959
"test_postgresql.py",
60-
"test_redhat_importer.py",
6160
"test_retiredotnet.py",
6261
"test_ruby.py",
6362
"test_rust.py",

0 commit comments

Comments
 (0)