diff options
| author | Adam Janovsky | 2025-01-22 10:48:00 +0100 |
|---|---|---|
| committer | J08nY | 2025-02-01 22:56:46 +0100 |
| commit | 3c34bbe70f36eb2c5504be7ec81cfcb3fdf543c6 (patch) | |
| tree | 70ed04f776c9dc745681b8079848e85ee0aaaa35 | |
| parent | 24187ac21e9ed59243cf7ca62224e2069f1e4c9b (diff) | |
| download | sec-certs-3c34bbe70f36eb2c5504be7ec81cfcb3fdf543c6.tar.gz sec-certs-3c34bbe70f36eb2c5504be7ec81cfcb3fdf543c6.tar.zst sec-certs-3c34bbe70f36eb2c5504be7ec81cfcb3fdf543c6.zip | |
refactor auxiliary dataset handling, heuristics computation
23 files changed, 891 insertions, 779 deletions
diff --git a/requirements/dev_requirements.txt b/requirements/dev_requirements.txt index c09140a0..31471eb4 100644 --- a/requirements/dev_requirements.txt +++ b/requirements/dev_requirements.txt @@ -2,7 +2,7 @@ accessible-pygments==0.0.4 # via pydata-sphinx-theme aiohappyeyeballs==2.4.0 # via aiohttp -aiohttp==3.10.2 +aiohttp==3.10.11 # via # datasets # fsspec @@ -12,7 +12,7 @@ alabaster==0.7.13 # via sphinx annotated-types==0.6.0 # via pydantic -appnope==0.1.3 +appnope==0.1.4 # via # ipykernel # ipython @@ -82,6 +82,8 @@ cymem==2.0.8 # thinc datasets==2.15.0 # via sec-certs (./../pyproject.toml) +dateparser==1.2.0 + # via sec-certs (./../pyproject.toml) debugpy==1.8.0 # via ipykernel decorator==5.1.1 @@ -221,7 +223,7 @@ murmurhash==1.0.10 # preshed # spacy # thinc -mypy==1.8.0 +mypy==1.13.0 # via sec-certs (./../pyproject.toml) mypy-extensions==1.0.0 # via mypy @@ -315,6 +317,8 @@ preshed==3.0.9 # thinc prompt-toolkit==3.0.41 # via ipython +propcache==0.2.0 + # via yarl psutil==5.9.6 # via # ipykernel @@ -377,6 +381,7 @@ pytest-profiling==1.7.0 # via sec-certs (./../pyproject.toml) python-dateutil==2.8.2 # via + # dateparser # jupyter-client # matplotlib # pandas @@ -384,7 +389,9 @@ python-dateutil==2.8.2 python-dotenv==1.0.0 # via pydantic-settings pytz==2023.3.post1 - # via pandas + # via + # dateparser + # pandas pyyaml==6.0.1 # via # datasets @@ -404,6 +411,8 @@ referencing==0.31.0 # via # jsonschema # jsonschema-specifications +regex==2024.9.11 + # via dateparser requests==2.32.0 # via # datasets @@ -418,7 +427,7 @@ rpds-py==0.13.1 # via # jsonschema # referencing -ruff==0.2.2 +ruff==0.7.4 # via sec-certs (./../pyproject.toml) scikit-learn==1.5.0 # via sec-certs (./../pyproject.toml) @@ -546,6 +555,8 @@ typing-extensions==4.8.0 # typer tzdata==2023.3 # via pandas +tzlocal==5.2 + # via dateparser urllib3==2.2.2 # via # requests @@ -573,7 +584,7 @@ wrapt==1.16.0 # via deprecated xxhash==3.4.1 # via datasets -yarl==1.9.3 +yarl==1.17.2 # via aiohttp zipp==3.19.1 # via importlib-metadata diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 4b985619..dcacaed8 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ annotated-types==0.6.0 # via pydantic -appnope==0.1.3 +appnope==0.1.4 # via # ipykernel # ipython @@ -50,6 +50,8 @@ cymem==2.0.8 # preshed # spacy # thinc +dateparser==1.2.0 + # via sec-certs (./../pyproject.toml) debugpy==1.8.0 # via ipykernel decorator==5.1.1 @@ -210,6 +212,7 @@ pytesseract==0.3.10 # via sec-certs (./../pyproject.toml) python-dateutil==2.8.2 # via + # dateparser # jupyter-client # matplotlib # pandas @@ -217,7 +220,9 @@ python-dateutil==2.8.2 python-dotenv==1.0.0 # via pydantic-settings pytz==2023.3.post1 - # via pandas + # via + # dateparser + # pandas pyyaml==6.0.1 # via sec-certs (./../pyproject.toml) pyzmq==25.1.1 @@ -230,6 +235,8 @@ referencing==0.31.0 # via # jsonschema # jsonschema-specifications +regex==2024.9.11 + # via dateparser requests==2.32.0 # via # sec-certs (./../pyproject.toml) @@ -311,6 +318,8 @@ typing-extensions==4.8.0 # typer tzdata==2023.3 # via pandas +tzlocal==5.2 + # via dateparser urllib3==2.2.2 # via requests wasabi==1.1.2 diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt index adb5e3ac..5f8cb248 100644 --- a/requirements/test_requirements.txt +++ b/requirements/test_requirements.txt @@ -1,6 +1,6 @@ annotated-types==0.6.0 # via pydantic -appnope==0.1.3 +appnope==0.1.4 # via # ipykernel # ipython @@ -54,6 +54,8 @@ cymem==2.0.8 # preshed # spacy # thinc +dateparser==1.2.0 + # via sec-certs (./../pyproject.toml) debugpy==1.8.0 # via ipykernel decorator==5.1.1 @@ -225,6 +227,7 @@ pytest-cov==4.1.0 # via sec-certs (./../pyproject.toml) python-dateutil==2.8.2 # via + # dateparser # jupyter-client # matplotlib # pandas @@ -232,7 +235,9 @@ python-dateutil==2.8.2 python-dotenv==1.0.0 # via pydantic-settings pytz==2023.3.post1 - # via pandas + # via + # dateparser + # pandas pyyaml==6.0.1 # via sec-certs (./../pyproject.toml) pyzmq==25.1.1 @@ -245,6 +250,8 @@ referencing==0.31.0 # via # jsonschema # jsonschema-specifications +regex==2024.9.11 + # via dateparser requests==2.32.0 # via # sec-certs (./../pyproject.toml) @@ -326,6 +333,8 @@ typing-extensions==4.8.0 # typer tzdata==2023.3 # via pandas +tzlocal==5.2 + # via dateparser urllib3==2.2.2 # via requests wasabi==1.1.2 diff --git a/src/sec_certs/dataset/auxiliary_dataset_handling.py b/src/sec_certs/dataset/auxiliary_dataset_handling.py new file mode 100644 index 00000000..afd2b820 --- /dev/null +++ b/src/sec_certs/dataset/auxiliary_dataset_handling.py @@ -0,0 +1,252 @@ +import gzip +import itertools +import json +import logging +import tempfile +from abc import ABC, abstractmethod +from collections.abc import Iterable +from pathlib import Path +from typing import Any + +from sec_certs import constants +from sec_certs.configuration import config +from sec_certs.dataset.cc_scheme import CCSchemeDataset +from sec_certs.dataset.cpe import CPEDataset +from sec_certs.dataset.cve import CVEDataset +from sec_certs.dataset.fips_algorithm import FIPSAlgorithmDataset +from sec_certs.dataset.protection_profile import ProtectionProfileDataset +from sec_certs.sample.cc import CCCertificate +from sec_certs.sample.cc_maintenance_update import CCMaintenanceUpdate +from sec_certs.utils import helpers +from sec_certs.utils.nvd_dataset_builder import CpeMatchNvdDatasetBuilder, CpeNvdDatasetBuilder, CveNvdDatasetBuilder +from sec_certs.utils.profiling import staged + +logger = logging.getLogger(__name__) + + +class AuxiliaryDatasetHandler(ABC): + def __init__(self, root_dir: str | Path) -> None: + self.root_dir = Path(root_dir) + self.dset: Any + + @property + @abstractmethod + def dset_path(self) -> Path: + raise NotImplementedError("Not meant to be implemented by base class") + + def set_local_paths(self, new_root_dir: str | Path) -> None: + self.root_dir = Path(new_root_dir) + + def process_dataset(self, download_fresh: bool = False) -> None: + self.root_dir.mkdir(parents=True, exist_ok=True) + self._process_dataset_body(download_fresh) + + @abstractmethod + def load_dataset(self) -> None: + raise NotImplementedError("Not meant to be implemented by base class") + + @abstractmethod + def _process_dataset_body(self, download_fresh: bool = False) -> None: + raise NotImplementedError("Not meant to be implemented by base class") + + +class CPEDatasetHandler(AuxiliaryDatasetHandler): + @property + def dset_path(self) -> Path: + return self.root_dir / "cpe_dataset.json" + + @staged(logger, "Processing CPE dataset") + def _process_dataset_body(self, download_fresh: bool = False) -> None: + if self.dset_path.exists(): + logger.info("Preparing CPEDataset from json.") + self.dset = CPEDataset.from_json(self.dset_path) + else: + self.dset = CPEDataset(json_path=self.dset_path) + download_fresh = True + + if download_fresh: + if config.preferred_source_nvd_datasets == "api": + logger.info("Fetching new CPE records from NVD API") + with CpeNvdDatasetBuilder(api_key=config.nvd_api_key) as builder: + self.dset = builder.build_dataset(self.dset) + else: + logger.info("Preparing CPEDataset from sec-certs.org.") + self.dset = CPEDataset.from_web(self.dset_path) + self.dset.to_json() + + def load_dataset(self) -> None: + self.dset = CPEDataset.from_json(self.dset_path) + + +class CVEDatasetHandler(AuxiliaryDatasetHandler): + @property + def dset_path(self) -> Path: + return self.root_dir / "cve_dataset.json" + + @staged(logger, "Processing CVE dataset") + def _process_dataset_body(self, download_fresh: bool = False) -> None: + if self.dset_path.exists(): + logger.info("Preparing CVEDataset from json.") + self.dset = CVEDataset.from_json(self.dset_path) + else: + self.dset = CVEDataset(json_path=self.dset_path) + download_fresh = True + + if download_fresh: + if config.preferred_source_nvd_datasets == "api": + logger.info("Fetching new CVE records from NVD API.") + with CveNvdDatasetBuilder(api_key=config.nvd_api_key) as builder: + self.dset = builder.build_dataset(self.dset) + else: + logger.info("Preparing CVEDataset from sec-certs.org") + self.dset = CVEDataset.from_web(self.dset_path) + self.dset.to_json() + + def load_dataset(self): + self.dset = CVEDataset.from_json(self.dset_path) + + +class CPEMatchDictHandler(AuxiliaryDatasetHandler): + @property + def dset_path(self) -> Path: + return self.root_dir / "cpe_match.json" + + @staged(logger, "Processing CPE Match dictionary") + def _process_dataset_body(self, download_fresh: bool = False) -> None: + if self.dset_path.exists(): + logger.info("Preparing CPE Match feed from json.") + with self.dset_path.open("r") as handle: + self.dset = json.load(handle) + else: + self.dset = CpeMatchNvdDatasetBuilder._init_new_dataset() + download_fresh = True + + if download_fresh: + if config.preferred_source_nvd_datasets == "api": + logger.info("Fetchnig CPE Match feed from NVD APi.") + with CpeMatchNvdDatasetBuilder(api_key=config.nvd_api_key) as builder: + self.dset = builder.build_dataset(self.dset) + else: + logger.info("Preparing CPE Match feed from sec-certs.org.") + with tempfile.TemporaryDirectory() as tmp_dir: + dset_path = Path(tmp_dir) / "cpe_match_feed.json.gz" + if ( + not helpers.download_file( + config.cpe_match_latest_snapshot, + dset_path, + progress_bar_desc="Downloading CPE Match feed from web", + ) + == constants.RESPONSE_OK + ): + raise RuntimeError( + f"Could not download CPE Match feed from {config.cpe_match_latest_snapshot}." + ) + with gzip.open(str(dset_path)) as handle: + json_str = handle.read().decode("utf-8") + self.dset = json.loads(json_str) + with self.dset_path.open("w") as handle: + json.dump(self.dset, handle, indent=4) + + def load_dataset(self): + with self.dset_path.open("r") as handle: + self.dset = json.load(handle) + + +class FIPSAlgorithmDatasetHandler(AuxiliaryDatasetHandler): + @property + def dset_path(self) -> Path: + return self.root_dir / "algorithms.json" + + @staged(logger, "Processing FIPS Algorithms") + def _process_dataset_body(self, download_fresh: bool = False) -> None: + if not self.dset_path.exists() or download_fresh: + self.dset = FIPSAlgorithmDataset.from_web(self.dset_path) + self.dset.to_json() + else: + self.dset = FIPSAlgorithmDataset.from_json(self.dset_path) + + def load_dataset(self): + self.dset = FIPSAlgorithmDataset.from_json(self.dset_path) + + +class CCSchemeDatasetHandler(AuxiliaryDatasetHandler): + def __init__(self, root_dir: str | Path = constants.DUMMY_NONEXISTING_PATH, only_schemes: set[str] | None = None): + self.root_dir = Path(root_dir) + self.only_schemes = only_schemes + self.dset: Any + + @property + def dset_path(self) -> Path: + return self.root_dir / "cc_scheme.json" + + @staged(logger, "Processing CC Schemes") + def _process_dataset_body(self, download_fresh: bool = False) -> None: + if not self.dset_path.exists() or download_fresh: + self.dset = CCSchemeDataset.from_web(self.dset_path, self.only_schemes) + self.dset.to_json() + else: + self.dset = CCSchemeDataset.from_json(self.dset_path) + + def load_dataset(self): + self.dset = CCSchemeDataset.from_json(self.dset_path) + + +class CCMaintenanceUpdateDatasetHandler(AuxiliaryDatasetHandler): + def __init__( + self, root_dir: str | Path = constants.DUMMY_NONEXISTING_PATH, certs_with_updates: Iterable[CCCertificate] = [] + ) -> None: + self.root_dir = Path(root_dir) + self.certs_with_updates = certs_with_updates + self.dset: Any + + @property + def dset_path(self) -> Path: + return self.root_dir / "maintenances" + + @property + def _dset_json_path(self) -> Path: + return self.dset_path / "maintenance_updates.json" + + def load_dataset(self) -> None: + from sec_certs.dataset.cc import CCDatasetMaintenanceUpdates + + self.dset = CCDatasetMaintenanceUpdates.from_json(self._dset_json_path) + + @staged(logger, "Processing CC Maintenance updates") + def _process_dataset_body(self, download_fresh: bool = False): + from sec_certs.dataset.cc import CCDatasetMaintenanceUpdates + + if not self.dset_path.exists() or download_fresh: + updates = list( + itertools.chain.from_iterable( + CCMaintenanceUpdate.get_updates_from_cc_cert(x) for x in self.certs_with_updates + ) + ) + self.dset = CCDatasetMaintenanceUpdates( + {x.dgst: x for x in updates}, root_dir=self.dset_path, name="maintenance_updates" + ) + else: + self.dset = CCDatasetMaintenanceUpdates.from_json(self._dset_json_path) + + if not self.dset.state.artifacts_downloaded: + self.dset.download_all_artifacts() + if not self.dset.state.pdfs_converted: + self.dset.convert_all_pdfs() + if not self.dset.state.certs_analyzed: + self.dset.extract_data() + + +class ProtectionProfileDatasetHandler(AuxiliaryDatasetHandler): + @property + def dset_path(self) -> Path: + return self.root_dir / "pp.json" + + def load_dataset(self) -> None: + self.dset = ProtectionProfileDataset.from_json(self.dset_path) + + @staged(logger, "Processing Protection profiles") + def _process_dataset_body(self, download_fresh: bool = False): + if not self.dset_path.exists() or download_fresh: + self.dset = ProtectionProfileDataset.from_web(self.dset_path) + else: + self.dset = ProtectionProfileDataset.from_json(self.dset_path) diff --git a/src/sec_certs/dataset/cc.py b/src/sec_certs/dataset/cc.py index 568e68ce..529beb85 100644 --- a/src/sec_certs/dataset/cc.py +++ b/src/sec_certs/dataset/cc.py @@ -1,11 +1,9 @@ from __future__ import annotations -import itertools import locale import shutil import tempfile from collections.abc import Iterator -from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import ClassVar, cast @@ -17,19 +15,27 @@ from bs4 import BeautifulSoup, Tag import sec_certs.utils.sanitization from sec_certs import constants from sec_certs.configuration import config -from sec_certs.dataset.cc_scheme import CCSchemeDataset -from sec_certs.dataset.cpe import CPEDataset -from sec_certs.dataset.cve import CVEDataset -from sec_certs.dataset.dataset import AuxiliaryDatasets, Dataset, logger -from sec_certs.dataset.protection_profile import ProtectionProfileDataset -from sec_certs.model import ( - ReferenceFinder, - SARTransformer, - TransitiveVulnerabilityFinder, +from sec_certs.dataset.auxiliary_dataset_handling import ( + AuxiliaryDatasetHandler, + CCMaintenanceUpdateDatasetHandler, + CCSchemeDatasetHandler, + CPEDatasetHandler, + CPEMatchDictHandler, + CVEDatasetHandler, + ProtectionProfileDatasetHandler, ) +from sec_certs.dataset.dataset import Dataset, logger +from sec_certs.heuristics.cc import ( + compute_cert_labs, + compute_normalized_cert_ids, + compute_references, + compute_sars, + compute_scheme_data, + link_to_protection_profiles, +) +from sec_certs.heuristics.common import compute_cpe_heuristics, compute_related_cves, compute_transitive_vulnerabilities from sec_certs.model.cc_matching import CCSchemeMatcher from sec_certs.sample.cc import CCCertificate -from sec_certs.sample.cc_certificate_id import CertificateId from sec_certs.sample.cc_maintenance_update import CCMaintenanceUpdate from sec_certs.sample.cc_scheme import EntryType from sec_certs.sample.protection_profile import ProtectionProfile @@ -39,16 +45,7 @@ from sec_certs.utils import parallel_processing as cert_processing from sec_certs.utils.profiling import staged -@dataclass -class CCAuxiliaryDatasets(AuxiliaryDatasets): - cpe_dset: CPEDataset | None = None - cve_dset: CVEDataset | None = None - pp_dset: ProtectionProfileDataset | None = None - mu_dset: CCDatasetMaintenanceUpdates | None = None - scheme_dset: CCSchemeDataset | None = None - - -class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializableType): +class CCDataset(Dataset[CCCertificate], ComplexSerializableType): """ Class that holds CCCertificate. Serializable into json, pandas, dictionary. Conveys basic certificate manipulations and dataset transformations. Many private methods that perform internal operations, feel free to exploit them. @@ -61,7 +58,7 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable name: str | None = None, description: str = "", state: Dataset.DatasetInternalState | None = None, - auxiliary_datasets: CCAuxiliaryDatasets | None = None, + aux_handlers: dict[type[AuxiliaryDatasetHandler], AuxiliaryDatasetHandler] = {}, ): self.certs = certs self.timestamp = datetime.now() @@ -69,13 +66,21 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable self.name = name if name else type(self).__name__ + " dataset" self.description = description if description else datetime.now().strftime("%d/%m/%Y %H:%M:%S") self.state = state if state else self.DatasetInternalState() - - self.auxiliary_datasets: CCAuxiliaryDatasets = ( - auxiliary_datasets if auxiliary_datasets else CCAuxiliaryDatasets() - ) - + self.aux_handlers = aux_handlers self.root_dir = Path(root_dir) + if not self.aux_handlers: + self.aux_handlers[CPEDatasetHandler] = CPEDatasetHandler(self.auxiliary_datasets_dir) + self.aux_handlers[CVEDatasetHandler] = CVEDatasetHandler(self.auxiliary_datasets_dir) + self.aux_handlers[CPEMatchDictHandler] = CPEMatchDictHandler(self.auxiliary_datasets_dir) + self.aux_handlers[CCSchemeDatasetHandler] = CCSchemeDatasetHandler(self.auxiliary_datasets_dir) + self.aux_handlers[ProtectionProfileDatasetHandler] = ProtectionProfileDatasetHandler( + self.auxiliary_datasets_dir + ) + self.aux_handlers[CCMaintenanceUpdateDatasetHandler] = CCMaintenanceUpdateDatasetHandler( + self.auxiliary_datasets_dir + ) + def to_pandas(self) -> pd.DataFrame: """ Return self serialized into pandas DataFrame @@ -173,37 +178,9 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable return self.certificates_dir / "txt" @property - def pp_dataset_path(self) -> Path: - """ - Returns a path to the dataset of Protection Profiles - """ - return self.auxiliary_datasets_dir / "pp_dataset.json" - - @property - def mu_dataset_dir(self) -> Path: - """ - Returns directory that holds dataset of maintenance updates - """ - return self.auxiliary_datasets_dir / "maintenances" - - @property - def mu_dataset_path(self) -> Path: - """ - Returns a path to the dataset of maintenance updates - """ - return self.mu_dataset_dir / "maintenance_updates.json" - - @property def reference_annotator_dir(self) -> Path: return self.root_dir / "reference_annotator" - @property - def scheme_dataset_path(self) -> Path: - """ - Returns a path to the scheme dataset - """ - return self.auxiliary_datasets_dir / "scheme_dataset.json" - BASE_URL: ClassVar[str] = "https://www.commoncriteriaportal.org" HTML_PRODUCTS_URL = { @@ -288,15 +265,6 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable def _set_local_paths(self): super()._set_local_paths() - if self.auxiliary_datasets.pp_dset: - self.auxiliary_datasets.pp_dset.json_path = self.pp_dataset_path - - if self.auxiliary_datasets.mu_dset: - self.auxiliary_datasets.mu_dset.root_dir = self.mu_dataset_dir - - if self.auxiliary_datasets.scheme_dset: - self.auxiliary_datasets.scheme_dset.json_path = self.scheme_dataset_path - for cert in self: cert.set_local_paths( self.reports_pdf_dir, @@ -307,6 +275,13 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable self.certificates_txt_dir, ) + def process_auxiliary_datasets(self, download_fresh: bool = False) -> None: + self.aux_handlers[CCMaintenanceUpdateDatasetHandler].certs_with_updates = [ # type: ignore + x for x in self if x.maintenance_updates + ] + self.aux_handlers[CCSchemeDatasetHandler].only_schemes = {x.scheme for x in self} # type: ignore + super().process_auxiliary_datasets(download_fresh) + def _merge_certs(self, certs: dict[str, CCCertificate], cert_source: str | None = None) -> None: """ Merges dictionary of certificates into the dataset. Assuming they all are CommonCriteria certificates @@ -842,193 +817,38 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable for cert in certs_to_process: cert.compute_heuristics_cert_lab() - @staged( - logger, - "Computing heuristics: Deriving information about certificate ids from artifacts.", - ) - def _compute_normalized_cert_ids(self) -> None: - for cert in self: - cert.compute_heuristics_cert_id() - - @staged( - logger, - "Computing heuristics: Transitive vulnerabilities in referenc(ed/ing) certificates.", - ) - def _compute_transitive_vulnerabilities(self): - transitive_cve_finder = TransitiveVulnerabilityFinder(lambda cert: cert.heuristics.cert_id) - transitive_cve_finder.fit(self.certs, lambda cert: cert.heuristics.report_references) - - for dgst in self.certs: - transitive_cve = transitive_cve_finder.predict_single_cert(dgst) - - self.certs[dgst].heuristics.direct_transitive_cves = transitive_cve.direct_transitive_cves - self.certs[dgst].heuristics.indirect_transitive_cves = transitive_cve.indirect_transitive_cves - @staged(logger, "Computing heuristics: Matching scheme data.") def _compute_scheme_data(self): - if self.auxiliary_datasets.scheme_dset: - for scheme in self.auxiliary_datasets.scheme_dset: - if certified := scheme.lists.get(EntryType.Certified): - certs = [cert for cert in self if cert.status == "active"] - matches, scores = CCSchemeMatcher.match_all(certified, scheme.country, certs) - for dgst, match in matches.items(): - self[dgst].heuristics.scheme_data = match - if archived := scheme.lists.get(EntryType.Archived): - certs = [cert for cert in self if cert.status == "archived"] - matches, scores = CCSchemeMatcher.match_all(archived, scheme.country, certs) - for dgst, match in matches.items(): - self[dgst].heuristics.scheme_data = match - - @staged(logger, "Computing heuristics: SARs") - def _compute_sars(self) -> None: - transformer = SARTransformer().fit(self.certs.values()) - for cert in self: - cert.heuristics.extracted_sars = transformer.transform_single_cert(cert) - - @staged(logger, "Computing heuristics: certificate versions") - def _compute_cert_versions(self) -> None: - cert_ids = { - cert.dgst: CertificateId(cert.scheme, cert.heuristics.cert_id) - if cert.heuristics.cert_id is not None - else None - for cert in self - } - for cert in self: - cert.compute_heuristics_cert_versions(cert_ids) - - def _compute_heuristics(self) -> None: - self._compute_normalized_cert_ids() - super()._compute_heuristics() - self._compute_scheme_data() - self._compute_cert_versions() - self._compute_cert_labs() - self._compute_sars() - - @staged(logger, "Computing heuristics: references between certificates.") - def _compute_references(self) -> None: - def ref_lookup(kw_attr): - def func(cert): - kws = getattr(cert.pdf_data, kw_attr) - if not kws: - return set() - res = set() - for scheme, matches in kws["cc_cert_id"].items(): - for match in matches: - try: - canonical = CertificateId(scheme, match).canonical - res.add(canonical) - except Exception: - res.add(match) - return res - - return func - - for ref_source in ("report", "st"): - kw_source = f"{ref_source}_keywords" - dep_attr = f"{ref_source}_references" - - finder = ReferenceFinder() - finder.fit(self.certs, lambda cert: cert.heuristics.cert_id, ref_lookup(kw_source)) # type: ignore + for scheme in self.aux_handlers[CCSchemeDatasetHandler].dset: + if certified := scheme.lists.get(EntryType.Certified): + certs = [cert for cert in self if cert.status == "active"] + matches = CCSchemeMatcher.match_all(certified, scheme.country, certs) + for dgst, match in matches.items(): + self[dgst].heuristics.scheme_data = match + if archived := scheme.lists.get(EntryType.Archived): + certs = [cert for cert in self if cert.status == "archived"] + matches = CCSchemeMatcher.match_all(archived, scheme.country, certs) + for dgst, match in matches.items(): + self[dgst].heuristics.scheme_data = match - for dgst in self.certs: - setattr( - self.certs[dgst].heuristics, - dep_attr, - finder.predict_single_cert(dgst, keep_unknowns=False), - ) - - @serialize - def process_auxiliary_datasets(self, download_fresh: bool = False) -> None: - """ - Processes all auxiliary datasets needed during computation. On top of base-class processing, - CC handles protection profiles, maintenance updates and schemes. - """ - super().process_auxiliary_datasets(download_fresh) - self.auxiliary_datasets.pp_dset = self.process_protection_profiles(to_download=download_fresh) - self.auxiliary_datasets.mu_dset = self.process_maintenance_updates(to_download=download_fresh) - self.auxiliary_datasets.scheme_dset = self.process_schemes( - to_download=download_fresh, only_schemes={cert.scheme for cert in self} + def _compute_heuristics_body(self, skip_schemes: bool = False) -> None: + link_to_protection_profiles(self.aux_handlers[ProtectionProfileDatasetHandler].dset, self.certs.values()) + compute_cpe_heuristics(self.aux_handlers[CPEDatasetHandler].dset, self.certs.values()) + compute_related_cves( + self.aux_handlers[CPEDatasetHandler].dset, + self.aux_handlers[CVEDatasetHandler].dset, + self.aux_handlers[CPEMatchDictHandler].dset, + self.certs.values(), ) + compute_normalized_cert_ids(self.certs.values()) + compute_references(self.certs) + compute_transitive_vulnerabilities(self.certs) - @staged(logger, "Processing protection profiles.") - def process_protection_profiles( - self, to_download: bool = True, keep_metadata: bool = True - ) -> ProtectionProfileDataset: - """ - Downloads new snapshot of dataset with processed protection profiles (if it doesn't exist) and links PPs - with certificates within self. Assigns PPs to all certificates, based on name and fname match. - - :param bool to_download: If dataset should be downloaded or fetched from json, defaults to True - :param bool keep_metadata: If json related to the PP dataset should be kept on drive, defaults to True - :raises RuntimeError: When building of PPDataset fails - """ - - self.auxiliary_datasets_dir.mkdir(parents=True, exist_ok=True) - - if to_download or not self.pp_dataset_path.exists(): - pp_dataset = ProtectionProfileDataset.from_web(self.pp_dataset_path) - else: - pp_dataset = ProtectionProfileDataset.from_json(self.pp_dataset_path) - - # Map protection profiles to their name and file name for matching to certs. - pps = {(pp.pp_name, sanitization.sanitize_link_fname(pp.pp_link)): pp for pp in pp_dataset} - - for cert in self: - if cert.protection_profiles is None: - raise RuntimeError("Building of the dataset probably failed - this should not be happening.") - cert.protection_profiles = { - pps.get((x.pp_name, sanitization.sanitize_link_fname(x.pp_link)), x) for x in cert.protection_profiles - } - - if not keep_metadata: - self.pp_dataset_path.unlink() - - return pp_dataset - - @staged(logger, "Processing maintenace updates.") - def process_maintenance_updates(self, to_download: bool = True) -> CCDatasetMaintenanceUpdates: - """ - Downloads or loads from json a dataset of maintenance updates. Runs analysis on that dataset if it's not completed. - :return CCDatasetMaintenanceUpdates: the resulting dataset of maintenance updates - """ - self.mu_dataset_dir.mkdir(parents=True, exist_ok=True) - - if to_download or not self.mu_dataset_path.exists(): - maintained_certs: list[CCCertificate] = [x for x in self if x.maintenance_updates] - updates = list( - itertools.chain.from_iterable(CCMaintenanceUpdate.get_updates_from_cc_cert(x) for x in maintained_certs) - ) - update_dset = CCDatasetMaintenanceUpdates( - {x.dgst: x for x in updates}, - root_dir=self.mu_dataset_dir, - name="maintenance_updates", - ) - else: - update_dset = CCDatasetMaintenanceUpdates.from_json(self.mu_dataset_path) - - if not update_dset.state.artifacts_downloaded: - update_dset.download_all_artifacts() - if not update_dset.state.pdfs_converted: - update_dset.convert_all_pdfs() - if not update_dset.state.certs_analyzed: - update_dset.extract_data() - - return update_dset - - @staged(logger, "Processing CC scheme dataset.") - def process_schemes(self, to_download: bool = True, only_schemes: set[str] | None = None) -> CCSchemeDataset: - """ - Downloads or loads from json a dataset of CC scheme data. - """ - self.auxiliary_datasets_dir.mkdir(parents=True, exist_ok=True) - - if to_download or not self.scheme_dataset_path.exists(): - scheme_dset = CCSchemeDataset.from_web(only_schemes) - scheme_dset.to_json(self.scheme_dataset_path) - else: - scheme_dset = CCSchemeDataset.from_json(self.scheme_dataset_path) + if not skip_schemes: + compute_scheme_data(self.aux_handlers[CCSchemeDatasetHandler].dset, self.certs.values()) - return scheme_dset + compute_cert_labs(self.certs.values()) + compute_sars(self.certs.values()) class CCDatasetMaintenanceUpdates(CCDataset, ComplexSerializableType): @@ -1056,7 +876,7 @@ class CCDatasetMaintenanceUpdates(CCDataset, ComplexSerializableType): def __iter__(self) -> Iterator[CCMaintenanceUpdate]: yield from self.certs.values() # type: ignore - def _compute_heuristics(self) -> None: + def _compute_heuristics_body(self, skip_schemes: bool = False) -> None: raise NotImplementedError def compute_related_cves(self) -> None: diff --git a/src/sec_certs/dataset/cc_scheme.py b/src/sec_certs/dataset/cc_scheme.py index 19f1a3de..e7f4433e 100644 --- a/src/sec_certs/dataset/cc_scheme.py +++ b/src/sec_certs/dataset/cc_scheme.py @@ -50,7 +50,11 @@ class CCSchemeDataset(JSONPathDataset, ComplexSerializableType): @classmethod def from_web( - cls, only_schemes: set[str] | None = None, enhanced: bool | None = None, artifacts: bool | None = None + cls, + json_path: str | Path = constants.DUMMY_NONEXISTING_PATH, + only_schemes: set[str] | None = None, + enhanced: bool | None = None, + artifacts: bool | None = None, ) -> CCSchemeDataset: schemes = {} for scheme, sources in CCScheme.methods.items(): @@ -60,4 +64,4 @@ class CCSchemeDataset(JSONPathDataset, ComplexSerializableType): schemes[scheme] = CCScheme.from_web(scheme, sources.keys(), enhanced=enhanced, artifacts=artifacts) except Exception as e: logger.warning(f"Could not download CC scheme: {scheme} due to error {e}.") - return cls(schemes) + return cls(schemes, json_path=json_path) diff --git a/src/sec_certs/dataset/cpe.py b/src/sec_certs/dataset/cpe.py index efcb3ded..c93ddd71 100644 --- a/src/sec_certs/dataset/cpe.py +++ b/src/sec_certs/dataset/cpe.py @@ -9,8 +9,8 @@ from typing import Any import pandas as pd -import sec_certs.configuration as config_module from sec_certs import constants +from sec_certs.configuration import config from sec_certs.dataset.json_path_dataset import JSONPathDataset from sec_certs.sample.cpe import CPE from sec_certs.serialization.json import ComplexSerializableType @@ -19,6 +19,10 @@ from sec_certs.utils import helpers logger = logging.getLogger(__name__) +class CPEMatchDict(dict): + pass + + class CPEDataset(JSONPathDataset, ComplexSerializableType): """ Dataset of CPE records. Includes look-up dictionaries for fast search. @@ -78,13 +82,13 @@ class CPEDataset(JSONPathDataset, ComplexSerializableType): dset_path = Path(tmp_dir) / "cpe_dataset.json.gz" if ( not helpers.download_file( - config_module.config.cpe_latest_snapshot, + config.cpe_latest_snapshot, dset_path, progress_bar_desc="Downloading CPEDataset from web", ) == constants.RESPONSE_OK ): - raise RuntimeError(f"Could not download CPEDataset from {config_module.config.cpe_latest_snapshot}.") + raise RuntimeError(f"Could not download CPEDataset from {config.cpe_latest_snapshot}.") dset = cls.from_json(dset_path, is_compressed=True) dset.json_path = json_path diff --git a/src/sec_certs/dataset/dataset.py b/src/sec_certs/dataset/dataset.py index 8caeafcf..44f47494 100644 --- a/src/sec_certs/dataset/dataset.py +++ b/src/sec_certs/dataset/dataset.py @@ -1,10 +1,6 @@ from __future__ import annotations -import gzip -import itertools -import json import logging -import re import shutil import tarfile import tempfile @@ -18,41 +14,23 @@ from typing import Any, Generic, TypeVar, cast import pandas as pd from sec_certs import constants -from sec_certs.configuration import config -from sec_certs.dataset.cpe import CPEDataset -from sec_certs.dataset.cve import CVEDataset -from sec_certs.model.cpe_matching import CPEClassifier +from sec_certs.dataset.auxiliary_dataset_handling import AuxiliaryDatasetHandler from sec_certs.sample.certificate import Certificate -from sec_certs.sample.cpe import CPE from sec_certs.serialization.json import ( ComplexSerializableType, get_class_fullname, serialize, ) from sec_certs.utils import helpers -from sec_certs.utils.nvd_dataset_builder import ( - CpeMatchNvdDatasetBuilder, - CpeNvdDatasetBuilder, - CveNvdDatasetBuilder, -) from sec_certs.utils.profiling import staged -from sec_certs.utils.tqdm import tqdm logger = logging.getLogger(__name__) - -@dataclass -class AuxiliaryDatasets: - cpe_dset: CPEDataset | None = None - cve_dset: CVEDataset | None = None - - CertSubType = TypeVar("CertSubType", bound=Certificate) -AuxiliaryDatasetsSubType = TypeVar("AuxiliaryDatasetsSubType", bound=AuxiliaryDatasets) DatasetSubType = TypeVar("DatasetSubType", bound="Dataset") -class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializableType, ABC): +class Dataset(Generic[CertSubType], ComplexSerializableType, ABC): """ Base class for dataset of certificates from CC and FIPS 140 schemes. Layouts public functions, the processing pipeline and common operations on the dataset and certs. @@ -73,7 +51,7 @@ class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializabl name: str | None = None, description: str = "", state: DatasetInternalState | None = None, - auxiliary_datasets: AuxiliaryDatasetsSubType | None = None, + aux_handlers: dict[type[AuxiliaryDatasetHandler], AuxiliaryDatasetHandler] = {}, ): self.certs = certs @@ -82,13 +60,8 @@ class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializabl self.name = name if name else type(self).__name__.lower() + "_dataset" self.description = description if description else "No description provided" self.state = state if state else self.DatasetInternalState() - - if not auxiliary_datasets: - self.auxiliary_datasets = AuxiliaryDatasets() - else: - self.auxiliary_datasets = auxiliary_datasets - self.root_dir = Path(root_dir) + self.aux_handlers = aux_handlers @property def root_dir(self) -> Path: @@ -98,7 +71,7 @@ class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializabl return self._root_dir @root_dir.setter - def root_dir(self: DatasetSubType, new_dir: str | Path) -> None: + def root_dir(self, new_dir: str | Path) -> None: """ This setter will only set the root dir and all internal paths so that they point to the new root dir. No data is being moved around. @@ -132,22 +105,6 @@ class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializabl return self.root_dir / "certs" @property - def cpe_dataset_path(self) -> Path: - return self.auxiliary_datasets_dir / "cpe_dataset.json" - - @property - def cpe_match_json_path(self) -> Path: - return self.auxiliary_datasets_dir / "cpe_match_feed.json" - - @property - def cve_dataset_path(self) -> Path: - return self.auxiliary_datasets_dir / "cve_dataset.json" - - @property - def nist_cve_cpe_matching_dset_path(self) -> Path: - return self.auxiliary_datasets_dir / "nvdcpematch-1.0.json" - - @property def json_path(self) -> Path: return self.root_dir / (self.name + ".json") @@ -259,7 +216,7 @@ class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializabl } @classmethod - def from_dict(cls: type[DatasetSubType], dct: dict) -> DatasetSubType: + def from_dict(cls, dct: dict) -> Dataset: certs = {x.dgst: x for x in dct["certs"]} dset = cls(certs, name=dct["name"], description=dct["description"], state=dct["state"]) if len(dset) != (claimed := dct["n_certs"]): @@ -279,10 +236,8 @@ class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializabl return dset def _set_local_paths(self) -> None: - if self.auxiliary_datasets.cpe_dset: - self.auxiliary_datasets.cpe_dset.json_path = self.cpe_dataset_path - if self.auxiliary_datasets.cve_dset: - self.auxiliary_datasets.cve_dset.json_path = self.cve_dataset_path + for handler in self.aux_handlers.values(): + handler.set_local_paths(self.auxiliary_datasets_dir) def move_dataset(self, new_root_dir: str | Path) -> None: """ @@ -311,7 +266,7 @@ class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializabl shutil.copytree(str(self.root_dir), str(new_root_dir), dirs_exist_ok=True) self.root_dir = new_root_dir - def _get_certs_by_name(self, name: str) -> set[CertSubType]: + def get_certs_by_name(self, name: str) -> set[CertSubType]: """ Returns list of certificates that match given name. """ @@ -321,22 +276,22 @@ class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializabl def get_certs_from_web(self) -> None: raise NotImplementedError("Not meant to be implemented by the base class.") + @staged(logger, "Processing auxiliary datasets") @serialize - @abstractmethod def process_auxiliary_datasets(self, download_fresh: bool = False) -> None: """ Processes all auxiliary datasets (CPE, CVE, ...) that are required during computation. """ logger.info("Processing auxiliary datasets.") - self.auxiliary_datasets_dir.mkdir(parents=True, exist_ok=True) - self.auxiliary_datasets.cpe_dset = self._prepare_cpe_dataset(download_fresh) - self.auxiliary_datasets.cve_dset = self._prepare_cve_dataset(download_fresh) - - if download_fresh or not self.cpe_match_json_path.exists(): - self._prepare_cpe_match_dict(download_fresh=download_fresh) - + for handler in self.aux_handlers.values(): + handler.process_dataset(download_fresh) self.state.auxiliary_datasets_processed = True + def load_auxiliary_datasets(self) -> None: + for handler in self.aux_handlers.values(): + if not hasattr(handler, "dset"): + handler.load_dataset() + @serialize def download_all_artifacts(self, fresh: bool = True) -> None: """ @@ -397,307 +352,24 @@ class Dataset(Generic[CertSubType, AuxiliaryDatasetsSubType], ComplexSerializabl self.state.certs_analyzed = True def _analyze_certificates_body(self) -> None: + logger.info("Extracting data and heuristics") self.extract_data() - self._compute_heuristics() + self.compute_heuristics() @abstractmethod def extract_data(self) -> None: raise NotImplementedError("Not meant to be implemented by the base class.") - def _compute_heuristics(self) -> None: + @serialize + def compute_heuristics(self) -> None: logger.info("Computing various heuristics from the certificates.") - self.compute_cpe_heuristics() - self.compute_related_cves() - self._compute_references() - self._compute_transitive_vulnerabilities() - - @abstractmethod - def _compute_references(self) -> None: - raise NotImplementedError("Not meant to be implemented by the base class.") + self.load_auxiliary_datasets() + self._compute_heuristics_body() @abstractmethod - def _compute_transitive_vulnerabilities(self) -> None: + def _compute_heuristics_body(self) -> None: raise NotImplementedError("Not meant to be implemented by the base class.") - @staged(logger, "Processing CPEDataset.") - def _prepare_cpe_dataset(self, download_fresh: bool = False) -> CPEDataset: - if not self.auxiliary_datasets_dir.exists(): - self.auxiliary_datasets_dir.mkdir(parents=True) - - if self.cpe_dataset_path.exists(): - logger.info("Preparing CPEDataset from json.") - cpe_dataset = CPEDataset.from_json(self.cpe_dataset_path) - else: - cpe_dataset = CPEDataset(json_path=self.cpe_dataset_path) - download_fresh = True - - if download_fresh: - if config.preferred_source_nvd_datasets == "api": - logger.info("Fetching new CPE records from NVD API.") - with CpeNvdDatasetBuilder(api_key=config.nvd_api_key) as builder: - cpe_dataset = builder.build_dataset(cpe_dataset) - cpe_dataset.to_json() - else: - logger.info("Preparing CPEDataset from sec-certs.org.") - cpe_dataset = CPEDataset.from_web(self.cpe_dataset_path) - - return cpe_dataset - - @staged(logger, "Processing CVEDataset.") - def _prepare_cve_dataset(self, download_fresh: bool = False) -> CVEDataset: - if not self.auxiliary_datasets_dir.exists(): - logger.info("Loading CVEDataset from json.") - self.auxiliary_datasets_dir.mkdir(parents=True) - - if self.cve_dataset_path.exists(): - logger.info("Preparing CVEDataset from json.") - cve_dataset = CVEDataset.from_json(self.cve_dataset_path) - else: - cve_dataset = CVEDataset(json_path=self.cve_dataset_path) - download_fresh = True - - if download_fresh: - if config.preferred_source_nvd_datasets == "api": - logger.info("Fetching new CVE records from NVD API.") - with CveNvdDatasetBuilder(api_key=config.nvd_api_key) as builder: - cve_dataset = builder.build_dataset(cve_dataset) - cve_dataset.to_json() - else: - logger.info("Preparing CVEDataset from sec-certs.org") - cve_dataset = CVEDataset.from_web(self.cve_dataset_path) - - return cve_dataset - - @staged(logger, "Processing CPE match dict.") - def _prepare_cpe_match_dict(self, download_fresh: bool = False) -> dict: - if self.cpe_match_json_path.exists(): - logger.info("Preparing CPE Match feed from json.") - with self.cpe_match_json_path.open("r") as handle: - cpe_match_dict = json.load(handle) - else: - cpe_match_dict = CpeMatchNvdDatasetBuilder._init_new_dataset() - download_fresh = True - - if download_fresh: - if config.preferred_source_nvd_datasets == "api": - logger.info("Fetching CPE Match feed from NVD APi.") - with CpeMatchNvdDatasetBuilder(api_key=config.nvd_api_key) as builder: - cpe_match_dict = builder.build_dataset(cpe_match_dict) - else: - logger.info("Preparing CPE Match feed from sec-certs.org.") - with tempfile.TemporaryDirectory() as tmp_dir: - dset_path = Path(tmp_dir) / "cpe_match_feed.json.gz" - if ( - not helpers.download_file( - config.cpe_match_latest_snapshot, - dset_path, - progress_bar_desc="Downloading CPE Match feed from web", - ) - == constants.RESPONSE_OK - ): - raise RuntimeError( - f"Could not download CPE Match feed from {config.cpe_match_latest_snapshot}." - ) - with gzip.open(str(dset_path)) as handle: - json_str = handle.read().decode("utf-8") - cpe_match_dict = json.loads(json_str) - with self.cpe_match_json_path.open("w") as handle: - json.dump(cpe_match_dict, handle, indent=4) - - return cpe_match_dict - - @serialize - @staged(logger, "Computing heuristics: Finding CPE matches for certificates") - def compute_cpe_heuristics(self) -> CPEClassifier: - """ - Computes matching CPEs for the certificates. - """ - WINDOWS_WEAK_CPES: set[CPE] = { - CPE( - "", - "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:x64:*", - "Microsoft Windows on X64", - ), - CPE( - "", - "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:x86:*", - "Microsoft Windows on X86", - ), - } - - def filter_condition(cpe: CPE) -> bool: - """ - Filters out very weak CPE matches that don't improve our database. - """ - if ( - cpe.title - and (cpe.version == "-" or cpe.version == "*") - and not any(char.isdigit() for char in cpe.title) - ): - return False - if ( - not cpe.title - and cpe.item_name - and (cpe.version == "-" or cpe.version == "*") - and not any(char.isdigit() for char in cpe.item_name) - ): - return False - if re.match(constants.RELEASE_CANDIDATE_REGEX, cpe.update): - return False - return cpe not in WINDOWS_WEAK_CPES - - if not self.auxiliary_datasets.cpe_dset: - self.auxiliary_datasets.cpe_dset = self._prepare_cpe_dataset() - - clf = CPEClassifier(config.cpe_matching_threshold, config.cpe_n_max_matches) - - if self.auxiliary_datasets.cpe_dset is None: - raise ValueError("CPE dataset cannot be None") - - clf.fit([x for x in self.auxiliary_datasets.cpe_dset if filter_condition(x)]) - - cert: CertSubType - for cert in tqdm(self, desc="Predicting CPE matches with the classifier"): - cert.compute_heuristics_version() - - cert.heuristics.cpe_matches = ( - clf.predict_single_cert(cert.manufacturer, cert.name, cert.heuristics.extracted_versions) - if cert.name - else None - ) - - return clf - - @serialize - def to_label_studio_json(self, output_path: str | Path) -> None: - cpe_dset = self._prepare_cpe_dataset() - - lst = [] - for cert in [x for x in self if x.heuristics.cpe_matches]: - dct = {"text": cert.label_studio_title} - candidates = [cpe_dset[x].title for x in cert.heuristics.cpe_matches] - candidates += ["No good match"] * (config.cpe_n_max_matches - len(candidates)) - options = ["option_" + str(x) for x in range(1, config.cpe_n_max_matches)] - dct.update(dict(zip(options, candidates))) - lst.append(dct) - - with Path(output_path).open("w") as handle: - json.dump(lst, handle, indent=4) - - @serialize - def load_label_studio_labels(self, input_path: str | Path) -> set[str]: - with Path(input_path).open("r") as handle: - data = json.load(handle) - - cpe_dset = self._prepare_cpe_dataset() - title_to_cpes_dict = cpe_dset.get_title_to_cpes_dict() - labeled_cert_digests: set[str] = set() - - logger.info("Translating label studio matches into their CPE representations and assigning to certificates.") - for annotation in tqdm(data, desc="Translating label studio matches"): - cpe_candidate_keys = {key for key in annotation if "option_" in key and annotation[key] != "No good match"} - - if "verified_cpe_match" not in annotation: - incorrect_keys: set[str] = set() - elif isinstance(annotation["verified_cpe_match"], str): - incorrect_keys = {annotation["verified_cpe_match"]} - else: - incorrect_keys = set(annotation["verified_cpe_match"]["choices"]) - - incorrect_keys = {x.lstrip("$") for x in incorrect_keys} - predicted_annotations = {annotation[x] for x in cpe_candidate_keys - incorrect_keys} - - cpes: set[CPE] = set() - for x in predicted_annotations: - if x not in title_to_cpes_dict: - logger.error(f"{x} not in dataset") - else: - to_update = title_to_cpes_dict[x] - if to_update and not cpes: - cpes = to_update - elif to_update and cpes: - cpes.update(to_update) - - # distinguish between FIPS and CC - if "\n" in annotation["text"]: - cert_name = annotation["text"].split("\nModule name: ")[1].split("\n")[0] - else: - cert_name = annotation["text"] - - certs = self._get_certs_by_name(cert_name) - labeled_cert_digests.update({x.dgst for x in certs}) - - for c in certs: - c.heuristics.verified_cpe_matches = {x.uri for x in cpes if x is not None} if cpes else None - - return labeled_cert_digests - - def enrich_automated_cpes_with_manual_labels(self) -> None: - """ - Prior to CVE matching, it is wise to expand the database of automatic CPE matches with those that were manually assigned. - """ - for cert in cast(Iterator[Certificate], self): - if not cert.heuristics.cpe_matches and cert.heuristics.verified_cpe_matches: - cert.heuristics.cpe_matches = cert.heuristics.verified_cpe_matches - elif cert.heuristics.cpe_matches and cert.heuristics.verified_cpe_matches: - cert.heuristics.cpe_matches = set(cert.heuristics.cpe_matches).union( - set(cert.heuristics.verified_cpe_matches) - ) - - def _get_all_cpes_in_dataset(self) -> set[CPE]: - if not self.auxiliary_datasets.cpe_dset: - raise ValueError( - "Cannot retrieve all cpes in dataset when cpe_dset is not set. You can prepare it with obj._prepare_cpe_dataset()" - ) - - cpe_matches = [ - [self.auxiliary_datasets.cpe_dset.cpes[y] for y in x.heuristics.cpe_matches] - for x in self - if x.heuristics.cpe_matches - ] - return set(itertools.chain.from_iterable(cpe_matches)) - - @serialize - @staged(logger, "Computing heuristics: CVEs in certificates.") - def compute_related_cves(self) -> None: - """ - Computes CVEs for the certificates, given their CPE matches. - """ - - if not self.auxiliary_datasets.cpe_dset: - self.auxiliary_datasets.cpe_dset = self._prepare_cpe_dataset() - - if not self.auxiliary_datasets.cve_dset: - self.auxiliary_datasets.cve_dset = self._prepare_cve_dataset() - - if self.auxiliary_datasets.cve_dset is None: - raise ValueError("CVE dataset cannot be None") - - if not self.auxiliary_datasets.cve_dset.look_up_dicts_built: - cpe_match_dict = self._prepare_cpe_match_dict() - all_cpes = self._get_all_cpes_in_dataset() - self.auxiliary_datasets.cve_dset.build_lookup_dict(cpe_match_dict, all_cpes) - - self.enrich_automated_cpes_with_manual_labels() - cpe_rich_certs = [x for x in cast(Iterator[Certificate], self) if x.heuristics.cpe_matches] - - if not cpe_rich_certs: - logger.error( - "No certificates with verified CPE match detected. You must run dset.manually_verify_cpe_matches() first. Returning." - ) - return - - cert: Certificate - for cert in tqdm(cpe_rich_certs, desc="Computing related CVES"): - related_cves = self.auxiliary_datasets.cve_dset.get_cves_from_matched_cpe_uris(cert.heuristics.cpe_matches) - cert.heuristics.related_cves = related_cves if related_cves else None - - n_vulnerable = len([x for x in cpe_rich_certs if x.heuristics.related_cves]) - n_vulnerabilities = sum([len(x.heuristics.related_cves) for x in cpe_rich_certs if x.heuristics.related_cves]) - logger.info( - f"In total, we identified {n_vulnerabilities} vulnerabilities in {n_vulnerable} vulnerable certificates." - ) - def get_keywords_df(self, var: str) -> pd.DataFrame: """ Get dataframe of keyword hits for attribute (var) that is member of PdfData class. diff --git a/src/sec_certs/dataset/fips.py b/src/sec_certs/dataset/fips.py index 0feb920f..2b454370 100644 --- a/src/sec_certs/dataset/fips.py +++ b/src/sec_certs/dataset/fips.py @@ -13,14 +13,16 @@ from bs4 import BeautifulSoup, NavigableString from sec_certs import constants from sec_certs.configuration import config -from sec_certs.dataset.cpe import CPEDataset -from sec_certs.dataset.cve import CVEDataset -from sec_certs.dataset.dataset import AuxiliaryDatasets, Dataset -from sec_certs.dataset.fips_algorithm import FIPSAlgorithmDataset -from sec_certs.model.reference_finder import ReferenceFinder -from sec_certs.model.transitive_vulnerability_finder import ( - TransitiveVulnerabilityFinder, +from sec_certs.dataset.auxiliary_dataset_handling import ( + AuxiliaryDatasetHandler, + CPEDatasetHandler, + CPEMatchDictHandler, + CVEDatasetHandler, + FIPSAlgorithmDatasetHandler, ) +from sec_certs.dataset.dataset import Dataset +from sec_certs.heuristics.common import compute_cpe_heuristics, compute_related_cves, compute_transitive_vulnerabilities +from sec_certs.heuristics.fips import compute_references from sec_certs.sample.fips import FIPSCertificate from sec_certs.serialization.json import ComplexSerializableType, serialize from sec_certs.utils import helpers @@ -31,13 +33,7 @@ from sec_certs.utils.profiling import staged logger = logging.getLogger(__name__) -class FIPSAuxiliaryDatasets(AuxiliaryDatasets): - cpe_dset: CPEDataset | None = None - cve_dset: CVEDataset | None = None - algorithm_dset: FIPSAlgorithmDataset | None = None - - -class FIPSDataset(Dataset[FIPSCertificate, FIPSAuxiliaryDatasets], ComplexSerializableType): +class FIPSDataset(Dataset[FIPSCertificate], ComplexSerializableType): """ Class for processing of FIPSCertificate samples. Inherits from `ComplexSerializableType` and base abstract `Dataset` class. """ @@ -49,7 +45,7 @@ class FIPSDataset(Dataset[FIPSCertificate, FIPSAuxiliaryDatasets], ComplexSerial name: str | None = None, description: str = "", state: Dataset.DatasetInternalState | None = None, - auxiliary_datasets: FIPSAuxiliaryDatasets | None = None, + aux_handlers: dict[type[AuxiliaryDatasetHandler], AuxiliaryDatasetHandler] = {}, ): self.certs = certs self.timestamp = datetime.datetime.now() @@ -57,12 +53,15 @@ class FIPSDataset(Dataset[FIPSCertificate, FIPSAuxiliaryDatasets], ComplexSerial self.name = name if name else type(self).__name__ + " dataset" self.description = description if description else datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S") self.state = state if state else self.DatasetInternalState() - self.auxiliary_datasets: FIPSAuxiliaryDatasets = ( - auxiliary_datasets if auxiliary_datasets else FIPSAuxiliaryDatasets() - ) - + self.aux_handlers = aux_handlers self.root_dir = Path(root_dir) + if not self.aux_handlers: + self.aux_handlers[CPEDatasetHandler] = CPEDatasetHandler(self.auxiliary_datasets_dir) + self.aux_handlers[CVEDatasetHandler] = CVEDatasetHandler(self.auxiliary_datasets_dir) + self.aux_handlers[FIPSAlgorithmDatasetHandler] = FIPSAlgorithmDatasetHandler(self.auxiliary_datasets_dir) + self.aux_handlers[CPEMatchDictHandler] = CPEMatchDictHandler(self.auxiliary_datasets_dir) + LIST_OF_CERTS_HTML: Final[dict[str, str]] = { "fips_modules_active.html": constants.FIPS_ACTIVE_MODULES_URL, "fips_modules_historical.html": constants.FIPS_HISTORICAL_MODULES_URL, @@ -85,10 +84,6 @@ class FIPSDataset(Dataset[FIPSCertificate, FIPSAuxiliaryDatasets], ComplexSerial def module_dir(self) -> Path: return self.certs_dir / "modules" - @property - def algorithm_dataset_path(self) -> Path: - return self.auxiliary_datasets_dir / "algorithms.json" - def __getitem__(self, item: str) -> FIPSCertificate: try: return super().__getitem__(item) @@ -110,6 +105,17 @@ class FIPSDataset(Dataset[FIPSCertificate, FIPSAuxiliaryDatasets], ComplexSerial ) self.update_with_certs(processed_certs) + def _compute_heuristics_body(self): + compute_cpe_heuristics(self.aux_handlers[CPEDatasetHandler].dset, self.certs.values()) + compute_related_cves( + self.aux_handlers[CPEDatasetHandler].dset, + self.aux_handlers[CVEDatasetHandler].dset, + self.aux_handlers[CPEMatchDictHandler].dset, + self.certs.values(), + ) + compute_references(self.certs) + compute_transitive_vulnerabilities(self.certs) + @serialize def extract_data(self) -> None: logger.info("Extracting various data from certification artifacts.") @@ -246,11 +252,7 @@ class FIPSDataset(Dataset[FIPSCertificate, FIPSAuxiliaryDatasets], ComplexSerial def _set_local_paths(self) -> None: super()._set_local_paths() - if self.auxiliary_datasets.algorithm_dset: - self.auxiliary_datasets.algorithm_dset.json_path = self.algorithm_dataset_path - - cert: FIPSCertificate - for cert in self.certs.values(): + for cert in self: cert.set_local_paths(self.policies_pdf_dir, self.policies_txt_dir, self.module_dir) @serialize @@ -270,21 +272,6 @@ class FIPSDataset(Dataset[FIPSCertificate, FIPSAuxiliaryDatasets], ComplexSerial self._set_local_paths() self.state.meta_sources_parsed = True - @serialize - def process_auxiliary_datasets(self, download_fresh: bool = False) -> None: - super().process_auxiliary_datasets(download_fresh) - self.auxiliary_datasets.algorithm_dset = self._prepare_algorithm_dataset(download_fresh) - - @staged(logger, "Processing FIPSAlgorithm dataset.") - def _prepare_algorithm_dataset(self, download_fresh_algs: bool = False) -> FIPSAlgorithmDataset: - if not self.algorithm_dataset_path.exists() or download_fresh_algs: - alg_dset = FIPSAlgorithmDataset.from_web(self.algorithm_dataset_path) - alg_dset.to_json() - else: - alg_dset = FIPSAlgorithmDataset.from_json(self.algorithm_dataset_path) - - return alg_dset - @staged(logger, "Extracting Algorithms from policy tables") def _extract_algorithms_from_policy_tables(self): certs_to_process = [x for x in self if x.state.policy_is_ok_to_analyze()] @@ -306,52 +293,6 @@ class FIPSDataset(Dataset[FIPSCertificate, FIPSAuxiliaryDatasets], ComplexSerial ) self.update_with_certs(processed_certs) - @staged( - logger, - "Computing heuristics: Transitive vulnerabilities in referenc(ed/ing) certificates.", - ) - def _compute_transitive_vulnerabilities(self) -> None: - transitive_cve_finder = TransitiveVulnerabilityFinder(lambda cert: str(cert.cert_id)) - transitive_cve_finder.fit(self.certs, lambda cert: cert.heuristics.policy_processed_references) - - for dgst in self.certs: - transitive_cve = transitive_cve_finder.predict_single_cert(dgst) - self.certs[dgst].heuristics.direct_transitive_cves = transitive_cve.direct_transitive_cves - self.certs[dgst].heuristics.indirect_transitive_cves = transitive_cve.indirect_transitive_cves - - @staged(logger, "Computing heuristics: references between certificates.") - def _compute_references(self, keep_unknowns: bool = False) -> None: - # Previously, a following procedure was used to prune reference_candidates: - # - A set of algorithms was obtained via self.auxiliary_datasets.algorithm_dset.get_algorithms_by_id(reference_candidate) - # - If any of these algorithms had the same vendor as the reference_candidate, the candidate was rejected - # - The rationale is that if an ID appears in a certificate s.t. an algorithm with the same ID was produced by the same vendor, the reference likely refers to alg. - # - Such reference should then be discarded. - # - We are uncertain of the effectivity of such measure, disabling it for now. - for cert in self: - cert.prune_referenced_cert_ids() - - policy_reference_finder = ReferenceFinder() - policy_reference_finder.fit( - self.certs, - lambda cert: str(cert.cert_id), - lambda cert: cert.heuristics.policy_prunned_references, - ) - - module_reference_finder = ReferenceFinder() - module_reference_finder.fit( - self.certs, - lambda cert: str(cert.cert_id), - lambda cert: cert.heuristics.module_prunned_references, - ) - - for cert in self: - cert.heuristics.policy_processed_references = policy_reference_finder.predict_single_cert( - cert.dgst, keep_unknowns - ) - cert.heuristics.module_processed_references = module_reference_finder.predict_single_cert( - cert.dgst, keep_unknowns - ) - def to_pandas(self) -> pd.DataFrame: df = pd.DataFrame( [x.pandas_tuple for x in self.certs.values()], diff --git a/src/sec_certs/dataset/protection_profile.py b/src/sec_certs/dataset/protection_profile.py index af7733a8..c3440c56 100644 --- a/src/sec_certs/dataset/protection_profile.py +++ b/src/sec_certs/dataset/protection_profile.py @@ -2,22 +2,21 @@ from __future__ import annotations import json import logging -import shutil import tempfile from dataclasses import dataclass from pathlib import Path from sec_certs import constants from sec_certs.configuration import config +from sec_certs.dataset.json_path_dataset import JSONPathDataset from sec_certs.sample.protection_profile import ProtectionProfile -from sec_certs.serialization.json import get_class_fullname from sec_certs.utils import helpers logger = logging.getLogger(__name__) @dataclass -class ProtectionProfileDataset: +class ProtectionProfileDataset(JSONPathDataset): pps: dict[tuple[str, str | None], ProtectionProfile] _json_path: Path @@ -29,29 +28,6 @@ class ProtectionProfileDataset: self.pps = pps self.json_path = Path(json_path) - @property - def json_path(self): - return self._json_path - - @json_path.setter - def json_path(self, new_path: str | Path): - new_path = Path(new_path) - if new_path.is_dir(): - raise ValueError(f"Json path of {get_class_fullname(self)} cannot be a directory.") - - self._json_path = new_path - - def move_dataset(self, new_json_path: str | Path) -> None: - logger.info(f"Moving {get_class_fullname(self)} dataset to {new_json_path}") - new_json_path = Path(new_json_path) - new_json_path.parent.mkdir(parents=True, exist_ok=True) - - if not self.json_path.exists(): - raise ValueError("Cannot move the PPDataset if the json path does not exist.") - - shutil.move(str(self.json_path), str(new_json_path)) - self.json_path = new_json_path - def __iter__(self): yield from self.pps.values() @@ -68,8 +44,8 @@ class ProtectionProfileDataset: return len(self.pps) @classmethod - def from_json(cls, json_path: str | Path): - with Path(json_path).open("r") as handle: + def from_json(cls, input_path: str | Path, is_compressed: bool = False): + with Path(input_path).open("r") as handle: data = json.load(handle) pps = [ProtectionProfile.from_old_api_dict(x) for x in data.values()] @@ -79,7 +55,10 @@ class ProtectionProfileDataset: logger.warning(f"Duplicate entry in PP dataset: {(item.pp_name, item.pp_link)}") dct[(item.pp_name, item.pp_link)] = item - return cls(dct) + dset = cls(dct) + dset.json_path = Path(input_path) + + return dset @classmethod def from_web(cls, store_dataset_path: Path | None = None): diff --git a/src/sec_certs/heuristics/cc.py b/src/sec_certs/heuristics/cc.py new file mode 100644 index 00000000..b646a085 --- /dev/null +++ b/src/sec_certs/heuristics/cc.py @@ -0,0 +1,86 @@ +import logging +from collections.abc import Iterable + +from sec_certs.dataset.cc_scheme import CCSchemeDataset +from sec_certs.dataset.protection_profile import ProtectionProfileDataset +from sec_certs.model.cc_matching import CCSchemeMatcher +from sec_certs.model.reference_finder import ReferenceFinder +from sec_certs.model.sar_transformer import SARTransformer +from sec_certs.sample.cc import CCCertificate +from sec_certs.sample.cc_certificate_id import CertificateId +from sec_certs.sample.cc_scheme import EntryType +from sec_certs.utils.profiling import staged + +logger = logging.getLogger(__name__) + + +@staged(logger, "Computing heuristics: Linking certificates to protection profiles") +def link_to_protection_profiles(pp_dset: ProtectionProfileDataset, certs: Iterable[CCCertificate]) -> None: + for cert in certs: + if cert.protection_profiles is None: + continue + cert.protection_profiles = {pp_dset.pps.get((x.pp_name, x.pp_link), x) for x in cert.protection_profiles} + + +@staged(logger, "Computing heuristics: references between certificates.") +def compute_references(certs: dict[str, CCCertificate]) -> None: + def ref_lookup(kw_attr): + def func(cert): + kws = getattr(cert.pdf_data, kw_attr) + if not kws: + return set() + res = set() + for scheme, matches in kws["cc_cert_id"].items(): + for match in matches: + try: + canonical = CertificateId(scheme, match).canonical + res.add(canonical) + except Exception: + res.add(match) + return res + + return func + + for ref_source in ("report", "st"): + kw_source = f"{ref_source}_keywords" + dep_attr = f"{ref_source}_references" + + finder = ReferenceFinder() + finder.fit(certs, lambda cert: cert.heuristics.cert_id, ref_lookup(kw_source)) # type: ignore + + for dgst in certs: + setattr(certs[dgst].heuristics, dep_attr, finder.predict_single_cert(dgst, keep_unknowns=False)) + + +@staged(logger, "Computing heuristics: Deriving information about certificate ids from artifacts.") +def compute_normalized_cert_ids(certs: Iterable[CCCertificate]) -> None: + for cert in certs: + cert.compute_heuristics_cert_id() + + +@staged(logger, "Computing heuristics: Matching scheme data.") +def compute_scheme_data(scheme_dset: CCSchemeDataset, certs: dict[str, CCCertificate]): + for scheme in scheme_dset: + if certified := scheme.lists.get(EntryType.Certified): + active_certs = [cert for cert in certs.values() if cert.status == "active"] + matches, _ = CCSchemeMatcher.match_all(certified, scheme.country, active_certs) + for dgst, match in matches.items(): + certs[dgst].heuristics.scheme_data = match + if archived := scheme.lists.get(EntryType.Archived): + archived_certs = [cert for cert in certs.values() if cert.status == "archived"] + matches, _ = CCSchemeMatcher.match_all(archived, scheme.country, archived_certs) + for dgst, match in matches.items(): + certs[dgst].heuristics.scheme_data = match + + +@staged(logger, "Computing heuristics: Deriving information about laboratories involved in certification.") +def compute_cert_labs(certs: Iterable[CCCertificate]) -> None: + for cert in certs: + cert.compute_heuristics_cert_lab() + + +@staged(logger, "Computing heuristics: SARs") +def compute_sars(certs: Iterable[CCCertificate]) -> None: + transformer = SARTransformer().fit(certs) + for cert in certs: + cert.heuristics.extracted_sars = transformer.transform_single_cert(cert) diff --git a/src/sec_certs/heuristics/common.py b/src/sec_certs/heuristics/common.py new file mode 100644 index 00000000..8a2b4769 --- /dev/null +++ b/src/sec_certs/heuristics/common.py @@ -0,0 +1,132 @@ +import itertools +import logging +import re +from collections.abc import Iterable + +from tqdm import tqdm + +from sec_certs import constants +from sec_certs.configuration import config +from sec_certs.dataset.cpe import CPEDataset +from sec_certs.dataset.cve import CVEDataset +from sec_certs.dataset.dataset import CertSubType +from sec_certs.model.cpe_matching import CPEClassifier +from sec_certs.model.transitive_vulnerability_finder import TransitiveVulnerabilityFinder +from sec_certs.sample.cc import CCCertificate +from sec_certs.sample.certificate import Certificate +from sec_certs.sample.cpe import CPE +from sec_certs.sample.fips import FIPSCertificate +from sec_certs.utils.profiling import staged + +logger = logging.getLogger(__name__) + + +@staged(logger, "Computing heuristics: Finding CPE matches for certificates") +def compute_cpe_heuristics(cpe_dataset: CPEDataset, certs: Iterable[CertSubType]) -> None: + """ + Computes matching CPEs for the certificates. + """ + WINDOWS_WEAK_CPES: set[CPE] = { + CPE("", "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:x64:*", "Microsoft Windows on X64"), + CPE("", "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:x86:*", "Microsoft Windows on X86"), + } + + def filter_condition(cpe: CPE) -> bool: + """ + Filters out very weak CPE matches that don't improve our database. + """ + if cpe.title and (cpe.version == "-" or cpe.version == "*") and not any(char.isdigit() for char in cpe.title): + return False + if ( + not cpe.title + and cpe.item_name + and (cpe.version == "-" or cpe.version == "*") + and not any(char.isdigit() for char in cpe.item_name) + ): + return False + if re.match(constants.RELEASE_CANDIDATE_REGEX, cpe.update): + return False + return cpe not in WINDOWS_WEAK_CPES + + logger.info("Computing CPE heuristics.") + clf = CPEClassifier(config.cpe_matching_threshold, config.cpe_n_max_matches) + clf.fit([x for x in cpe_dataset if filter_condition(x)]) + + for cert in tqdm(certs, desc="Predicting CPE matches with the classifier"): + cert.compute_heuristics_version() + cert.heuristics.cpe_matches = ( + clf.predict_single_cert(cert.manufacturer, cert.name, cert.heuristics.extracted_versions) + if cert.name + else None + ) + + +def get_all_cpes_in_dataset(cpe_dset: CPEDataset, certs: Iterable[Certificate]) -> set[CPE]: + cpe_matches = [[cpe_dset.cpes[y] for y in x.heuristics.cpe_matches] for x in certs if x.heuristics.cpe_matches] + return set(itertools.chain.from_iterable(cpe_matches)) + + +def enrich_automated_cpes_with_manual_labels(certs: Iterable[Certificate]) -> None: + """ + Prior to CVE matching, it is wise to expand the database of automatic CPE matches with those that were manually assigned. + """ + for cert in certs: + if not cert.heuristics.cpe_matches and cert.heuristics.verified_cpe_matches: + cert.heuristics.cpe_matches = cert.heuristics.verified_cpe_matches + elif cert.heuristics.cpe_matches and cert.heuristics.verified_cpe_matches: + cert.heuristics.cpe_matches = set(cert.heuristics.cpe_matches).union( + set(cert.heuristics.verified_cpe_matches) + ) + + +@staged(logger, "Computing heuristics: CVEs in certificates.") +def compute_related_cves( + cpe_dset: CPEDataset, cve_dset: CVEDataset, cpe_match_dict: dict, certs: Iterable[Certificate] +) -> None: + """ + Computes CVEs for the certificates, given their CPE matches. + """ + + logger.info("Computing related CVEs") + if not cve_dset.look_up_dicts_built: + all_cpes = get_all_cpes_in_dataset(cpe_dset, certs) + cve_dset.build_lookup_dict(cpe_match_dict, all_cpes) + + enrich_automated_cpes_with_manual_labels(certs) + cpe_rich_certs = [x for x in certs if x.heuristics.cpe_matches] + + for cert in tqdm(cpe_rich_certs, desc="Computing related CVES"): + related_cves = cve_dset.get_cves_from_matched_cpe_uris(cert.heuristics.cpe_matches) + cert.heuristics.related_cves = related_cves if related_cves else None + + n_vulnerable = len([x for x in cpe_rich_certs if x.heuristics.related_cves]) + n_vulnerabilities = sum([len(x.heuristics.related_cves) for x in cpe_rich_certs if x.heuristics.related_cves]) + logger.info( + f"In total, we identified {n_vulnerabilities} vulnerabilities in {n_vulnerable} vulnerable certificates." + ) + + +@staged( + logger, + "Computing heuristics: Transitive vulnerabilities in referenc(ed/ing) certificates.", +) +def compute_transitive_vulnerabilities(certs: dict[str, CertSubType]) -> None: + logger.info("Computing transitive vulnerabilities") + if not certs: + return + + some_cert = next(iter(certs.values())) + + if isinstance(some_cert, FIPSCertificate): + transitive_cve_finder = TransitiveVulnerabilityFinder(lambda cert: str(cert.cert_id)) + transitive_cve_finder.fit(certs, lambda cert: cert.heuristics.policy_processed_references) + elif isinstance(some_cert, CCCertificate): + transitive_cve_finder = TransitiveVulnerabilityFinder(lambda cert: str(cert.heuristics.cert_id)) + transitive_cve_finder.fit(certs, lambda cert: cert.heuristics.report_references) + else: + raise ValueError("Members of `certs` object must be either FIPSCertificate or CCCertificate instances.") + + for cert in certs.values(): + transitive_cve = transitive_cve_finder.predict_single_cert(cert.dgst) + cert.heuristics.direct_transitive_cves = transitive_cve.direct_transitive_cves + cert.heuristics.indirect_transitive_cves = transitive_cve.indirect_transitive_cves diff --git a/src/sec_certs/heuristics/fips.py b/src/sec_certs/heuristics/fips.py new file mode 100644 index 00000000..4abecc4e --- /dev/null +++ b/src/sec_certs/heuristics/fips.py @@ -0,0 +1,42 @@ +import logging + +from sec_certs.model.reference_finder import ReferenceFinder +from sec_certs.sample.fips import FIPSCertificate +from sec_certs.utils.profiling import staged + +logger = logging.getLogger(__name__) + + +@staged(logger, "Computing heuristics: references between certificates.") +def compute_references(certs: dict[str, FIPSCertificate], keep_unknowns: bool = False) -> None: + # Previously, a following procedure was used to prune reference_candidates: + # - A set of algorithms was obtained via self.auxiliary_datasets.algorithm_dset.get_algorithms_by_id(reference_candidate) + # - If any of these algorithms had the same vendor as the reference_candidate, the candidate was rejected + # - The rationale is that if an ID appears in a certificate s.t. an algorithm with the same ID was produced by the same vendor, the reference likely refers to alg. + # - Such reference should then be discarded. + # - We are uncertain of the effectivity of such measure, disabling it for now. + logger.info("Computing references") + for cert in certs.values(): + cert.prune_referenced_cert_ids() + + policy_reference_finder = ReferenceFinder() + policy_reference_finder.fit( + certs, + lambda cert: str(cert.cert_id), + lambda cert: cert.heuristics.policy_prunned_references, + ) + + module_reference_finder = ReferenceFinder() + module_reference_finder.fit( + certs, + lambda cert: str(cert.cert_id), + lambda cert: cert.heuristics.module_prunned_references, + ) + + for cert in certs.values(): + cert.heuristics.policy_processed_references = policy_reference_finder.predict_single_cert( + cert.dgst, keep_unknowns + ) + cert.heuristics.module_processed_references = module_reference_finder.predict_single_cert( + cert.dgst, keep_unknowns + ) diff --git a/src/sec_certs/utils/label_studio_utils.py b/src/sec_certs/utils/label_studio_utils.py new file mode 100644 index 00000000..9c70b938 --- /dev/null +++ b/src/sec_certs/utils/label_studio_utils.py @@ -0,0 +1,78 @@ +import json +import logging +from pathlib import Path + +from tqdm import tqdm + +from sec_certs.configuration import config +from sec_certs.dataset.auxiliary_dataset_handling import CPEDatasetHandler +from sec_certs.dataset.dataset import Dataset +from sec_certs.sample.cpe import CPE + +logger = logging.getLogger(__name__) + + +def to_label_studio_json(dataset: Dataset, output_path: str | Path) -> None: + dataset.load_auxiliary_datasets() + cpe_dset = dataset.aux_handlers[CPEDatasetHandler].dset + + lst = [] + for cert in [x for x in dataset if x.heuristics.cpe_matches]: + dct = {"text": cert.label_studio_title} + candidates = [cpe_dset[x].title for x in cert.heuristics.cpe_matches] + candidates += ["No good match"] * (config.cpe_n_max_matches - len(candidates)) + options = ["option_" + str(x) for x in range(1, config.cpe_n_max_matches)] + dct.update(dict(zip(options, candidates))) + lst.append(dct) + + with Path(output_path).open("w") as handle: + json.dump(lst, handle, indent=4) + + +def load_label_studio_labels(dataset: Dataset, input_path: str | Path) -> set[str]: + with Path(input_path).open("r") as handle: + data = json.load(handle) + + dataset.load_auxiliary_datasets() + cpe_dset = dataset.aux_handlers[CPEDatasetHandler].dset + title_to_cpes_dict = cpe_dset.get_title_to_cpes_dict() + labeled_cert_digests: set[str] = set() + + logger.info("Translating label studio matches into their CPE representations and assigning to certificates.") + for annotation in tqdm(data, desc="Translating label studio matches"): + cpe_candidate_keys = {key for key in annotation if "option_" in key and annotation[key] != "No good match"} + + if "verified_cpe_match" not in annotation: + incorrect_keys: set[str] = set() + elif isinstance(annotation["verified_cpe_match"], str): + incorrect_keys = {annotation["verified_cpe_match"]} + else: + incorrect_keys = set(annotation["verified_cpe_match"]["choices"]) + + incorrect_keys = {x.lstrip("$") for x in incorrect_keys} + predicted_annotations = {annotation[x] for x in cpe_candidate_keys - incorrect_keys} + + cpes: set[CPE] = set() + for x in predicted_annotations: + if x not in title_to_cpes_dict: + logger.error(f"{x} not in dataset") + else: + to_update = title_to_cpes_dict[x] + if to_update and not cpes: + cpes = to_update + elif to_update and cpes: + cpes.update(to_update) + + # distinguish between FIPS and CC + if "\n" in annotation["text"]: + cert_name = annotation["text"].split("\nModule name: ")[1].split("\n")[0] + else: + cert_name = annotation["text"] + + certs = dataset.get_certs_by_name(cert_name) + labeled_cert_digests.update({x.dgst for x in certs}) + + for c in certs: + c.heuristics.verified_cpe_matches = {x.uri for x in cpes if x is not None} if cpes else None + + return labeled_cert_digests diff --git a/src/sec_certs/utils/nvd_dataset_builder.py b/src/sec_certs/utils/nvd_dataset_builder.py index 08f65d30..4e7162eb 100644 --- a/src/sec_certs/utils/nvd_dataset_builder.py +++ b/src/sec_certs/utils/nvd_dataset_builder.py @@ -16,13 +16,13 @@ import requests from requests import RequestException, Response from sec_certs import constants -from sec_certs.dataset.cpe import CPEDataset +from sec_certs.dataset.cpe import CPEDataset, CPEMatchDict from sec_certs.dataset.cve import CVEDataset from sec_certs.utils.parallel_processing import process_parallel logger = logging.getLogger(__name__) -DatasetType = TypeVar("DatasetType", CPEDataset, CVEDataset, dict) +DatasetType = TypeVar("DatasetType", CPEDataset, CVEDataset, CPEMatchDict) @dataclass @@ -320,7 +320,7 @@ class CveNvdDatasetBuilder(NvdDatasetBuilder[CVEDataset]): return CVEDataset() -class CpeMatchNvdDatasetBuilder(NvdDatasetBuilder[dict]): +class CpeMatchNvdDatasetBuilder(NvdDatasetBuilder[CPEMatchDict]): _ENDPOINT: Final[str] = "CPEMatch" _ENDPOINT_URL: Final[str] = "https://services.nvd.nist.gov/rest/json/cpematch/2.0" _RESULTS_PER_PAGE: Final[int] = 500 @@ -331,7 +331,7 @@ class CpeMatchNvdDatasetBuilder(NvdDatasetBuilder[dict]): "versionEndExcluding", ] - def _process_responses(self, responses: list[Response], dataset_to_fill: dict) -> dict: + def _process_responses(self, responses: list[Response], dataset_to_fill: CPEMatchDict) -> CPEMatchDict: timestamp = self._end_mod_date.isoformat() if self._end_mod_date else responses[-1].json()["timestamp"] match_strings = list(itertools.chain.from_iterable(response.json()["matchStrings"] for response in responses)) dataset_to_fill["timestamp"] = timestamp @@ -361,5 +361,5 @@ class CpeMatchNvdDatasetBuilder(NvdDatasetBuilder[dict]): return datetime.fromisoformat(previous_data["timestamp"]) @staticmethod - def _init_new_dataset() -> dict: - return {"timestamp": datetime.fromtimestamp(0).isoformat(), "match_strings": {}} + def _init_new_dataset() -> CPEMatchDict: + return CPEMatchDict({"timestamp": datetime.fromtimestamp(0).isoformat(), "match_strings": {}}) diff --git a/tests/cc/test_cc_analysis.py b/tests/cc/test_cc_analysis.py index ce83d705..9a57da1e 100644 --- a/tests/cc/test_cc_analysis.py +++ b/tests/cc/test_cc_analysis.py @@ -10,9 +10,17 @@ import tests.data.cc.analysis import tests.data.common from sec_certs.cert_rules import SARS_IMPLIED_FROM_EAL +from sec_certs.dataset.auxiliary_dataset_handling import ( + CPEDatasetHandler, + CPEMatchDictHandler, + CVEDatasetHandler, + ProtectionProfileDatasetHandler, +) from sec_certs.dataset.cc import CCDataset from sec_certs.dataset.cpe import CPEDataset from sec_certs.dataset.cve import CVEDataset +from sec_certs.heuristics.cc import compute_references, link_to_protection_profiles +from sec_certs.heuristics.common import compute_related_cves, compute_transitive_vulnerabilities from sec_certs.sample.cc import CCCertificate from sec_certs.sample.protection_profile import ProtectionProfile from sec_certs.sample.sar import SAR @@ -32,11 +40,14 @@ def processed_cc_dset( shutil.copytree(analysis_data_dir, tmp_dir, dirs_exist_ok=True) cc_dset = CCDataset.from_json(tmp_dir / "vulnerable_dataset.json") - cc_dset.process_protection_profiles() + + cc_dset.aux_handlers[ProtectionProfileDatasetHandler].process_dataset() + cc_dset.aux_handlers[CPEMatchDictHandler].dset = {} + cc_dset.aux_handlers[CVEDatasetHandler].dset = cve_dataset + cc_dset.aux_handlers[CPEDatasetHandler].dset = cpe_dataset + cc_dset.extract_data() - cc_dset.auxiliary_datasets.cve_dset = cve_dataset - cc_dset.auxiliary_datasets.cpe_dset = cpe_dataset - cc_dset._compute_heuristics() + cc_dset._compute_heuristics_body(skip_schemes=True) return cc_dset @@ -66,7 +77,13 @@ def test_find_related_cves(processed_cc_dset: CCDataset, random_certificate: CCC random_certificate.heuristics.cpe_matches = { "cpe:2.3:a:ibm:security_access_manager_for_enterprise_single_sign-on:8.2.2:*:*:*:*:*:*:*" } - processed_cc_dset.compute_related_cves() + compute_related_cves( + processed_cc_dset.aux_handlers[CPEDatasetHandler].dset, + processed_cc_dset.aux_handlers[CVEDatasetHandler].dset, + {}, + processed_cc_dset.certs.values(), + ) + assert random_certificate.heuristics.related_cves == {"CVE-2017-1732", "CVE-2019-4513"} @@ -75,7 +92,14 @@ def test_find_related_cves_criteria_configuration(processed_cc_dset: CCDataset, "cpe:2.3:a:ibm:websphere_application_server:7.0:*:*:*:*:*:*:*", "cpe:2.3:o:ibm:zos:6.0.1:*:*:*:*:*:*:*", } - processed_cc_dset.compute_related_cves() + + compute_related_cves( + processed_cc_dset.aux_handlers[CPEDatasetHandler].dset, + processed_cc_dset.aux_handlers[CVEDatasetHandler].dset, + {}, + processed_cc_dset.certs.values(), + ) + assert random_certificate.heuristics.related_cves == {"CVE-2010-2325"} @@ -148,7 +172,10 @@ def test_protection_profile_matching(processed_cc_dset: CCDataset, random_certif pp_ids=frozenset(["KECS-PP-0822-2017 SSO V1.0"]), ) - processed_cc_dset.process_protection_profiles(to_download=False) + link_to_protection_profiles( + processed_cc_dset.aux_handlers[ProtectionProfileDatasetHandler].dset, processed_cc_dset.certs.values() + ) + assert random_certificate.protection_profiles == {expected_pp} @@ -161,7 +188,8 @@ def test_single_record_references_heuristics(random_certificate: CCCertificate): def test_reference_dataset(reference_dataset: CCDataset): - reference_dataset._compute_references() + compute_references(reference_dataset.certs) + test_cert = reference_dataset["d1b238729b25d745"] assert test_cert.heuristics.report_references.directly_referenced_by == {"BSI-DSZ-CC-0370-2006"} @@ -174,12 +202,12 @@ def test_reference_dataset(reference_dataset: CCDataset): def test_direct_transitive_vulnerability_dataset(transitive_vulnerability_dataset: CCDataset): - transitive_vulnerability_dataset._compute_transitive_vulnerabilities() + compute_transitive_vulnerabilities(transitive_vulnerability_dataset.certs) assert transitive_vulnerability_dataset["11f77cb31b931a57"].heuristics.direct_transitive_cves == {"CVE-2013-5385"} def test_indirect_transitive_vulnerability_dataset(transitive_vulnerability_dataset: CCDataset): - transitive_vulnerability_dataset._compute_transitive_vulnerabilities() + compute_transitive_vulnerabilities(transitive_vulnerability_dataset.certs) assert transitive_vulnerability_dataset["11f77cb31b931a57"].heuristics.indirect_transitive_cves == {"CVE-2013-5385"} diff --git a/tests/cc/test_cc_dataset.py b/tests/cc/test_cc_dataset.py index bc1433aa..c501054c 100644 --- a/tests/cc/test_cc_dataset.py +++ b/tests/cc/test_cc_dataset.py @@ -6,6 +6,7 @@ from tempfile import TemporaryDirectory import pytest from sec_certs import constants +from sec_certs.dataset.auxiliary_dataset_handling import ProtectionProfileDatasetHandler from sec_certs.dataset.cc import CCDataset from sec_certs.sample.cc import CCCertificate @@ -132,9 +133,12 @@ def test_build_dataset(data_dir: Path, cert_one: CCCertificate, toy_dataset: CCD def test_process_pp_dataset(toy_dataset: CCDataset): with TemporaryDirectory() as tmp_dir: toy_dataset.copy_dataset(tmp_dir) - toy_dataset.process_protection_profiles() - assert toy_dataset.pp_dataset_path.exists() - assert toy_dataset.pp_dataset_path.stat().st_size > constants.MIN_CC_PP_DATASET_SIZE + toy_dataset.aux_handlers[ProtectionProfileDatasetHandler].process_dataset() + assert toy_dataset.aux_handlers[ProtectionProfileDatasetHandler].dset_path.exists() + assert ( + toy_dataset.aux_handlers[ProtectionProfileDatasetHandler].dset_path.stat().st_size + > constants.MIN_CC_PP_DATASET_SIZE + ) @pytest.mark.xfail(reason="May fail due to error on CC server") diff --git a/tests/cc/test_cc_maintenance_updates.py b/tests/cc/test_cc_maintenance_updates.py index 9c89c748..5a24af94 100644 --- a/tests/cc/test_cc_maintenance_updates.py +++ b/tests/cc/test_cc_maintenance_updates.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest import tests.data.cc.dataset -from sec_certs.dataset import CCDatasetMaintenanceUpdates +from sec_certs.dataset.cc import CCDatasetMaintenanceUpdates from sec_certs.sample.cc_maintenance_update import CCMaintenanceUpdate @@ -29,7 +29,7 @@ def test_methods_not_meant_to_be_implemented(): with pytest.raises(NotImplementedError): dset.analyze_certificates() with pytest.raises(NotImplementedError): - dset._compute_heuristics() + dset._compute_heuristics_body() with pytest.raises(NotImplementedError): dset.process_auxiliary_datasets() with pytest.raises(NotImplementedError): diff --git a/tests/cc/test_cc_schemes.py b/tests/cc/test_cc_schemes.py index afaeda9f..8808fff3 100644 --- a/tests/cc/test_cc_schemes.py +++ b/tests/cc/test_cc_schemes.py @@ -5,7 +5,9 @@ import pytest from requests import RequestException import sec_certs.sample.cc_scheme as CCSchemes +from sec_certs.dataset.auxiliary_dataset_handling import CCSchemeDatasetHandler from sec_certs.dataset.cc import CCDataset +from sec_certs.heuristics.cc import compute_scheme_data from sec_certs.model.cc_matching import CCSchemeMatcher from sec_certs.sample.cc import CCCertificate @@ -231,6 +233,7 @@ def test_matching(toy_dataset: CCDataset, canada_certified): def test_process_dataset(toy_dataset: CCDataset): - toy_dataset.auxiliary_datasets.scheme_dset = toy_dataset.process_schemes(True, only_schemes={"CA"}) - toy_dataset._compute_scheme_data() + toy_dataset.aux_handlers[CCSchemeDatasetHandler].only_schemes = {"CA"} # type: ignore + toy_dataset.aux_handlers[CCSchemeDatasetHandler].process_dataset() + compute_scheme_data(toy_dataset.aux_handlers[CCSchemeDatasetHandler].dset, toy_dataset.certs) assert toy_dataset["8f08cacb49a742fb"].heuristics.scheme_data is not None diff --git a/tests/fips/conftest.py b/tests/fips/conftest.py index f2da80c4..f9377c7d 100644 --- a/tests/fips/conftest.py +++ b/tests/fips/conftest.py @@ -4,6 +4,9 @@ import pytest import tests.data.fips.dataset from sec_certs.dataset import CPEDataset, CVEDataset, FIPSDataset +from sec_certs.dataset.auxiliary_dataset_handling import CPEDatasetHandler, CPEMatchDictHandler, CVEDatasetHandler +from sec_certs.heuristics.common import compute_cpe_heuristics, compute_related_cves, compute_transitive_vulnerabilities +from sec_certs.heuristics.fips import compute_references @pytest.fixture(scope="module") @@ -27,15 +30,30 @@ def processed_dataset( ] toy_dataset.certs = {x.dgst: x for x in tested_certs} + cpe_handler = CPEDatasetHandler(toy_dataset.auxiliary_datasets_dir) + cpe_handler.dset = cpe_dataset + cve_handler = CVEDatasetHandler(toy_dataset.auxiliary_datasets_dir) + cve_handler.dset = cve_dataset + cpe_match_dict_handler = CPEMatchDictHandler(toy_dataset.auxiliary_datasets_dir) + cpe_match_dict_handler.dset = {} + toy_dataset.aux_handlers = { + CPEDatasetHandler: cpe_handler, + CVEDatasetHandler: cve_handler, + CPEMatchDictHandler: cpe_match_dict_handler, + } + toy_dataset.download_all_artifacts() toy_dataset.convert_all_pdfs() toy_dataset.extract_data() - toy_dataset._compute_references(keep_unknowns=True) - toy_dataset.auxiliary_datasets.cpe_dset = cpe_dataset - toy_dataset.auxiliary_datasets.cve_dset = cve_dataset - toy_dataset.compute_cpe_heuristics() - toy_dataset.compute_related_cves() - toy_dataset._compute_transitive_vulnerabilities() + compute_cpe_heuristics(toy_dataset.aux_handlers[CPEDatasetHandler].dset, toy_dataset.certs.values()) + compute_related_cves( + toy_dataset.aux_handlers[CPEDatasetHandler].dset, + toy_dataset.aux_handlers[CVEDatasetHandler].dset, + toy_dataset.aux_handlers[CPEMatchDictHandler].dset, + toy_dataset.certs.values(), + ) + compute_references(toy_dataset.certs, keep_unknowns=True) + compute_transitive_vulnerabilities(toy_dataset.certs) return toy_dataset diff --git a/tests/fips/test_fips_analysis.py b/tests/fips/test_fips_analysis.py index 61f56848..54954c41 100644 --- a/tests/fips/test_fips_analysis.py +++ b/tests/fips/test_fips_analysis.py @@ -2,7 +2,9 @@ from __future__ import annotations import pytest +from sec_certs.dataset.auxiliary_dataset_handling import CPEDatasetHandler, CVEDatasetHandler from sec_certs.dataset.fips import FIPSDataset +from sec_certs.heuristics.common import compute_related_cves @pytest.mark.parametrize( @@ -104,11 +106,16 @@ def test_match_cpe(processed_dataset: FIPSDataset): def test_find_related_cves(processed_dataset: FIPSDataset): - assert processed_dataset.auxiliary_datasets.cve_dset - processed_dataset.auxiliary_datasets.cve_dset._cpe_uri_to_cve_ids_lookup[ + assert processed_dataset.aux_handlers[CVEDatasetHandler].dset + processed_dataset.aux_handlers[CVEDatasetHandler].dset._cpe_uri_to_cve_ids_lookup[ "cpe:2.3:o:redhat:enterprise_linux:7.1:*:*:*:*:*:*:*" ] = {"CVE-123456"} - processed_dataset.compute_related_cves() + compute_related_cves( + processed_dataset.aux_handlers[CPEDatasetHandler].dset, + processed_dataset.aux_handlers[CVEDatasetHandler].dset, + {}, + processed_dataset.certs.values(), + ) assert processed_dataset["2441"].heuristics.related_cves == {"CVE-123456"} @@ -117,7 +124,12 @@ def test_find_related_cves_criteria_configuration(processed_dataset: FIPSDataset "cpe:2.3:a:nalin_dahyabhai:vte:0.11.21:*:*:*:*:*:*:*", "cpe:2.3:a:gnome:gnome-terminal:2.2:*:*:*:*:*:*:*", } - processed_dataset.compute_related_cves() + compute_related_cves( + processed_dataset.aux_handlers[CPEDatasetHandler].dset, + processed_dataset.aux_handlers[CVEDatasetHandler].dset, + {}, + processed_dataset.certs.values(), + ) assert processed_dataset["2441"].heuristics.related_cves == {"CVE-2003-0070"} diff --git a/tests/fips/test_fips_dataset.py b/tests/fips/test_fips_dataset.py index 9b18b754..81ad019d 100644 --- a/tests/fips/test_fips_dataset.py +++ b/tests/fips/test_fips_dataset.py @@ -93,18 +93,18 @@ def test_download_and_convert_artifacts(toy_dataset: FIPSDataset, data_dir: Path toy_dataset.copy_dataset(tmp_dir) toy_dataset.download_all_artifacts() - if not crt.state.policy_download_ok or crt.state.module_download_ok: - pytest.xfail(reason="Fail due to error during download") + if not crt.state.policy_download_ok or not crt.state.module_download_ok: + pytest.xfail(reason="Fail due to error during download") - toy_dataset.convert_all_pdfs() + toy_dataset.convert_all_pdfs() - assert not crt.state.policy_convert_garbage - assert crt.state.policy_convert_ok - assert crt.state.policy_pdf_hash == "36b63890182f0aed29b305a0b4acc0d70b657262516f4be69138c70c2abdb1f1" - assert crt.state.policy_txt_path.exists() + assert not crt.state.policy_convert_garbage + assert crt.state.policy_convert_ok + assert crt.state.policy_pdf_hash == "36b63890182f0aed29b305a0b4acc0d70b657262516f4be69138c70c2abdb1f1" + assert crt.state.policy_txt_path.exists() - template_policy_txt_path = data_dir / "template_policy_184097a88a9b4ad9.txt" - assert abs(crt.state.policy_txt_path.stat().st_size - template_policy_txt_path.stat().st_size) < 1000 + template_policy_txt_path = data_dir / "template_policy_184097a88a9b4ad9.txt" + assert abs(crt.state.policy_txt_path.stat().st_size - template_policy_txt_path.stat().st_size) < 1000 def test_to_pandas(toy_dataset: FIPSDataset): diff --git a/tests/test_cve_matching.py b/tests/test_cve_matching.py index c7e7da28..ed7ecc17 100644 --- a/tests/test_cve_matching.py +++ b/tests/test_cve_matching.py @@ -4,7 +4,9 @@ import tempfile import pytest +from sec_certs.dataset.auxiliary_dataset_handling import CPEDatasetHandler, CPEMatchDictHandler, CVEDatasetHandler from sec_certs.dataset.cc import CCDataset +from sec_certs.heuristics.common import compute_cpe_heuristics, compute_related_cves @pytest.fixture(scope="module") @@ -12,11 +14,17 @@ def processed_cc_dataset() -> CCDataset: with tempfile.TemporaryDirectory() as tmp_dir: cc_dset = CCDataset(root_dir=tmp_dir) cc_dset.get_certs_from_web() - cc_dset._prepare_cpe_dataset() - cc_dset._prepare_cve_dataset() - cc_dset._prepare_cpe_match_dict() - cc_dset.compute_cpe_heuristics() - cc_dset.compute_related_cves() + cc_dset.aux_handlers[CPEDatasetHandler].process_dataset() + cc_dset.aux_handlers[CVEDatasetHandler].process_dataset() + cc_dset.aux_handlers[CPEMatchDictHandler].process_dataset() + + compute_cpe_heuristics(cc_dset.aux_handlers[CPEDatasetHandler].dset, cc_dset.certs.values()) + compute_related_cves( + cc_dset.aux_handlers[CPEDatasetHandler].dset, + cc_dset.aux_handlers[CVEDatasetHandler].dset, + cc_dset.aux_handlers[CPEMatchDictHandler].dset, + cc_dset.certs.values(), + ) return cc_dset |
