Skip to content

Commit 5bba7d7

Browse files
committed
Migrate redhat importer
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 14b814c commit 5bba7d7

7 files changed

Lines changed: 2431 additions & 177 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: 60 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -20,73 +20,84 @@
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 typing import Dict
26+
from typing import Iterable
27+
from typing import List
28+
2329
import requests
2430
from packageurl import PackageURL
31+
from univers.version_range import RpmVersionRange
2532

2633
from vulnerabilities import severity_systems
27-
from vulnerabilities.helpers import nearest_patched_package
34+
from vulnerabilities.helpers import get_item
2835
from vulnerabilities.helpers import requests_with_5xx_retry
2936
from vulnerabilities.importer import AdvisoryData
37+
from vulnerabilities.importer import AffectedPackage
3038
from vulnerabilities.importer import Importer
3139
from vulnerabilities.importer import Reference
3240
from vulnerabilities.importer import VulnerabilitySeverity
41+
from vulnerabilities.rpm_utils import rpm_to_purl
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):
69+
70+
spdx_license_expression = "CC-BY-4.0"
71+
license_url = "https://access.redhat.com/documentation/en-us/red_hat_security_data_api/1.0/html/red_hat_security_data_api/legal-notice"
7972

80-
return cves
73+
def advisory_data(self) -> Iterable[AdvisoryData]:
74+
for list_of_redhat_cves in fetch_list_of_cves():
75+
for redhat_cve in list_of_redhat_cves:
76+
yield to_advisory(redhat_cve)
8177

8278

8379
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)
80+
affected_packages: List[AffectedPackage] = []
81+
for rpm in advisory_data.get("affected_packages") or []:
82+
purl = rpm_to_purl(rpm_string=rpm, namespace="redhat")
83+
if purl:
84+
try:
85+
affected_version_range = RpmVersionRange.from_versions(sequence=[purl.version])
86+
affected_packages.append(
87+
AffectedPackage(
88+
package=PackageURL(
89+
type=purl.type,
90+
name=purl.name,
91+
namespace=purl.namespace,
92+
qualifiers=purl.qualifiers,
93+
subpath=purl.subpath,
94+
),
95+
affected_version_range=affected_version_range,
96+
fixed_version=None,
97+
)
98+
)
99+
except Exception as e:
100+
logger.error(f"Failed to parse version range {purl.version} for {purl} {e}")
90101

91102
references = []
92103
bugzilla = advisory_data.get("bugzilla")
@@ -114,10 +125,14 @@ def to_advisory(advisory_data):
114125
)
115126
)
116127

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

