Skip to content

Commit d626b0c

Browse files
committed
Migrate Nginx importer to aboutcode pipeline
Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent 094c2bf commit d626b0c

8 files changed

Lines changed: 116 additions & 113 deletions

File tree

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from vulnerabilities.importers import gitlab
2525
from vulnerabilities.importers import istio
2626
from vulnerabilities.importers import mozilla
27-
from vulnerabilities.importers import nginx
2827
from vulnerabilities.importers import nvd
2928
from vulnerabilities.importers import openssl
3029
from vulnerabilities.importers import oss_fuzz
@@ -40,14 +39,14 @@
4039
from vulnerabilities.importers import vulnrichment
4140
from vulnerabilities.importers import xen
4241
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipeline
42+
from vulnerabilities.pipelines import nginx_importer
4343
from vulnerabilities.pipelines import npm_importer
4444
from vulnerabilities.pipelines import pypa_importer
4545

4646
IMPORTERS_REGISTRY = [
4747
nvd.NVDImporter,
4848
github.GitHubAPIImporter,
4949
gitlab.GitLabAPIImporter,
50-
nginx.NginxImporter,
5150
pysec.PyPIImporter,
5251
alpine_linux.AlpineImporter,
5352
openssl.OpensslImporter,
@@ -78,6 +77,7 @@
7877
vulnrichment.VulnrichImporter,
7978
pypa_importer.PyPaImporterPipeline,
8079
npm_importer.NpmImporterPipeline,
80+
nginx_importer.NginxImporterPipeline,
8181
]
8282

8383
IMPORTERS_REGISTRY = {

vulnerabilities/improvers/valid_versions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636
from vulnerabilities.importers.github_osv import GithubOSVImporter
3737
from vulnerabilities.importers.gitlab import GitLabAPIImporter
3838
from vulnerabilities.importers.istio import IstioImporter
39-
from vulnerabilities.importers.nginx import NginxImporter
4039
from vulnerabilities.importers.oss_fuzz import OSSFuzzImporter
4140
from vulnerabilities.importers.ruby import RubyImporter
4241
from vulnerabilities.importers.ubuntu import UbuntuImporter
4342
from vulnerabilities.improver import MAX_CONFIDENCE
4443
from vulnerabilities.improver import Improver
4544
from vulnerabilities.improver import Inference
4645
from vulnerabilities.models import Advisory
46+
from vulnerabilities.pipelines.nginx_importer import NginxImporterPipeline
4747
from vulnerabilities.pipelines.npm_importer import NpmImporterPipeline
4848
from vulnerabilities.utils import AffectedPackage as LegacyAffectedPackage
4949
from vulnerabilities.utils import clean_nginx_git_tag
@@ -220,7 +220,7 @@ class NginxBasicImprover(Improver):
220220

221221
@property
222222
def interesting_advisories(self) -> QuerySet:
223-
return Advisory.objects.filter(created_by=NginxImporter.qualified_name).paginated()
223+
return Advisory.objects.filter(created_by=NginxImporterPipeline.qualified_name).paginated()
224224

225225
def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
226226
all_versions = list(self.fetch_nginx_version_from_git_tags())

vulnerabilities/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1103,7 +1103,7 @@ class Advisory(models.Model):
11031103
max_length=100,
11041104
help_text="Fully qualified name of the importer prefixed with the"
11051105
"module name importing the advisory. Eg:"
1106-
"vulnerabilities.importers.nginx.NginxImporter",
1106+
"vulnerabilities.pipeline.nginx_importer.NginxImporterPipeline",
11071107
)
11081108
url = models.URLField(
11091109
blank=True,

vulnerabilities/importers/nginx.py renamed to vulnerabilities/pipelines/nginx_importer.py

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,62 @@
33
# VulnerableCode is a trademark of nexB Inc.
44
# SPDX-License-Identifier: Apache-2.0
55
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6-
# See https://github.com/nexB/vulnerablecode for support or download.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10-
import logging
1110
from typing import Iterable
12-
from typing import List
1311
from typing import NamedTuple
1412

1513
import requests
1614
from bs4 import BeautifulSoup
17-
from django.db.models.query import QuerySet
1815
from packageurl import PackageURL
1916
from univers.version_range import NginxVersionRange
2017
from univers.versions import NginxVersion
2118

2219
from vulnerabilities.importer import AdvisoryData
2320
from vulnerabilities.importer import AffectedPackage
24-
from vulnerabilities.importer import Importer
2521
from vulnerabilities.importer import Reference
2622
from vulnerabilities.importer import VulnerabilitySeverity
23+
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipeline
2724
from vulnerabilities.severity_systems import GENERIC
2825

29-
logger = logging.getLogger(__name__)
3026

27+
class NginxImporterPipeline(VulnerableCodeBaseImporterPipeline):
28+
"""Collect Nginx security advisories."""
3129

32-
class NginxImporter(Importer):
33-
34-
url = "https://nginx.org/en/security_advisories.html"
30+
pipeline_id = "nginx_importer"
3531

3632
spdx_license_expression = "BSD-2-Clause"
3733
license_url = "https://nginx.org/LICENSE"
34+
url = "https://nginx.org/en/security_advisories.html"
3835
importer_name = "Nginx Importer"
3936

40-
def advisory_data(self) -> Iterable[AdvisoryData]:
41-
text = self.fetch()
42-
yield from advisory_data_from_text(text)
37+
@classmethod
38+
def steps(cls):
39+
return (
40+
cls.fetch,
41+
cls.collect_and_store_advisories,
42+
cls.import_new_advisories,
43+
)
4344

4445
def fetch(self):
45-
return requests.get(self.url).content
46+
self.log(f"Fetch `{self.url}`")
47+
self.advisory_data = requests.get(self.url).text
4648

49+
def advisories_count(self):
50+
return self.advisory_data.count("<li><p>")
4751

48-
def advisory_data_from_text(text):
49-
"""
50-
Yield AdvisoryData from the ``text`` of the nginx security advisories HTML
51-
web page.
52-
"""
53-
soup = BeautifulSoup(text, features="lxml")
54-
vuln_list = soup.select("li p")
55-
for vuln_info in vuln_list:
56-
ngnix_adv = parse_advisory_data_from_paragraph(vuln_info)
57-
yield to_advisory_data(ngnix_adv)
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)
5862

