Skip to content

Commit d7d3206

Browse files
Update Apache_httpd importer from XML to JSON Advisory (#425)
* Update Apache httpd importer from importing XML to JSON data Signed-off-by: AmitGupta7580 <amitgupta758000@gmail.com>
1 parent e090246 commit d7d3206

4 files changed

Lines changed: 325 additions & 44 deletions

File tree

vulnerabilities/importers/apache_httpd.py

Lines changed: 110 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,25 @@
2020
# VulnerableCode is a free software tool from nexB Inc. and others.
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222

23+
import asyncio
2324
import dataclasses
24-
from xml.etree import ElementTree
25+
import urllib
2526

2627
import requests
28+
from bs4 import BeautifulSoup
2729
from packageurl import PackageURL
30+
from univers.versions import MavenVersion
31+
from univers.version_specifier import VersionSpecifier
2832

2933
from vulnerabilities.data_source import Advisory
3034
from vulnerabilities.data_source import DataSource
3135
from vulnerabilities.data_source import DataSourceConfiguration
36+
from vulnerabilities.data_source import Reference
37+
from vulnerabilities.data_source import VulnerabilitySeverity
38+
from vulnerabilities.package_managers import GitHubTagsAPI
39+
from vulnerabilities.severity_systems import scoring_systems
3240
from vulnerabilities.helpers import create_etag
41+
from vulnerabilities.helpers import nearest_patched_package
3342

3443

3544
@dataclasses.dataclass
@@ -40,56 +49,113 @@ class ApacheHTTPDDataSourceConfiguration(DataSourceConfiguration):
4049
class ApacheHTTPDDataSource(DataSource):
4150

4251
CONFIG_CLASS = ApacheHTTPDDataSourceConfiguration
43-
url = "https://httpd.apache.org/security/vulnerabilities-httpd.xml"
52+
base_url = "https://httpd.apache.org/security/json/"
53+
54+
def set_api(self):
55+
self.version_api = GitHubTagsAPI()
56+
asyncio.run(self.version_api.load_api(["apache/httpd"]))
4457

4558
def updated_advisories(self):
46-
# Etags are like hashes of web responses. We maintain
47-
# (url, etag) mappings in the DB. `create_etag` creates
48-
# (url, etag) pair. If a (url, etag) already exists then the code
49-
# skips processing the response further to avoid duplicate work
50-
51-
if create_etag(data_src=self, url=self.url, etag_key="ETag"):
52-
data = fetch_xml(self.url)
53-
advisories = to_advisories(data)
54-
return self.batch_advisories(advisories)
55-
56-
return []
57-
58-
59-
def to_advisories(data):
60-
advisories = []
61-
for issue in data:
62-
resolved_packages = []
63-
impacted_packages = []
64-
for info in issue:
65-
if info.tag == "cve":
66-
cve = info.attrib["name"]
67-
68-
if info.tag == "title":
69-
summary = info.text
70-
71-
if info.tag == "fixed":
72-
resolved_packages.append(
73-
PackageURL(type="apache", name="httpd", version=info.attrib["version"])
59+
links = fetch_links(self.base_url)
60+
self.set_api()
61+
advisories = []
62+
for link in links:
63+
data = requests.get(link).json()
64+
advisories.append(self.to_advisory(data))
65+
return self.batch_advisories(advisories)
66+
67+
def to_advisory(self, data):
68+
cve = data["CVE_data_meta"]["ID"]
69+
descriptions = data["description"]["description_data"]
70+
description = None
71+
for desc in descriptions:
72+
if desc["lang"] == "eng":
73+
description = desc.get("value")
74+
break
75+
76+
severities = []
77+
impacts = data.get("impact", [])
78+
for impact in impacts:
79+
value = impact.get("other")
80+
if value:
81+
severities.append(
82+
VulnerabilitySeverity(
83+
system=scoring_systems["apache_httpd"],
84+
value=value,
85+
)
7486
)
87+
break
88+
reference = Reference(
89+
reference_id=cve,
90+
url=urllib.parse.urljoin(self.base_url, f"{cve}.json"),
91+
severities=severities,
92+
)
7593

76-
if info.tag == "affects" or info.tag == "maybeaffects":
77-
impacted_packages.append(
78-
PackageURL(type="apache", name="httpd", version=info.attrib["version"])
79-
)
94+
versions_data = []
95+
for vendor in data["affects"]["vendor"]["vendor_data"]:
96+
for products in vendor["product"]["product_data"]:
97+
for version_data in products["version"]["version_data"]:
98+
versions_data.append(version_data)
8099

81-
advisories.append(
82-
Advisory(
83-
vulnerability_id=cve,
84-
summary=summary,
85-
impacted_package_urls=impacted_packages,
86-
resolved_package_urls=resolved_packages,
100+
fixed_version_ranges, affected_version_ranges = self.to_version_ranges(versions_data)
101+
102+
affected_packages = []
103+
fixed_packages = []
104+
105+
for version_range in fixed_version_ranges:
106+
fixed_packages.extend(
107+
[
108+
PackageURL(type="apache", name="httpd", version=version)
109+
for version in self.version_api.get("apache/httpd")
110+
if MavenVersion(version) in version_range
111+
]
87112
)
113+
114+
for version_range in affected_version_ranges:
115+
affected_packages.extend(
116+
[
117+
PackageURL(type="apache", name="httpd", version=version)
118+
for version in self.version_api.get("apache/httpd")
119+
if MavenVersion(version) in version_range
120+
]
121+
)
122+
123+
return Advisory(
124+
vulnerability_id=cve,
125+
summary=description,
126+
affected_packages=nearest_patched_package(affected_packages, fixed_packages),
127+
references=[reference],
88128
)
89129

90-
return advisories
130+
def to_version_ranges(self, versions_data):
131+
fixed_version_ranges = []
132+
affected_version_ranges = []
133+
for version_data in versions_data:
134+
version_value = version_data["version_value"]
135+
range_expression = version_data["version_affected"]
136+
if range_expression == "<":
137+
fixed_version_ranges.append(
138+
VersionSpecifier.from_scheme_version_spec_string(
139+
"maven", ">={}".format(version_value)
140+
)
141+
)
142+
elif range_expression == "=" or range_expression == "?=":
143+
affected_version_ranges.append(
144+
VersionSpecifier.from_scheme_version_spec_string(
145+
"maven", "{}".format(version_value)
146+
)
147+
)
148+
149+
return (fixed_version_ranges, affected_version_ranges)
91150

92151

93-
def fetch_xml(url):
94-
resp = requests.get(url).content
95-
return ElementTree.fromstring(resp)
152+
def fetch_links(url):
153+
links = []
154+
data = requests.get(url).content
155+
soup = BeautifulSoup(data, features="lxml")
156+
for tag in soup.find_all("a"):
157+
link = tag.get("href")
158+
if not link.endswith("json"):
159+
continue
160+
links.append(urllib.parse.urljoin(url, link))
161+
return links

vulnerabilities/severity_systems.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,9 @@ def as_score(self, value):
8787
url="",
8888
notes="Severity for unknown scoring systems. Contains generic textual values like High, Low etc",
8989
),
90+
"apache_httpd": ScoringSystem(
91+
identifier="apache_httpd",
92+
name="Apache Httpd Severity",
93+
url="https://httpd.apache.org/security/impact_levels.html",
94+
),
9095
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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 from nexB Inc. and others.
21+
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
22+
23+
import os
24+
import json
25+
from unittest import TestCase
26+
27+
from packageurl import PackageURL
28+
from univers.version_specifier import VersionSpecifier
29+
30+
from vulnerabilities.data_source import Reference
31+
from vulnerabilities.data_source import Advisory
32+
from vulnerabilities.data_source import VulnerabilitySeverity
33+
from vulnerabilities.package_managers import GitHubTagsAPI
34+
from vulnerabilities.severity_systems import scoring_systems
35+
from vulnerabilities.importers.apache_httpd import ApacheHTTPDDataSource
36+
from vulnerabilities.helpers import AffectedPackage
37+
38+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
39+
TEST_DATA = os.path.join(BASE_DIR, "test_data", "apache_httpd", "CVE-1999-1199.json")
40+
41+
42+
class TestApacheHTTPDDataSource(TestCase):
43+
@classmethod
44+
def setUpClass(cls):
45+
data_source_cfg = {"etags": {}}
46+
cls.data_src = ApacheHTTPDDataSource(1, config=data_source_cfg)
47+
known_versions = ["1.3.2", "1.3.1", "1.3.0"]
48+
cls.data_src.version_api = GitHubTagsAPI(cache={"apache/httpd": known_versions})
49+
with open(TEST_DATA) as f:
50+
cls.data = json.load(f)
51+
52+
def test_to_version_ranges(self):
53+
data = [
54+
{
55+
"version_affected": "?=",
56+
"version_value": "1.3.0",
57+
},
58+
{
59+
"version_affected": "=",
60+
"version_value": "1.3.1",
61+
},
62+
{
63+
"version_affected": "<",
64+
"version_value": "1.3.2",
65+
},
66+
]
67+
fixed_version_ranges, affected_version_ranges = self.data_src.to_version_ranges(data)
68+
69+
# Check fixed packages
70+
assert [
71+
VersionSpecifier.from_scheme_version_spec_string("maven", ">=1.3.2")
72+
] == fixed_version_ranges
73+
74+
# Check vulnerable packages
75+
assert [
76+
VersionSpecifier.from_scheme_version_spec_string("maven", "==1.3.0"),
77+
VersionSpecifier.from_scheme_version_spec_string("maven", "==1.3.1"),
78+
] == affected_version_ranges
79+
80+
def test_to_advisory(self):
81+
expected_advisories = [
82+
Advisory(
83+
summary="A serious problem exists when a client sends a large number of "
84+
"headers with the same header name. Apache uses up memory faster than the "
85+
"amount of memory required to simply store the received data itself. That "
86+
"is, memory use increases faster and faster as more headers are received, "
87+
"rather than increasing at a constant rate. This makes a denial of service "
88+
"attack based on this method more effective than methods which cause Apache"
89+
" to use memory at a constant rate, since the attacker has to send less data.",
90+
affected_packages=[
91+
AffectedPackage(
92+
vulnerable_package=PackageURL(
93+
type="apache",
94+
name="httpd",
95+
version="1.3.0",
96+
),
97+
),
98+
AffectedPackage(
99+
vulnerable_package=PackageURL(
100+
type="apache",
101+
name="httpd",
102+
version="1.3.1",
103+
),
104+
),
105+
],
106+
references=[
107+
Reference(
108+
url="https://httpd.apache.org/security/json/CVE-1999-1199.json",
109+
severities=[
110+
VulnerabilitySeverity(
111+
system=scoring_systems["apache_httpd"],
112+
value="important",
113+
),
114+
],
115+
reference_id="CVE-1999-1199",
116+
),
117+
],
118+
vulnerability_id="CVE-1999-1199",
119+
)
120+
]
121+
found_advisories = [self.data_src.to_advisory(self.data)]
122+
found_advisories = list(map(Advisory.normalized, found_advisories))
123+
expected_advisories = list(map(Advisory.normalized, expected_advisories))
124+
assert sorted(found_advisories) == sorted(expected_advisories)

0 commit comments

Comments
 (0)