aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Janovsky2025-01-22 10:48:00 +0100
committerJ08nY2025-02-01 22:56:46 +0100
commit3c34bbe70f36eb2c5504be7ec81cfcb3fdf543c6 (patch)
tree70ed04f776c9dc745681b8079848e85ee0aaaa35
parent24187ac21e9ed59243cf7ca62224e2069f1e4c9b (diff)
downloadsec-certs-3c34bbe70f36eb2c5504be7ec81cfcb3fdf543c6.tar.gz
sec-certs-3c34bbe70f36eb2c5504be7ec81cfcb3fdf543c6.tar.zst
sec-certs-3c34bbe70f36eb2c5504be7ec81cfcb3fdf543c6.zip
refactor auxiliary dataset handling, heuristics computation
-rw-r--r--requirements/dev_requirements.txt23
-rw-r--r--requirements/requirements.txt13
-rw-r--r--requirements/test_requirements.txt13
-rw-r--r--src/sec_certs/dataset/auxiliary_dataset_handling.py252
-rw-r--r--src/sec_certs/dataset/cc.py314
-rw-r--r--src/sec_certs/dataset/cc_scheme.py8
-rw-r--r--src/sec_certs/dataset/cpe.py10
-rw-r--r--src/sec_certs/dataset/dataset.py376
-rw-r--r--src/sec_certs/dataset/fips.py119
-rw-r--r--src/sec_certs/dataset/protection_profile.py37
-rw-r--r--src/sec_certs/heuristics/cc.py86
-rw-r--r--src/sec_certs/heuristics/common.py132
-rw-r--r--src/sec_certs/heuristics/fips.py42
-rw-r--r--src/sec_certs/utils/label_studio_utils.py78
-rw-r--r--src/sec_certs/utils/nvd_dataset_builder.py12
-rw-r--r--tests/cc/test_cc_analysis.py48
-rw-r--r--tests/cc/test_cc_dataset.py10
-rw-r--r--tests/cc/test_cc_maintenance_updates.py4
-rw-r--r--tests/cc/test_cc_schemes.py7
-rw-r--r--tests/fips/conftest.py30
-rw-r--r--tests/fips/test_fips_analysis.py20
-rw-r--r--tests/fips/test_fips_dataset.py18
-rw-r--r--tests/test_cve_matching.py18
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