diff --git a/vulnerabilities/importers/curl.py b/vulnerabilities/importers/curl.py new file mode 100644 index 000000000..038c232bf --- /dev/null +++ b/vulnerabilities/importers/curl.py @@ -0,0 +1,90 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/nexB/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +import logging +from datetime import datetime +from datetime import timezone +from typing import Iterable +from typing import Mapping + +import requests +from packageurl import PackageURL +from univers.version_range import GenericVersionRange +from univers.versions import SemverVersion +from utils import fetch_response +from utils import get_item + +from vulnerabilities.importer import AdvisoryData +from vulnerabilities.importer import AffectedPackage +from vulnerabilities.importer import Importer +from vulnerabilities.importer import Reference +from vulnerabilities.importer import VulnerabilitySeverity +from vulnerabilities.severity_systems import SCORING_SYSTEMS + +logger = logging.getLogger(__name__) + + +class CurlImporter(Importer): + + spdx_license_expression = "MIT" + license_url = "https://github.com/curl/curl-www/blob/master/LICENSE" + repo_url = "https://github.com/curl/curl-www/" + importer_name = "Curl Importer" + api_url = "https://curl.se/docs/vuln.json" + + def fetch(self) -> Iterable[Mapping]: + response = fetch_response(self.url) + return response.json() + + def advisory_data(self) -> Iterable[AdvisoryData]: + raw_data = self.fetch() + for data in raw_data: + cve_id = data.get("aliases") or [] + cve_id = cve_id[0] if len(cve_id) > 0 else None + if not cve_id.startswith("CVE"): + package = data.get("database_specific").get("package") + logger.error(f"Invalid CVE ID: {cve_id} in package {package}") + continue + yield parse_advisory_data(data) + + +def parse_advisory_data(raw_data) -> AdvisoryData: + + d1 = get_item(raw_data, "affected")[0] if len(get_item(raw_data, "affected")) > 0 else [] + d2 = get_item(d1, "ranges")[0] if len(get_item(d1, "ranges")) > 0 else [] + d3 = get_item(d2, "events")[1] if len(get_item(d2, "events")) > 1 else {} + + fixed_version = SemverVersion(d3.get("fixed") or "") + purl = PackageURL(type="generic", namespace="curl.se", name="curl") + affected_version_range = GenericVersionRange.from_versions( + raw_data.get("affected")[0].get("versions") or [] + ) + + affected_package = AffectedPackage( + package=purl, affected_version_range=affected_version_range, fixed_version=fixed_version + ) + + database_specific = raw_data.get("database_specific") or {} + severity = VulnerabilitySeverity( + system=SCORING_SYSTEMS["generic_textual"], value=database_specific.get("severity", "") + ) + + references = [Reference(url=database_specific.get("www") or "", severities=[severity])] + date_published = datetime.strptime(raw_data.get("published") or "", "%d-%m-%Y %Z").replace( + tzinfo=timezone.utc + ) + + return AdvisoryData( + aliases=raw_data.get("aliases") or [], + summary=raw_data.get("summary") or "", + affected_packages=[affected_package], + references=references, + date_published=date_published, + url=raw_data.get("database_specific", {}).get("URL", ""), + ) diff --git a/vulnerabilities/tests/test_curl.py b/vulnerabilities/tests/test_curl.py new file mode 100644 index 000000000..92959d66d --- /dev/null +++ b/vulnerabilities/tests/test_curl.py @@ -0,0 +1,53 @@ +# +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/nexB/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +import json +import os +from unittest.mock import patch + +import requests +from bs4 import BeautifulSoup + +# from importer import AdvisoryData +from univers.versions import SemverVersion + +from vulnerabilities.importers import curl +from vulnerabilities.tests import util_tests + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +TEST_DATA = os.path.join(BASE_DIR, "test_data/curl") + + +def test_is_semver_version(): + url = "https://curl.se/docs/releases.html" + + response = requests.get(url) + + if response.status_code == 200: + soup = BeautifulSoup(response.text, "html.parser") + rows = soup.find_all("tr") + versions = [] + for row in rows: + + cells = row.find_all("td") + if len(cells) >= 2: + version = cells[1].get_text().strip() + versions.append(version) + c = 0 + for version in versions: + semver_version = SemverVersion(version) + if semver_version.is_valid(version): + c += 1 + if c == len(versions): + print("All versions of curl are SemVer versions.") + else: + print("Not all versions are SemVer versions.") + else: + print("Failed to retrieve data from the webpage") diff --git a/vulntotal/tests/test_osv.py b/vulntotal/tests/test_osv.py index 8a050cd5a..3a6ea0121 100644 --- a/vulntotal/tests/test_osv.py +++ b/vulntotal/tests/test_osv.py @@ -73,4 +73,4 @@ def test_generate_payload_nuget_with_api_call(self): purl = PackageURL.from_string("pkg:nuget/moment.js@2.18.0") results = osv.generate_payload(purl) expected = {"package": {"ecosystem": "NuGet", "name": "Moment.js"}, "version": "2.18.0"} - assert results == expected + assert results == expected \ No newline at end of file