132+
if not isinstance(rh_adv, str):
133+
logger.error(f"Invalid advisory type {rh_adv}")
134+
continue
135+
121136
if "RHSA" in rh_adv.upper():
122137
rhsa_data = requests_session.get(
123138
f"https://access.redhat.com/hydra/rest/securitydata/cvrf/{rh_adv}.json"
@@ -126,7 +141,7 @@ def to_advisory(advisory_data):
126141
rhsa_aggregate_severities = []
127142
if rhsa_data.get("cvrfdoc"):
128143
# not all RHSA errata have a corresponding CVRF document
129-
value = rhsa_data["cvrfdoc"]["aggregate_severity"]
144+
value = get_item(rhsa_data, "cvrfdoc", "aggregate_severity")
130145
rhsa_aggregate_severities.append(
131146
VulnerabilitySeverity(
132147
system=severity_systems.REDHAT_AGGREGATE,
@@ -164,27 +179,14 @@ def to_advisory(advisory_data):
164179
)
165180
)
166181

182+
aliases = []
183+
alias = advisory_data.get("CVE")
184+
if alias:
185+
aliases.append(alias)
167186
references.append(Reference(severities=redhat_scores, url=advisory_data["resource_url"]))
168187
return AdvisoryData(
169-
vulnerability_id=advisory_data["CVE"],
170-
summary=advisory_data["bugzilla_description"],
171-
affected_packages=nearest_patched_package(affected_purls, []),
188+
aliases=aliases,
189+
summary=advisory_data.get("bugzilla_description") or "",
190+
affected_packages=affected_packages,
172191
references=references,
173192
)
174-
175-
176-
def rpm_to_purl(rpm_string):
177-
# FIXME: there is code in scancode to handle RPM conversion AND this should
178-
# be all be part of the packageurl library
179-
180-
# FIXME: the comment below is not correct, this is the Epoch in the RPM version and not redhat specific
181-
# Red Hat uses `-:0` instead of just `-` to separate
182-
# package name and version
183-
components = rpm_string.split("-0:")
184-
if len(components) != 2:
185-
return
186-
187-
name, version = components
188-
189-
if version[0].isdigit():
190-
return PackageURL(namespace="redhat", name=name, type="rpm", version=version)

vulnerabilities/rpm_utils.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Copyright (c) nexB Inc. and others. All rights reserved.
2+
# http://nexb.com and https://github.com/nexB/vulnerablecode/
3+
# The VulnerableCode software is licensed under the Apache License version 2.0.
4+
# Data generated with VulnerableCode require an acknowledgment.
5+
#
6+
# You may not use this software except in compliance with the License.
7+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
8+
# Unless required by applicable law or agreed to in writing, software distributed
9+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
# specific language governing permissions and limitations under the License.
12+
#
13+
# When you publish or redistribute any data created with VulnerableCode or any VulnerableCode
14+
# derivative work, you must accompany this data with the following acknowledgment:
15+
#
16+
# Generated with VulnerableCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES
17+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
18+
# VulnerableCode should be considered or used as legal advice. Consult an Attorney
19+
# for any legal advice.
20+
# VulnerableCode is a free software code from nexB Inc. and others.
21+
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
22+
23+
import logging
24+
import re
25+
from collections import namedtuple
26+
27+
from packageurl import PackageURL
28+
29+
logger = logging.getLogger(__name__)
30+
31+
# This code has been vendored from scancode.
32+
33+
# https://github.com/nexB/scancode-toolkit/blob/16ae20a343c5332114edac34c7b6fcf2fb6bca74/src/packagedcode/rpm.py#L91
34+
class EVR(namedtuple("EVR", "epoch version release")):
35+
"""
36+
The RPM Epoch, Version, Release tuple.
37+
"""
38+
39+
def __new__(self, version, release=None, epoch=None):
40+
"""
41+
note: the sort order of the named tuple is the sort order.
42+
But for creation we put the rarely used epoch last with a default to None.
43+
"""
44+
if not isinstance(epoch, int):
45+
if epoch and epoch.strip():
46+
logger.error("Invalid epoch: must be a number or empty.")
47+
return None
48+
if not version:
49+
logger.error("Version is required: {}".format(repr(version)))
50+
return None
51+
52+
return super().__new__(EVR, epoch, version, release)
53+
54+
def __str__(self, *args, **kwargs):
55+
return self.to_string()
56+
57+
def to_string(self):
58+
if self.release:
59+
vr = f"{self.version}-{self.release}"
60+
else:
61+
vr = self.version
62+
63+
if self.epoch:
64+
vr = ":".join([str(self.epoch), vr])
65+
return vr
66+
67+
68+
# https://github.com/nexB/scancode-toolkit/blob/16ae20a343c5332114edac34c7b6fcf2fb6bca74/src/packagedcode/nevra.py#L36
69+
def from_name(rpm_string):
70+
"""
71+
Return an (E, N, V, R, A) tuple given a file name, by splitting
72+
[e:]name-version-release.arch into the four possible subcomponents.
73+
Default epoch, version, release and arch to None if not specified.
74+
Accepts RPM names with and without extensions
75+
"""
76+
parse_nevra = re.compile("^" "(.*)" "-" "([^-]*)" "-" "([^-]*)" "\\." "([^.]*)" "$").match
77+
m = parse_nevra(rpm_string)
78+
if not m:
79+
return None
80+
n, v, r, a = m.groups()
81+
if ":" not in v:
82+
return None, n, v, r, a
83+
e, v = v.split(":", 1)
84+
if e.isdigit():
85+
e = int(e)
86+
return (e, n, v, r, a)
87+
88+
89+
def rpm_to_purl(rpm_string, namespace):
90+
# FIXME: there is code in scancode to handle RPM conversion AND this should
91+
# be all be part of the packageurl library
92+
93+
# FIXME: the comment below is not correct, this is the Epoch in the RPM version and not redhat specific
94+
# Red Hat uses `-:0` instead of just `-` to separate
95+
# package name and version
96+
97+
# https://github.com/nexB/scancode-toolkit/blob/16ae20a343c5332114edac34c7b6fcf2fb6bca74/src/packagedcode/rpm.py#L310
98+
99+
envra = from_name(rpm_string)
100+
101+
if not envra:
102+
logger.error(f"Invalid RPM name can't get envra: {rpm_string}")
103+
return None
104+
sepoch, sname, sversion, srel, sarch = envra
105+
106+
evr = EVR(sversion, srel, sepoch)
107+
if not evr:
108+
logger.error(f"Invalid RPM name can't get evr: {rpm_string}")
109+
return None
110+
src_evr = evr.to_string()
111+
src_qualifiers = {}
112+
if sarch:
113+
src_qualifiers["arch"] = sarch
114+
115+
return PackageURL(
116+
type="rpm", namespace=namespace, name=sname, version=src_evr, qualifiers=src_qualifiers
117+
)

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)