Skip to content

Commit 24b5eaa

Browse files
committed
Add curl advisories
- added curl importer - added tests for curl importer Signed-off-by: ambuj <[email protected]>
1 parent 766cb3e commit 24b5eaa

File tree

11 files changed

+557
-0
lines changed

11 files changed

+557
-0
lines changed

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from vulnerabilities.importers import apache_kafka
1313
from vulnerabilities.importers import apache_tomcat
1414
from vulnerabilities.importers import archlinux
15+
from vulnerabilities.importers import curl
1516
from vulnerabilities.importers import debian
1617
from vulnerabilities.importers import debian_oval
1718
from vulnerabilities.importers import elixir_security
@@ -71,6 +72,7 @@
7172
oss_fuzz.OSSFuzzImporter,
7273
ruby.RubyImporter,
7374
github_osv.GithubOSVImporter,
75+
curl.CurlImporter,
7476
]
7577

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

vulnerabilities/importers/curl.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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/nexB/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import logging
11+
from datetime import datetime
12+
from datetime import timezone
13+
from typing import Iterable
14+
from typing import Mapping
15+
16+
import requests
17+
from cwe2.database import Database
18+
from packageurl import PackageURL
19+
from univers.version_range import GenericVersionRange
20+
from univers.versions import SemverVersion
21+
22+
from vulnerabilities.importer import AdvisoryData
23+
from vulnerabilities.importer import AffectedPackage
24+
from vulnerabilities.importer import Importer
25+
from vulnerabilities.importer import Reference
26+
from vulnerabilities.importer import VulnerabilitySeverity
27+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
28+
from vulnerabilities.utils import fetch_response
29+
from vulnerabilities.utils import get_cwe_id
30+
from vulnerabilities.utils import get_item
31+
32+
logger = logging.getLogger(__name__)
33+
34+
35+
class CurlImporter(Importer):
36+
37+
spdx_license_expression = "curl"
38+
license_url = "https://curl.se/docs/copyright.html"
39+
repo_url = "https://github.com/curl/curl-www/"
40+
importer_name = "Curl Importer"
41+
api_url = "https://curl.se/docs/vuln.json"
42+
43+
def fetch(self) -> Iterable[Mapping]:
44+
response = fetch_response(self.api_url)
45+
return response.json()
46+
47+
def advisory_data(self) -> Iterable[AdvisoryData]:
48+
raw_data = self.fetch()
49+
for data in raw_data:
50+
cve_id = data.get("aliases") or []
51+
cve_id = cve_id[0] if len(cve_id) > 0 else None
52+
if not cve_id.startswith("CVE"):
53+
package = data.get("database_specific").get("package")
54+
logger.error(f"Invalid CVE ID: {cve_id} in package {package}")
55+
continue
56+
yield parse_advisory_data(data)
57+
58+
59+
def parse_advisory_data(raw_data) -> AdvisoryData:
60+
"""
61+
Parse advisory data from raw JSON data and return an AdvisoryData object.
62+
63+
Args:
64+
raw_data (dict): Raw JSON data containing advisory information.
65+
66+
Returns:
67+
AdvisoryData: Parsed advisory data as an AdvisoryData object.
68+
69+
Example:
70+
>>> raw_data = {
71+
... "aliases": ["CVE-2024-2379"],
72+
... "summary": "QUIC certificate check bypass with wolfSSL",
73+
... "database_specific": {
74+
... "package": "curl",
75+
... "URL": "https://curl.se/docs/CVE-2024-2379.json",
76+
... "www": "https://curl.se/docs/CVE-2024-2379.html",
77+
... "issue": "https://hackerone.com/reports/2410774",
78+
... "severity": "Low",
79+
... "CWE": {
80+
... "id": "CWE-297",
81+
... "desc": "Improper Validation of Certificate with Host Mismatch"
82+
... },
83+
... },
84+
... "published": "2024-03-27T08:00:00.00Z",
85+
... "affected": [
86+
... {
87+
... "ranges": [
88+
... {
89+
... "type": "SEMVER",
90+
... "events": [
91+
... {"introduced": "8.6.0"},
92+
... {"fixed": "8.7.0"}
93+
... ]
94+
... }
95+
... ],
96+
... "versions": ["8.6.0"]
97+
... }
98+
... ]
99+
... }
100+
>>> parse_advisory_data(raw_data)
101+
AdvisoryData(aliases=['CVE-2024-2379'], summary='QUIC certificate check bypass with wolfSSL', affected_packages=[AffectedPackage(package=PackageURL(type='generic', namespace='curl.se', name='curl', version=None, qualifiers={}, subpath=None), affected_version_range=GenericVersionRange(constraints=(VersionConstraint(comparator='=', version=SemverVersion(string='8.6.0')),)), fixed_version=SemverVersion(string='8.7.0'))], references=[Reference(reference_id='', url='https://curl.se/docs/CVE-2024-2379.html', severities=[VulnerabilitySeverity(system=ScoringSystem(identifier='generic_textual', name='Generic textual severity rating', url='', notes='Severity for generic scoring systems. Contains generic textual values like High, Low etc'), value='Low', scoring_elements='')]), Reference(reference_id='', url='https://hackerone.com/reports/2410774', severities=[])], date_published=datetime.datetime(2024, 3, 27, 8, 0, tzinfo=datetime.timezone.utc), weaknesses=[297], url='https://curl.se/docs/CVE-2024-2379.json')
102+
"""
103+
104+
affected = get_item(raw_data, "affected")[0] if len(get_item(raw_data, "affected")) > 0 else []
105+
106+
ranges = get_item(affected, "ranges")[0] if len(get_item(affected, "ranges")) > 0 else []
107+
events = get_item(ranges, "events")[1] if len(get_item(ranges, "events")) > 1 else {}
108+
version_type = get_item(ranges, "type") if get_item(ranges, "type") else ""
109+
fixed_version = events.get("fixed")
110+
if version_type == "SEMVER" and fixed_version:
111+
fixed_version = SemverVersion(fixed_version)
112+
113+
purl = PackageURL(type="generic", namespace="curl.se", name="curl")
114+
versions = affected.get("versions") or []
115+
affected_version_range = GenericVersionRange.from_versions(versions)
116+
117+
affected_package = AffectedPackage(
118+
package=purl, affected_version_range=affected_version_range, fixed_version=fixed_version
119+
)
120+
121+
database_specific = raw_data.get("database_specific") or {}
122+
severity = VulnerabilitySeverity(
123+
system=SCORING_SYSTEMS["generic_textual"], value=database_specific.get("severity", "")
124+
)
125+
126+
references = []
127+
ref_www = database_specific.get("www") or ""
128+
ref_issue = database_specific.get("issue") or ""
129+
if ref_www:
130+
references.append(Reference(url=ref_www, severities=[severity]))
131+
if ref_issue:
132+
references.append(Reference(url=ref_issue))
133+
134+
date_published = datetime.strptime(
135+
raw_data.get("published") or "", "%Y-%m-%dT%H:%M:%S.%fZ"
136+
).replace(tzinfo=timezone.utc)
137+
weaknesses = get_cwe_from_curl_advisory(raw_data)
138+
139+
return AdvisoryData(
140+
aliases=raw_data.get("aliases") or [],
141+
summary=raw_data.get("summary") or "",
142+
affected_packages=[affected_package],
143+
references=references,
144+
date_published=date_published,
145+
weaknesses=weaknesses,
146+
url=raw_data.get("database_specific", {}).get("URL", ""),
147+
)
148+
149+
150+
def get_cwe_from_curl_advisory(raw_data):
151+
"""
152+
Extracts CWE IDs from the given raw_data and returns a list of CWE IDs.
153+
154+
>>> get_cwe_from_curl_advisory({"database_specific": {"CWE": {"id": "CWE-333"}}})
155+
[333]
156+
>>> get_cwe_from_curl_advisory({"database_specific": {"CWE": {"id": ""}}})
157+
[]
158+
"""
159+
weaknesses = []
160+
db = Database()
161+
cwe_string = get_item(raw_data, "database_specific", "CWE", "id") or ""
162+
163+
if cwe_string:
164+
cwe_id = get_cwe_id(cwe_string)
165+
try:
166+
db.get(cwe_id)
167+
weaknesses.append(cwe_id)
168+
except Exception:
169+
logger.error("Invalid CWE id")
170+
return weaknesses

