diff --git a/schema/vulnerability/os/schema-1.0.0.json b/schema/vulnerability/os/schema-1.0.0.json index e7d6eb12..12165f12 100644 --- a/schema/vulnerability/os/schema-1.0.0.json +++ b/schema/vulnerability/os/schema-1.0.0.json @@ -142,15 +142,14 @@ "type": "object", "properties": { "Score": { - "type": "integer" + "type": "number" }, "Vectors": { "type": "string" } }, "required": [ - "Score", - "Vectors" + "Score" ] } } diff --git a/src/vunnel/providers/debian/__init__.py b/src/vunnel/providers/debian/__init__.py index 05b4c129..d107fb94 100644 --- a/src/vunnel/providers/debian/__init__.py +++ b/src/vunnel/providers/debian/__init__.py @@ -45,6 +45,8 @@ def __init__(self, root: str, config: Config | None = None): ) # this provider requires the previous state from former runs + # note: we MUST keep the input directory, since it may have out-of-band updates to support + # legacy vulns that are not in the Debian security tracker anymore. provider.disallow_existing_input_policy(config.runtime) @classmethod diff --git a/src/vunnel/providers/debian/parser.py b/src/vunnel/providers/debian/parser.py index c4e479dc..ab57f630 100644 --- a/src/vunnel/providers/debian/parser.py +++ b/src/vunnel/providers/debian/parser.py @@ -6,6 +6,7 @@ import os import re from collections import namedtuple +from typing import Any import requests @@ -51,6 +52,7 @@ def __init__(self, workspace, download_timeout=125, logger=None, distro_map=None self.debian_distro_map = distro_map self.json_file_path = os.path.join(workspace.input_path, self._json_file_) self.dsa_file_path = os.path.join(workspace.input_path, self._dsa_file_) + self.legacy_records_path = os.path.join(self.workspace.input_path, "legacy") self.urls = [self._json_url_, self._dsa_url_] if not logger: @@ -453,6 +455,40 @@ def _normalize_json(self, ns_cve_dsalist=None): # noqa: PLR0912,PLR0915 return vuln_records + def _get_legacy_records(self): + legacy_records = {} + + def process_file(contents: list[dict[str, Any]]) -> None: + for record in contents: + relno = record["Vulnerability"]["NamespaceName"].split(":")[-1] + vid = record["Vulnerability"]["Name"] + if relno not in legacy_records: + legacy_records[relno] = {} + + # ensure results are compliant with the current schema + cvss_metadata = record["Vulnerability"].get("Metadata", {}).get("NVD", {}).get("CVSSv2", {}) + if cvss_metadata: + if cvss_metadata["Vectors"] is None: + del cvss_metadata["Vectors"] + record["Vulnerability"]["Metadata"]["NVD"]["CVSSv2"] = cvss_metadata + + # write the record back + legacy_records[relno][vid] = record + + # read every json file in the legacy directory + for root, _dirs, files in os.walk(self.legacy_records_path): + for file in files: + if file.endswith(".json") and file.startswith("vulnerabilities"): + with open(os.path.join(root, file)) as f: + process_file(json.load(f)) + + if legacy_records: + self.logger.info(f"found existing legacy data for the following releases: {list(legacy_records.keys())}") + else: + self.logger.info("no existing legacy data found") + + return legacy_records + def get(self): # download the files self._download_json() @@ -464,6 +500,10 @@ def get(self): # normalize json file vuln_records = self._normalize_json(ns_cve_dsalist=ns_cve_dsalist) + # fetch records from legacy (if they exist) + legacy_records = self._get_legacy_records() + vuln_records.update(legacy_records) + if vuln_records: for relno, vuln_dict in vuln_records.items(): for vid, vuln_record in vuln_dict.items(): diff --git a/tests/quality/config.yaml b/tests/quality/config.yaml index a63a0250..a868fb3e 100644 --- a/tests/quality/config.yaml +++ b/tests/quality/config.yaml @@ -63,6 +63,10 @@ tests: # - docker.io/centos:6@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf - provider: debian + # ideally we would not use cache, however, the in order to test if we are properly keeping the processing + # of legacy information that is in the debian data cache (for debian 7, 8, and 9) we must test with + # cache enabled. + use_cache: true images: - docker.io/debian:7@sha256:81e88820a7759038ffa61cff59dfcc12d3772c3a2e75b7cfe963c952da2ad264 diff --git a/tests/quality/configure.py b/tests/quality/configure.py index 6766f7b9..eb0f5bd6 100644 --- a/tests/quality/configure.py +++ b/tests/quality/configure.py @@ -121,6 +121,7 @@ def provider_data_source(self, providers: list[str]) -> tuple[list[str], list[st uncached_providers = [] tests = [] + providers_under_test_that_require_cache = set() for provider in providers: test = self.test_configuration_by_provider(provider) if test is None: @@ -129,10 +130,13 @@ def provider_data_source(self, providers: list[str]) -> tuple[list[str], list[st tests.append(test) + # note: we always include the subject in the uncached providers, but also add it to the cached providers. + # the subject must always be run even when cache is involved. + uncached_providers.append(test.provider) if test.use_cache: + providers_under_test_that_require_cache.add(test.provider) cached_providers.append(test.provider) - else: - uncached_providers.append(test.provider) + if test.additional_providers: for additional_provider in test.additional_providers: if additional_provider.use_cache: @@ -141,7 +145,7 @@ def provider_data_source(self, providers: list[str]) -> tuple[list[str], list[st uncached_providers.append(additional_provider.name) for provider in uncached_providers: - if provider in cached_providers: + if provider in cached_providers and provider not in providers_under_test_that_require_cache: cached_providers.remove(provider) return cached_providers, uncached_providers, self.yardstick_application_config(tests) @@ -407,6 +411,8 @@ def configure(cfg: Config, provider_names: list[str]): cached_providers, uncached_providers, yardstick_app_cfg = cfg.provider_data_source(provider_names) + logging.info(f"providers uncached={uncached_providers!r} cached={cached_providers!r}") + if not cached_providers and not uncached_providers: logging.error(f"no test configuration found for provider {provider_names!r}") return [], [] @@ -493,11 +499,6 @@ def build_db(cfg: Config): shutil.rmtree(data_dir, ignore_errors=True) shutil.rmtree(build_dir, ignore_errors=True) - # run providers - for provider in state.uncached_providers: - logging.info(f"running provider {provider!r}") - subprocess.run(["vunnel", "-v", "run", provider], check=True) - # fetch cache for other providers for provider in state.cached_providers: logging.info(f"fetching cache for {provider!r}") @@ -505,6 +506,11 @@ def build_db(cfg: Config): subprocess.run([GRYPE_DB, "cache", "restore", "--path", cache_file], check=True) os.remove(cache_file) + # run providers + for provider in state.uncached_providers: + logging.info(f"running provider {provider!r}") + subprocess.run(["vunnel", "-v", "run", provider], check=True) + logging.info("building DB") subprocess.run([GRYPE_DB, "build", "-v"], check=True) subprocess.run([GRYPE_DB, "package", "-v"], check=True) diff --git a/tests/unit/providers/debian/test-fixtures/input/legacy/vulnerabilities-debian:7-0.json b/tests/unit/providers/debian/test-fixtures/input/legacy/vulnerabilities-debian:7-0.json new file mode 100644 index 00000000..b62d6584 --- /dev/null +++ b/tests/unit/providers/debian/test-fixtures/input/legacy/vulnerabilities-debian:7-0.json @@ -0,0 +1,294 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2004-1653", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 6.4, + "Vectors": null + } + } + }, + "Name": "CVE-2004-1653", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2016-5105", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 1.9, + "Vectors": "AV:L/AC:M/Au:N/C:P/I:N" + } + } + }, + "Name": "CVE-2016-5105", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2016-5106", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 1.5, + "Vectors": "AV:L/AC:M/Au:S/C:N/I:N" + } + } + }, + "Name": "CVE-2016-5106", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2016-5107", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 1.5, + "Vectors": "AV:L/AC:M/Au:S/C:N/I:N" + } + } + }, + "Name": "CVE-2016-5107", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2005-3330", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 7.5, + "Vectors": null + } + } + }, + "Name": "CVE-2005-3330", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2014-3230", + "Metadata": {}, + "Name": "CVE-2014-3230", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2016-9816", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 4.9, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N" + } + } + }, + "Name": "CVE-2016-9816", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2016-9812", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 5, + "Vectors": "AV:N/AC:L/Au:N/C:N/I:N" + } + } + }, + "Name": "CVE-2016-9812", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2013-7171", + "Metadata": {}, + "Name": "CVE-2013-7171", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2016-4450", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 5, + "Vectors": "AV:N/AC:L/Au:N/C:N/I:N" + } + } + }, + "Name": "CVE-2016-4450", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2007-3072", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 7.1, + "Vectors": "AV:N/AC:M/Au:N/C:C/I:N" + } + } + }, + "Name": "CVE-2007-3072", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2013-2188", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 4.7, + "Vectors": "AV:L/AC:M/Au:N/C:N/I:N" + } + } + }, + "Name": "CVE-2013-2188", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2013-7353", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 5, + "Vectors": "AV:N/AC:L/Au:N/C:N/I:N" + } + } + }, + "Name": "CVE-2013-7353", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2011-1758", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 3.7, + "Vectors": "AV:L/AC:H/Au:N/C:P/I:P" + } + } + }, + "Name": "CVE-2011-1758", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2015-4177", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 4.9, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N" + } + } + }, + "Name": "CVE-2015-4177", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2015-4176", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 2.1, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:P" + } + } + }, + "Name": "CVE-2015-4176", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [], + "Link": "https://security-tracker.debian.org/tracker/CVE-2015-4170", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 4.7, + "Vectors": "AV:L/AC:M/Au:N/C:N/I:N" + } + } + }, + "Name": "CVE-2015-4170", + "NamespaceName": "debian:7", + "Severity": "Negligible" + } + } +] diff --git a/tests/unit/providers/debian/test_debian.py b/tests/unit/providers/debian/test_debian.py index cdbea03e..17dcafae 100644 --- a/tests/unit/providers/debian/test_debian.py +++ b/tests/unit/providers/debian/test_debian.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os.path import shutil import pytest @@ -18,6 +19,7 @@ def disabled(*args, **kwargs): class TestParser: _sample_dsa_data_ = "test-fixtures/input/DSA" _sample_json_data_ = "test-fixtures/input/debian.json" + _sample_legacy_data = "test-fixtures/input/legacy/vulnerabilities-debian:7-0.json" def test_normalize_dsa_list(self, tmpdir, helpers, disable_get_requests): subject = parser.Parser(workspace=workspace.Workspace(tmpdir, "test", create=True)) @@ -87,6 +89,28 @@ def test_normalize_json(self, tmpdir, helpers, disable_get_requests): assert all(x.get("Vulnerability", {}).get("Description") is not None for x in vuln_dict.values()) + def test_get_legacy_records(self, tmpdir, helpers, disable_get_requests): + subject = parser.Parser(workspace=workspace.Workspace(tmpdir, "test", create=True)) + + mock_data_path = helpers.local_dir("test-fixtures/input") + shutil.copytree(mock_data_path, subject.workspace.input_path, dirs_exist_ok=True) + + legacy_records = subject._get_legacy_records() + + assert isinstance(legacy_records, dict) + assert len(legacy_records) > 0 + assert "7" in legacy_records.keys() + assert len(legacy_records["7"]) > 0 + + for _rel, vuln_dict in legacy_records.items(): + assert isinstance(vuln_dict, dict) + assert len(vuln_dict) > 0 + assert all("Vulnerability" in x for x in vuln_dict.values()) + + assert all(x.get("Vulnerability", {}).get("Name") for x in vuln_dict.values()) + + assert all(x.get("Vulnerability", {}).get("Description") is not None for x in vuln_dict.values()) + def test_provider_schema(helpers, disable_get_requests, monkeypatch): workspace = helpers.provider_workspace_helper(name=Provider.name()) @@ -109,5 +133,7 @@ def mock_download(): p.update(None) - assert workspace.num_result_entries() == 21 + # 17 entries from the legacy records, 21 from the mock json data + expected = 38 + assert workspace.num_result_entries() == expected assert workspace.result_schemas_valid(require_entries=True)