Skip to content

Commit f3058d1

Browse files
authored
Merge pull request #1014 from nexB/971-migrate-apache-httpd-importer
Migrate apache httpd importer#971
2 parents 40a3974 + b689916 commit f3058d1

15 files changed

Lines changed: 894 additions & 172 deletions

CHANGELOG.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ Release notes
22
=============
33

44

5+
Version v31.1.1
6+
---------------
7+
8+
- We re-enabled support for the Apache HTTPD security advisories importer.
9+
10+
511
Version v31.1.0
612
----------------
713

8-
- We re-enabled support for the NPM vulnerabilities advisories importer.
14+
- We re-enabled support for the NPM vulnerabilities advisories importer.
915
- We re-enabled support for the Retiredotnet vulnerabilities advisories importer.
1016
- We are now handling purl fragments in package search. For example:
1117
you can now serch using queries in the UI like this : ``cherrypy@2.1.1``,
@@ -30,7 +36,7 @@ Version v31.0.0
3036
- We made bulk search faster by pre-computing `package_url` and
3137
`plain_package_url` in Package model. And provided two options in package
3238
bulk search ``purl_only`` option to get only vulnerable purls without any
33-
extra details, ``plain_purl`` option to filter purls without qualifiers and
39+
extra details, ``plain_purl`` option to filter purls without qualifiers and
3440
subpath and also return them without qualifiers and subpath. The names used
3541
are provisional and may be updated in a future release.
3642

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ toml==0.10.2
107107
tomli==2.0.1
108108
traitlets==5.1.1
109109
typing_extensions==4.1.1
110-
univers==30.9.0
110+
univers==30.9.1
111111
urllib3==1.26.9
112112
wcwidth==0.2.5
113113
websocket-client==0.59.0
@@ -120,4 +120,4 @@ drf-spectacular-sidecar==2022.10.1
120120
drf-spectacular==0.24.2
121121
coreapi==2.3.3
122122
coreschema==0.0.4
123-
itypes==1.2.0
123+
itypes==1.2.0

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ install_requires =
7070

7171
#essentials
7272
packageurl-python>=0.10.5rc1
73-
univers>=30.9.0
73+
univers>=30.9.1
7474
license-expression>=21.6.14
7575

7676
# file and data formats

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#
99

1010
from vulnerabilities.importers import alpine_linux
11+
from vulnerabilities.importers import apache_httpd
1112
from vulnerabilities.importers import archlinux
1213
from vulnerabilities.importers import debian
1314
from vulnerabilities.importers import debian_oval
@@ -41,6 +42,7 @@
4142
debian_oval.DebianOvalImporter,
4243
npm.NpmImporter,
4344
retiredotnet.RetireDotnetImporter,
45+
apache_httpd.ApacheHTTPDImporter,
4446
]
4547

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

vulnerabilities/importers/apache_httpd.py

Lines changed: 59 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,32 @@
1313
import requests
1414
from bs4 import BeautifulSoup
1515
from packageurl import PackageURL
16-
from univers.version_range import VersionRange
16+
from univers.version_constraint import VersionConstraint
17+
from univers.version_range import ApacheVersionRange
1718
from univers.versions import SemverVersion
1819

1920
from vulnerabilities.importer import AdvisoryData
21+
from vulnerabilities.importer import AffectedPackage
2022
from vulnerabilities.importer import Importer
2123
from vulnerabilities.importer import Reference
2224
from vulnerabilities.importer import VulnerabilitySeverity
23-
from vulnerabilities.package_managers import GitHubTagsAPI
2425
from vulnerabilities.severity_systems import APACHE_HTTPD
25-
from vulnerabilities.utils import nearest_patched_package
2626

2727

2828
class ApacheHTTPDImporter(Importer):
2929

3030
base_url = "https://httpd.apache.org/security/json/"
31+
spdx_license_expression = "Apache-2.0"
32+
license_url = "https://www.apache.org/licenses/LICENSE-2.0"
3133

32-
def set_api(self):
33-
self.version_api = GitHubTagsAPI()
34-
asyncio.run(self.version_api.load_api(["apache/httpd"]))
35-
self.version_api.cache["apache/httpd"] = set(
36-
filter(
37-
lambda version: version.value not in ignore_tags,
38-
self.version_api.cache["apache/httpd"],
39-
)
40-
)
41-
42-
def updated_advisories(self):
34+
def advisory_data(self):
4335
links = fetch_links(self.base_url)
44-
self.set_api()
45-
advisories = []
4636
for link in links:
4737
data = requests.get(link).json()
48-
advisories.append(self.to_advisory(data))
49-
return self.batch_advisories(advisories)
38+
yield self.to_advisory(data)
5039