vulnerabilities/improvers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
valid_versions.RubyImprover,
2828
valid_versions.GithubOSVImprover,
2929
vulnerability_status.VulnerabilityStatusImprover,
30+
valid_versions.CurlImprover,
3031
]
3132

3233
IMPROVERS_REGISTRY = {x.qualified_name: x for x in IMPROVERS_REGISTRY}

vulnerabilities/improvers/valid_versions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from vulnerabilities.importers.apache_httpd import ApacheHTTPDImporter
2929
from vulnerabilities.importers.apache_kafka import ApacheKafkaImporter
3030
from vulnerabilities.importers.apache_tomcat import ApacheTomcatImporter
31+
from vulnerabilities.importers.curl import CurlImporter
3132
from vulnerabilities.importers.debian import DebianImporter
3233
from vulnerabilities.importers.debian_oval import DebianOvalImporter
3334
from vulnerabilities.importers.elixir_security import ElixirSecurityImporter
@@ -472,3 +473,8 @@ class RubyImprover(ValidVersionImprover):
472473
class GithubOSVImprover(ValidVersionImprover):
473474
importer = GithubOSVImporter
474475
ignorable_versions = []
476+
477+
478+
class CurlImprover(ValidVersionImprover):
479+
importer = CurlImporter
480+
ignorable_versions = []

