Skip to content

Commit 55ba9c2

Browse files
authored
Merge branch 'main' into improved-export-command
2 parents 1375847 + a7b002f commit 55ba9c2

File tree

13 files changed

+583
-4
lines changed

13 files changed

+583
-4
lines changed

.github/workflows/pypi-release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151

5252
steps:
5353
- name: Download built archives
54-
uses: actions/download-artifact@v3
54+
uses: actions/download-artifact@v4.1.7
5555
with:
5656
name: pypi_archives
5757
path: dist
@@ -71,7 +71,7 @@ jobs:
7171

7272
steps:
7373
- name: Download built archives
74-
uses: actions/download-artifact@v3
74+
uses: actions/download-artifact@v4.1.7
7575
with:
7676
name: pypi_archives
7777
path: dist

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ charset-normalizer==2.0.12
2020
click==8.1.2
2121
coreapi==2.3.3
2222
coreschema==0.0.4
23-
cryptography==42.0.4
23+
cryptography==43.0.1
2424
cwe2==3.0.0
2525
dateparser==1.1.1
2626
decorator==5.1.1
@@ -30,8 +30,8 @@ Django==4.2.15
3030
django-crispy-forms==2.3
3131
django-environ==0.11.2
3232
django-filter==24.3
33-
djangorestframework==3.15.2
3433
django-widget-tweaks==1.5.0
34+
djangorestframework==3.15.2
3535
doc8==0.11.1
3636
docopt==0.6.2
3737
docutils==0.17.1

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
@@ -72,6 +73,7 @@
7273
oss_fuzz.OSSFuzzImporter,
7374
ruby.RubyImporter,
7475
github_osv.GithubOSVImporter,
76+
curl.CurlImporter,
7577
epss.EPSSImporter,
7678
vulnrichment.VulnrichImporter,
7779
pypa_importer.PyPaImporterPipeline,

vulnerabilities/importers/curl.py

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

vulnerabilities/improvers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
valid_versions.RubyImprover,
3030
valid_versions.GithubOSVImprover,
3131
vulnerability_status.VulnerabilityStatusImprover,
32+
valid_versions.CurlImprover,
3233
vulnerability_kev.VulnerabilityKevImprover,
3334
flag_ghost_packages.FlagGhostPackagePipeline,
3435
]

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)