5140
def to_advisory(self, data):
52-
cve = data["CVE_data_meta"]["ID"]
41+
alias = data["CVE_data_meta"]["ID"]
5342
descriptions = data["description"]["description_data"]
5443
description = None
5544
for desc in descriptions:
@@ -66,12 +55,13 @@ def to_advisory(self, data):
6655
VulnerabilitySeverity(
6756
system=APACHE_HTTPD,
6857
value=value,
58+
scoring_elements="",
6959
)
7060
)
7161
break
7262
reference = Reference(
73-
reference_id=cve,
74-
url=urllib.parse.urljoin(self.base_url, f"{cve}.json"),
63+
reference_id=alias,
64+
url=urllib.parse.urljoin(self.base_url, f"{alias}.json"),
7565
severities=severities,
7666
)
7767

@@ -81,56 +71,68 @@ def to_advisory(self, data):
8171
for version_data in products["version"]["version_data"]:
8272
versions_data.append(version_data)
8373

84-
fixed_version_ranges, affected_version_ranges = self.to_version_ranges(versions_data)
74+
fixed_versions = []
75+
for timeline_object in data.get("timeline") or []:
76+
timeline_value = timeline_object["value"]
77+
if "release" in timeline_value:
78+
split_timeline_value = timeline_value.split(" ")
79+
if "never" in timeline_value:
80+
continue
81+
if "release" in split_timeline_value[-1]:
82+
fixed_versions.append(split_timeline_value[0])
83+
if "release" in split_timeline_value[0]:
84+
fixed_versions.append(split_timeline_value[-1])
8585