5963

6064
class NginxAdvisory(NamedTuple):
@@ -69,15 +73,15 @@ def to_dict(self):
6973
return self._asdict()
7074

7175

72-
def to_advisory_data(ngnx_adv: NginxAdvisory) -> AdvisoryData:
76+
def to_advisory_data(nginx_adv: NginxAdvisory) -> AdvisoryData:
7377
"""
7478
Return AdvisoryData from an NginxAdvisory tuple.
7579
"""
7680
package_name = "nginx"
7781
package_type = "nginx"
7882
qualifiers = {}
7983

80-
_, _, affected_version_range = ngnx_adv.vulnerable.partition(":")
84+
_, _, affected_version_range = nginx_adv.vulnerable.partition(":")
8185
if "nginx/Windows" in affected_version_range:
8286
qualifiers["os"] = "windows"
8387
affected_version_range = affected_version_range.replace("nginx/Windows", "")
@@ -87,7 +91,7 @@ def to_advisory_data(ngnx_adv: NginxAdvisory) -> AdvisoryData:
8791
affected_version_range = NginxVersionRange.from_native(affected_version_range)
8892

8993
affected_packages = []
90-
_, _, fixed_versions = ngnx_adv.not_vulnerable.partition(":")
94+
_, _, fixed_versions = nginx_adv.not_vulnerable.partition(":")
9195

9296
for fixed_version in fixed_versions.split(","):
9397
fixed_version = fixed_version.rstrip("+")
@@ -112,17 +116,17 @@ def to_advisory_data(ngnx_adv: NginxAdvisory) -> AdvisoryData:
112116
)
113117

