Skip to content

Commit f9a91e7

Browse files
committed
Initial migration of nginx importer
Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 4171dbe commit f9a91e7

File tree

7 files changed

+4220
-0
lines changed

7 files changed

+4220
-0
lines changed

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from vulnerabilities.pipelines.v2_importers import istio_importer as istio_importer_v2
5656
from vulnerabilities.pipelines.v2_importers import mattermost_importer as mattermost_importer_v2
5757
from vulnerabilities.pipelines.v2_importers import mozilla_importer as mozilla_importer_v2
58+
from vulnerabilities.pipelines.v2_importers import nginx_importer as nginx_importer_v2
5859
from vulnerabilities.pipelines.v2_importers import npm_importer as npm_importer_v2
5960
from vulnerabilities.pipelines.v2_importers import nvd_importer as nvd_importer_v2
6061
from vulnerabilities.pipelines.v2_importers import oss_fuzz as oss_fuzz_v2
@@ -89,6 +90,7 @@
8990
aosp_importer_v2.AospImporterPipeline,
9091
ruby_importer_v2.RubyImporterPipeline,
9192
epss_importer_v2.EPSSImporterPipeline,
93+
nginx_importer_v2.NginxImporterPipeline,
9294
mattermost_importer_v2.MattermostImporterPipeline,
9395
nvd_importer.NVDImporterPipeline,
9496
github_importer.GitHubAPIImporterPipeline,
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
from typing import Iterable
11+
from typing import NamedTuple
12+
13+
import requests
14+
from bs4 import BeautifulSoup
15+
from packageurl import PackageURL
16+
from univers.version_range import NginxVersionRange
17+
from univers.versions import InvalidVersion
18+
19+
from vulnerabilities.importer import AdvisoryData
20+
from vulnerabilities.importer import AffectedPackageV2
21+
from vulnerabilities.importer import ReferenceV2
22+
from vulnerabilities.importer import VulnerabilitySeverity
23+
from vulnerabilities.importer import logger
24+
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipeline
25+
from vulnerabilities.severity_systems import GENERIC
26+
27+
28+
class NginxImporterPipeline(VulnerableCodeBaseImporterPipeline):
29+
"""Collect Nginx security advisories."""
30+
31+
pipeline_id = "nginx_importer_v2"
32+
33+
spdx_license_expression = "BSD-2-Clause"
34+
license_url = "https://nginx.org/LICENSE"
35+
url = "https://nginx.org/en/security_advisories.html"
36+
importer_name = "Nginx Importer"
37+
38+
@classmethod
39+
def steps(cls):
40+
return (
41+
cls.fetch,
42+
cls.collect_and_store_advisories,
43+
)
44+
45+
def fetch(self):
46+
self.log(f"Fetch `{self.url}`")
47+
self.advisory_data = requests.get(self.url).text
48+
49+
def advisories_count(self):
50+
return self.advisory_data.count("<li><p>")
51+
52+
def collect_advisories(self) -> Iterable[AdvisoryData]:
53+
"""
54+
Yield AdvisoryData from nginx security advisories HTML
55+
web page.
56+
"""
57+
soup = BeautifulSoup(self.advisory_data, features="lxml")
58+
vulnerability_list = soup.select("li p")
59+
for vulnerability_info in vulnerability_list:
60+
ngnix_advisory = parse_advisory_data_from_paragraph(vulnerability_info)
61+
yield to_advisory_data(ngnix_advisory)
62+
63+
64+
class NginxAdvisory(NamedTuple):
65+
advisory_id: str
66+
aliases: list
67+
summary: str
68+
severities: list
69+
not_vulnerable: str
70+
vulnerable: str
71+
references: list
72+
73+
def to_dict(self):
74+
return self._asdict()
75+
76+
77+
def to_advisory_data(nginx_adv: NginxAdvisory) -> AdvisoryData:
78+
"""
79+
Return AdvisoryData from an NginxAdvisory tuple.
80+
"""
81+
package_name = "nginx"
82+
package_type = "nginx"
83+
qualifiers = {}
84+
85+
purl = PackageURL(type=package_type, name=package_name, qualifiers=qualifiers)
86+
87+
_, _, affected_versions = nginx_adv.vulnerable.partition(":")
88+
affected_versions = affected_versions.strip()
89+
90+
if "nginx/Windows" in affected_versions:
91+
qualifiers["os"] = "windows"
92+
affected_versions = affected_versions.replace("nginx/Windows", "")
93+
94+
_, _, fixed_versions = nginx_adv.not_vulnerable.partition(":")
95+
fixed_versions = fixed_versions.strip()
96+
97+
if "nginx/Windows" in fixed_versions:
98+
qualifiers["os"] = "windows"
99+
fixed_versions = fixed_versions.replace("nginx/Windows", "")
100+
101+
fixed_version_range = None
102+
try:
103+
fixed_version_range = NginxVersionRange.from_native(fixed_versions)
104+
except InvalidVersion:
105+
logger.error(f"Invalid vulnerable range {fixed_versions}")
106+
107+
affected_version_range = None
108+
try:
109+
affected_version_range = NginxVersionRange.from_native(affected_versions)
110+
except InvalidVersion:
111+
logger.error(f"Invalid non vulnerable range {affected_versions}")
112+
113+
affected_packages = []
114+
if purl and affected_version_range or fixed_version_range:
115+
affected_packages.append(
116+
AffectedPackageV2(
117+
package=purl,
118+
affected_version_range=affected_version_range,
119+
fixed_version_range=fixed_version_range,
120+
)
121+
)
122+
123+
return AdvisoryData(
124+
advisory_id=nginx_adv.advisory_id,
125+
aliases=nginx_adv.aliases,
126+
summary=nginx_adv.summary,
127+
affected_packages=affected_packages,
128+
references_v2=nginx_adv.references,
129+
url="https://nginx.org/en/security_advisories.html",
130+
)
131+
132+
133+
def parse_advisory_data_from_paragraph(vulnerability_info):
134+
"""
135+
Return an NginxAdvisory from a ``vulnerability_info`` bs4 paragraph.
136+
137+
An advisory paragraph, without html markup, looks like this:
138+
139+
1-byte memory overwrite in resolver
140+
Severity: medium
141+
Advisory
142+
CVE-2021-23017
143+
Not vulnerable: 1.21.0+, 1.20.1+
144+
Vulnerable: 0.6.18-1.20.0
145+
The patch pgp
146+
147+
"""
148+
aliases = []
149+
summary = None
150+
severities = []
151+
not_vulnerable = None
152+
vulnerable = None
153+
references = []
154+
is_first = True
155+
156+
# we iterate on the children to accumulate values in variables
157+
# FIXME: using an explicit xpath-like query could be simpler
158+
for child in vulnerability_info.children:
159+
if is_first:
160+
summary = child
161+
is_first = False
162+
continue
163+
164+
text = child.text.strip()
165+
text_low = text.lower()
166+
167+
if text.startswith(
168+
(
169+
"CVE-",
170+
"CORE-",
171+
"VU#",
172+
)
173+
):
174+
aliases.append(text)
175+
if text.startswith("CVE-"):
176+
# always keep the CVE as a reference too
177+
link = f"https://nvd.nist.gov/vuln/detail/{text}"
178+
reference = ReferenceV2(reference_id=text, url=link)
179+
references.append(reference)
180+
181+
elif "severity" in text_low:
182+
severity = build_severity(severity=text)
183+
if severity:
184+
severities.append(severity)
185+
186+
elif "not vulnerable" in text_low:
187+
not_vulnerable = text
188+
189+
elif "vulnerable" in text_low:
190+
vulnerable = text
191+
192+
elif hasattr(child, "attrs"):
193+
link = child.attrs.get("href")
194+
if link:
195+
if "cve.mitre.org" in link:
196+
references.append(ReferenceV2(reference_id=text, url=link))
197+
elif "mailman.nginx.org" in link:
198+
references.append(ReferenceV2(url=link))
199+
else:
200+
link = requests.compat.urljoin("https://nginx.org", link)
201+
references.append(ReferenceV2(url=link))
202+
203+
advisory_id = aliases.pop()
204+
return NginxAdvisory(
205+
advisory_id=advisory_id,
206+
aliases=aliases,
207+
summary=summary,
208+
severities=severities,
209+
not_vulnerable=not_vulnerable,
210+
vulnerable=vulnerable,
211+
references=references,
212+
)
213+
214+
215+
def build_severity(severity):
216+
"""
217+
Return a VulnerabilitySeverity built from a ``severity`` string, or None.
218+
219+
For example::
220+
>>> severity = "Severity: medium"
221+
>>> expected = VulnerabilitySeverity(system=GENERIC, value="medium")
222+
>>> assert build_severity(severity) == expected
223+
"""
224+
if severity.startswith("Severity:"):
225+
_, _, severity = severity.partition("Severity:")
226+
227+
severity = severity.strip()
228+
if severity:
229+
return VulnerabilitySeverity(system=GENERIC, value=severity)
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
from pathlib import Path
11+
12+
from bs4 import BeautifulSoup
13+
from commoncode import testcase
14+
from univers.version_range import NginxVersionRange
15+
16+
from vulnerabilities.importer import ReferenceV2
17+
from vulnerabilities.importer import VulnerabilitySeverity
18+
from vulnerabilities.pipelines.v2_importers import nginx_importer
19+
from vulnerabilities.severity_systems import GENERIC
20+
from vulnerabilities.tests import util_tests
21+
from vulnerabilities.utils import is_vulnerable_nginx_version
22+
23+
ADVISORY_FIELDS_TO_TEST = (
24+
"unique_content_id",
25+
"summary",
26+
"affected_packages",
27+
"references",
28+
"date_published",
29+
"weaknesses",
30+
)
31+
32+
33+
class NginxImporterPipeline(testcase.FileBasedTesting):
34+
test_data_dir = Path(__file__).parent.parent.parent / "test_data" / "nginx_v2"
35+
36+
def test_is_vulnerable(self):
37+
# Not vulnerable: 1.17.3+, 1.16.1+
38+
# Vulnerable: 1.9.5-1.17.2
39+
40+
vcls = NginxVersionRange.version_class
41+
affected_version_range = NginxVersionRange.from_native("1.9.5-1.17.2")
42+
fixed_versions = [vcls("1.17.3"), vcls("1.16.1")]
43+
44+
version = vcls("1.9.4")
45+
assert not is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
46+
47+
version = vcls("1.9.5")
48+
assert is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
49+
50+
version = vcls("1.9.6")
51+
assert is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
52+
53+
version = vcls("1.16.0")
54+
assert is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
55+
56+
version = vcls("1.16.1")
57+
assert not is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
58+
59+
version = vcls("1.16.2")
60+
assert not is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
61+
62+
version = vcls("1.16.99")
63+
assert not is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
64+
65+
version = vcls("1.17.0")
66+
assert is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
67+
68+
version = vcls("1.17.1")
69+
assert is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
70+
71+
version = vcls("1.17.2")
72+
assert is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
73+
74+
version = vcls("1.17.3")
75+
assert not is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
76+
77+
version = vcls("1.17.4")
78+
assert not is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
79+
80+
version = vcls("1.18.0")
81+
assert not is_vulnerable_nginx_version(version, affected_version_range, fixed_versions)
82+
83+
def test_parse_advisory_data_from_paragraph(self):
84+
paragraph = (
85+
"<p>1-byte memory overwrite in resolver"
86+
"<br/>Severity: medium<br/>"
87+
'<a href="http://mailman.nginx.org/pipermail/nginx-announce/2021/000300.html">Advisory</a>'
88+
"<br/>"
89+
'<a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-23017">CVE-2021-23017</a>'
90+
"<br/>Not vulnerable: 1.21.0+, 1.20.1+<br/>"
91+
"Vulnerable: 0.6.18-1.20.0<br/>"
92+
'<a href="/download/patch.2021.resolver.txt">'
93+
'The patch</a>  <a href="/download/patch.2021.resolver.txt.asc">pgp</a>'
94+
"</p>"
95+
)
96+
vuln_info = BeautifulSoup(paragraph, features="lxml").p
97+
expected = {
98+
"advisory_id": "CVE-2021-23017",
99+
"aliases": [],
100+
"summary": "1-byte memory overwrite in resolver",
101+
"severities": [
102+
VulnerabilitySeverity(
103+
system=GENERIC,
104+
value="medium",
105+
scoring_elements="",
106+
published_at=None,
107+
url=None,
108+
)
109+
],
110+
"not_vulnerable": "Not vulnerable: 1.21.0+, 1.20.1+",
111+
"vulnerable": "Vulnerable: 0.6.18-1.20.0",
112+
"references": [
113+
ReferenceV2(
114+
reference_id="",
115+
reference_type="",
116+
url="http://mailman.nginx.org/pipermail/nginx-announce/2021/000300.html",
117+
),
118+
ReferenceV2(
119+
reference_id="CVE-2021-23017",
120+
reference_type="",
121+
url="https://nvd.nist.gov/vuln/detail/CVE-2021-23017",
122+
),
123+
ReferenceV2(
124+
reference_id="",
125+
reference_type="",
126+
url="https://nginx.org/download/patch.2021.resolver.txt",
127+
),
128+
ReferenceV2(
129+
reference_id="",
130+
reference_type="",
131+
url="https://nginx.org/download/patch.2021.resolver.txt.asc",
132+
),
133+
],
134+
}
135+
136+
result = nginx_importer.parse_advisory_data_from_paragraph(vuln_info)
137+
assert result.to_dict() == expected
138+
139+
def test_collect_advisories(self):
140+
test_file = self.get_test_loc("security_advisories.html")
141+
with open(test_file) as tf:
142+
test_text = tf.read()
143+
144+
expected_file = self.get_test_loc(
145+
"security_advisories-advisory_data-expected.json", must_exist=False
146+
)
147+
148+
test_pipeline = nginx_importer.NginxImporterPipeline()
149+
test_pipeline.advisory_data = test_text
150+
results = [na.to_dict() for na in test_pipeline.collect_advisories()]
151+
util_tests.check_results_against_json(results, expected_file)

0 commit comments

Comments
 (0)