Skip to content

Commit 4a5f320

Browse files
committed
Migrate redhat importer
Fix severities in UI Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 75b2920 commit 4a5f320

12 files changed

Lines changed: 1464 additions & 184 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: 77 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -20,81 +20,99 @@
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+
from typing import Dict
25+
from typing import Iterable
26+
from typing import List
27+
2328
import requests
2429
from packageurl import PackageURL
30+
from univers.version_range import RpmVersionRange
2531

2632
from vulnerabilities import severity_systems
27-
from vulnerabilities.helpers import nearest_patched_package
33+
from vulnerabilities.helpers import get_item
2834
from vulnerabilities.helpers import requests_with_5xx_retry
2935
from vulnerabilities.importer import AdvisoryData
36+
from vulnerabilities.importer import AffectedPackage
3037
from vulnerabilities.importer import Importer
3138
from vulnerabilities.importer import Reference
3239
from vulnerabilities.importer import VulnerabilitySeverity
40+
from vulnerabilities.rpm_utils import rpm_to_purl
3341

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-
42+
logger = logging.getLogger(__name__)
4443

4544
requests_session = requests_with_5xx_retry(max_retries=5, backoff_factor=1)
4645

4746

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 = []
47+
def fetch_list_of_cves() -> Iterable[List[Dict]]:
5548
page_no = 1
56-
url_template = "https://access.redhat.com/hydra/rest/securitydata/cve.json?per_page=10000&page={}" # nopep8
57-
5849
cve_data = None
5950
while True:
60-
current_url = url_template.format(page_no)
51+
current_url = f"https://access.redhat.com/hydra/rest/securitydata/cve.json?per_page=10000&page={page_no}" # nopep8
6152
try:
62-
print(f"Fetching: {current_url}")
6353
response = requests_session.get(current_url)
6454
if response.status_code != requests.codes.ok:
65-
# TODO: log me
66-
print(f"Failed to fetch results from {current_url}")
55+
logger.error(f"Failed to fetch results from {current_url}")
6756
break
6857
cve_data = response.json()
6958
except Exception as e:
70-
# TODO: log me
71-
msg = f"Failed to fetch results from {current_url}:\n{e}"
72-
print(msg)
59+
logger.error(f"Failed to fetch results from {current_url} {e}")
7360
break
74-
7561
if not cve_data:
7662
break
77-
cves.extend(cve_data)
7863
page_no += 1
64+
yield cve_data
65+
66+
67+
def get_bugzilla_data(bugzilla):
68+
return requests_session.get(f"https://bugzilla.redhat.com/rest/bug/{bugzilla}").json()
7969

80-
return cves
70+
71+
def get_rhsa_data(rh_adv):
72+
return requests_session.get(
73+
f"https://access.redhat.com/hydra/rest/securitydata/cvrf/{rh_adv}.json"
74+
).json()
75+
76+
77+
class RedhatImporter(Importer):
78+
79+
spdx_license_expression = "CC-BY-4.0"
80+
license_url = "https://access.redhat.com/documentation/en-us/red_hat_security_data_api/1.0/html/red_hat_security_data_api/legal-notice"
81+
82+
def advisory_data(self) -> Iterable[AdvisoryData]:
83+
for list_of_redhat_cves in fetch_list_of_cves():
84+
for redhat_cve in list_of_redhat_cves:
85+
yield to_advisory(redhat_cve)
8186

8287

8388
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)
89+
affected_packages: List[AffectedPackage] = []
90+
for rpm in advisory_data.get("affected_packages") or []:
91+
purl = rpm_to_purl(rpm_string=rpm, namespace="redhat")
92+
if purl:
93+
try:
94+
affected_version_range = RpmVersionRange.from_versions(sequence=[purl.version])
95+
affected_packages.append(
96+
AffectedPackage(
97+
package=PackageURL(
98+
type=purl.type,
99+
name=purl.name,
100+
namespace=purl.namespace,
101+
qualifiers=purl.qualifiers,
102+
subpath=purl.subpath,
103+
),
104+
affected_version_range=affected_version_range,
105+
fixed_version=None,
106+
)
107+
)
108+
except Exception as e:
109+
logger.error(f"Failed to parse version range {purl.version} for {purl} {e}")
90110

91111
references = []
92112
bugzilla = advisory_data.get("bugzilla")
93113
if bugzilla:
94114
url = "https://bugzilla.redhat.com/show_bug.cgi?id={}".format(bugzilla)
95-
bugzilla_data = requests_session.get(
96-
f"https://bugzilla.redhat.com/rest/bug/{bugzilla}"
97-
).json()
115+
bugzilla_data = get_bugzilla_data(bugzilla)
98116
if (
99117
bugzilla_data.get("bugs")
100118
and len(bugzilla_data["bugs"])
@@ -114,25 +132,28 @@ def to_advisory(advisory_data):
114132
)
115133
)
116134

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

139+
if not isinstance(rh_adv, str):
140+
logger.error(f"Invalid advisory type {rh_adv}")
141+
continue
142+
121143
if "RHSA" in rh_adv.upper():
122-
rhsa_data = requests_session.get(
123-
f"https://access.redhat.com/hydra/rest/securitydata/cvrf/{rh_adv}.json"
124-
).json() # nopep8
144+
rhsa_data = get_rhsa_data(rh_adv)
125145

126146
rhsa_aggregate_severities = []
127147
if rhsa_data.get("cvrfdoc"):
128148
# not all RHSA errata have a corresponding CVRF document
129-
value = rhsa_data["cvrfdoc"]["aggregate_severity"]
130-
rhsa_aggregate_severities.append(
131-
VulnerabilitySeverity(
132-
system=severity_systems.REDHAT_AGGREGATE,
133-
value=value,
149+
value = get_item(rhsa_data, "cvrfdoc", "aggregate_severity")
150+
if value:
151+
rhsa_aggregate_severities.append(
152+
VulnerabilitySeverity(
153+
system=severity_systems.REDHAT_AGGREGATE,
154+
value=value,
155+
)
134156
)
135-
)
136157

137158
references.append(
138159
Reference(
@@ -164,27 +185,14 @@ def to_advisory(advisory_data):
164185
)
165186
)
166187

188+
aliases = []
189+
alias = advisory_data.get("CVE")
190+
if alias:
191+
aliases.append(alias)
167192
references.append(Reference(severities=redhat_scores, url=advisory_data["resource_url"]))
168193
return AdvisoryData(
169-
vulnerability_id=advisory_data["CVE"],
170-
summary=advisory_data["bugzilla_description"],
171-
affected_packages=nearest_patched_package(affected_purls, []),
194+
aliases=aliases,
195+
summary=advisory_data.get("bugzilla_description") or "",
196+
affected_packages=affected_packages,
172197
references=references,
173198
)
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/templates/vulnerability.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ <h3>Severity</h3>
6666
<th> Found At </th>
6767
</tr>
6868
{% for ref in object_list %}
69-
{% for obj in ref.scores %}
69+
{% for obj in ref.severities %}
7070
<tr>
7171
<td>{{obj.scoring_system}}</td>
7272

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)