diff options
| author | Adam Janovsky | 2025-01-25 18:20:49 +0100 |
|---|---|---|
| committer | J08nY | 2025-02-01 22:57:08 +0100 |
| commit | 2500d9536beb658853cff3d0baad30b8f414a817 (patch) | |
| tree | 05bc8daaa46805eb90011054a6b868fc4557b4b9 | |
| parent | a4ba427b7224f97267f0e54fd4c58164cc0dc4cb (diff) | |
| download | sec-certs-2500d9536beb658853cff3d0baad30b8f414a817.tar.gz sec-certs-2500d9536beb658853cff3d0baad30b8f414a817.tar.zst sec-certs-2500d9536beb658853cff3d0baad30b8f414a817.zip | |
implement PP processing
24 files changed, 1303 insertions, 495 deletions
diff --git a/src/sec_certs/configuration.py b/src/sec_certs/configuration.py index 09e24539..031d2d07 100644 --- a/src/sec_certs/configuration.py +++ b/src/sec_certs/configuration.py @@ -54,6 +54,10 @@ class Configuration(BaseSettings): "https://sec-certs.org/cc/cc.tar.gz", description="URL from where to fetch the latest full archive of fully processed CC dataset.", ) + pp_latest_full_archive: AnyHttpUrl = Field( + "https://sec-certs.org/cc/pp.tar.gz", + description="URL from where to fetch the latest full archive of fully processed PP dataset.", + ) cc_maintenances_latest_snapshot: AnyHttpUrl = Field( "https://sec-certs.org/cc/maintenance_updates.json", description="URL from where to fetch the latest snapshot of CC maintenance updates", diff --git a/src/sec_certs/constants.py b/src/sec_certs/constants.py index d134b3fd..afa1969d 100644 --- a/src/sec_certs/constants.py +++ b/src/sec_certs/constants.py @@ -27,6 +27,44 @@ MIN_CC_PP_DATASET_SIZE = 2500000 CPE_VERSION_NA = "-" +CC_CAT_ABBREVIATIONS = [ + "AC", + "BD", + "BP", + "DP", + "DB", + "DD", + "IC", + "KM", + "MD", + "MF", + "NS", + "OS", + "OD", + "DG", + "TC", +] + +CC_CATEGORIES = [ + "Access Control Devices and Systems", + "Biometric Systems and Devices", + "Boundary Protection Devices and Systems", + "Data Protection", + "Databases", + "Detection Devices and Systems", + "ICs, Smart Cards and Smart Card-Related Devices and Systems", + "Key Management Systems", + "Mobility", + "Multi-Function Devices", + "Network and Network-Related Devices and Systems", + "Operating Systems", + "Other Devices and Systems", + "Products for Digital Signatures", + "Trusted Computing", +] + +CC_PORTAL_BASE_URL = "https://www.commoncriteriaportal.org" + RELEASE_CANDIDATE_REGEX: re.Pattern = re.compile(r"rc\d{0,2}$", re.IGNORECASE) FIPS_BASE_URL = "https://csrc.nist.gov" diff --git a/src/sec_certs/dataset/auxiliary_dataset_handling.py b/src/sec_certs/dataset/auxiliary_dataset_handling.py index afd2b820..829ecf07 100644 --- a/src/sec_certs/dataset/auxiliary_dataset_handling.py +++ b/src/sec_certs/dataset/auxiliary_dataset_handling.py @@ -6,7 +6,7 @@ import tempfile from abc import ABC, abstractmethod from collections.abc import Iterable from pathlib import Path -from typing import Any +from typing import Any, ClassVar from sec_certs import constants from sec_certs.configuration import config @@ -14,7 +14,6 @@ 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 @@ -25,17 +24,25 @@ logger = logging.getLogger(__name__) class AuxiliaryDatasetHandler(ABC): - def __init__(self, root_dir: str | Path) -> None: - self.root_dir = Path(root_dir) + RELATIVE_DIR: ClassVar[str | None] = None + + def __init__(self, aux_datasets_dir: str | Path) -> None: + self.aux_datasets_dir = Path(aux_datasets_dir) self.dset: Any @property + def root_dir(self) -> Path: + if self.RELATIVE_DIR: + return self.aux_datasets_dir / Path(self.RELATIVE_DIR) + return self.aux_datasets_dir + + @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 set_local_paths(self, aux_datasets_dir: str | Path) -> None: + self.aux_datasets_dir = Path(aux_datasets_dir) def process_dataset(self, download_fresh: bool = False) -> None: self.root_dir.mkdir(parents=True, exist_ok=True) @@ -170,8 +177,12 @@ class FIPSAlgorithmDatasetHandler(AuxiliaryDatasetHandler): 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) + def __init__( + self, + aux_datasets_dir: str | Path = constants.DUMMY_NONEXISTING_PATH, + only_schemes: set[str] | None = None, + ): + self.aux_datasets_dir = Path(aux_datasets_dir) self.only_schemes = only_schemes self.dset: Any @@ -192,25 +203,25 @@ class CCSchemeDatasetHandler(AuxiliaryDatasetHandler): class CCMaintenanceUpdateDatasetHandler(AuxiliaryDatasetHandler): + RELATIVE_DIR: ClassVar[str] = "maintenances" + def __init__( - self, root_dir: str | Path = constants.DUMMY_NONEXISTING_PATH, certs_with_updates: Iterable[CCCertificate] = [] + self, + aux_datasets_dir: str | Path = constants.DUMMY_NONEXISTING_PATH, + certs_with_updates: Iterable[CCCertificate] = [], ) -> None: - self.root_dir = Path(root_dir) + self.aux_datasets_dir = Path(aux_datasets_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" + return self.root_dir / "maintenance_updates.json" def load_dataset(self) -> None: from sec_certs.dataset.cc import CCDatasetMaintenanceUpdates - self.dset = CCDatasetMaintenanceUpdates.from_json(self._dset_json_path) + self.dset = CCDatasetMaintenanceUpdates.from_json(self.dset_path) @staged(logger, "Processing CC Maintenance updates") def _process_dataset_body(self, download_fresh: bool = False): @@ -223,30 +234,40 @@ class CCMaintenanceUpdateDatasetHandler(AuxiliaryDatasetHandler): ) ) self.dset = CCDatasetMaintenanceUpdates( - {x.dgst: x for x in updates}, root_dir=self.dset_path, name="maintenance_updates" + {x.dgst: x for x in updates}, root_dir=self.dset_path.parent, 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() + else: + self.dset = CCDatasetMaintenanceUpdates.from_json(self.dset_path) class ProtectionProfileDatasetHandler(AuxiliaryDatasetHandler): + RELATIVE_DIR: ClassVar[str] = "protection_profiles" + + def __init__(self, aux_datasets_dir: str | Path = constants.DUMMY_NONEXISTING_PATH): + self.aux_datasets_dir = Path(aux_datasets_dir) + @property def dset_path(self) -> Path: return self.root_dir / "pp.json" def load_dataset(self) -> None: + from sec_certs.dataset.protection_profile import ProtectionProfileDataset + self.dset = ProtectionProfileDataset.from_json(self.dset_path) @staged(logger, "Processing Protection profiles") def _process_dataset_body(self, download_fresh: bool = False): + from sec_certs.dataset.protection_profile import ProtectionProfileDataset + if not self.dset_path.exists() or download_fresh: - self.dset = ProtectionProfileDataset.from_web(self.dset_path) + self.dset_path.parent.mkdir(exist_ok=True, parents=True) + self.dset = ProtectionProfileDataset(root_dir=self.dset_path.parent) + self.dset.get_certs_from_web() + self.dset.download_all_artifacts() + self.dset.convert_all_pdfs() + self.dset.analyze_certificates() 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 4d09106d..71c691fc 100644 --- a/src/sec_certs/dataset/cc.py +++ b/src/sec_certs/dataset/cc.py @@ -12,7 +12,6 @@ import numpy as np import pandas as pd 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.auxiliary_dataset_handling import ( @@ -27,6 +26,7 @@ from sec_certs.dataset.auxiliary_dataset_handling import ( from sec_certs.dataset.dataset import Dataset, logger from sec_certs.heuristics.cc import ( compute_cert_labs, + compute_eals, compute_normalized_cert_ids, compute_references, compute_sars, @@ -34,11 +34,8 @@ from sec_certs.heuristics.cc import ( 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_maintenance_update import CCMaintenanceUpdate -from sec_certs.sample.cc_scheme import EntryType -from sec_certs.sample.protection_profile import ProtectionProfile from sec_certs.serialization.json import ComplexSerializableType, serialize from sec_certs.utils import helpers, sanitization from sec_certs.utils import parallel_processing as cert_processing @@ -75,10 +72,10 @@ class CCDataset(Dataset[CCCertificate], ComplexSerializableType): 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.auxiliary_datasets_dir / "protection_profiles" ) self.aux_handlers[CCMaintenanceUpdateDatasetHandler] = CCMaintenanceUpdateDatasetHandler( - self.auxiliary_datasets_dir + self.auxiliary_datasets_dir / "maintenances" ) def to_pandas(self) -> pd.DataFrame: @@ -275,12 +272,34 @@ class CCDataset(Dataset[CCCertificate], ComplexSerializableType): 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 process_auxiliary_datasets( + self, + download_fresh: bool = False, + processed_pp_dataset_root_dir: Path | None = None, + skip_schemes: bool = False, + **kwargs, + ) -> None: + if CCMaintenanceUpdateDatasetHandler in self.aux_handlers: + self.aux_handlers[CCMaintenanceUpdateDatasetHandler].certs_with_updates = [ # type: ignore + x for x in self if x.maintenance_updates + ] + if CCSchemeDatasetHandler in self.aux_handlers: + self.aux_handlers[CCSchemeDatasetHandler].only_schemes = {x.scheme for x in self} # type: ignore + + if processed_pp_dataset_root_dir: + if self.aux_handlers[ProtectionProfileDatasetHandler].root_dir.exists(): + logger.warning( + f"Overwriting PP Dataset at {self.aux_handlers[ProtectionProfileDatasetHandler].root_dir} with dataset from {processed_pp_dataset_root_dir}." + ) + shutil.copytree( + processed_pp_dataset_root_dir, + self.aux_handlers[ProtectionProfileDatasetHandler].root_dir, + dirs_exist_ok=True, + ) + + if skip_schemes: + del self.aux_handlers[CCSchemeDatasetHandler] + super().process_auxiliary_datasets(download_fresh, **kwargs) def _merge_certs(self, certs: dict[str, CCCertificate], cert_source: str | None = None) -> None: """ @@ -449,13 +468,6 @@ class CCDataset(Dataset[CCCertificate], ComplexSerializableType): df_base = df_base.drop_duplicates(subset=["dgst"]) df_main = df_main.drop_duplicates() - profiles = { - x.dgst: { - ProtectionProfile(pp_name=y, pp_eal=None) - for y in sec_certs.utils.sanitization.sanitize_protection_profiles(x.protection_profiles) - } - for x in df_base.itertuples() - } updates: dict[str, set] = {x.dgst: set() for x in df_base.itertuples()} for x in df_main.itertuples(): updates[x.dgst].add( @@ -481,7 +493,7 @@ class CCDataset(Dataset[CCCertificate], ComplexSerializableType): x.st_link, None, None, - profiles.get(x.dgst, None), + None, updates.get(x.dgst, None), None, None, @@ -526,9 +538,9 @@ class CCDataset(Dataset[CCCertificate], ComplexSerializableType): ) -> dict[str, CCCertificate]: tables = soup.find_all("table", id=table_id) - if not len(tables) <= 1: + if len(tables) > 1: raise ValueError( - f'The "{file.name}" was expected to contain <1 <table> element. Instead, it contains: {len(tables)} <table> elements.' + f'The "{file.name}" was expected to contain 0-1 <table> element. Instead, it contains: {len(tables)} <table> elements.' ) if not tables: @@ -557,40 +569,8 @@ class CCDataset(Dataset[CCCertificate], ComplexSerializableType): cert_status = "active" if "active" in str(file) else "archived" - cc_cat_abbreviations = [ - "AC", - "BP", - "DP", - "DB", - "DD", - "IC", - "KM", - "MD", - "MF", - "NS", - "OS", - "OD", - "DG", - "TC", - ] - cc_table_ids = ["tbl" + x for x in cc_cat_abbreviations] - cc_categories = [ - "Access Control Devices and Systems", - "Boundary Protection Devices and Systems", - "Data Protection", - "Databases", - "Detection Devices and Systems", - "ICs, Smart Cards and Smart Card-Related Devices and Systems", - "Key Management Systems", - "Mobility", - "Multi-Function Devices", - "Network and Network-Related Devices and Systems", - "Operating Systems", - "Other Devices and Systems", - "Products for Digital Signatures", - "Trusted Computing", - ] - cat_dict = dict(zip(cc_table_ids, cc_categories)) + cc_table_ids = ["tbl" + x for x in constants.CC_CAT_ABBREVIATIONS] + cat_dict = dict(zip(cc_table_ids, constants.CC_CATEGORIES)) with file.open("r") as handle: soup = BeautifulSoup(handle, "html5lib") @@ -808,31 +788,8 @@ class CCDataset(Dataset[CCCertificate], ComplexSerializableType): self._extract_pdf_frontpage() self._extract_pdf_keywords() - @staged( - logger, - "Computing heuristics: Deriving information about laboratories involved in certification.", - ) - def _compute_cert_labs(self) -> None: - certs_to_process = [x for x in self if x.state.report.is_ok_to_analyze()] - for cert in certs_to_process: - cert.compute_heuristics_cert_lab() - - @staged(logger, "Computing heuristics: Matching scheme data.") - def _compute_scheme_data(self): - 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 - def _compute_heuristics_body(self, skip_schemes: bool = False) -> None: - link_to_protection_profiles(self.aux_handlers[ProtectionProfileDatasetHandler].dset, self.certs.values()) + link_to_protection_profiles(self.certs.values(), self.aux_handlers[ProtectionProfileDatasetHandler].dset) compute_cpe_heuristics(self.aux_handlers[CPEDatasetHandler].dset, self.certs.values()) compute_related_cves( self.aux_handlers[CPEDatasetHandler].dset, @@ -848,6 +805,7 @@ class CCDataset(Dataset[CCCertificate], ComplexSerializableType): compute_scheme_data(self.aux_handlers[CCSchemeDatasetHandler].dset, self.certs) compute_cert_labs(self.certs.values()) + compute_eals(self.certs.values(), self.aux_handlers[ProtectionProfileDatasetHandler].dset) compute_sars(self.certs.values()) @@ -867,6 +825,7 @@ class CCDatasetMaintenanceUpdates(CCDataset, ComplexSerializableType): state: CCDataset.DatasetInternalState | None = None, ): super().__init__(certs, root_dir, name, description, state) # type: ignore + self.aux_handlers = {} self.state.meta_sources_parsed = True @property @@ -882,7 +841,13 @@ class CCDatasetMaintenanceUpdates(CCDataset, ComplexSerializableType): def compute_related_cves(self) -> None: raise NotImplementedError - def process_auxiliary_datasets(self, download_fresh: bool = False) -> None: + def process_auxiliary_datasets( + self, + download_fresh: bool = False, + processed_pp_dataset_root_dir: Path | None = None, + skip_schemes: bool = False, + **kwargs, + ) -> None: raise NotImplementedError def analyze_certificates(self) -> None: diff --git a/src/sec_certs/dataset/dataset.py b/src/sec_certs/dataset/dataset.py index 44f47494..9659294e 100644 --- a/src/sec_certs/dataset/dataset.py +++ b/src/sec_certs/dataset/dataset.py @@ -60,8 +60,8 @@ class Dataset(Generic[CertSubType], ComplexSerializableType, ABC): 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() - self.root_dir = Path(root_dir) self.aux_handlers = aux_handlers + self.root_dir = Path(root_dir) @property def root_dir(self) -> Path: @@ -278,7 +278,7 @@ class Dataset(Generic[CertSubType], ComplexSerializableType, ABC): @staged(logger, "Processing auxiliary datasets") @serialize - def process_auxiliary_datasets(self, download_fresh: bool = False) -> None: + def process_auxiliary_datasets(self, download_fresh: bool = False, **kwargs) -> None: """ Processes all auxiliary datasets (CPE, CVE, ...) that are required during computation. """ @@ -288,9 +288,15 @@ class Dataset(Generic[CertSubType], ComplexSerializableType, ABC): self.state.auxiliary_datasets_processed = True def load_auxiliary_datasets(self) -> None: + logger.info("Loading auxiliary datasets into memory.") for handler in self.aux_handlers.values(): if not hasattr(handler, "dset"): - handler.load_dataset() + try: + handler.load_dataset() + except Exception: + logger.warning( + f"Failed to load auxiliary dataset bound to {handler}, some functionality may not work." + ) @serialize def download_all_artifacts(self, fresh: bool = True) -> None: diff --git a/src/sec_certs/dataset/protection_profile.py b/src/sec_certs/dataset/protection_profile.py index c3440c56..bb3ab35e 100644 --- a/src/sec_certs/dataset/protection_profile.py +++ b/src/sec_certs/dataset/protection_profile.py @@ -1,76 +1,396 @@ -from __future__ import annotations - -import json -import logging -import tempfile -from dataclasses import dataclass +import shutil +from datetime import datetime from pathlib import Path +from typing import ClassVar, Literal + +from bs4 import BeautifulSoup from sec_certs import constants from sec_certs.configuration import config -from sec_certs.dataset.json_path_dataset import JSONPathDataset +from sec_certs.dataset.auxiliary_dataset_handling import AuxiliaryDatasetHandler +from sec_certs.dataset.dataset import Dataset, logger from sec_certs.sample.protection_profile import ProtectionProfile +from sec_certs.serialization.json import ComplexSerializableType, serialize from sec_certs.utils import helpers +from sec_certs.utils import parallel_processing as cert_processing +from sec_certs.utils.profiling import staged + + +class ProtectionProfileDataset(Dataset[ProtectionProfile], ComplexSerializableType): + def __init__( + self, + certs: dict[str, ProtectionProfile] = {}, + root_dir: str | Path = constants.DUMMY_NONEXISTING_PATH, + name: str | None = None, + description: str = "", + state: Dataset.DatasetInternalState | None = None, + aux_handlers: dict[type[AuxiliaryDatasetHandler], AuxiliaryDatasetHandler] = {}, + ): + self.certs = certs + self.timestamp = datetime.now() + self.sha256_digest = "not implemented" + 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.aux_handlers = aux_handlers + self.root_dir = Path(root_dir) -logger = logging.getLogger(__name__) + @property + def json_path(self) -> Path: + return self.root_dir / "pp.json" + @property + def reports_dir(self) -> Path: + return self.root_dir / "reports" -@dataclass -class ProtectionProfileDataset(JSONPathDataset): - pps: dict[tuple[str, str | None], ProtectionProfile] - _json_path: Path + @property + def pps_dir(self) -> Path: + return self.root_dir / "pps" - def __init__( + @property + def reports_pdf_dir(self) -> Path: + return self.reports_dir / "pdf" + + @property + def reports_txt_dir(self) -> Path: + return self.reports_dir / "txt" + + @property + def pps_pdf_dir(self) -> Path: + return self.pps_dir / "pdf" + + @property + def pps_txt_dir(self) -> Path: + return self.pps_dir / "txt" + + @classmethod + def from_web_latest(cls, path: str | Path | None = None, artifacts: bool = False) -> "ProtectionProfileDataset": + return cls.from_web( + str(config.pp_latest_full_archive), str(config.pp_latest_snapshot), "Downloading PP", path, False, artifacts + ) + + def _compute_heuristics_body(self): + logger.info("Protection profile dataset has no heuristics to compute, skipping.") + + @property + def web_dir(self) -> Path: + return self.root_dir / "web" + + BASE_URL: ClassVar[str] = "https://www.commoncriteriaportal.org" + HTML_URL = { + "pp_active.html": BASE_URL + "/pps/index.cfm", + "pp_archived.html": BASE_URL + "/pps/index.cfm?archived=1", + "pp_collaborative.html": BASE_URL + "/pps/collaborativePP.cfm?cpp=1", + } + CSV_URL = {"pp_active.csv": BASE_URL + "/pps/pps.csv", "pp_archived.csv": BASE_URL + "/pps/pps-archived.csv"} + + @property + def active_html_tuples(self) -> list[tuple[str, Path]]: + return [(x, self.web_dir / y) for y, x in self.HTML_URL.items() if "active" in y] + + @property + def archived_html_tuples(self) -> list[tuple[str, Path]]: + return [(x, self.web_dir / y) for y, x in self.HTML_URL.items() if "archived" in y] + + @property + def collaborative_html_tuples(self) -> list[tuple[str, Path]]: + return [(x, self.web_dir / y) for y, x in self.HTML_URL.items() if "collaborative" in y] + + @property + def active_csv_tuples(self) -> list[tuple[str, Path]]: + return [(x, self.web_dir / y) for y, x in self.CSV_URL.items() if "active" in y] + + @property + def archived_csv_tuples(self) -> list[tuple[str, Path]]: + return [(x, self.web_dir / y) for y, x in self.CSV_URL.items() if "archived" in y] + + @serialize + @staged(logger, "Downloading and processing CSV and HTML files of certificates.") + def get_certs_from_web( self, - pps: dict[tuple[str, str | None], ProtectionProfile], - json_path: str | Path = constants.DUMMY_NONEXISTING_PATH, + to_download: bool = True, + keep_metadata: bool = True, + get_active: bool = True, + get_archived: bool = True, + get_collaborative: bool = True, ) -> None: - self.pps = pps - self.json_path = Path(json_path) + if to_download: + self._download_csv_html_resources(get_active, get_archived, get_collaborative) - def __iter__(self): - yield from self.pps.values() + # TODO: Implement CSV processing if needed. If not, delete this and the corresponding methods. Also get rid of URLs. + # logger.info("Adding PPs from CSV to ProtectionProfileDataset.") + # csv_certs = self._get_all_certs_from_csv(get_active, get_archived) + # self._merge_certs(csv_certs, cert_source="csv") - def __getitem__(self, item: tuple[str, str | None]) -> ProtectionProfile: - return self.pps.__getitem__(item) + logger.info("Adding HTML certificates to ProtectionProfile dataset.") + html_certs = self._get_all_certs_from_html(get_active, get_archived, get_collaborative) + self._merge_certs(html_certs, cert_source="html") - def __setitem__(self, key: tuple[str, str | None], value: ProtectionProfile): - self.pps.__setitem__(key, value) + logger.info(f"The resulting dataset has {len(self)} certificates.") - def __contains__(self, key): - return key in self.pps + if not keep_metadata: + shutil.rmtree(self.web_dir) - def __len__(self) -> int: - return len(self.pps) + self._set_local_paths() + self.state.meta_sources_parsed = True - @classmethod - 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()] + def _merge_certs(self, certs: dict[str, ProtectionProfile], cert_source: str | None = None) -> None: + """ + Merges dictionary of certificates into the dataset. Assuming they all are CommonCriteria certificates + """ + new_certs = {x.dgst: x for x in certs.values() if x not in self} + certs_to_merge = [x for x in certs.values() if x in self] + self.certs.update(new_certs) - dct = {} - for item in pps: - if (item.pp_name, item.pp_link) in dct: - logger.warning(f"Duplicate entry in PP dataset: {(item.pp_name, item.pp_link)}") - dct[(item.pp_name, item.pp_link)] = item + for crt in certs_to_merge: + self[crt.dgst].merge(crt, cert_source) - dset = cls(dct) - dset.json_path = Path(input_path) + logger.info(f"Added {len(new_certs)} new and merged further {len(certs_to_merge)} certificates to the dataset.") - return dset + def _get_all_certs_from_html( + self, get_active: bool = True, get_archived: bool = True, get_collaborative: bool = True + ) -> dict[str, ProtectionProfile]: + html_sources = [] + if get_active: + html_sources.extend([x for x in self.HTML_URL if "active" in x]) + if get_archived: + html_sources.extend([x for x in self.HTML_URL if "archived" in x]) + if get_collaborative: + html_sources.extend([x for x in self.HTML_URL if "collaborative" in x]) - @classmethod - def from_web(cls, store_dataset_path: Path | None = None): - logger.info(f"Downloading static PP dataset from: {config.pp_latest_snapshot}") - if not store_dataset_path: - tmp = tempfile.TemporaryDirectory() - store_dataset_path = Path(tmp.name) / "pp_dataset.json" + new_certs = {} + for file in html_sources: + partial_certs = self._parse_single_html(self.web_dir / file) + logger.info(f"Parsed {len(partial_certs)} protection profiles from: {file}.") + new_certs.update(partial_certs) + return new_certs + + def _get_all_certs_from_csv( + self, get_active: bool = True, get_archived: bool = True + ) -> dict[str, ProtectionProfile]: + csv_sources = list(self.CSV_URL.keys()) + csv_sources = [x for x in csv_sources if "active" not in x or get_active] + csv_sources = [x for x in csv_sources if "archived" not in x or get_archived] + + new_certs = {} + for file in csv_sources: + partial_certs = self._parse_single_csv(self.web_dir / file) + logger.info(f"Parsed {len(partial_certs)} certificates from: {file}") + new_certs.update(partial_certs) + return new_certs + + def _download_csv_html_resources( + self, get_active: bool = True, get_archived: bool = True, get_collaborative: bool = True + ) -> None: + self.web_dir.mkdir(parents=True, exist_ok=True) + html_items = [] + csv_items = [] + if get_active: + html_items.extend(self.active_html_tuples) + csv_items.extend(self.active_csv_tuples) + if get_archived: + html_items.extend(self.archived_html_tuples) + html_items.extend(self.archived_csv_tuples) + if get_collaborative: + html_items.extend(self.collaborative_html_tuples) + + html_urls, html_paths = [x[0] for x in html_items], [x[1] for x in html_items] + csv_urls, csv_paths = [x[0] for x in csv_items], [x[1] for x in csv_items] + + logger.info("Downloading required csv and html files.") + helpers.download_parallel(html_urls, html_paths) + helpers.download_parallel(csv_urls, csv_paths) + + @staticmethod + def _parse_single_csv(file: Path) -> dict[str, ProtectionProfile]: + return {} + + @staticmethod + def _parse_single_html(file: Path) -> dict[str, ProtectionProfile]: + def _parse_table( + soup: BeautifulSoup, + cert_status: Literal["active", "archived"], + table_id: str, + category_string: str, + is_collaborative: bool, + ) -> dict[str, ProtectionProfile]: + tables = soup.find_all("table", id=table_id) + if len(tables) > 1: + raise ValueError( + f'The "{file.name}" was expected to contain 0-1 <table> element. Instead, it contains: {len(tables)} <table> elements.' + ) + + if not tables: + return {} + + body = list(tables[0].find_all("tr"))[1:] + try: + table_certs = { + x.dgst: x + for x in [ + ProtectionProfile.from_html_row(row, cert_status, category_string, is_collaborative) + for row in body + ] + } + except ValueError as e: + raise ValueError(f"Bad html file: {file.name} ({str(e)})") from e + return table_certs + + cert_status: Literal["active", "archived"] = "active" if "active" in file.name else "archived" + is_collaborative = "collaborative" in file.name + cc_table_ids = ["tbl" + x for x in constants.CC_CAT_ABBREVIATIONS] + if is_collaborative: + cc_table_ids = [x + "1" for x in cc_table_ids] + cat_dict = dict(zip(cc_table_ids, constants.CC_CATEGORIES)) + + with file.open("r") as handle: + soup = BeautifulSoup(handle, "html5lib") + + certs = {} + for key, val in cat_dict.items(): + certs.update(_parse_table(soup, cert_status, key, val, is_collaborative)) + + return certs + + def _convert_all_pdfs_body(self, fresh=True): + self._convert_reports_to_txt(fresh) + self._convert_pps_to_txt(fresh) + + @staged(logger, "Converting PDFs of PP certification reports to text.") + def _convert_reports_to_txt(self, fresh: bool = True): + self.reports_txt_dir.mkdir(parents=True, exist_ok=True) + certs_to_process = [x for x in self if x.state.report.is_ok_to_convert(fresh)] + + if not fresh and certs_to_process: + logger.info( + f"Converting {len(certs_to_process)} PDFs of PP certification reports to text for which previous conversion failed." + ) + + cert_processing.process_parallel( + ProtectionProfile.convert_report_pdf, + certs_to_process, + progress_bar_desc="Converting PDFs of PP certification reports to text.", + ) + + @staged(logger, "Converting PDFs of actual Protection Profiles to text.") + def _convert_pps_to_txt(self, fresh: bool = True): + self.pps_txt_dir.mkdir(parents=True, exist_ok=True) + certs_to_process = [x for x in self if x.state.pp.is_ok_to_convert(fresh)] + + if not fresh and certs_to_process: + logger.info( + f"Converting {len(certs_to_process)} PDFs of actual Protection Profiles to text for which previous conversion failed." + ) + + cert_processing.process_parallel( + ProtectionProfile.convert_pp_pdf, + certs_to_process, + progress_bar_desc="Converting PDFs of actual Protection Profiles to text.", + ) + + def _download_all_artifacts_body(self, fresh=True): + self._download_reports(fresh) + self._download_pps(fresh) + + @staged(logger, "Downloading PDFs of PP certification reports.") + def _download_reports(self, fresh: bool = True): + self.reports_pdf_dir.mkdir(parents=True, exist_ok=True) + certs_to_process = [x for x in self if x.state.report.is_ok_to_download(fresh) and x.web_data.report_link] + + if not fresh and certs_to_process: + logger.info( + f"Downloading {len(certs_to_process)} PDFs of PP certification reports for which previous download failed." + ) + + cert_processing.process_parallel( + ProtectionProfile.download_pdf_report, + certs_to_process, + progress_bar_desc="Downloading PDFs of PP certification reports.", + ) + + @staged(logger, "Downloading PDFs of actual Protection Profiles.") + def _download_pps(self, fresh: bool = True): + self.pps_pdf_dir.mkdir(parents=True, exist_ok=True) + certs_to_process = [x for x in self if x.state.pp.is_ok_to_download(fresh) and x.web_data.pp_link] + + if not fresh and certs_to_process: + logger.info( + f"Downloading {len(certs_to_process)} PDFs of actual Protection Profiles for which previous download failed." + ) + + cert_processing.process_parallel( + ProtectionProfile.download_pdf_pp, + certs_to_process, + progress_bar_desc="Downloading PDFs of actual Protection Profiles.", + ) + + def extract_data(self): + logger.info("Extracting various data from certification artifacts.") + self._extract_pdf_metadata() + self._extract_pdf_keywords() + + @staged(logger, "Extracting metadata from certification artifacts.") + def _extract_pdf_metadata(self): + self._extract_report_metadata() + self._extract_pp_metadata() + + @staged(logger, "Extracting keywords from certification artifacts.") + def _extract_pdf_keywords(self): + self._extract_report_keywords() + self._extract_pp_keywords() + + def _extract_report_metadata(self): + certs_to_process = [x for x in self if x.state.report.is_ok_to_analyze()] + processed_certs = cert_processing.process_parallel( + ProtectionProfile.extract_report_pdf_metadata, + certs_to_process, + use_threading=False, + progress_bar_desc="Extracting metadata from PP certification reports.", + ) + self.update_with_certs(processed_certs) + + def _extract_pp_metadata(self): + certs_to_process = [x for x in self if x.state.pp.is_ok_to_analyze()] + processed_certs = cert_processing.process_parallel( + ProtectionProfile.extract_pp_pdf_metadata, + certs_to_process, + use_threading=False, + progress_bar_desc="Extracting metadata from actual Protection Profiles.", + ) + self.update_with_certs(processed_certs) + + def _extract_report_keywords(self): + certs_to_process = [x for x in self if x.state.report.is_ok_to_analyze()] + processed_certs = cert_processing.process_parallel( + ProtectionProfile.extract_report_pdf_keywords, + certs_to_process, + use_threading=False, + progress_bar_desc="Extracting keywords from PP certification reports.", + ) + self.update_with_certs(processed_certs) + + def _extract_pp_keywords(self): + certs_to_process = [x for x in self if x.state.pp.is_ok_to_analyze()] + processed_certs = cert_processing.process_parallel( + ProtectionProfile.extract_pp_pdf_keywords, + certs_to_process, + use_threading=False, + progress_bar_desc="Extracting keywords from actual Protection Profiles.", + ) + self.update_with_certs(processed_certs) + + def _set_local_paths(self): + super()._set_local_paths() - helpers.download_file(config.pp_latest_snapshot, store_dataset_path) - obj = cls.from_json(store_dataset_path) + for cert in self: + cert.set_local_paths(self.reports_pdf_dir, self.pps_pdf_dir, self.reports_txt_dir, self.pps_txt_dir) - if not store_dataset_path: - tmp.cleanup() + def process_auxiliary_datasets(self) -> None: + logger.info("Protection Profile dataset has no auxiliary datasets to process, skipping.") + self.state.auxiliary_datasets_processed = True - return obj + def get_pp_by_pp_link(self, pp_link: str) -> ProtectionProfile | None: + for pp in self: + if pp.web_data.pp_link == pp_link: + return pp + return None diff --git a/src/sec_certs/heuristics/cc.py b/src/sec_certs/heuristics/cc.py index b646a085..435c69ed 100644 --- a/src/sec_certs/heuristics/cc.py +++ b/src/sec_certs/heuristics/cc.py @@ -1,6 +1,8 @@ import logging +import re from collections.abc import Iterable +from sec_certs.cert_rules import security_level_csv_scan from sec_certs.dataset.cc_scheme import CCSchemeDataset from sec_certs.dataset.protection_profile import ProtectionProfileDataset from sec_certs.model.cc_matching import CCSchemeMatcher @@ -9,17 +11,25 @@ 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.helpers import choose_lowest_eal 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: +def link_to_protection_profiles( + certs: Iterable[CCCertificate], + pp_dset: ProtectionProfileDataset, +) -> 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} + if cert.protection_profile_links: + pps = [pp_dset.get_pp_by_pp_link(x) for x in cert.protection_profile_links] + pp_digests = {x.dgst for x in pps if x} + cert.heuristics.protection_profiles = pp_digests if pp_digests else None + logger.info( + f"Linked {len([x for x in certs if x.heuristics.protection_profiles])} certificates to their protection profiles." + ) @staged(logger, "Computing heuristics: references between certificates.") @@ -84,3 +94,24 @@ def compute_sars(certs: Iterable[CCCertificate]) -> None: transformer = SARTransformer().fit(certs) for cert in certs: cert.heuristics.extracted_sars = transformer.transform_single_cert(cert) + + +@staged(logger, "Computing heuristics: EALs") +def compute_eals(certs: Iterable[CCCertificate], pp_dataset: ProtectionProfileDataset) -> None: + def compute_cert_eal(cert: CCCertificate) -> str | None: + res = [x for x in cert.security_level if re.match(security_level_csv_scan, x)] + if res and len(res) == 1: + return res[0] + elif res and len(res) > 1: + raise ValueError(f"Expected single EAL in security_level field, got: {res}") + else: + if cert.heuristics.protection_profiles: + eals: set[str] = { + eal for x in cert.heuristics.protection_profiles if (eal := pp_dataset[x].web_data.eal) is not None + } + return choose_lowest_eal(eals) + else: + return None + + for cert in certs: + cert.heuristics.eal = compute_cert_eal(cert) diff --git a/src/sec_certs/sample/cc.py b/src/sec_certs/sample/cc.py index a9aa2262..3069ae6a 100644 --- a/src/sec_certs/sample/cc.py +++ b/src/sec_certs/sample/cc.py @@ -17,13 +17,13 @@ from bs4 import Tag import sec_certs.utils.extract import sec_certs.utils.pdf from sec_certs import constants -from sec_certs.cert_rules import SARS_IMPLIED_FROM_EAL, cc_rules, rules, security_level_csv_scan +from sec_certs.cert_rules import SARS_IMPLIED_FROM_EAL, cc_rules, rules from sec_certs.configuration import config from sec_certs.sample.cc_certificate_id import CertificateId, canonicalize, schemes from sec_certs.sample.certificate import Certificate, References, logger from sec_certs.sample.certificate import Heuristics as BaseHeuristics from sec_certs.sample.certificate import PdfData as BasePdfData -from sec_certs.sample.protection_profile import ProtectionProfile +from sec_certs.sample.document_state import DocumentState from sec_certs.sample.sar import SAR from sec_certs.serialization.json import ComplexSerializableType from sec_certs.serialization.pandas import PandasSerializableType @@ -43,8 +43,6 @@ class CCCertificate( the certificate can handle itself. `CCDataset` class then instrument this functionality. """ - cc_url = "https://www.commoncriteriaportal.org" - @dataclass(eq=True, frozen=True) class MaintenanceReport(ComplexSerializableType): """ @@ -76,86 +74,15 @@ class CCCertificate( return self.maintenance_date < other.maintenance_date @dataclass - class DocumentState(ComplexSerializableType): - download_ok: bool = False # Whether download went OK - convert_garbage: bool = False # Whether initial conversion resulted in garbage - convert_ok: bool = False # Whether overall conversion went OK (either pdftotext or via OCR) - extract_ok: bool = False # Whether extraction went OK - - pdf_hash: str | None = None - txt_hash: str | None = None - - _pdf_path: Path | None = None - _txt_path: Path | None = None - - def is_ok_to_download(self, fresh: bool = True) -> bool: - return True if fresh else not self.download_ok - - def is_ok_to_convert(self, fresh: bool = True) -> bool: - return self.download_ok if fresh else self.download_ok and not self.convert_ok - - def is_ok_to_analyze(self, fresh: bool = True) -> bool: - if fresh: - return self.download_ok and self.convert_ok - else: - return self.download_ok and self.convert_ok and not self.extract_ok - - @property - def pdf_path(self) -> Path: - if not self._pdf_path: - raise ValueError(f"pdf_path not set on {type(self)}") - return self._pdf_path - - @pdf_path.setter - def pdf_path(self, pth: str | Path | None) -> None: - self._pdf_path = Path(pth) if pth else None - - @property - def txt_path(self) -> Path: - if not self._txt_path: - raise ValueError(f"txt_path not set on {type(self)}") - return self._txt_path - - @txt_path.setter - def txt_path(self, pth: str | Path | None) -> None: - self._txt_path = Path(pth) if pth else None - - @property - def serialized_attributes(self) -> list[str]: - return [ - "download_ok", - "convert_garbage", - "convert_ok", - "extract_ok", - "pdf_hash", - "txt_hash", - ] - - @dataclass(init=False) class InternalState(ComplexSerializableType): """ Holds internal state of the certificate, whether downloads and converts of individual components succeeded. Also holds information about errors and paths to the files. """ - report: CCCertificate.DocumentState - st: CCCertificate.DocumentState - cert: CCCertificate.DocumentState - - def __init__( - self, - report: CCCertificate.DocumentState | None = None, - st: CCCertificate.DocumentState | None = None, - cert: CCCertificate.DocumentState | None = None, - ): - super().__init__() - self.report = report if report is not None else CCCertificate.DocumentState() - self.st = st if st is not None else CCCertificate.DocumentState() - self.cert = cert if cert is not None else CCCertificate.DocumentState() - - @property - def serialized_attributes(self) -> list[str]: - return ["report", "st", "cert"] + report: DocumentState = field(default_factory=DocumentState) + st: DocumentState = field(default_factory=DocumentState) + cert: DocumentState = field(default_factory=DocumentState) @dataclass class PdfData(BasePdfData, ComplexSerializableType): @@ -350,7 +277,6 @@ class CCCertificate( next_certificates: list[str] | None = field(default=None) st_references: References = field(default_factory=References) report_references: References = field(default_factory=References) - # Contains direct outward references merged from both st, and report sources, annotated with ReferenceAnnotator # TODO: Reference meanings as Enum if we work with it further. annotated_references: dict[str, str] | None = field(default=None) @@ -358,6 +284,8 @@ class CCCertificate( direct_transitive_cves: set[str] | None = field(default=None) indirect_transitive_cves: set[str] | None = field(default=None) scheme_data: dict[str, Any] | None = field(default=None) + protection_profiles: set[str] | None = field(default=None) + eal: str | None = field(default=None) @property def serialized_attributes(self) -> list[str]: @@ -406,7 +334,7 @@ class CCCertificate( st_link: str | None, cert_link: str | None, manufacturer_web: str | None, - protection_profiles: set[ProtectionProfile] | None, + protection_profile_links: set[str] | None, maintenance_updates: set[MaintenanceReport] | None, state: InternalState | None, pdf_data: PdfData | None, @@ -430,7 +358,7 @@ class CCCertificate( self.st_link = sanitization.sanitize_link(st_link) self.cert_link = sanitization.sanitize_link(cert_link) self.manufacturer_web = sanitization.sanitize_link(manufacturer_web) - self.protection_profiles = protection_profiles + self.protection_profile_links = protection_profile_links self.maintenance_updates = maintenance_updates self.state = state if state else self.InternalState() self.pdf_data = pdf_data if pdf_data else self.PdfData() @@ -469,22 +397,6 @@ class CCCertificate( return helpers.get_first_16_bytes_sha256(self.category + self.name + self.report_link) @property - def eal(self) -> str | None: - """ - Returns EAL of certificate if it was extracted, None otherwise. - """ - res = [x for x in self.security_level if re.match(security_level_csv_scan, x)] - if res and len(res) == 1: - return res[0] - if res and len(res) > 1: - raise ValueError(f"Expected single EAL in security_level field, got: {res}") - else: - if self.protection_profiles: - return helpers.choose_lowest_eal({x.pp_eal for x in self.protection_profiles if x.pp_eal}) - else: - return None - - @property def actual_sars(self) -> set[SAR] | None: """ Computes actual SARs. First, SARs implied by EAL are computed. Then, these are augmented with heuristically extracted SARs. @@ -492,8 +404,8 @@ class CCCertificate( :return Optional[Set[SAR]]: Set of actual SARs of a certificate, None if empty """ sars = {} - if self.eal: - sars = {x[0]: SAR(x[0], x[1]) for x in SARS_IMPLIED_FROM_EAL[self.eal[:4]]} + if self.heuristics.eal: + sars = {x[0]: SAR(x[0], x[1]) for x in SARS_IMPLIED_FROM_EAL[self.heuristics.eal[:4]]} if self.heuristics.extracted_sars: for sar in self.heuristics.extracted_sars: @@ -520,7 +432,7 @@ class CCCertificate( self.manufacturer, self.scheme, self.security_level, - self.eal, + self.heuristics.eal, self.not_valid_before, self.not_valid_after, self.report_link, @@ -536,7 +448,7 @@ class CCCertificate( self.heuristics.report_references.directly_referencing, self.heuristics.report_references.indirectly_referencing, self.heuristics.extracted_sars, - [x.pp_name for x in self.protection_profiles] if self.protection_profiles else np.nan, + self.heuristics.protection_profiles if self.heuristics.protection_profiles else np.nan, self.heuristics.cert_lab[0] if (self.heuristics.cert_lab and self.heuristics.cert_lab[0]) else np.nan, ) @@ -557,7 +469,13 @@ class CCCertificate( # Prefer some values from the HTML # Links in CSV are currently (13.08.2024) broken. - html_preferred_attrs = {"protection_profiles", "maintenance_updates", "cert_link", "report_link", "st_link"} + html_preferred_attrs = { + "protection_profile_links", + "maintenance_updates", + "cert_link", + "report_link", + "st_link", + } for att, val in vars(self).items(): if (not val) or (other_source == "html" and att in html_preferred_attrs) or (att == "state"): @@ -575,7 +493,8 @@ class CCCertificate( """ new_dct = dct.copy() new_dct["maintenance_updates"] = set(dct["maintenance_updates"]) - new_dct["protection_profiles"] = set(dct["protection_profiles"]) + if dct["protection_profile_links"]: + new_dct["protection_profile_links"] = set(dct["protection_profile_links"]) new_dct["not_valid_before"] = ( date.fromisoformat(dct["not_valid_before"]) if isinstance(dct["not_valid_before"], str) @@ -615,16 +534,12 @@ class CCCertificate( return None @staticmethod - def _html_row_get_protection_profiles(cell: Tag) -> set: - protection_profiles = set() + def _html_row_get_protection_profile_links(cell: Tag) -> set: + protection_profile_links = set() for link in list(cell.find_all("a")): if link.get("href") is not None and "/ppfiles/" in link.get("href"): - protection_profiles.add( - ProtectionProfile( - pp_name=str(link.contents[0]), pp_eal=None, pp_link=CCCertificate.cc_url + link.get("href") - ) - ) - return protection_profiles + protection_profile_links.add(constants.CC_PORTAL_BASE_URL + link.get("href")) + return protection_profile_links @staticmethod def _html_row_get_date(cell: Tag) -> date | None: @@ -643,16 +558,16 @@ class CCCertificate( if not title: continue if title.startswith("Certification Report"): - report_link = CCCertificate.cc_url + link.get("href") + report_link = constants.CC_PORTAL_BASE_URL + link.get("href") elif title.startswith("Security Target"): - security_target_link = CCCertificate.cc_url + link.get("href") + security_target_link = constants.CC_PORTAL_BASE_URL + link.get("href") return report_link, security_target_link @staticmethod def _html_row_get_cert_link(cell: Tag) -> str | None: links = cell.find_all("a") - return CCCertificate.cc_url + links[0].get("href") if links else None + return constants.CC_PORTAL_BASE_URL + links[0].get("href") if links else None @staticmethod def _html_row_get_maintenance_div(cell: Tag) -> Tag | None: @@ -675,9 +590,9 @@ class CCCertificate( links = u.find_all("a") for link in links: if link.get("title").startswith("Maintenance Report:"): - main_report_link = CCCertificate.cc_url + link.get("href") + main_report_link = constants.CC_PORTAL_BASE_URL + link.get("href") elif link.get("title").startswith("Maintenance ST"): - main_st_link = CCCertificate.cc_url + link.get("href") + main_st_link = constants.CC_PORTAL_BASE_URL + link.get("href") else: logger.error("Unknown link in Maintenance part!") maintenance_updates.add( @@ -700,7 +615,7 @@ class CCCertificate( manufacturer_web = CCCertificate._html_row_get_manufacturer_web(cells[1]) scheme = CCCertificate._html_row_get_scheme(cells[6]) security_level = CCCertificate._html_row_get_security_level(cells[5]) - protection_profiles = CCCertificate._html_row_get_protection_profiles(cells[0]) + protection_profile_links = CCCertificate._html_row_get_protection_profile_links(cells[0]) not_valid_before = CCCertificate._html_row_get_date(cells[3]) not_valid_after = CCCertificate._html_row_get_date(cells[4]) report_link, st_link = CCCertificate._html_row_get_report_st_links(cells[0]) @@ -721,7 +636,7 @@ class CCCertificate( st_link, cert_link, manufacturer_web, - protection_profiles, + protection_profile_links, maintenances, None, None, diff --git a/src/sec_certs/sample/certificate.py b/src/sec_certs/sample/certificate.py index 74b1af96..6fbf8af4 100644 --- a/src/sec_certs/sample/certificate.py +++ b/src/sec_certs/sample/certificate.py @@ -30,8 +30,7 @@ class References(ComplexSerializableType): class Heuristics: - cpe_matches: set[str] | None - related_cves: set[str] | None + pass class PdfData: @@ -87,7 +86,3 @@ class Certificate(Generic[T, H, P], ABC, ComplexSerializableType): def from_dict(cls: type[T], dct: dict) -> T: dct.pop("dgst") return cls(**dct) - - @abstractmethod - def compute_heuristics_version(self) -> None: - raise NotImplementedError("Not meant to be implemented") diff --git a/src/sec_certs/sample/document_state.py b/src/sec_certs/sample/document_state.py new file mode 100644 index 00000000..a2cf1769 --- /dev/null +++ b/src/sec_certs/sample/document_state.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass +from pathlib import Path + +from sec_certs.serialization.json import ComplexSerializableType + + +@dataclass +class DocumentState(ComplexSerializableType): + download_ok: bool = False # Whether download went OK + convert_garbage: bool = False # Whether initial conversion resulted in garbage + convert_ok: bool = False # Whether overall conversion went OK (either pdftotext or via OCR) + extract_ok: bool = False # Whether extraction went OK + + pdf_hash: str | None = None + txt_hash: str | None = None + + _pdf_path: Path | None = None + _txt_path: Path | None = None + + def is_ok_to_download(self, fresh: bool = True) -> bool: + return True if fresh else not self.download_ok + + def is_ok_to_convert(self, fresh: bool = True) -> bool: + return self.download_ok if fresh else self.download_ok and not self.convert_ok + + def is_ok_to_analyze(self, fresh: bool = True) -> bool: + if fresh: + return self.download_ok and self.convert_ok + else: + return self.download_ok and self.convert_ok and not self.extract_ok + + @property + def pdf_path(self) -> Path: + if not self._pdf_path: + raise ValueError(f"pdf_path not set on {type(self)}") + return self._pdf_path + + @pdf_path.setter + def pdf_path(self, pth: str | Path | None) -> None: + self._pdf_path = Path(pth) if pth else None + + @property + def txt_path(self) -> Path: + if not self._txt_path: + raise ValueError(f"txt_path not set on {type(self)}") + return self._txt_path + + @txt_path.setter + def txt_path(self, pth: str | Path | None) -> None: + self._txt_path = Path(pth) if pth else None + + @property + def serialized_attributes(self) -> list[str]: + return [ + "download_ok", + "convert_garbage", + "convert_ok", + "extract_ok", + "pdf_hash", + "txt_hash", + ] diff --git a/src/sec_certs/sample/protection_profile.py b/src/sec_certs/sample/protection_profile.py index 4c26a1c7..36f13130 100644 --- a/src/sec_certs/sample/protection_profile.py +++ b/src/sec_certs/sample/protection_profile.py @@ -1,55 +1,303 @@ from __future__ import annotations -import copy -import logging -from dataclasses import dataclass -from typing import Any +from dataclasses import dataclass, field +from datetime import date, datetime +from pathlib import Path +from typing import Any, Literal +from urllib.parse import unquote_plus, urlparse +import requests +from bs4 import Tag + +import sec_certs.utils.extract +import sec_certs.utils.pdf +from sec_certs import constants +from sec_certs.cert_rules import cc_rules +from sec_certs.configuration import config +from sec_certs.sample.certificate import Certificate, logger +from sec_certs.sample.certificate import Heuristics as BaseHeuristics +from sec_certs.sample.certificate import PdfData as BasePdfData +from sec_certs.sample.document_state import DocumentState from sec_certs.serialization.json import ComplexSerializableType -from sec_certs.utils import sanitization +from sec_certs.utils import helpers -logger = logging.getLogger(__name__) +class ProtectionProfile( + Certificate["ProtectionProfile", "ProtectionProfile.Heuristics", "ProtectionProfile.PdfData"], + ComplexSerializableType, +): + @dataclass + class Heuristics(BaseHeuristics, ComplexSerializableType): + pass -@dataclass(frozen=True) -class ProtectionProfile(ComplexSerializableType): - """ - Object for holding protection profiles. - """ + @dataclass + class PdfData(BasePdfData, ComplexSerializableType): + report_metadata: dict[str, Any] | None = field(default=None) + pp_metadata: dict[str, Any] | None = field(default=None) + report_keywords: dict[str, Any] | None = field(default=None) + pp_keywords: dict[str, Any] | None = field(default=None) + report_filename: str | None = field(default=None) + pp_filename: str | None = field(default=None) - pp_name: str - pp_eal: str | None - pp_link: str | None = None - pp_ids: frozenset[str] | None = None + def __bool__(self) -> bool: + return any(x is not None for x in vars(self)) - def __post_init__(self): - super().__setattr__("pp_name", sanitization.sanitize_string(self.pp_name)) - super().__setattr__("pp_link", sanitization.sanitize_link(self.pp_link)) + @dataclass(eq=True) + class WebData(ComplexSerializableType): + category: str + status: Literal["active", "archived"] + is_collaborative: bool + name: str + version: str + security_level: set[str] + not_valid_before: date | None + not_valid_after: date | None + report_link: str | None + pp_link: str | None + scheme: str | None + maintenances: list[tuple[date, str, str]] - @classmethod - def from_dict(cls, dct: dict[str, Any]) -> ProtectionProfile: - new_dct = copy.deepcopy(dct) - new_dct["pp_ids"] = frozenset(new_dct["pp_ids"]) if new_dct["pp_ids"] else None - return cls(*tuple(new_dct.values())) + @property + def eal(self) -> str | None: + return helpers.choose_lowest_eal(self.security_level) + + @classmethod + def from_html_row( + cls, row: Tag, status: Literal["active", "archived"], category: str, is_collaborative: bool + ) -> ProtectionProfile.WebData: + if is_collaborative: + return cls._from_html_row_collaborative(row, category) + return cls._from_html_row_classic_pp(row, status, category) + + @classmethod + def _from_html_row_classic_pp( + cls, row: Tag, status: Literal["active", "archived"], category: str + ) -> ProtectionProfile.WebData: + cells = list(row.find_all("td")) + if status == "active" and len(cells) != 6: + raise ValueError( + f"Unexpected number of <td> elements in PP html row. Expected: 6, actual: {len(cells)}" + ) + if status == "archived" and len(cells) != 7: + raise ValueError( + f"Unexpected number of <td> elements in PP html row. Expected: 6, actual: {len(cells)}" + ) + + # TODO: Parse maintenance div here. See CC parsing. + return cls( + category, + status, + False, + cls._html_row_get_name(cells[0]), + cls._html_row_get_version(cells[1]), + cls._html_row_get_security_level(cells[2]), + cls._html_row_get_date(cells[3]), + None if status == "active" else cls._html_row_get_date(cells[4]), + cls._html_row_get_link(cells[-1]), + cls._html_row_get_link(cells[0]), + cls._html_row_get_scheme(cells[-2]), + [], + ) + + @classmethod + def _from_html_row_collaborative(cls, row: Tag, category: str) -> ProtectionProfile.WebData: + cells = list(row.find_all("td")) + if len(cells) != 5: + raise ValueError( + f"Unexpected number of <td> elements in collaborative PP html row. Expected: 5, actual: {len(cells)}" + ) + + return cls( + category, + "active", + True, + cls._html_row_get_collaborative_name(cells[0]), + cls._html_row_get_version(cells[1]), + cls._html_row_get_security_level(cells[2]), + cls._html_row_get_date(cells[3]), + None, + cls._html_row_get_link(cells[-1]), + cls._html_row_get_collaborative_pp_link(cells[0]), + None, + [], + ) + + @staticmethod + def _html_row_get_date(cell: Tag) -> date | None: + text = cell.get_text() + extracted_date = datetime.strptime(text, "%Y-%m-%d").date() if text else None + return extracted_date + + @staticmethod + def _html_row_get_name(cell: Tag) -> str: + return cell.find_all("a")[0].string + + @staticmethod + def _html_row_get_link(cell: Tag) -> str: + return constants.CC_PORTAL_BASE_URL + cell.find_all("a")[0].get("href") + + @staticmethod + def _html_row_get_version(cell: Tag) -> str: + return cell.text + + @staticmethod + def _html_row_get_security_level(cell: Tag) -> set[str]: + return set(cell.stripped_strings) + + @staticmethod + def _html_row_get_scheme(cell: Tag) -> str | None: + schemes = list(cell.stripped_strings) + return schemes[0] if schemes else None + + @staticmethod + def _html_row_get_collaborative_name(cell: Tag) -> str: + return list(cell.stripped_strings)[0] + + @staticmethod + def _html_row_get_collaborative_pp_link(cell: Tag) -> str: + return constants.CC_PORTAL_BASE_URL + [x for x in cell.find_all("a") if x.string == "Protection Profile"][ + 0 + ].get("href") + + @dataclass + class InternalState(ComplexSerializableType): + pp: DocumentState = field(default_factory=DocumentState) + report: DocumentState = field(default_factory=DocumentState) + + def __init__( + self, + web_data: WebData, + pdf_data: PdfData | None = None, + heuristics: Heuristics | None = None, + state: InternalState | None = None, + ): + super().__init__() + self.web_data: ProtectionProfile.WebData = web_data + self.pdf_data: ProtectionProfile.PdfData = pdf_data if pdf_data else ProtectionProfile.PdfData() + self.heuristics: ProtectionProfile.Heuristics = heuristics if heuristics else ProtectionProfile.Heuristics() + self.state: ProtectionProfile.InternalState = state if state else ProtectionProfile.InternalState() + + @property + def dgst(self) -> str: + return helpers.get_first_16_bytes_sha256( + "|".join([self.web_data.category, self.web_data.name, self.web_data.version]) + ) + + @property + def label_studio_title(self) -> str: + return self.web_data.name + + def merge(self, other: ProtectionProfile, other_source: str | None = None) -> None: + raise ValueError("Merging of PPs not implemented.") + + def set_local_paths( + self, + report_pdf_dir: str | Path | None, + pp_pdf_dir: str | Path | None, + report_txt_dir: str | Path | None, + pp_txt_dir: str | Path | None, + ) -> None: + if report_pdf_dir: + self.state.report.pdf_path = Path(report_pdf_dir) / f"{self.dgst}.pdf" + if pp_pdf_dir: + self.state.pp.pdf_path = Path(pp_pdf_dir) / f"{self.dgst}.pdf" + if report_txt_dir: + self.state.report.txt_path = Path(report_txt_dir) / f"{self.dgst}.txt" + if pp_txt_dir: + self.state.pp.txt_path = Path(pp_txt_dir) / f"{self.dgst}.txt" @classmethod - def from_old_api_dict(cls, dct: dict[str, Any]) -> ProtectionProfile: - pp_name = sanitization.sanitize_string(dct["csv_scan"]["cc_pp_name"]) - pp_link = sanitization.sanitize_link(dct["csv_scan"]["link_pp_document"]) - pp_ids = frozenset(dct["processed"]["cc_pp_csvid"]) if dct["processed"]["cc_pp_csvid"] else None - eal_set = sanitization.sanitize_security_levels(dct["csv_scan"]["cc_security_level"]) + def from_html_row( + cls, row: Tag, status: Literal["active", "archived"], category: str, is_collaborative: bool + ) -> ProtectionProfile: + return cls(ProtectionProfile.WebData.from_html_row(row, status, category, is_collaborative)) + + @staticmethod + def download_pdf_report(cert: ProtectionProfile) -> ProtectionProfile: + exit_code: str | int + if not cert.web_data.report_link: + exit_code = "No link" + else: + exit_code = helpers.download_file( + cert.web_data.report_link, cert.state.report.pdf_path, proxy=config.cc_use_proxy + ) + if exit_code != requests.codes.ok: + error_msg = f"failed to download report from {cert.web_data.report_link}, code: {exit_code}" + logger.error(f"Cert dgst: {cert.dgst} " + error_msg) + cert.state.report.download_ok = False + else: + cert.state.report.download_ok = True + cert.state.report.pdf_hash = helpers.get_sha256_filepath(cert.state.report.pdf_path) + cert.pdf_data.report_filename = unquote_plus(str(urlparse(cert.web_data.report_link).path).split("/")[-1]) + return cert + + @staticmethod + def download_pdf_pp(cert: ProtectionProfile) -> ProtectionProfile: + exit_code: str | int + if not cert.web_data.pp_link: + exit_code = "No link" + else: + exit_code = helpers.download_file(cert.web_data.pp_link, cert.state.pp.pdf_path, proxy=config.cc_use_proxy) + if exit_code != requests.codes.ok: + error_msg = f"failed to download PP from {cert.web_data.pp_link}, code: {exit_code}" + logger.error(f"Cert dgst: {cert.dgst} " + error_msg) + cert.state.pp.download_ok = False + else: + cert.state.pp.download_ok = True + cert.state.pp.pdf_hash = helpers.get_sha256_filepath(cert.state.pp.pdf_path) + cert.pdf_data.pp_filename = unquote_plus(str(urlparse(cert.web_data.pp_link).path).split("/")[-1]) + return cert + + @staticmethod + def convert_report_pdf(cert: ProtectionProfile) -> ProtectionProfile: + ocr_done, ok_result = sec_certs.utils.pdf.convert_pdf_file( + cert.state.report.pdf_path, cert.state.report.txt_path + ) + cert.state.report.convert_garbage = ocr_done + cert.state.report.convert_ok = ok_result + if not ok_result: + logger.error(f"Cert dgst: {cert.dgst} failed to convert report pdf to txt") + else: + cert.state.report.txt_hash = helpers.get_sha256_filepath(cert.state.report.txt_path) + return cert - if not len(eal_set) <= 1: - raise ValueError("EAL field should have single value or should be empty.") + @staticmethod + def convert_pp_pdf(cert: ProtectionProfile) -> ProtectionProfile: + ocr_done, ok_result = sec_certs.utils.pdf.convert_pdf_file(cert.state.pp.pdf_path, cert.state.pp.txt_path) + cert.state.pp.convert_garbage = ocr_done + cert.state.pp.convert_ok = ok_result + if not ok_result: + logger.error(f"Cert dgst: {cert.dgst} failed to convert PP pdf to txt") + else: + cert.state.pp.txt_hash = helpers.get_sha256_filepath(cert.state.pp.txt_path) + return cert - eal_str = list(eal_set)[0] if eal_set else None + @staticmethod + def extract_report_pdf_metadata(cert: ProtectionProfile) -> ProtectionProfile: + response, cert.pdf_data.report_metadata = sec_certs.utils.pdf.extract_pdf_metadata(cert.state.report.pdf_path) + cert.state.report.extract_ok = response == constants.RETURNCODE_OK + return cert - return cls(pp_name, eal_str, pp_link, pp_ids) + @staticmethod + def extract_pp_pdf_metadata(cert: ProtectionProfile) -> ProtectionProfile: + response, cert.pdf_data.pp_metadata = sec_certs.utils.pdf.extract_pdf_metadata(cert.state.pp.pdf_path) + cert.state.pp.extract_ok = response == constants.RETURNCODE_OK + return cert - def __eq__(self, other: object) -> bool: - if not isinstance(other, ProtectionProfile): - return False - return self.pp_name == other.pp_name and self.pp_link == other.pp_link + @staticmethod + def extract_report_pdf_keywords(cert: ProtectionProfile) -> ProtectionProfile: + report_keywords = sec_certs.utils.extract.extract_keywords(cert.state.report.txt_path, cc_rules) + if report_keywords is None: + cert.state.report.extract_ok = False + else: + cert.pdf_data.report_keywords = report_keywords + return cert - def __lt__(self, other: ProtectionProfile) -> bool: - return self.pp_name < other.pp_name + @staticmethod + def extract_pp_pdf_keywords(cert: ProtectionProfile) -> ProtectionProfile: + pp_keywords = sec_certs.utils.extract.extract_keywords(cert.state.pp.txt_path, cc_rules) + if pp_keywords is None: + cert.state.pp.extract_ok = False + else: + cert.pdf_data.pp_keywords = pp_keywords + return cert diff --git a/src/sec_certs/utils/helpers.py b/src/sec_certs/utils/helpers.py index 6a6a9954..d34e9f0e 100644 --- a/src/sec_certs/utils/helpers.py +++ b/src/sec_certs/utils/helpers.py @@ -264,7 +264,18 @@ def choose_lowest_eal(eals: set[str] | None) -> str | None: if not eals: return None - matches = [(re.search(r"\d+", x)) for x in eals] - min_number = min([int(x.group()) for x in matches if x]) - candidates = [x for x in eals if str(min_number) in x] - return "EAL" + str(min_number) if len(candidates) == 2 else candidates[0] + eal_pattern = re.compile(r"(EAL(\d+)\+?)") + eal_entries = [] + + for s in eals: + match = eal_pattern.search(s) + if match: + full_match = match.group(1) + number = int(match.group(2)) + has_plus = "+" in full_match + eal_entries.append((number, has_plus, full_match)) + + if eal_entries: + eal_entries.sort(key=lambda x: (x[0], x[1])) + return eal_entries[0][2] + return None diff --git a/tests/cc/conftest.py b/tests/cc/conftest.py index 1f5050be..68bf3cd5 100644 --- a/tests/cc/conftest.py +++ b/tests/cc/conftest.py @@ -8,7 +8,6 @@ import tests.data.cc.dataset from sec_certs.dataset.cc import CCDataset from sec_certs.sample.cc import CCCertificate -from sec_certs.sample.protection_profile import ProtectionProfile @pytest.fixture(scope="module") @@ -38,7 +37,7 @@ def cert_one() -> CCCertificate: "https://www.commoncriteriaportal.org/files/epfiles/ST%20-%20NetIQ%20Identity%20Manager%204.7.pdf", "https://www.commoncriteriaportal.org/files/epfiles/Certifikat%20CCRA%20-%20NetIQ%20Identity%20Manager%204.7_signed.pdf", "https://www.netiq.com/", - set(), + None, set(), None, None, @@ -48,7 +47,6 @@ def cert_one() -> CCCertificate: @pytest.fixture(scope="module") def cert_two() -> CCCertificate: - pp = ProtectionProfile("sample_pp", None, pp_link="https://sample.pp") update = CCCertificate.MaintenanceReport( date(1900, 1, 1), "Sample maintenance", "https://maintenance.up", "https://maintenance.up" ) @@ -66,7 +64,7 @@ def cert_two() -> CCCertificate: "https://path.to/st/link", "https://path.to/cert/link", "https://path.to/manufacturer/web", - {pp}, + {"https://sample.pp"}, {update}, None, None, diff --git a/tests/cc/test_cc_analysis.py b/tests/cc/test_cc_analysis.py index 9a57da1e..849eda68 100644 --- a/tests/cc/test_cc_analysis.py +++ b/tests/cc/test_cc_analysis.py @@ -19,10 +19,9 @@ from sec_certs.dataset.auxiliary_dataset_handling import ( 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.cc import compute_references 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 @@ -40,6 +39,8 @@ 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.aux_handlers[ProtectionProfileDatasetHandler].root_dir.mkdir(parents=True, exist_ok=True) + shutil.copy(tmp_dir / "pp.json", cc_dset.aux_handlers[ProtectionProfileDatasetHandler].dset_path) cc_dset.aux_handlers[ProtectionProfileDatasetHandler].process_dataset() cc_dset.aux_handlers[CPEMatchDictHandler].dset = {} @@ -156,29 +157,6 @@ def test_keywords_heuristics(random_certificate: CCCertificate): assert extracted_keywords["cipher_mode"]["CBC"]["CBC"] == 2 -def test_protection_profile_matching(processed_cc_dset: CCDataset, random_certificate: CCCertificate): - artificial_pp: ProtectionProfile = ProtectionProfile( - "Korean National Protection Profile for Single Sign On V1.0", - "EAL1+", - pp_link="http://www.commoncriteriaportal.org/files/ppfiles/KECS-PP-0822-2017%20Korean%20National%20PP%20for%20Single%20Sign%20On%20V1.0(eng).pdf", - ) - - random_certificate.protection_profiles = {artificial_pp} - - expected_pp: ProtectionProfile = ProtectionProfile( - "Korean National Protection Profile for Single Sign On V1.0", - "EAL1+", - pp_link="http://www.commoncriteriaportal.org/files/ppfiles/KECS-PP-0822-2017%20Korean%20National%20PP%20for%20Single%20Sign%20On%20V1.0(eng).pdf", - pp_ids=frozenset(["KECS-PP-0822-2017 SSO V1.0"]), - ) - - link_to_protection_profiles( - processed_cc_dset.aux_handlers[ProtectionProfileDatasetHandler].dset, processed_cc_dset.certs.values() - ) - - assert random_certificate.protection_profiles == {expected_pp} - - def test_single_record_references_heuristics(random_certificate: CCCertificate): # Single record in daset is not affecting nor affected by other records assert not random_certificate.heuristics.report_references.directly_referenced_by @@ -246,3 +224,28 @@ def test_eal_implied_sar_inference(random_certificate: CCCertificate): actual_sars = random_certificate.actual_sars eal_3_sars = {SAR(x[0], x[1]) for x in SARS_IMPLIED_FROM_EAL["EAL3"]} assert eal_3_sars.issubset(actual_sars) + + +def test_eal_inference(processed_cc_dset: CCDataset): + assert processed_cc_dset["ed91ff3e658457fd"].heuristics.eal == "EAL1" + assert processed_cc_dset["95e3850bef32f410"].heuristics.eal == "EAL1+" + + +def test_pp_linking(processed_cc_dset: CCDataset): + assert processed_cc_dset["ed91ff3e658457fd"].heuristics.protection_profiles == {"e315e3e834a61448"} + assert processed_cc_dset["95e3850bef32f410"].heuristics.protection_profiles == { + "b02ed76d2545326a", + "c8b175590bb7fdfb", + } + pp_dset = processed_cc_dset.aux_handlers[ProtectionProfileDatasetHandler].dset + assert processed_cc_dset["ed91ff3e658457fd"].protection_profile_links + assert processed_cc_dset["95e3850bef32f410"].protection_profile_links + assert ( + pp_dset["e315e3e834a61448"].web_data.pp_link in processed_cc_dset["ed91ff3e658457fd"].protection_profile_links + ) + assert ( + pp_dset["b02ed76d2545326a"].web_data.pp_link in processed_cc_dset["95e3850bef32f410"].protection_profile_links + ) + assert ( + pp_dset["c8b175590bb7fdfb"].web_data.pp_link in processed_cc_dset["95e3850bef32f410"].protection_profile_links + ) diff --git a/tests/cc/test_cc_dataset.py b/tests/cc/test_cc_dataset.py index c501054c..7606a921 100644 --- a/tests/cc/test_cc_dataset.py +++ b/tests/cc/test_cc_dataset.py @@ -6,7 +6,6 @@ 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 @@ -130,17 +129,6 @@ def test_build_dataset(data_dir: Path, cert_one: CCCertificate, toy_dataset: CCD assert dset == toy_dataset -def test_process_pp_dataset(toy_dataset: CCDataset): - with TemporaryDirectory() as tmp_dir: - toy_dataset.copy_dataset(tmp_dir) - 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") def test_download_csv_html_files(): with TemporaryDirectory() as tmp_dir: diff --git a/tests/data/cc/analysis/cc_full_dataset.json b/tests/data/cc/analysis/cc_full_dataset.json index ba64903c..a2149b67 100644 --- a/tests/data/cc/analysis/cc_full_dataset.json +++ b/tests/data/cc/analysis/cc_full_dataset.json @@ -35,18 +35,9 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/0683b_pdf.pdf", "cert_link": null, "manufacturer_web": "https://www.ibm.com", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [ - { - "_type": "sec_certs.sample.protection_profile.ProtectionProfile", - "pp_name": "Korean National Protection Profile for Single Sign On V1.0", - "pp_eal": "EAL1+", - "pp_link": "https://www.commoncriteriaportal.org/files/ppfiles/KECS-PP-0822-2017%20Korean%20National%20PP%20for%20Single%20Sign%20On%20V1.0(eng).pdf", - "pp_ids": [ - "KECS-PP-0822-2017 SSO V1.0" - ] - } ] }, "maintenance_updates": { @@ -56,7 +47,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -65,7 +56,7 @@ "txt_hash": "35627594d3806ac3926ec47f466503fe27781533da12beb6f8705882fccf125e" }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -74,7 +65,7 @@ "txt_hash": "c8b4c5667a3f60edc845051e5a31a2d17b9d9a11df9e56dd89681d25e727a622" }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -730,8 +721,10 @@ }, "direct_transitive_cves": null, "indirect_transitive_cves": null, - "next_certificates": null, - "prev_certificates": null + "next_certificates": null, + "prev_certificates": null, + "protection_profiles": null, + "eal": null } } ] diff --git a/tests/data/cc/analysis/pp.json b/tests/data/cc/analysis/pp.json new file mode 100644 index 00000000..7b96cf8c --- /dev/null +++ b/tests/data/cc/analysis/pp.json @@ -0,0 +1,191 @@ +{ + "_type": "sec_certs.dataset.protection_profile.ProtectionProfileDataset", + "state": { + "_type": "sec_certs.dataset.dataset.Dataset.DatasetInternalState", + "meta_sources_parsed": true, + "artifacts_downloaded": false, + "pdfs_converted": false, + "auxiliary_datasets_processed": false, + "certs_analyzed": false + }, + "timestamp": "2025-01-25 17:39:26.873380", + "sha256_digest": "not implemented", + "name": "ProtectionProfileDataset dataset", + "description": "25/01/2025 17:39:26", + "n_certs": 3, + "certs": [ + { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile", + "dgst": "c8b175590bb7fdfb", + "web_data": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.WebData", + "category": "Access Control Devices and Systems", + "status": "active", + "is_collaborative": false, + "name": "Korean National Protection Profile for Single Sign On V1.0", + "version": "V1.0", + "security_level": { + "_type": "Set", + "elements": [ + "ATE_FUN.1", + "EAL1+" + ] + }, + "not_valid_before": "2017-08-18", + "not_valid_after": null, + "report_link": "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/KECS-CR-17-58 Korean National PP for Single Sign On V1.0(eng).pdf", + "pp_link": "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/KECS-PP-0822-2017 Korean National PP for Single Sign On V1.0(eng).pdf", + "scheme": "KR", + "maintenances": [] + }, + "pdf_data": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.PdfData", + "report_metadata": null, + "pp_metadata": null, + "report_keywords": null, + "pp_keywords": null, + "report_filename": null, + "pp_filename": null + }, + "heuristics": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.Heuristics" + }, + "state": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.InternalState", + "pp": { + "_type": "sec_certs.sample.document_state.DocumentState", + "download_ok": false, + "convert_garbage": false, + "convert_ok": false, + "extract_ok": false, + "pdf_hash": null, + "txt_hash": null + }, + "report": { + "_type": "sec_certs.sample.document_state.DocumentState", + "download_ok": false, + "convert_garbage": false, + "convert_ok": false, + "extract_ok": false, + "pdf_hash": null, + "txt_hash": null + } + } + }, + { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile", + "dgst": "e315e3e834a61448", + "web_data": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.WebData", + "category": "Other Devices and Systems", + "status": "active", + "is_collaborative": false, + "name": "Protection Profile for Security Module of General-Purpose Health Informatics Software", + "version": "1.0", + "security_level": { + "_type": "Set", + "elements": [ + "EAL2" + ] + }, + "not_valid_before": "2016-09-20", + "not_valid_after": null, + "report_link": "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/HBYS_PP_CR.pdf", + "pp_link": "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/HBYS_PP_07_09_2016_Updated.pdf", + "scheme": "TR", + "maintenances": [] + }, + "pdf_data": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.PdfData", + "report_metadata": null, + "pp_metadata": null, + "report_keywords": null, + "pp_keywords": null, + "report_filename": null, + "pp_filename": null + }, + "heuristics": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.Heuristics" + }, + "state": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.InternalState", + "pp": { + "_type": "sec_certs.sample.document_state.DocumentState", + "download_ok": false, + "convert_garbage": false, + "convert_ok": false, + "extract_ok": false, + "pdf_hash": null, + "txt_hash": null + }, + "report": { + "_type": "sec_certs.sample.document_state.DocumentState", + "download_ok": false, + "convert_garbage": false, + "convert_ok": false, + "extract_ok": false, + "pdf_hash": null, + "txt_hash": null + } + } + }, + { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile", + "dgst": "b02ed76d2545326a", + "web_data": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.WebData", + "category": "Biometric Systems and Devices", + "status": "active", + "is_collaborative": false, + "name": "Fingerprint Spoof Detection Protection Profile based on Organisational Security Policies (FSDPP_OSP), Version 1.7", + "version": "1.7", + "security_level": { + "_type": "Set", + "elements": [ + "ALC_FLR.1", + "EAL2+" + ] + }, + "not_valid_before": "2010-02-25", + "not_valid_after": null, + "report_link": "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/pp0062a_pdf.pdf", + "pp_link": "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/pp0062b_pdf.pdf", + "scheme": "DE", + "maintenances": [] + }, + "pdf_data": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.PdfData", + "report_metadata": null, + "pp_metadata": null, + "report_keywords": null, + "pp_keywords": null, + "report_filename": null, + "pp_filename": null + }, + "heuristics": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.Heuristics" + }, + "state": { + "_type": "sec_certs.sample.protection_profile.ProtectionProfile.InternalState", + "pp": { + "_type": "sec_certs.sample.document_state.DocumentState", + "download_ok": false, + "convert_garbage": false, + "convert_ok": false, + "extract_ok": false, + "pdf_hash": null, + "txt_hash": null + }, + "report": { + "_type": "sec_certs.sample.document_state.DocumentState", + "download_ok": false, + "convert_garbage": false, + "convert_ok": false, + "extract_ok": false, + "pdf_hash": null, + "txt_hash": null + } + } + } + ] +} diff --git a/tests/data/cc/analysis/reference_dataset.json b/tests/data/cc/analysis/reference_dataset.json index 28234b7e..4ea176de 100644 --- a/tests/data/cc/analysis/reference_dataset.json +++ b/tests/data/cc/analysis/reference_dataset.json @@ -34,7 +34,7 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/0517b.pdf", "cert_link": null, "manufacturer_web": "https://global.oce.com/", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [] }, @@ -45,7 +45,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -54,7 +54,7 @@ "txt_hash": "460e8010dbc8f5de5b87bf96fd45c71cfd9f3869f34ca6ac1ab02cbd70d2523f" }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -63,7 +63,7 @@ "txt_hash": "81c53d1e5b1c2fcb129ce1053d13cd1308f7a556921f0b9024cedf75c6b2efb7" }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -580,7 +580,9 @@ "direct_transitive_cves": null, "indirect_transitive_cves": null, "next_certificates": null, - "prev_certificates": null + "prev_certificates": null, + "protection_profiles": null, + "eal": null } }, { @@ -604,7 +606,7 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/0370b.pdf", "cert_link": null, "manufacturer_web": "https://global.oce.com/", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [] }, @@ -615,7 +617,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -624,7 +626,7 @@ "txt_hash": "0535df1c56fb4f87153cbffee51ba4d77fac47a6f17f024aa7d9df461028bc65" }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -633,7 +635,7 @@ "txt_hash": "926668bea7c427a4fcf82857bfc63420f3597b6bff39699927a58f335620eaac" }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -1228,7 +1230,9 @@ "direct_transitive_cves": null, "indirect_transitive_cves": null, "next_certificates": null, - "prev_certificates": null + "prev_certificates": null, + "protection_profiles": null, + "eal": null } }, { @@ -1252,7 +1256,7 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/0325b.pdf", "cert_link": null, "manufacturer_web": "https://global.oce.com/", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [] }, @@ -1263,7 +1267,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -1272,7 +1276,7 @@ "txt_hash": "11e1262fd8f5df1b140f5e8813883b71447503781399427b35adbbecd00b4d63" }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -1281,7 +1285,7 @@ "txt_hash": "179b07b4fc7402066a884edea494b28e324315108a5e0820184031f2e2062ad5" }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -1870,7 +1874,9 @@ "direct_transitive_cves": null, "indirect_transitive_cves": null, "next_certificates": null, - "prev_certificates": null + "prev_certificates": null, + "protection_profiles": null, + "eal": null } } ] diff --git a/tests/data/cc/analysis/transitive_vulnerability_dataset.json b/tests/data/cc/analysis/transitive_vulnerability_dataset.json index 586ac5a6..7ed77910 100644 --- a/tests/data/cc/analysis/transitive_vulnerability_dataset.json +++ b/tests/data/cc/analysis/transitive_vulnerability_dataset.json @@ -34,18 +34,10 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/0874b_pdf.pdf", "cert_link": null, "manufacturer_web": "https://www.ibm.com", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [ - { - "_type": "sec_certs.sample.protection_profile.ProtectionProfile", - "pp_name": "Operating System Protection Profile, Version 2.0", - "pp_eal": "EAL4+", - "pp_link": "https://www.commoncriteriaportal.org/files/ppfiles/pp0067b_pdf.pdf", - "pp_ids": [ - "OSPP_V2.0" - ] - } + "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/pp0067b_pdf.pdf" ] }, "maintenance_updates": { @@ -55,7 +47,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -64,7 +56,7 @@ "txt_hash": "9d360141a98e764b15855f519b456c4e4639f993c4f8b5ab67e9c8ae7fbfc9e4" }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -73,7 +65,7 @@ "txt_hash": "66271d8bf0b581a2f189301438f2aee13ff3da0bb0bb180bcf518261eb695496" }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -1336,7 +1328,9 @@ ] }, "next_certificates": null, - "prev_certificates": null + "prev_certificates": null, + "protection_profiles": null, + "eal": null } }, { @@ -1360,7 +1354,7 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/0875b_pdf.pdf", "cert_link": null, "manufacturer_web": "https://www.ibm.com", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [] }, @@ -1371,7 +1365,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -1380,7 +1374,7 @@ "txt_hash": "dd120ba7667c2385839c96ee70c56f2a4d464fc95e3ea2818d31b3347d06fd4f" }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -1389,7 +1383,7 @@ "txt_hash": "f7f7b8f31dddde3f0756cde8843061f01b606bdf266eca71dbcc56b3672d1db5" }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -2287,7 +2281,9 @@ ] }, "next_certificates": null, - "prev_certificates": null + "prev_certificates": null, + "protection_profiles": null, + "eal": null } }, { @@ -2311,18 +2307,10 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/0948b_pdf.pdf", "cert_link": null, "manufacturer_web": "https://www.ibm.com", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [ - { - "_type": "sec_certs.sample.protection_profile.ProtectionProfile", - "pp_name": "Operating System Protection Profile, Version 2.0", - "pp_eal": "EAL4+", - "pp_link": "https://www.commoncriteriaportal.org/files/ppfiles/pp0067b_pdf.pdf", - "pp_ids": [ - "OSPP_V2.0" - ] - } + "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/pp0067b_pdf.pdf" ] }, "maintenance_updates": { @@ -2332,7 +2320,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -2341,7 +2329,7 @@ "txt_hash": "0a7c65e3d11f082c8f75aba7de0079c0b1aa5e67bb28d4635cbcaa4cd200d1c2" }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -2350,7 +2338,7 @@ "txt_hash": "90b8e48add278faea4668eccba591d3992bf782669cca1b0a63bf6f21b514cd9" }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -3640,7 +3628,9 @@ "direct_transitive_cves": null, "indirect_transitive_cves": null, "next_certificates": null, - "prev_certificates": null + "prev_certificates": null, + "protection_profiles": null, + "eal": null } } ] diff --git a/tests/data/cc/analysis/vulnerable_dataset.json b/tests/data/cc/analysis/vulnerable_dataset.json index 01d720c0..776db230 100644 --- a/tests/data/cc/analysis/vulnerable_dataset.json +++ b/tests/data/cc/analysis/vulnerable_dataset.json @@ -26,7 +26,7 @@ "_type": "Set", "elements": [ "ALC_FLR.1", - "EAL3+" + "EAL1" ] }, "not_valid_before": "2014-12-05", @@ -35,12 +35,17 @@ "st_link": "http://www.commoncriteriaportal.org/files/epfiles/0683b_pdf.pdf", "cert_link": null, "manufacturer_web": "http://www.ibm.com", - "protection_profiles": [], + "protection_profile_links": { + "_type": "Set", + "elements": [ + "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/HBYS_PP_07_09_2016_Updated.pdf" + ] + }, "maintenance_updates": [], "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": true, "convert_garbage": false, "convert_ok": true, @@ -49,7 +54,7 @@ "txt_hash": null }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": true, "convert_garbage": false, "convert_ok": true, @@ -58,7 +63,7 @@ "txt_hash": null }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -94,7 +99,9 @@ "cert_lab": null, "cert_id": null, "next_certificates": null, - "prev_certificates": null + "prev_certificates": null, + "protection_profiles": null, + "eal": null } }, { @@ -108,8 +115,7 @@ "security_level": { "_type": "Set", "elements": [ - "ALC_FLR.1", - "EAL3+" + "ALC_FLR.1" ] }, "not_valid_before": "2010-12-05", @@ -118,12 +124,18 @@ "st_link": "", "cert_link": null, "manufacturer_web": "http://www.ibm.com", - "protection_profiles": [], + "protection_profile_links": { + "_type": "Set", + "elements": [ + "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/KECS-PP-0822-2017 Korean National PP for Single Sign On V1.0(eng).pdf", + "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/pp0062b_pdf.pdf" + ] + }, "maintenance_updates": [], "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": true, "convert_garbage": false, "convert_ok": true, @@ -132,7 +144,7 @@ "txt_hash": null }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": true, "convert_garbage": false, "convert_ok": true, @@ -141,7 +153,7 @@ "txt_hash": null }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -177,7 +189,9 @@ "cert_lab": null, "cert_id": null, "next_certificates": null, - "prev_certificates": null + "prev_certificates": null, + "protection_profiles": null, + "eal": null } } ] diff --git a/tests/data/cc/certificate/fictional_cert.json b/tests/data/cc/certificate/fictional_cert.json index 8239c908..327aaf46 100644 --- a/tests/data/cc/certificate/fictional_cert.json +++ b/tests/data/cc/certificate/fictional_cert.json @@ -15,18 +15,12 @@ "not_valid_before": "1900-01-02", "not_valid_after": "1900-01-03", "manufacturer_web": "https://path.to/manufacturer/web", - "protection_profiles": { - "_type": "Set", - "elements": [ - { - "_type": "sec_certs.sample.protection_profile.ProtectionProfile", - "pp_name": "sample_pp", - "pp_eal": null, - "pp_link": "https://sample.pp", - "pp_ids": null - } - ] - }, + "protection_profile_links": { + "_type": "Set", + "elements": [ + "https://sample.pp" + ] + }, "maintenance_updates": { "_type": "Set", "elements": [ @@ -42,7 +36,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -51,7 +45,7 @@ "txt_hash": null }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -60,7 +54,7 @@ "txt_hash": null }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -112,7 +106,9 @@ "indirectly_referenced_by": null, "indirectly_referencing": null }, - "scheme_data": null + "scheme_data": null, + "protection_profiles": null, + "eal": null }, "report_link": "https://path.to/report/link", "st_link": "https://path.to/st/link", diff --git a/tests/data/cc/dataset/auxiliary_datasets/maintenances/maintenance_updates.json b/tests/data/cc/dataset/auxiliary_datasets/maintenances/maintenance_updates.json index 0c8b1306..d8de0f3a 100644 --- a/tests/data/cc/dataset/auxiliary_datasets/maintenances/maintenance_updates.json +++ b/tests/data/cc/dataset/auxiliary_datasets/maintenances/maintenance_updates.json @@ -23,7 +23,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": true, "convert_garbage": false, "convert_ok": false, @@ -32,7 +32,7 @@ "txt_hash": null }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": true, "convert_garbage": false, "convert_ok": false, @@ -41,7 +41,7 @@ "txt_hash": null }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -93,7 +93,9 @@ "indirect_transitive_cves": null, "scheme_data": null, "prev_certificates": null, - "next_certificates": null + "next_certificates": null, + "protection_profiles": null, + "eal": null }, "related_cert_digest": "8f08cacb49a742fb", "maintenance_date": "2019-08-26" diff --git a/tests/data/cc/dataset/toy_dataset.json b/tests/data/cc/dataset/toy_dataset.json index 3395d3e8..d593f382 100644 --- a/tests/data/cc/dataset/toy_dataset.json +++ b/tests/data/cc/dataset/toy_dataset.json @@ -35,7 +35,7 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/ST%20-%20NetIQ%20Identity%20Manager%204.7.pdf", "cert_link": "https://www.commoncriteriaportal.org/files/epfiles/Certifikat%20CCRA%20-%20NetIQ%20Identity%20Manager%204.7_signed.pdf", "manufacturer_web": "https://www.netiq.com/", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [] }, @@ -46,7 +46,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -55,7 +55,7 @@ "txt_hash": null }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -64,7 +64,7 @@ "txt_hash": null }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -116,7 +116,9 @@ "indirectly_referenced_by": null, "indirectly_referencing": null }, - "scheme_data": null + "scheme_data": null, + "protection_profiles": null, + "eal": null } }, { @@ -137,16 +139,10 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/Magic_SSO_V4.0-ST-v1.4_EN.pdf", "cert_link": null, "manufacturer_web": "https://www.dreamsecurity.com/", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [ - { - "_type": "sec_certs.sample.protection_profile.ProtectionProfile", - "pp_name": "Korean National Protection Profile for Single Sign On V1.0", - "pp_eal": "EAL1+", - "pp_link": "https://www.commoncriteriaportal.org/files/ppfiles/KECS-PP-0822-2017%20Korean%20National%20PP%20for%20Single%20Sign%20On%20V1.0(eng).pdf", - "pp_ids": null - } + "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/KECS-PP-0822-2017%20Korean%20National%20PP%20for%20Single%20Sign%20On%20V1.0(eng).pdf" ] }, "maintenance_updates": { @@ -156,7 +152,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -165,7 +161,7 @@ "txt_hash": null }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -174,7 +170,7 @@ "txt_hash": null }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -226,7 +222,9 @@ "indirectly_referenced_by": null, "indirectly_referencing": null }, - "scheme_data": null + "scheme_data": null, + "protection_profiles": null, + "eal": null } }, { @@ -247,16 +245,10 @@ "st_link": "https://www.commoncriteriaportal.org/files/epfiles/383-4-450%20ST%20v1.3A.pdf", "cert_link": "https://www.commoncriteriaportal.org/files/epfiles/383-4-450%20CT%20v1.0a.pdf", "manufacturer_web": "https://www.fortinet.com/", - "protection_profiles": { + "protection_profile_links": { "_type": "Set", "elements": [ - { - "_type": "sec_certs.sample.protection_profile.ProtectionProfile", - "pp_name": "collaborative Protection Profile for Stateful Traffic Filter Firewalls v2.0 + Errata 20180314", - "pp_eal": null, - "pp_link": "https://www.commoncriteriaportal.org/files/ppfiles/CPP_FW_V2.0E.pdf", - "pp_ids": null - } + "https://www.commoncriteriaportal.org/nfs/ccpfiles/files/ppfiles/CPP_FW_V2.0E.pdf" ] }, "maintenance_updates": { @@ -274,7 +266,7 @@ "state": { "_type": "sec_certs.sample.cc.CCCertificate.InternalState", "report": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -283,7 +275,7 @@ "txt_hash": null }, "st": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -292,7 +284,7 @@ "txt_hash": null }, "cert": { - "_type": "sec_certs.sample.cc.CCCertificate.DocumentState", + "_type": "sec_certs.sample.document_state.DocumentState", "download_ok": false, "convert_garbage": false, "convert_ok": false, @@ -344,7 +336,9 @@ "indirectly_referenced_by": null, "indirectly_referencing": null }, - "scheme_data": null + "scheme_data": null, + "protection_profiles": null, + "eal": null } } ] diff --git a/tests/test_common.py b/tests/test_common.py index 7b29dd8d..66065923 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,4 +1,7 @@ +import pytest + from sec_certs.cert_rules import cc_rules, fips_rules, rules +from sec_certs.utils.helpers import choose_lowest_eal def test_rules(): @@ -7,3 +10,18 @@ def test_rules(): for rule_group in rules: if rule_group not in ("cc_rules", "fips_rules", "cc_filename_cert_id"): assert rule_group in cc_rules or rule_group in fips_rules + + +@pytest.mark.parametrize( + "strings, expected", + [ + ({"EAL5", "EAL4+", "EAL3", "random", "EAL7+", "EAL2"}, "EAL2"), + ({"EAL1", "EAL1+", "EAL2", "EAL3+"}, "EAL1"), + ({"random", "no_match"}, None), + ({"EAL5+", "EAL6"}, "EAL5+"), + (set(), None), + ({"EAL100", "EAL10", "EAL20+"}, "EAL10"), + ], +) +def test_find_min_eal(strings, expected): + assert choose_lowest_eal(strings) == expected |