114118
return AdvisoryData(
115-
aliases=ngnx_adv.aliases,
116-
summary=ngnx_adv.summary,
119+
aliases=nginx_adv.aliases,
120+
summary=nginx_adv.summary,
117121
affected_packages=affected_packages,
118-
references=ngnx_adv.references,
122+
references=nginx_adv.references,
119123
url="https://nginx.org/en/security_advisories.html",
120124
)
121125

122126

123-
def parse_advisory_data_from_paragraph(vuln_info):
127+
def parse_advisory_data_from_paragraph(vulnerability_info):
124128
"""
125-
Return an NginxAdvisory from a ``vuln_info`` bs4 paragraph.
129+
Return an NginxAdvisory from a ``vulnerability_info`` bs4 paragraph.
126130
127131
An advisory paragraph, without html markup, looks like this:
128132
@@ -145,7 +149,7 @@ def parse_advisory_data_from_paragraph(vuln_info):
145149

146150
# we iterate on the children to accumulate values in variables
147151
# FIXME: using an explicit xpath-like query could be simpler
148-
for child in vuln_info.children:
152+
for child in vulnerability_info.children:
149153
if is_first:
150154
summary = child
151155
is_first = False

vulnerabilities/pipelines/pypa_importer.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipeline
1818
from vulnerabilities.utils import get_advisory_url
1919

20-
module_logger = logging.getLogger(__name__)
21-
2220

2321
class PyPaImporterPipeline(VulnerableCodeBaseImporterPipeline):
2422
"""Collect advisories from PyPA GitHub repository."""

vulnerabilities/tests/test_nginx.py renamed to vulnerabilities/tests/pipelines/test_nginx_importer_pipeline.py

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# VulnerableCode is a trademark of nexB Inc.
44
# SPDX-License-Identifier: Apache-2.0
55
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6-
# See https://github.com/nexB/vulnerablecode for support or download.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

@@ -15,16 +15,18 @@
1515
from bs4 import BeautifulSoup
1616
from commoncode import testcase
1717
from django.db.models.query import QuerySet
18+
from univers.version_range import NginxVersionRange
1819

1920
from vulnerabilities import models
2021
from vulnerabilities import severity_systems
21-
from vulnerabilities.import_runner import ImportRunner
22+
23+
# from vulnerabilities.import_runner import ImportRunner
2224
from vulnerabilities.importer import AdvisoryData
2325
from vulnerabilities.importer import Reference
2426
from vulnerabilities.importer import VulnerabilitySeverity
25-
from vulnerabilities.importers import nginx
2627
from vulnerabilities.improvers.valid_versions import NginxBasicImprover
2728
from vulnerabilities.models import Advisory
29+
from vulnerabilities.pipelines import nginx_importer
2830
from vulnerabilities.tests import util_tests
2931
from vulnerabilities.utils import is_vulnerable_nginx_version
3032

@@ -40,14 +42,14 @@
4042

4143

4244
class TestNginxImporterAndImprover(testcase.FileBasedTesting):
43-
test_data_dir = str(Path(__file__).resolve().parent / "test_data" / "nginx")
45+
test_data_dir = Path(__file__).parent.parent / "test_data" / "nginx"
4446

4547
def test_is_vulnerable(self):
4648
# Not vulnerable: 1.17.3+, 1.16.1+
4749
# Vulnerable: 1.9.5-1.17.2
4850

49-
vcls = nginx.NginxVersionRange.version_class
50-
affected_version_range = nginx.NginxVersionRange.from_native("1.9.5-1.17.2")
51+
vcls = NginxVersionRange.version_class
52+
affected_version_range = NginxVersionRange.from_native("1.9.5-1.17.2")
5153
fixed_versions = [vcls("1.17.3"), vcls("1.16.1")]
5254

5355
version = vcls("1.9.4")
@@ -133,10 +135,10 @@ def test_parse_advisory_data_from_paragraph(self):
133135
],
134136
}
135137

136-
result = nginx.parse_advisory_data_from_paragraph(vuln_info)
138+
result = nginx_importer.parse_advisory_data_from_paragraph(vuln_info)
137139
assert result.to_dict() == expected
138140

139-
def test_advisory_data_from_text(self):
141+
def test_collect_advisories(self):
140142
test_file = self.get_test_loc("security_advisories.html")
141143
with open(test_file) as tf:
142144
test_text = tf.read()
@@ -145,52 +147,51 @@ def test_advisory_data_from_text(self):
145147
"security_advisories-advisory_data-expected.json", must_exist=False
146148
)
147149

148-
results = [na.to_dict() for na in nginx.advisory_data_from_text(test_text)]
150+
test_pipeline = nginx_importer.NginxImporterPipeline()
151+
test_pipeline.advisory_data = test_text
152+
results = [na.to_dict() for na in test_pipeline.collect_advisories()]
149153
util_tests.check_results_against_json(results, expected_file)
150154

151155
@pytest.mark.django_db(transaction=True)
152156
def test_NginxImporter(self):
157+
test_file = self.get_test_loc("security_advisories.html")
158+
with open(test_file) as tf:
159+
test_text = tf.read()
160+
161+
test_pipeline = nginx_importer.NginxImporterPipeline()
162+
test_pipeline.advisory_data = test_text
153163

154164
expected_file = self.get_test_loc(
155165
"security_advisories-importer-expected.json", must_exist=False
156166
)
157167

158-
results, _cls = self.run_import()
159-
util_tests.check_results_against_json(results, expected_file)
168+
test_pipeline.collect_and_store_advisories()
169+
test_pipeline.import_new_advisories()
160170

161-
# run again as there should be no duplicates
162-
results, _cls = self.run_import()
171+
results = list(models.Advisory.objects.all().values(*ADVISORY_FIELDS_TO_TEST))
163172
util_tests.check_results_against_json(results, expected_file)
164173

165-
def run_import(self):
166-
"""
167-
Return a list of imported Advisory model objects and the MockImporter
168-
used.
169-
"""
170-
171-
class MockImporter(nginx.NginxImporter):
172-
"""
173-
A mocked NginxImporter that loads content from a file rather than
174-
making a network call.
175-
"""
176-
177-
def fetch(self):
178-
with open(test_file) as tf:
179-
return tf.read()
180-
181-
test_file = self.get_test_loc("security_advisories.html")
174+
# run again as there should be no duplicates
175+
test_pipeline.collect_and_store_advisories()
176+
test_pipeline.import_new_advisories()
182177

183-
ImportRunner(MockImporter).run()
184-
return list(models.Advisory.objects.all().values(*ADVISORY_FIELDS_TO_TEST)), MockImporter
178+
results = list(models.Advisory.objects.all().values(*ADVISORY_FIELDS_TO_TEST))
179+
util_tests.check_results_against_json(results, expected_file)
185180

186181
@pytest.mark.django_db(transaction=True)
187182
def test_NginxBasicImprover__interesting_advisories(self):
188-
advisories, importer_class = self.run_import()
183+
test_file = self.get_test_loc("security_advisories.html")
184+
with open(test_file) as tf:
185+
test_text = tf.read()
186+
187+
test_pipeline = nginx_importer.NginxImporterPipeline()
188+
test_pipeline.advisory_data = test_text
189+
advisories = list(models.Advisory.objects.all().values(*ADVISORY_FIELDS_TO_TEST))
189190

190191
class MockNginxBasicImprover(NginxBasicImprover):
191192
@property
192193
def interesting_advisories(self) -> QuerySet:
193-
return Advisory.objects.filter(created_by=importer_class.qualified_name)
194+
return Advisory.objects.filter(created_by=test_pipeline.pipeline_id)
194195

195196
improver = MockNginxBasicImprover()
196197
interesting_advisories = list(

vulnerabilities/tests/pipelines/test_pypa_importer_pipeline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from vulnerabilities.importers.osv import parse_advisory_data
1717
from vulnerabilities.tests import util_tests
1818

19-
TEST_DATA = data = Path(__file__).parent.parent / "test_data" / "pypa"
19+
TEST_DATA = Path(__file__).parent.parent / "test_data" / "pypa"
2020

2121

2222
class TestPyPaImporterPipeline(TestCase):

0 commit comments

Comments
 (0)