8686
affected_packages = []
87-
fixed_packages = []
88-
89-
for version_range in fixed_version_ranges:
90-
fixed_packages.extend(
91-
[
92-
PackageURL(type="apache", name="httpd", version=version)
93-
for version in self.version_api.get("apache/httpd").valid_versions
94-
if SemverVersion(version) in version_range
95-
]
96-
)
97-
98-
for version_range in affected_version_ranges:
99-
affected_packages.extend(
100-
[
101-
PackageURL(type="apache", name="httpd", version=version)
102-
for version in self.version_api.get("apache/httpd").valid_versions
103-
if SemverVersion(version) in version_range
104-
]
87+
affected_version_range = self.to_version_ranges(versions_data, fixed_versions)
88+
if affected_version_range:
89+
affected_packages.append(
90+
AffectedPackage(
91+
package=PackageURL(
92+
type="apache",
93+
name="httpd",
94+
),
95+
affected_version_range=affected_version_range,
96+
)
10597
)
10698

10799
return AdvisoryData(
108-
vulnerability_id=cve,
100+
aliases=[alias],
109101
summary=description,
110-
affected_packages=nearest_patched_package(affected_packages, fixed_packages),
102+
affected_packages=affected_packages,
111103
references=[reference],
112104
)
113105

114-
def to_version_ranges(self, versions_data):
115-
fixed_version_ranges = []
116-
affected_version_ranges = []
106+
def to_version_ranges(self, versions_data, fixed_versions):
107+
constraints = []
117108
for version_data in versions_data:
118109
version_value = version_data["version_value"]
119110
range_expression = version_data["version_affected"]
120-
if range_expression == "<":
121-
fixed_version_ranges.append(
122-
VersionRange.from_scheme_version_spec_string(
123-
"semver", ">={}".format(version_value)
124-
)
125-
)
126-
elif range_expression == "=" or range_expression == "?=":
127-
affected_version_ranges.append(
128-
VersionRange.from_scheme_version_spec_string(
129-
"semver", "{}".format(version_value)
130-
)
111+
if range_expression not in {"<=", ">=", "?=", "!<", "="}:
112+
raise ValueError(f"unknown comparator found! {range_expression}")
113+
comparator_by_range_expression = {
114+
">=": ">=",
115+
"!<": ">=",
116+
"<=": "<=",
117+
"=": "=",
118+
}
119+
comparator = comparator_by_range_expression.get(range_expression)
120+
if comparator:
121+
constraints.append(
122+
VersionConstraint(comparator=comparator, version=SemverVersion(version_value))
131123
)
132124

133-
return (fixed_version_ranges, affected_version_ranges)
125+
for fixed_version in fixed_versions:
126+
# The VersionConstraint method `invert()` inverts the fixed_version's comparator,
127+
# enabling inclusion of multiple fixed versions with the `affected_version_range` values.
128+
constraints.append(
129+
VersionConstraint(
130+
comparator="=",
131+
version=SemverVersion(fixed_version),
132+
).invert()
133+
)
134+
135+
return ApacheVersionRange(constraints=constraints)
134136

135137

136138
def fetch_links(url):

vulnerabilities/improvers/default.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,22 @@ def interesting_advisories(self) -> QuerySet:
3939
return Advisory.objects.all()
4040

4141
def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
42-
4342
if not advisory_data:
4443
return []
4544

4645
if advisory_data.affected_packages:
4746
for affected_package in advisory_data.affected_packages:
48-
affected_purls, fixed_purl = get_exact_purls(affected_package)
49-
yield Inference(
50-
aliases=advisory_data.aliases,
51-
confidence=MAX_CONFIDENCE,
52-
summary=advisory_data.summary,
53-
affected_purls=affected_purls,
54-
fixed_purl=fixed_purl,
55-
references=advisory_data.references,
56-
)
47+
# To deal with multiple fixed versions in a single affected package
48+
affected_purls, fixed_purls = get_exact_purls(affected_package)
49+
for fixed_purl in fixed_purls:
50+
yield Inference(
51+
aliases=advisory_data.aliases,
52+
confidence=MAX_CONFIDENCE,
53+
summary=advisory_data.summary,
54+
affected_purls=affected_purls,
55+
fixed_purl=fixed_purl,
56+
references=advisory_data.references,
57+
)
5758

5859
else:
5960
yield Inference.from_advisory_data(
@@ -78,7 +79,7 @@ def get_exact_purls(affected_package: AffectedPackage) -> Tuple[List[PackageURL]
7879
>>> got = get_exact_purls(affected_package)
7980
>>> expected = (
8081
... [PackageURL(type='turtle', namespace=None, name='green', version='2.0.0', qualifiers={}, subpath=None)],
81-
... PackageURL(type='turtle', namespace=None, name='green', version='5.0.0', qualifiers={}, subpath=None)
82+
... [PackageURL(type='turtle', namespace=None, name='green', version='5.0.0', qualifiers={}, subpath=None)]
8283
... )
8384
>>> assert expected == got
8485
"""
@@ -89,16 +90,25 @@ def get_exact_purls(affected_package: AffectedPackage) -> Tuple[List[PackageURL]
8990
# TODO: Revisit after https://github.com/nexB/univers/issues/33
9091
try:
9192
affected_purls = []
93+
fixed_versions = []
9294
if vr:
9395
range_versions = [c.version for c in vr.constraints if c]
96+
# Any version that's not affected by a vulnerability is considered
97+
# fixed.
98+
fixed_versions = [c.version for c in vr.constraints if c and c.comparator == "!="]
9499
resolved_versions = [v for v in range_versions if v and v in vr]
95100
for version in resolved_versions:
96101
affected_purl = evolve_purl(purl=affected_package.package, version=str(version))
97102
affected_purls.append(affected_purl)
98103

99-
fixed_purl = affected_package.get_fixed_purl() if affected_package.fixed_version else None
104+
if affected_package.fixed_version:
105+
fixed_versions.append(affected_package.fixed_version)
100106

101-
return affected_purls, fixed_purl
107+
fixed_purls = [
108+
evolve_purl(purl=affected_package.package, version=str(version))
109+
for version in fixed_versions
110+
]
111+
return affected_purls, fixed_purls
102112
except Exception as e:
103113
logger.error(f"Failed to get exact purls for {affected_package} {e}")
104-
return [], None
114+
return [], []

vulnerabilities/tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ def no_rmtree(monkeypatch):
2525
# Step 2: Run test for importer only if it is activated (pytestmark = pytest.mark.skipif(...))
2626
# Step 3: Migrate all the tests
2727
collect_ignore = [
28-
"test_apache_httpd.py",
2928
"test_apache_kafka.py",
3029
"test_apache_tomcat.py",
3130
"test_api.py",

0 commit comments

Comments
 (0)