aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Janovsky2025-01-25 18:20:49 +0100
committerJ08nY2025-02-01 22:57:08 +0100
commit2500d9536beb658853cff3d0baad30b8f414a817 (patch)
tree05bc8daaa46805eb90011054a6b868fc4557b4b9
parenta4ba427b7224f97267f0e54fd4c58164cc0dc4cb (diff)
downloadsec-certs-2500d9536beb658853cff3d0baad30b8f414a817.tar.gz
sec-certs-2500d9536beb658853cff3d0baad30b8f414a817.tar.zst
sec-certs-2500d9536beb658853cff3d0baad30b8f414a817.zip
implement PP processing
-rw-r--r--src/sec_certs/configuration.py4
-rw-r--r--src/sec_certs/constants.py38
-rw-r--r--src/sec_certs/dataset/auxiliary_dataset_handling.py69
-rw-r--r--src/sec_certs/dataset/cc.py127
-rw-r--r--src/sec_certs/dataset/dataset.py12
-rw-r--r--src/sec_certs/dataset/protection_profile.py422
-rw-r--r--src/sec_certs/heuristics/cc.py39
-rw-r--r--src/sec_certs/sample/cc.py151
-rw-r--r--src/sec_certs/sample/certificate.py7
-rw-r--r--src/sec_certs/sample/document_state.py61
-rw-r--r--src/sec_certs/sample/protection_profile.py324
-rw-r--r--src/sec_certs/utils/helpers.py19
-rw-r--r--tests/cc/conftest.py6
-rw-r--r--tests/cc/test_cc_analysis.py53
-rw-r--r--tests/cc/test_cc_dataset.py12
-rw-r--r--tests/data/cc/analysis/cc_full_dataset.json23
-rw-r--r--tests/data/cc/analysis/pp.json191
-rw-r--r--tests/data/cc/analysis/reference_dataset.json36
-rw-r--r--tests/data/cc/analysis/transitive_vulnerability_dataset.json56
-rw-r--r--tests/data/cc/analysis/vulnerable_dataset.json40
-rw-r--r--tests/data/cc/certificate/fictional_cert.json28
-rw-r--r--tests/data/cc/dataset/auxiliary_datasets/maintenances/maintenance_updates.json10
-rw-r--r--tests/data/cc/dataset/toy_dataset.json52
-rw-r--r--tests/test_common.py18
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