vulnerabilities/tests/test_curl.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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/nexB/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import os
11+
from unittest import TestCase
12+
from unittest.mock import patch
13+
14+
from vulnerabilities.importers.curl import get_cwe_from_curl_advisory
15+
from vulnerabilities.importers.curl import parse_advisory_data
16+
from vulnerabilities.tests import util_tests
17+
from vulnerabilities.utils import load_json
18+
19+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
20+
TEST_DATA = os.path.join(BASE_DIR, "test_data/curl")
21+
22+
23+
class TestCurlImporter(TestCase):
24+
def test_parse_advisory_data1(self):
25+
mock_response = load_json(os.path.join(TEST_DATA, "curl_advisory_mock1.json"))
26+
expected_file = os.path.join(TEST_DATA, "expected_curl_advisory_output1.json")
27+
result = parse_advisory_data(mock_response)
28+
result = result.to_dict()
29+
util_tests.check_results_against_json(result, expected_file)
30+
31+
def test_parse_advisory_data2(self):
32+
mock_response = load_json(os.path.join(TEST_DATA, "curl_advisory_mock2.json"))
33+
expected_file = os.path.join(TEST_DATA, "expected_curl_advisory_output2.json")
34+
result = parse_advisory_data(mock_response)
35+
result = result.to_dict()
36+
util_tests.check_results_against_json(result, expected_file)
37+
38+
def test_parse_advisory_data3(self):
39+
mock_response = load_json(os.path.join(TEST_DATA, "curl_advisory_mock3.json"))
40+
expected_file = os.path.join(TEST_DATA, "expected_curl_advisory_output3.json")
41+
result = parse_advisory_data(mock_response)
42+
result = result.to_dict()
43+
util_tests.check_results_against_json(result, expected_file)
44+
45+
def test_get_cwe_from_curl_advisory(self):
46+
assert get_cwe_from_curl_advisory(
47+
{
48+
"id": "CURL-CVE-2024-2466",
49+
"database_specific": {
50+
"CWE": {
51+
"id": "CWE-297",
52+
"desc": "Improper Validation of Certificate with Host Mismatch",
53+
},
54+
},
55+
}
56+
) == [297]
57+
58+
mock_advisory = [
59+
{
60+
"id": "CURL-CVE-XXXX-XXXX",
61+
"database_specific": {"CWE": {"id": "CWE-111111111", "desc": "Invalid weaknesses"}},
62+
},
63+
{
64+
"id": "CURL-CVE-2024-2466",
65+
"database_specific": {
66+
"CWE": {"id": "CWE-311", "desc": "Missing Encryption of Sensitive Data"},
67+
},
68+
},
69+
]
70+
mock_cwe_list = []
71+
for advisory in mock_advisory:
72+
mock_cwe_list.extend(get_cwe_from_curl_advisory(advisory))
73+
assert mock_cwe_list == [311]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.5.0",
3+
"id": "CURL-CVE-2024-2379",
4+
"aliases": [
5+
"CVE-2024-2379"
6+
],
7+
"summary": "QUIC certificate check bypass with wolfSSL",
8+
"modified": "2024-03-26T10:36:00.00Z",
9+
"database_specific": {
10+
"package": "curl",
11+
"URL": "https://curl.se/docs/CVE-2024-2379.json",
12+
"www": "https://curl.se/docs/CVE-2024-2379.html",
13+
"issue": "https://hackerone.com/reports/2410774",
14+
"CWE": {
15+
"id": "CWE-295",
16+
"desc": "Improper Certificate Validation"
17+
},
18+
"award": {
19+
"amount": "540",
20+
"currency": "USD"
21+
},
22+
"last_affected": "8.6.0",
23+
"severity": "Low"
24+
},
25+
"published": "2024-03-27T08:00:00.00Z",
26+
"affected": [
27+
{
28+
"ranges": [
29+
{
30+
"type": "SEMVER",
31+
"events": [
32+
{"introduced": "8.6.0"},
33+
{"fixed": "8.7.0"}
34+
]
35+
},
36+
{
37+
"type": "GIT",
38+
"repo": "https://github.com/curl/curl.git",
39+
"events": [
40+
{"introduced": "5d044ad9480a9f556f4b6a252d7533b1ba7fe57e"},
41+
{"fixed": "aedbbdf18e689a5eee8dc39600914f5eda6c409c"}
42+
]
43+
}
44+
],
45+
"versions": [
46+
"8.6.0"
47+
]
48+
}
49+
],
50+
"credits": [
51+
{
52+
"name": "Dexter Gerig",
53+
"type": "FINDER"
54+
},
55+
{
56+
"name": "Daniel Stenberg",
57+
"type": "REMEDIATION_DEVELOPER"
58+
}
59+
],
60+
"details": "libcurl skips the certificate verification for a QUIC connection under certain\nconditions, when built to use wolfSSL. If told to use an unknown/bad cipher or\ncurve, the error path accidentally skips the verification and returns OK, thus\nignoring any certificate problems."
61+
}

0 commit comments

Comments
 (0)