diff options
| author | J08nY | 2023-04-21 15:33:18 +0200 |
|---|---|---|
| committer | J08nY | 2023-04-21 15:33:18 +0200 |
| commit | c04a2fb105ed3196f0ccbfb55b9b5765a798d960 (patch) | |
| tree | 5c55b732b7aa00fd67e911db02bb509756123721 | |
| parent | 0c181f6c2748b4dea5e82fe87a1b3d04c7363076 (diff) | |
| parent | 5893352a41ce8e513dfbbb7c3e35cac3925966ea (diff) | |
| download | sec-certs-c04a2fb105ed3196f0ccbfb55b9b5765a798d960.tar.gz sec-certs-c04a2fb105ed3196f0ccbfb55b9b5765a798d960.tar.zst sec-certs-c04a2fb105ed3196f0ccbfb55b9b5765a798d960.zip | |
Merge branch 'fix/dup-dedup' into issue/324-Switch-from-NVD-data-feeds-to-API
33 files changed, 2426 insertions, 914 deletions
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 316e8679..961a1058 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,6 +16,10 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.8" + cache: "pip" + cache-dependency-path: | + requirements/requirements.txt + requirements/test_requirements.txt - name: Install python dependencies run: | pip install -r requirements/requirements.txt diff --git a/docs/quickstart.md b/docs/quickstart.md index d916bc5a..1088e51a 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -6,7 +6,7 @@ 1. Install the latest version with `pip install -U sec-certs && python -m spacy download en_core_web_sm` (see [installation](installation.md)). 2. In your Python interpreter, type ```python -from sec_certs.dataset import CCDataset +from sec_certs.dataset.cc import CCDataset dset = CCDataset.from_web_latest() ``` @@ -19,7 +19,7 @@ to obtain to obtain freshly processed dataset from [seccerts.org](https://seccer 1. Install the latest version with `pip install -U sec-certs && python -m spacy download en_core_web_sm` (see [installation](installation.md)). 2. In your Python interpreter, type ```python -from sec_certs.dataset import FIPSDataset +from sec_certs.dataset.fips import FIPSDataset dset = FIPSDataset.from_web_latest() ``` diff --git a/notebooks/fips/in_process.ipynb b/notebooks/fips/in_process.ipynb index 00ff5eb1..d127d854 100644 --- a/notebooks/fips/in_process.ipynb +++ b/notebooks/fips/in_process.ipynb @@ -7,18 +7,22 @@ "metadata": {}, "outputs": [], "source": [ + "from itertools import takewhile\n", + "from operator import itemgetter\n", + "\n", "from sec_certs.dataset.fips_mip import MIPDataset\n", "from sec_certs.dataset.fips_iut import IUTDataset\n", "from sec_certs.sample.fips_mip import MIPStatus\n", + "from sec_certs.model.fips_matching import FIPSProcessMatcher\n", + "from sec_certs.dataset.fips import FIPSDataset\n", + "from sec_certs.configuration import config\n", "import pandas as pd\n", "import seaborn as sns\n", "import matplotlib.pyplot as plt\n", - "import math\n", "import numpy as np\n", - "import tqdm\n", + "from tqdm import tqdm\n", "import matplotlib.ticker as mtick\n", "import warnings\n", - "from pathlib import Path\n", "\n", "plt.style.use(\"seaborn-whitegrid\")\n", "sns.set_palette(\"deep\")\n", @@ -28,6 +32,20 @@ ] }, { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "fips = FIPSDataset.from_web_latest()\n" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { "cell_type": "markdown", "id": "bd5f0fd6", "metadata": {}, @@ -97,10 +115,10 @@ "\n", "iut_first_seen = {}\n", "iut_last_seen = {}\n", - "for snapshot in sorted(iut_dset.snapshots, key=lambda x: x.timestamp):\n", + "for snapshot in tqdm(sorted(iut_dset.snapshots, key=lambda x: x.timestamp)):\n", " snapshot_date = snapshot.timestamp.date()\n", " for entry in snapshot.entries:\n", - " entry_key = entry #iut_key(entry) # or entry here\n", + " entry_key = entry # iut_key(entry) # or entry here\n", " if entry_key not in iut_first_seen:\n", " iut_first_seen[entry_key] = snapshot_date\n", " if entry_key not in iut_last_seen or iut_last_seen[entry_key] < snapshot_date:\n", @@ -183,6 +201,33 @@ }, { "cell_type": "markdown", + "source": [ + "### IUT - Certificate mapping" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "first_snapshot = iut_dset.snapshots[-1]\n", + "matches = FIPSProcessMatcher.match_snapshot(first_snapshot, fips)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } + }, + { + "cell_type": "markdown", "id": "cbc02977", "metadata": {}, "source": [ @@ -338,6 +383,33 @@ " g.set_titles(\"{col_name}\")\n", " plt.show()" ] + }, + { + "cell_type": "markdown", + "source": [ + "### MIP - Certificate matching" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%% md\n" + } + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "first_snapshot = mip_dset.snapshots[-1]\n", + "matches = FIPSProcessMatcher.match_snapshot(first_snapshot, fips)" + ], + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + } } ], "metadata": { @@ -366,4 +438,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +}
\ No newline at end of file diff --git a/src/sec_certs/cli.py b/src/sec_certs/cli.py index d1a1eb90..9bd74108 100644 --- a/src/sec_certs/cli.py +++ b/src/sec_certs/cli.py @@ -12,8 +12,9 @@ import click from pydantic import ValidationError from sec_certs.configuration import config -from sec_certs.dataset import CCDataset, FIPSDataset +from sec_certs.dataset.cc import CCDataset from sec_certs.dataset.dataset import Dataset +from sec_certs.dataset.fips import FIPSDataset from sec_certs.utils.helpers import warn_if_missing_poppler, warn_if_missing_tesseract logger = logging.getLogger(__name__) diff --git a/src/sec_certs/configuration.py b/src/sec_certs/configuration.py index 44523c7f..f4883dbd 100644 --- a/src/sec_certs/configuration.py +++ b/src/sec_certs/configuration.py @@ -79,6 +79,12 @@ class Configuration(BaseSettings): "https://seccerts.org/cpe/cpe_match_dataset.json.gz", description="URL for the latest snapshot of cpe match json.", ) + fips_matching_threshold: int = Field( + 90, + description="Level of required similarity before FIPS IUT/MIP entry is considered to match a FIPS certificate.", + ge=0, + le=100, + ) minimal_token_length: int = Field( 3, description="Minimal length of a string that will be considered as a token during keyword extraction in CVE matching", @@ -94,7 +100,12 @@ class Configuration(BaseSettings): cc_reference_annotator_should_train: bool = Field( True, description="True if new reference annotator model shall be build, False otherwise." ) - + cc_matching_threshold: int = Field( + 90, + description="Level of required similarity before CC scheme entry is considered to match a CC certificate.", + ge=0, + le=100, + ) enable_progress_bars: bool = Field( True, description="If true, progress bars will be printed to stdout during computation." ) diff --git a/src/sec_certs/constants.py b/src/sec_certs/constants.py index 56910307..b28b925d 100644 --- a/src/sec_certs/constants.py +++ b/src/sec_certs/constants.py @@ -69,8 +69,9 @@ GARBAGE_EVERY_SECOND_CHAR_THRESHOLD = 15 GARBAGE_ALPHA_CHARS_THRESHOLD = 0.5 CC_AUSTRALIA_BASE_URL = "https://www.cyber.gov.au" -CC_AUSTRALIA_CERTIFIED_URL = ( - CC_AUSTRALIA_BASE_URL + "/acsc/view-all-content/programs/australian-information-security-evaluation-program" +CC_AUSTRALIA_INEVAL_URL = ( + CC_AUSTRALIA_BASE_URL + + "/resources-business-and-government/assessment-and-evaluation-programs/australian-information-security-evaluation-program-aisep" ) CC_CANADA_CERTIFIED_URL = "https://www.cyber.gc.ca/en/tools-services/common-criteria/certified-products" CC_CANADA_INEVAL_URL = "https://www.cyber.gc.ca/en/tools-services/common-criteria/products-evaluation" @@ -81,32 +82,36 @@ CC_BSI_CERTIFIED_URL = ( CC_BSI_BASE_URL + "EN/Themen/Unternehmen-und-Organisationen/Standards-und-Zertifizierung/Zertifizierung-und-Anerkennung/Listen/Zertifizierte-Produkte-nach-CC/zertifizierte-produkte-nach-cc_node.html" ) -CC_INDIA_CERTIFIED_URL = "https://www.commoncriteria-india.gov.in/product-certified" -CC_INDIA_ARCHIVED_URL = "https://www.commoncriteria-india.gov.in/archived-prod-cer" +CC_INDIA_BASE_URL = "https://www.commoncriteria-india.gov.in" +CC_INDIA_CERTIFIED_URL = CC_INDIA_BASE_URL + "/Products-Certified" +CC_INDIA_ARCHIVED_URL = CC_INDIA_BASE_URL + "/Products-Archived" CC_ITALY_BASE_URL = "https://www.ocsi.gov.it" CC_ITALY_CERTIFIED_URL = CC_ITALY_BASE_URL + "/index.php/elenchi-certificazioni/prodotti-certificati.html" CC_ITALY_INEVAL_URL = CC_ITALY_BASE_URL + "/index.php/elenchi-certificazioni/in-corso-di-valutazione.html" -CC_JAPAN_BASE_URL = "https://www.ipa.go.jp/security/jisec/jisec_e" +CC_JAPAN_BASE_URL = "https://www.ipa.go.jp/en/security/jisec" CC_JAPAN_CERT_BASE_URL = CC_JAPAN_BASE_URL + "/certified_products" -CC_JAPAN_CERTIFIED_URL = CC_JAPAN_BASE_URL + "/certified_products/certfy_list_e31.html" -CC_JAPAN_ARCHIVED_URL = CC_JAPAN_BASE_URL + "/certified_products/certfy_list_e_archive.html" -CC_JAPAN_INEVAL_URL = CC_JAPAN_BASE_URL + "/prdct_in_eval.html" +CC_JAPAN_CERTIFIED_SW_URL = CC_JAPAN_BASE_URL + "/software/certified-cert/index.html" +CC_JAPAN_CERTIFIED_HW_URL = CC_JAPAN_BASE_URL + "/hardware/certified-cert/index.html" +CC_JAPAN_ARCHIVED_SW_URL = CC_JAPAN_BASE_URL + "/software/certified-cert/archive.html" +CC_JAPAN_INEVAL_URL = CC_JAPAN_BASE_URL + "/prdct-in-eval/in_eval_list.html" CC_MALAYSIA_BASE_URL = "https://iscb.cybersecurity.my" CC_MALAYSIA_CERTIFIED_URL = ( - CC_MALAYSIA_BASE_URL + "/en/index.php/certification/product-certification/mycc/certified-products-and-systems" + CC_MALAYSIA_BASE_URL + "/index.php/certification/product-certification/mycc/certified-products-and-systems" ) CC_MALAYSIA_INEVAL_URL = ( CC_MALAYSIA_BASE_URL - + "/en/index.php/certification/product-certification/mycc/list-of-products-and-systems-under-evaluation-or-maintenance" + + "/index.php/certification/product-certification/mycc/list-of-products-and-systems-under-evaluation-or-maintenance" ) CC_NETHERLANDS_BASE_URL = "https://www.tuv-nederland.nl/common-criteria" CC_NETHERLANDS_CERTIFIED_URL = CC_NETHERLANDS_BASE_URL + "/certificates.html" CC_NETHERLANDS_INEVAL_URL = CC_NETHERLANDS_BASE_URL + "/ongoing-certifications.html" -CC_NORWAY_CERTIFIED_URL = "https://sertit.no/certified-products/category1919.html" -CC_NORWAY_ARCHIVED_URL = "https://sertit.no/certified-products/product-archive/" -CC_KOREA_EN_URL = "https://itscc.kr/main/main.do?accessMode=home_en" -CC_KOREA_CERTIFIED_URL = "https://itscc.kr/certprod/list.do" -CC_KOREA_PRODUCT_URL = "https://itscc.kr/certprod/view.do?product_id={}&product_class=1" +CC_NORWAY_BASE_URL = "https://sertit.no" +CC_NORWAY_CERTIFIED_URL = CC_NORWAY_BASE_URL + "/certified-products/category1919.html" +CC_NORWAY_ARCHIVED_URL = CC_NORWAY_BASE_URL + "/certified-products/product-archive/" +CC_KOREA_BASE_URL = "https://itscc.kr" +CC_KOREA_EN_URL = CC_KOREA_BASE_URL + "/main/main.do?accessMode=home_en" +CC_KOREA_CERTIFIED_URL = CC_KOREA_BASE_URL + "/certprod/list.do" +CC_KOREA_PRODUCT_URL = CC_KOREA_BASE_URL + "/certprod/view.do?product_id={}&product_class=1" CC_SINGAPORE_BASE_URL = "https://www.csa.gov.sg" CC_SINGAPORE_CERTIFIED_URL = ( CC_SINGAPORE_BASE_URL + "/Programmes/certification-and-labelling-schemes/csa-common-criteria/product-list" @@ -114,6 +119,11 @@ CC_SINGAPORE_CERTIFIED_URL = ( CC_SINGAPORE_ARCHIVED_URL = ( CC_SINGAPORE_BASE_URL + "/Programmes/certification-and-labelling-schemes/csa-common-criteria/product-archives" ) +CC_SINGAPORE_API_URL = CC_SINGAPORE_BASE_URL + "/api/CsaCommonProductCriteria/getProduct" +CC_SINGAPORE_INEVAL_URL = ( + CC_SINGAPORE_BASE_URL + + "/our-programmes/certification-and-labelling-schemes/singapore-common-criteria-scheme/product-list/in-evaluation" +) CC_SPAIN_BASE_URL = "https://oc.ccn.cni.es" CC_SPAIN_CERTIFIED_URL = CC_SPAIN_BASE_URL + "/en/certified-products/certified-products" CC_SWEDEN_BASE_URL = "https://www.fmv.se" @@ -122,6 +132,7 @@ CC_SWEDEN_INEVAL_URL = CC_SWEDEN_BASE_URL + "/verksamhet/ovrig-verksamhet/csec/p CC_SWEDEN_ARCHIVED_URL = CC_SWEDEN_BASE_URL + "/verksamhet/ovrig-verksamhet/csec/arkiverade-certifikat-aldre-an-5-ar/" CC_TURKEY_ARCHIVED_URL = "https://statik.tse.org.tr/upload/tr/dosya/icerikyonetimi/3300/03112021143434-2.pdf" CC_USA_BASE_URL = "https://www.niap-ccevs.org" +CC_USA_PRODUCT_URL = CC_USA_BASE_URL + "/Product/" CC_USA_CERTIFIED_URL = CC_USA_BASE_URL + "/Product/PCL.cfm" CC_USA_INEVAL_URL = CC_USA_BASE_URL + "/Product/PINE.cfm" CC_USA_ARCHIVED_URL = CC_USA_BASE_URL + "/Product/Archived.cfm" diff --git a/src/sec_certs/dataset/__init__.py b/src/sec_certs/dataset/__init__.py index c557c630..c6407d40 100644 --- a/src/sec_certs/dataset/__init__.py +++ b/src/sec_certs/dataset/__init__.py @@ -1,6 +1,7 @@ """This package exposes Datasets of various Samples, both primary (Common Criteria, FIPS) and auxiliary (CVEs, CPEs, ...)""" -from sec_certs.dataset.cc import CCDataset, CCDatasetMaintenanceUpdates, CCSchemeDataset +from sec_certs.dataset.cc import CCDataset, CCDatasetMaintenanceUpdates +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 import FIPSDataset diff --git a/src/sec_certs/dataset/cc.py b/src/sec_certs/dataset/cc.py index c2d9ca0c..3ca3dff7 100644 --- a/src/sec_certs/dataset/cc.py +++ b/src/sec_certs/dataset/cc.py @@ -11,27 +11,28 @@ from typing import ClassVar, Iterator, cast import numpy as np import pandas as pd -import requests -from bs4 import BeautifulSoup, NavigableString, Tag +from bs4 import BeautifulSoup, Tag import sec_certs.utils.sanitization from sec_certs import constants from sec_certs.configuration import config +from sec_certs.dataset.cc_scheme import CCSchemeDataset from sec_certs.dataset.cpe import CPEDataset from sec_certs.dataset.cve import CVEDataset from sec_certs.dataset.dataset import AuxiliaryDatasets, Dataset, logger from sec_certs.dataset.protection_profile import ProtectionProfileDataset +from sec_certs.model.cc_matching import CCSchemeMatcher from sec_certs.model.reference_finder import ReferenceFinder from sec_certs.model.sar_transformer import SARTransformer from sec_certs.model.transitive_vulnerability_finder import TransitiveVulnerabilityFinder from sec_certs.sample.cc import CCCertificate from sec_certs.sample.cc_certificate_id import CertificateId from sec_certs.sample.cc_maintenance_update import CCMaintenanceUpdate +from sec_certs.sample.cc_scheme import EntryType from sec_certs.sample.protection_profile import ProtectionProfile 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.sanitization import sanitize_navigable_string as sns @dataclass @@ -40,6 +41,7 @@ class CCAuxiliaryDatasets(AuxiliaryDatasets): cve_dset: CVEDataset | None = None pp_dset: ProtectionProfileDataset | None = None mu_dset: CCDatasetMaintenanceUpdates | None = None + scheme_dset: CCSchemeDataset | None = None class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializableType): @@ -140,7 +142,7 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable @property def pp_dataset_path(self) -> Path: """ - Returns directory that holds files associated with Protection profiles + Returns a path to the dataset of Protection Profiles """ return self.auxiliary_datasets_dir / "pp_dataset.json" @@ -154,10 +156,17 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable @property def mu_dataset_path(self) -> Path: """ - Returns json that holds the datase of maintenance updates + Returns a path to the dataset of maintenance updates """ return self.mu_dataset_dir / "maintenance_updates.json" + @property + def scheme_dataset_path(self) -> Path: + """ + Returns a path to the scheme dataset + """ + return self.auxiliary_datasets_dir / "scheme_dataset.json" + BASE_URL: ClassVar[str] = "https://www.commoncriteriaportal.org" HTML_PRODUCTS_URL = { @@ -345,7 +354,7 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable # TODO: Now skipping bad lines, smarter heuristics to be built for dumb files df = pd.read_csv(file, engine="python", encoding="windows-1252", on_bad_lines="skip") - df = df.rename(columns={x: y for (x, y) in zip(list(df.columns), csv_header)}) + df = df.rename(columns=dict(zip(list(df.columns), csv_header))) df["is_maintenance"] = ~df.maintenance_title.isnull() df = df.fillna(value="") @@ -496,7 +505,7 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable "Products for Digital Signatures", "Trusted Computing", ] - cat_dict = {x: y for (x, y) in zip(cc_table_ids, cc_categories)} + cat_dict = dict(zip(cc_table_ids, cc_categories)) with file.open("r") as handle: soup = BeautifulSoup(handle, "html5lib") @@ -734,11 +743,12 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable def process_auxiliary_datasets(self, download_fresh: bool = False) -> None: """ Processes all auxiliary datasets needed during computation. On top of base-class processing, - CC handles protection profiles and maintenance updates. + CC handles protection profiles, maintenance updates and schemes. """ super().process_auxiliary_datasets(download_fresh) self.auxiliary_datasets.pp_dset = self.process_protection_profiles(to_download=download_fresh) self.auxiliary_datasets.mu_dset = self.process_maintenance_updates(to_download=download_fresh) + self.auxiliary_datasets.scheme_dset = self.process_schemes(to_download=download_fresh) def process_protection_profiles( self, to_download: bool = True, keep_metadata: bool = True @@ -799,6 +809,30 @@ class CCDataset(Dataset[CCCertificate, CCAuxiliaryDatasets], ComplexSerializable return update_dset + def process_schemes(self, to_download: bool = True, only_schemes: set[str] | None = None) -> CCSchemeDataset: + """ + Downloads or loads from json a dataset of CC scheme data. + """ + logger.info("Processing CC schemes") + + self.auxiliary_datasets_dir.mkdir(parents=True, exist_ok=True) + + if to_download or not self.scheme_dataset_path.exists(): + scheme_dset = CCSchemeDataset.from_web(only_schemes) + scheme_dset.to_json(self.scheme_dataset_path) + else: + scheme_dset = CCSchemeDataset.from_json(self.scheme_dataset_path) + + for scheme in scheme_dset: + certified = scheme.lists.get(EntryType.Certified) + if certified: + matches = CCSchemeMatcher.match_all(certified, scheme.country, self) + for dgst, match in matches.items(): + self[dgst].heuristics.scheme_data = match + # TODO: Archived?? + + return scheme_dset + class CCDatasetMaintenanceUpdates(CCDataset, ComplexSerializableType): """ @@ -884,762 +918,3 @@ class CCDatasetMaintenanceUpdates(CCDataset, ComplexSerializableType): main_dates.maintenance_date = main_dates.maintenance_date.map(lambda x: [x]) main_dates.index.name = "dgst" return main_dates.groupby("related_cert_digest").maintenance_date.agg("sum").rename("maintenance_dates") - - -class CCSchemeDataset: - @staticmethod - def _download_page(url, session=None): - conn = session if session else requests - resp = conn.get(url, headers={"User-Agent": "seccerts.org"}, verify=False) - if resp.status_code != requests.codes.ok: - raise ValueError(f"Unable to download: status={resp.status_code}") - return BeautifulSoup(resp.content, "html5lib") - - @staticmethod - def get_australia_in_evaluation(): - # TODO: Information could be expanded by following url. - soup = CCSchemeDataset._download_page(constants.CC_AUSTRALIA_CERTIFIED_URL) - header = soup.find("h2", text="Products in evaluation") - table = header.find_next_sibling("table") - results = [] - for tr in table.find_all("tr"): - tds = tr.find_all("td") - if not tds: - continue - cert = { - "vendor": sns(tds[0].text), - "product": sns(tds[1].text), - "url": constants.CC_AUSTRALIA_BASE_URL + tds[1].find("a")["href"], - "level": sns(tds[2].text), - } - results.append(cert) - return results - - @staticmethod - def get_canada_certified(): - soup = CCSchemeDataset._download_page(constants.CC_CANADA_CERTIFIED_URL) - tbody = soup.find("table").find("tbody") - results = [] - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - if not tds: - continue - cert = { - "product": sns(tds[0].text), - "vendor": sns(tds[1].text), - "level": sns(tds[2].text), - "certification_date": sns(tds[3].text), - } - results.append(cert) - return results - - @staticmethod - def get_canada_in_evaluation(): - soup = CCSchemeDataset._download_page(constants.CC_CANADA_INEVAL_URL) - tbody = soup.find("table").find("tbody") - results = [] - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - if not tds: - continue - cert = { - "product": sns(tds[0].text), - "vendor": sns(tds[1].text), - "level": sns(tds[2].text), - "cert_lab": sns(tds[3].text), - } - results.append(cert) - return results - - @staticmethod - def get_france_certified(): - # TODO: Information could be expanded by following product link. - base_soup = CCSchemeDataset._download_page(constants.CC_ANSSI_CERTIFIED_URL) - category_nav = base_soup.find("ul", class_="nav-categories") - results = [] - for li in category_nav.find_all("li"): - a = li.find("a") - url = a["href"] - category_name = sns(a.text) - soup = CCSchemeDataset._download_page(constants.CC_ANSSI_BASE_URL + url) - table = soup.find("table", class_="produits-liste cc") - if not table: - continue - tbody = table.find("tbody") - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - if not tds: - continue - cert = { - "product": sns(tds[0].text), - "vendor": sns(tds[1].text), - "level": sns(tds[2].text), - "id": sns(tds[3].text), - "certification_date": sns(tds[4].text), - "category": category_name, - "url": constants.CC_ANSSI_BASE_URL + tds[0].find("a")["href"], - } - results.append(cert) - return results - - @staticmethod - def get_germany_certified(): - # TODO: Information could be expanded by following url. - base_soup = CCSchemeDataset._download_page(constants.CC_BSI_CERTIFIED_URL) - category_nav = base_soup.find("ul", class_="no-bullet row") - results = [] - for li in category_nav.find_all("li"): - a = li.find("a") - url = a["href"] - category_name = sns(a.text) - soup = CCSchemeDataset._download_page(constants.CC_BSI_BASE_URL + url) - content = soup.find("div", class_="content").find("div", class_="column") - for table in content.find_all("table"): - tbody = table.find("tbody") - header = table.find_parent("div", class_="wrapperTable").find_previous_sibling("h2") - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - if len(tds) != 4: - continue - cert = { - "cert_id": sns(tds[0].text), - "product": sns(tds[1].text), - "vendor": sns(tds[2].text), - "certification_date": sns(tds[3].text), - "category": category_name, - "url": constants.CC_BSI_BASE_URL + tds[0].find("a")["href"], - } - if header is not None: - cert["subcategory"] = sns(header.text) - results.append(cert) - return results - - @staticmethod - def get_india_certified(): - pages = {0} - seen_pages = set() - results = [] - while pages: - page = pages.pop() - seen_pages.add(page) - url = constants.CC_INDIA_CERTIFIED_URL + f"?page={page}" - soup = CCSchemeDataset._download_page(url) - - # Update pages - pager = soup.find("ul", class_="pager") - for li in pager.find_all("li"): - try: - new_page = int(li.text) - 1 - except Exception: - continue - if new_page not in seen_pages: - pages.add(new_page) - - # Parse table - tbody = soup.find("div", class_="view-content").find("table").find("tbody") - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - if not tds: - continue - report_a = tds[5].find("a") - target_a = tds[6].find("a") - cert_a = tds[7].find("a") - cert = { - "serial_number": sns(tds[0].text), - "product": sns(tds[1].text), - "sponsor": sns(tds[2].text), - "developer": sns(tds[3].text), - "level": sns(tds[4].text), - "report_link": report_a["href"], - "report_name": sns(report_a.text), - "target_link": target_a["href"], - "target_name": sns(target_a.text), - "cert_link": cert_a["href"], - "cert_name": sns(cert_a.text), - } - results.append(cert) - return results - - @staticmethod - def get_india_archived(): - pages = {0} - seen_pages = set() - results = [] - while pages: - page = pages.pop() - seen_pages.add(page) - url = constants.CC_INDIA_ARCHIVED_URL + f"?page={page}" - soup = CCSchemeDataset._download_page(url) - - # Update pages - pager = soup.find("ul", class_="pager") - if pager: - for li in pager.find_all("li"): - try: - new_page = int(li.text) - 1 - except Exception: - continue - if new_page not in seen_pages: - pages.add(new_page) - - # Parse table - tbody = soup.find("div", class_="view-content").find("table").find("tbody") - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - if not tds: - continue - report_a = tds[5].find("a") - target_a = tds[6].find("a") - cert_a = tds[7].find("a") - cert = { - "serial_number": sns(tds[0].text), - "product": sns(tds[1].text), - "sponsor": sns(tds[2].text), - "developer": sns(tds[3].text), - "level": sns(tds[4].text), - "report_link": report_a["href"], - "report_name": sns(report_a.text), - "target_link": target_a["href"], - "target_name": sns(target_a.text), - "cert_link": cert_a["href"], - "cert_name": sns(cert_a.text), - "certification_date": sns(tds[8].text), - } - results.append(cert) - return results - - @staticmethod - def get_italy_certified(): # noqa: C901 - soup = CCSchemeDataset._download_page(constants.CC_ITALY_CERTIFIED_URL) - div = soup.find("div", class_="certificati") - results = [] - for cert_div in div.find_all("div", recursive=False): - title = cert_div.find("h3").text - data_div = cert_div.find("div", class_="collapse") - cert = {"title": title} - for data_p in data_div.find_all("p"): - p_text = sns(data_p.text) - if ":" not in p_text: - continue - p_name, p_data = p_text.split(":") - p_data = p_data - p_link = data_p.find("a") - if "Fornitore" in p_name: - cert["supplier"] = p_data - elif "Livello di garanzia" in p_name: - cert["level"] = p_data - elif "Data emissione certificato" in p_name: - cert["certification_date"] = p_data - elif "Data revisione" in p_name: - cert["revision_date"] = p_data - elif "Rapporto di Certificazione" in p_name and p_link: - cert["report_link_it"] = constants.CC_ITALY_BASE_URL + p_link["href"] - elif "Certification Report" in p_name and p_link: - cert["report_link_en"] = constants.CC_ITALY_BASE_URL + p_link["href"] - elif "Traguardo di Sicurezza" in p_name and p_link: - cert["target_link"] = constants.CC_ITALY_BASE_URL + p_link["href"] - elif "Nota su" in p_name and p_link: - cert["vulnerability_note_link"] = constants.CC_ITALY_BASE_URL + p_link["href"] - elif "Nota di chiarimento" in p_name and p_link: - cert["clarification_note_link"] = constants.CC_ITALY_BASE_URL + p_link["href"] - results.append(cert) - return results - - @staticmethod - def get_italy_in_evaluation(): - soup = CCSchemeDataset._download_page(constants.CC_ITALY_INEVAL_URL) - div = soup.find("div", class_="valutazioni") - results = [] - for cert_div in div.find_all("div", recursive=False): - title = cert_div.find("h3").text - data_div = cert_div.find("div", class_="collapse") - cert = {"title": title} - for data_p in data_div.find_all("p"): - p_text = sns(data_p.text) - if ":" not in p_text: - continue - p_name, p_data = p_text.split(":") - p_data = p_data - if "Committente" in p_name: - cert["client"] = p_data - elif "Livello di garanzia" in p_name: - cert["level"] = p_data - elif "Tipologia prodotto" in p_name: - cert["product_type"] = p_data - results.append(cert) - return results - - @staticmethod - def get_japan_certified(): - # TODO: Information could be expanded by following toe link. - soup = CCSchemeDataset._download_page(constants.CC_JAPAN_CERTIFIED_URL) - table = soup.find("div", id="cert_list").find("table") - results = [] - trs = list(table.find_all("tr")) - for tr in trs: - tds = tr.find_all("td") - if not tds: - continue - if len(tds) == 6: - cert = { - "cert_id": sns(tds[0].text), - "supplier": sns(tds[1].text), - "toe_overseas_name": sns(tds[2].text), - "certification_date": sns(tds[3].text), - "claim": sns(tds[4].text), - } - toe_a = tds[2].find("a") - if toe_a and "href" in toe_a.attrs: - cert["toe_overseas_link"] = constants.CC_JAPAN_CERT_BASE_URL + "/" + toe_a["href"] - results.append(cert) - if len(tds) == 1: - cert = results[-1] - cert["toe_japan_name"] = sns(tds[0].text) - toe_a = tds[0].find("a") - if toe_a and "href" in toe_a.attrs: - cert["toe_japan_link"] = constants.CC_JAPAN_CERT_BASE_URL + "/" + toe_a["href"] - return results - - @staticmethod - def get_japan_archived(): - # TODO: Information could be expanded by following toe link. - soup = CCSchemeDataset._download_page(constants.CC_JAPAN_ARCHIVED_URL) - table = soup.find("table") - results = [] - trs = list(table.find_all("tr")) - for tr in trs: - tds = tr.find_all("td") - if not tds: - continue - if len(tds) == 6: - cert = { - "cert_id": sns(tds[0].text), - "supplier": sns(tds[1].text), - "toe_overseas_name": sns(tds[2].text), - "certification_date": sns(tds[3].text), - "claim": sns(tds[4].text), - } - toe_a = tds[2].find("a") - if toe_a and "href" in toe_a.attrs: - cert["toe_overseas_link"] = constants.CC_JAPAN_CERT_BASE_URL + "/" + toe_a["href"] - results.append(cert) - if len(tds) == 1: - cert = results[-1] - cert["toe_japan_name"] = sns(tds[0].text) - toe_a = tds[0].find("a") - if toe_a and "href" in toe_a.attrs: - cert["toe_japan_link"] = constants.CC_JAPAN_CERT_BASE_URL + "/" + toe_a["href"] - return results - - @staticmethod - def get_japan_in_evaluation(): - # TODO: Information could be expanded by following toe link. - soup = CCSchemeDataset._download_page(constants.CC_JAPAN_INEVAL_URL) - table = soup.find("table") - results = [] - for tr in table.find_all("tr"): - tds = tr.find_all("td") - if not tds: - continue - toe_a = tds[1].find("a") - cert = { - "supplier": sns(tds[0].text), - "toe_name": sns(toe_a.text), - "toe_link": constants.CC_JAPAN_BASE_URL + "/" + toe_a["href"], - "claim": sns(tds[2].text), - } - results.append(cert) - return results - - @staticmethod - def get_malaysia_certified(): - soup = CCSchemeDataset._download_page(constants.CC_MALAYSIA_CERTIFIED_URL) - main_div = soup.find("div", attrs={"itemprop": "articleBody"}) - tables = main_div.find_all("table", recursive=False) - results = [] - for table in tables: - category_name = sns(table.find_previous_sibling("h3").text) - for tr in table.find_all("tr")[1:]: - tds = tr.find_all("td") - if len(tds) != 6: - continue - cert = { - "category": category_name, - "level": sns(tds[0].text), - "cert_id": sns(tds[1].text), - "certification_date": sns(tds[2].text), - "product": sns(tds[3].text), - "developer": sns(tds[4].text), - } - results.append(cert) - return results - - @staticmethod - def get_malaysia_in_evaluation(): - soup = CCSchemeDataset._download_page(constants.CC_MALAYSIA_INEVAL_URL) - main_div = soup.find("div", attrs={"itemprop": "articleBody"}) - tables = main_div.find_all("table", recursive=False) - results = [] - for table in tables: - category_name = sns(table.find_previous_sibling("h3").text) - for tr in table.find_all("tr")[1:]: - tds = tr.find_all("td") - if len(tds) != 5: - continue - cert = { - "category": category_name, - "level": sns(tds[0].text), - "project_id": sns(tds[1].text), - "toe_name": sns(tds[2].text), - "developer": sns(tds[3].text), - "expected_completion": sns(tds[4].text), - } - results.append(cert) - return results - - @staticmethod - def get_netherlands_certified(): - soup = CCSchemeDataset._download_page(constants.CC_NETHERLANDS_CERTIFIED_URL) - main_div = soup.select("body > main > div > div > div > div:nth-child(2) > div.col-lg-9 > div:nth-child(3)")[0] - rows = main_div.find_all("div", class_="row", recursive=False) - modals = main_div.find_all("div", class_="modal", recursive=False) - results = [] - for row, modal in zip(rows, modals): - row_entries = row.find_all("a") - modal_trs = modal.find_all("tr") - cert = { - "manufacturer": sns(row_entries[0].text), - "product": sns(row_entries[1].text), - "scheme": sns(row_entries[2].text), - "cert_id": sns(row_entries[3].text), - } - for tr in modal_trs: - th_text = tr.find("th").text - td = tr.find("td") - if "Manufacturer website" in th_text: - cert["manufacturer_link"] = td.find("a")["href"] - elif "Assurancelevel" in th_text: - cert["level"] = sns(td.text) - elif "Certificate" in th_text: - cert["cert_link"] = constants.CC_NETHERLANDS_BASE_URL + td.find("a")["href"] - elif "Certificationreport" in th_text: - cert["report_link"] = constants.CC_NETHERLANDS_BASE_URL + td.find("a")["href"] - elif "Securitytarget" in th_text: - cert["target_link"] = constants.CC_NETHERLANDS_BASE_URL + td.find("a")["href"] - elif "Maintenance report" in th_text: - cert["maintenance_link"] = constants.CC_NETHERLANDS_BASE_URL + td.find("a")["href"] - results.append(cert) - return results - - @staticmethod - def get_netherlands_in_evaluation(): - soup = CCSchemeDataset._download_page(constants.CC_NETHERLANDS_INEVAL_URL) - table = soup.find("table") - results = [] - for tr in table.find_all("tr")[1:]: - tds = tr.find_all("td") - cert = { - "developer": sns(tds[0].text), - "product": sns(tds[1].text), - "category": sns(tds[2].text), - "level": sns(tds[3].text), - "certification_id": sns(tds[4].text), - } - results.append(cert) - return results - - @staticmethod - def _get_norway(url): - # TODO: Information could be expanded by following product link. - soup = CCSchemeDataset._download_page(url) - results = [] - for tr in soup.find_all("tr", class_="certified-product"): - tds = tr.find_all("td") - cert = { - "product": sns(tds[0].text), - "product_link": tds[0].find("a")["href"], - "category": sns(tds[1].find("p", class_="value").text), - "developer": sns(tds[2].find("p", class_="value").text), - "certification_date": sns(tds[3].find("time").text), - } - results.append(cert) - return results - - @staticmethod - def get_norway_certified(): - return CCSchemeDataset._get_norway(constants.CC_NORWAY_CERTIFIED_URL) - - @staticmethod - def get_norway_archived(): - return CCSchemeDataset._get_norway(constants.CC_NORWAY_ARCHIVED_URL) - - @staticmethod - def _get_korea(product_class): - # TODO: Information could be expanded by following product link. - session = requests.session() - session.get(constants.CC_KOREA_EN_URL) - # Get base page - url = constants.CC_KOREA_CERTIFIED_URL + f"?product_class={product_class}" - soup = CCSchemeDataset._download_page(url, session=session) - seen_pages = set() - pages = {1} - results = [] - while pages: - page = pages.pop() - csrf = soup.find("form", id="fm").find("input", attrs={"name": "csrf"})["value"] - resp = session.post(url, data={"csrf": csrf, "selectPage": page, "product_class": product_class}) - soup = BeautifulSoup(resp.content, "html5lib") - tbody = soup.find("table", class_="cpl").find("tbody") - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - if len(tds) != 6: - continue - link = tds[0].find("a") - id = link["id"].split("-")[1] - cert = { - "product": sns(tds[0].text), - "cert_id": sns(tds[1].text), - "product_link": constants.CC_KOREA_PRODUCT_URL.format(id), - "vendor": sns(tds[2].text), - "level": sns(tds[3].text), - "category": sns(tds[4].text), - "certification_date": sns(tds[5].text), - } - results.append(cert) - seen_pages.add(page) - page_links = soup.find("div", class_="paginate").find_all("a", class_="number_off") - for page_link in page_links: - try: - new_page = int(page_link.text) - if new_page not in seen_pages: - pages.add(new_page) - except Exception: - pass - return results - - @staticmethod - def get_korea_certified(): - return CCSchemeDataset._get_korea(product_class=1) - - @staticmethod - def get_korea_suspended(): - return CCSchemeDataset._get_korea(product_class=2) - - @staticmethod - def get_korea_archived(): - return CCSchemeDataset._get_korea(product_class=4) - - @staticmethod - def _get_singapore(url): - soup = CCSchemeDataset._download_page(url) - table = soup.find("table") - skip = False - results = [] - category_name = None - for tr in table.find_all("tr"): - if skip: - skip = False - continue - tds = tr.find_all("td") - if len(tds) == 1: - category_name = sns(tds[0].text) - skip = True - continue - - cert = { - "product": sns(tds[0].text.split()[0]), - "vendor": sns(tds[1].text), - "level": sns(tds[2].text), - "certification_date": sns(tds[3].text), - "expiration_date": sns(tds[4].text), - "category": category_name, - } - for link in tds[0].find_all("a"): - link_text = sns(link.text) - if link_text == "Certificate": - cert["cert_link"] = constants.CC_SINGAPORE_BASE_URL + link["href"] - elif link_text in ("Certificate Report", "Certification Report"): - cert["report_link"] = constants.CC_SINGAPORE_BASE_URL + link["href"] - elif link_text == "Security Target": - cert["target_link"] = constants.CC_SINGAPORE_BASE_URL + link["href"] - results.append(cert) - return results - - @staticmethod - def get_singapore_certified(): - return CCSchemeDataset._get_singapore(constants.CC_SINGAPORE_CERTIFIED_URL) - - @staticmethod - def get_singapore_in_evaluation(): - soup = CCSchemeDataset._download_page(constants.CC_SINGAPORE_CERTIFIED_URL) - header = soup.find(lambda x: x.name == "h3" and x.text == "In Evaluation") - table = header.find_next("table") - results = [] - for tr in table.find_all("tr")[1:]: - tds = tr.find_all("td") - cert = { - "name": sns(tds[0].text), - "vendor": sns(tds[1].text), - "level": sns(tds[2].text), - } - results.append(cert) - return results - - @staticmethod - def get_singapore_archived(): - return CCSchemeDataset._get_singapore(constants.CC_SINGAPORE_ARCHIVED_URL) - - @staticmethod - def get_spain_certified(): - soup = CCSchemeDataset._download_page(constants.CC_SPAIN_CERTIFIED_URL) - tbody = soup.find("table", class_="djc_items_table").find("tbody") - results = [] - for tr in tbody.find_all("tr", recursive=False): - tds = tr.find_all("td") - cert = { - "product": sns(tds[0].text), - "product_link": constants.CC_SPAIN_BASE_URL + tds[0].find("a")["href"], - "category": sns(tds[1].text), - "manufacturer": sns(tds[2].text), - "certification_date": sns(tds[3].find("td", class_="djc_value").text), - } - results.append(cert) - return results - - @staticmethod - def _get_sweden(url): - # TODO: Information could be expanded by following product link. - soup = CCSchemeDataset._download_page(url) - nav = soup.find("main").find("nav", class_="component-nav-box__list") - results = [] - for link in nav.find_all("a"): - cert = {"product": sns(link.text), "product_link": constants.CC_SWEDEN_BASE_URL + link["href"]} - results.append(cert) - return results - - @staticmethod - def get_sweden_certified(): - return CCSchemeDataset._get_sweden(constants.CC_SWEDEN_CERTIFIED_URL) - - @staticmethod - def get_sweden_in_evaluation(): - return CCSchemeDataset._get_sweden(constants.CC_SWEDEN_INEVAL_URL) - - @staticmethod - def get_sweden_archived(): - return CCSchemeDataset._get_sweden(constants.CC_SWEDEN_ARCHIVED_URL) - - @staticmethod - def get_turkey_certified(): - import tabula - - results = [] - with tempfile.TemporaryDirectory() as tmpdir: - pdf_path = Path(tmpdir) / "turkey.pdf" - resp = requests.get(constants.CC_TURKEY_ARCHIVED_URL) - if resp.status_code != requests.codes.ok: - raise ValueError(f"Unable to download: status={resp.status_code}") - with pdf_path.open("wb") as f: - f.write(resp.content) - dfs = tabula.read_pdf(str(pdf_path), pages="all") - for df in dfs: - for line in df.values: - cert = { - # TODO: Split item number and generate several dicts for a range they include. - "item_no": line[0], - "developer": line[1], - "product": line[2], - "cc_version": line[3], - "level": line[4], - "cert_lab": line[5], - "certification_date": line[6], - "expiration_date": line[7], - # TODO: Parse "Ongoing Evaluation" out of this field as well. - "archived": isinstance(line[9], str) and "Archived" in line[9], - } - results.append(cert) - return results - - @staticmethod - def get_usa_certified(): - # TODO: Information could be expanded by following product link. - # TODO: Information could be expanded by following the cc_claims (has links to protection profiles). - soup = CCSchemeDataset._download_page(constants.CC_USA_CERTIFIED_URL) - tbody = soup.find("table", class_="tablesorter").find("tbody") - results = [] - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - vendor_span = tds[0].find("span", class_="b u") - product_link = tds[0].find("a") - scheme_img = tds[6].find("img") - # Only return the US certifications. - if scheme_img["title"] != "USA": - continue - cert = { - "product": sns(product_link.text), - "vendor": sns(vendor_span.text), - "product_link": product_link["href"], - "id": sns(tds[1].text), - "cc_claim": sns(tds[2].text), - "cert_lab": sns(tds[3].text), - "certification_date": sns(tds[4].text), - "assurance_maintenance_date": sns(tds[5].text), - } - results.append(cert) - return results - - @staticmethod - def get_usa_in_evaluation(): - # TODO: Information could be expanded by following the cc_claims (has links to protection profiles). - soup = CCSchemeDataset._download_page(constants.CC_USA_INEVAL_URL) - tbody = soup.find("table", class_="tablesorter").find("tbody") - results = [] - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - vendor_span = tds[0].find("span", class_="b u") - product_name = None - for child in tds[0].children: - if isinstance(child, NavigableString): - product_name = sns(child) - break - cert = { - "vendor": sns(vendor_span.text), - "id": sns(tds[1].text), - "cc_claim": sns(tds[2].text), - "cert_lab": sns(tds[3].text), - "kickoff_date": sns(tds[4].text), - } - if product_name: - cert["product"] = product_name - results.append(cert) - return results - - @staticmethod - def get_usa_archived(): - # TODO: Information could be expanded by following the cc_claims (has links to protection profiles). - soup = CCSchemeDataset._download_page(constants.CC_USA_ARCHIVED_URL) - tbody = soup.find("table", class_="tablesorter").find("tbody") - results = [] - for tr in tbody.find_all("tr"): - tds = tr.find_all("td") - scheme_img = tds[5].find("img") - # Only return the US certifications. - if scheme_img["title"] != "USA": - continue - vendor_span = tds[0].find("span", class_="b u") - product_name = None - for child in tds[0].children: - if isinstance(child, NavigableString): - product_name = sns(child) - break - cert = { - "vendor": sns(vendor_span.text), - "id": sns(tds[1].text), - "cc_claim": sns(tds[2].text), - "cert_lab": sns(tds[3].text), - "certification_date": sns(tds[4].text), - } - if product_name: - cert["product"] = product_name - results.append(cert) - return results diff --git a/src/sec_certs/dataset/cc_scheme.py b/src/sec_certs/dataset/cc_scheme.py new file mode 100644 index 00000000..5ac23dc4 --- /dev/null +++ b/src/sec_certs/dataset/cc_scheme.py @@ -0,0 +1,60 @@ +# This code is not a place of honor... no highly esteemed deed is commemorated here... nothing valued is here. +# What follows is a repulsive wall of BeautifulSoup garbage parsing code. +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Mapping + +from sec_certs import constants +from sec_certs.dataset.json_path_dataset import JSONPathDataset +from sec_certs.sample.cc_scheme import CCScheme +from sec_certs.serialization.json import ComplexSerializableType + +logger = logging.getLogger() + + +class CCSchemeDataset(JSONPathDataset, ComplexSerializableType): + """ + A dataset of data from CC scheme websites. + + Each `.get_*` method returns a list of dict entries from the given scheme. + The entries do not share many keys, but each one has at least some form + of a product name and most have a vendor/developer/manufacturer field. + """ + + def __init__(self, schemes: dict[str, CCScheme], json_path: str | Path = constants.DUMMY_NONEXISTING_PATH): + self.schemes = schemes + self.json_path = Path(json_path) + + @property + def serialized_attributes(self) -> list[str]: + return ["schemes"] + + def __iter__(self): + yield from self.schemes.values() + + def __getitem__(self, scheme: str): + return self.schemes.__getitem__(scheme.upper()) + + def __setitem__(self, key: str, value): + self.schemes.__setitem__(key.upper(), value) + + def __len__(self) -> int: + return len(self.schemes) + + def to_dict(self): + return {"schemes": self.schemes} + + @classmethod + def from_dict(cls, dct: Mapping) -> CCSchemeDataset: + return cls(dct["schemes"]) + + @classmethod + def from_web(cls, only_schemes: set[str] | None = None) -> CCSchemeDataset: + schemes = {} + for scheme, sources in CCScheme.methods.items(): + if only_schemes is not None and scheme not in only_schemes: + continue + schemes[scheme] = CCScheme.from_web(scheme, sources.keys()) + return cls(schemes) diff --git a/src/sec_certs/dataset/fips_algorithm.py b/src/sec_certs/dataset/fips_algorithm.py index df113381..fbc8351a 100644 --- a/src/sec_certs/dataset/fips_algorithm.py +++ b/src/sec_certs/dataset/fips_algorithm.py @@ -12,7 +12,7 @@ from bs4 import BeautifulSoup from sec_certs import constants from sec_certs.dataset.json_path_dataset import JSONPathDataset -from sec_certs.sample import FIPSAlgorithm +from sec_certs.sample.fips_algorithm import FIPSAlgorithm from sec_certs.serialization.json import ComplexSerializableType from sec_certs.utils import helpers diff --git a/src/sec_certs/dataset/fips_mip.py b/src/sec_certs/dataset/fips_mip.py index 1b0d2032..dd61e60e 100644 --- a/src/sec_certs/dataset/fips_mip.py +++ b/src/sec_certs/dataset/fips_mip.py @@ -1,6 +1,8 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import date +from operator import attrgetter from pathlib import Path from tempfile import NamedTemporaryFile from typing import Iterator, Mapping @@ -11,7 +13,7 @@ from sec_certs import constants from sec_certs.configuration import config from sec_certs.dataset.dataset import logger from sec_certs.dataset.json_path_dataset import JSONPathDataset -from sec_certs.sample.fips_mip import MIPSnapshot +from sec_certs.sample.fips_mip import MIPFlow, MIPSnapshot, MIPStatus from sec_certs.serialization.json import ComplexSerializableType from sec_certs.utils.tqdm import tqdm @@ -64,3 +66,36 @@ class MIPDataset(JSONPathDataset, ComplexSerializableType): with NamedTemporaryFile() as tmpfile: tmpfile.write(mip_resp.content) return cls.from_json(tmpfile.name) + + def compute_flows(self) -> list[MIPFlow]: + """ + Compute the MIPFlows, deduplicating the MIPEntries in the snapshots + and computing their state-changes. + + :return: The MIPFlows. + """ + flows: dict[tuple[str, str, str], list[tuple[date, MIPStatus]]] = {} + for snapshot in sorted(self.snapshots, key=attrgetter("timestamp")): + snapshot_date = snapshot.timestamp.date() + entries: dict[tuple[str, str, str], set] = {} + for entry in snapshot: + key = (entry.module_name, entry.vendor_name, entry.standard) + s = entries.setdefault(key, set()) + s.add(entry) + + for key, dups in entries.items(): + if len(dups) > 1: + logger.warning(f"Duplicate MIPEntry when computing MIPFlow, {key}.") + entry = sorted(dups)[0] + entry_flows = flows.setdefault(key, []) + if entry_flows: + last_state_change = entry_flows[-1] + last_date, last_status = last_state_change + if last_status != entry.status: + entry_flows.append((snapshot_date, entry.status)) + else: + entry_flows[-1] = (snapshot_date, entry.status) + else: + entry_flows.append((snapshot_date, entry.status)) + + return [MIPFlow(*key, value) for key, value in flows.items()] diff --git a/src/sec_certs/model/__init__.py b/src/sec_certs/model/__init__.py index fe1024f9..3c5ea459 100644 --- a/src/sec_certs/model/__init__.py +++ b/src/sec_certs/model/__init__.py @@ -3,9 +3,18 @@ This package exposes model (mostly transformers and classifiers) that apply comp leveraged by members of Dataset package and are directly applied on members of Sample class (or on built-in objects). """ +from sec_certs.model.cc_matching import CCSchemeMatcher from sec_certs.model.cpe_matching import CPEClassifier +from sec_certs.model.fips_matching import FIPSProcessMatcher from sec_certs.model.reference_finder import ReferenceFinder from sec_certs.model.sar_transformer import SARTransformer from sec_certs.model.transitive_vulnerability_finder import TransitiveVulnerabilityFinder -__all__ = ["CPEClassifier", "ReferenceFinder", "TransitiveVulnerabilityFinder", "SARTransformer"] +__all__ = [ + "CPEClassifier", + "CCSchemeMatcher", + "FIPSProcessMatcher", + "ReferenceFinder", + "TransitiveVulnerabilityFinder", + "SARTransformer", +] diff --git a/src/sec_certs/model/cc_matching.py b/src/sec_certs/model/cc_matching.py new file mode 100644 index 00000000..350a2e9d --- /dev/null +++ b/src/sec_certs/model/cc_matching.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from typing import Any, Iterable, Mapping, Sequence + +from sec_certs.configuration import config +from sec_certs.model.matching import AbstractMatcher +from sec_certs.sample.cc import CCCertificate +from sec_certs.sample.cc_certificate_id import CertificateId +from sec_certs.utils.strings import fully_sanitize_string + + +class CCSchemeMatcher(AbstractMatcher[CCCertificate]): + """ + A heuristic matcher between entries on CC scheme websites (see CCSchemeDataset) and + CC certificates from the Common Criteria portal (as in CCDataset). + """ + + def __init__(self, entry: Mapping, scheme: str): + self.entry = entry + self.scheme = scheme + self._prepare() + + def _get_from_entry(self, *keys: str) -> str | None: + for key in keys: + if val := self.entry.get(key): + return val + if e := self.entry.get("enhanced"): + for key in keys: + if val := e.get(key): + return val + return None + + def _prepare(self): + self._canonical_cert_id = None + if cert_id := self._get_from_entry("cert_id", "id"): + self._canonical_cert_id = CertificateId(self.scheme, cert_id).canonical + + self._product = None + if product_name := self._get_from_entry("product", "title", "name"): + self._product = fully_sanitize_string(product_name) + + self._vendor = None + if vendor_name := self._get_from_entry("vendor", "developer", "manufacturer", "supplier"): + self._vendor = fully_sanitize_string(vendor_name) + + self._report_hash = self._get_from_entry("report_hash") + self._target_hash = self._get_from_entry("target_hash") + + def match(self, cert: CCCertificate) -> float: + """ + Compute the match of this matcher to the certificate, a float from 0 to 100. + + A 100 is a certificate ID match that should be always correct, assuming correct + data in the entry and certificate. + + :param cert: The certificate to match against. + :return: The match score. + """ + # This one is full of magic numbers, there is some idea to it but adjust as necessary. + # We want to match the same scheme. + if self.scheme != cert.scheme: + return 0 + # If we have a perfect cert_id match, take it. + if self._canonical_cert_id and cert.heuristics.cert_id == self._canonical_cert_id: + return 100 + # We need to have something to match to. + if self._product is None or self._vendor is None or cert.name is None or cert.manufacturer is None: + return 0 + cert_name = fully_sanitize_string(cert.name) + cert_manufacturer = fully_sanitize_string(cert.manufacturer) + # If we match exactly, return early. + if self._product == cert.name and self._vendor == cert.manufacturer: + return 99 + # If we match the report hash, return early. + if cert.state.report_pdf_hash == self._report_hash and self._report_hash is not None: + return 95 + # If we match the target hash, return early. + if cert.state.st_pdf_hash == self._target_hash and self._target_hash is not None: + return 93 + + # Fuzzy match at the end with some penalization. + product_rating = self._compute_match(self._product, cert_name) + vendor_rating = self._compute_match(self._vendor, cert_manufacturer) + return max((0, product_rating * 0.5 + vendor_rating * 0.5 - 2)) + + @classmethod + def match_all( + cls, entries: list[dict[str, Any]], scheme: str, certificates: Iterable[CCCertificate] + ) -> dict[str, dict[str, Any]]: + """ + Match all entries of a given CC scheme to certificates from the dataset. + + :param entries: The entries from the scheme, obtained from CCSchemeDataset. + :param scheme: The scheme, e.g. "DE". + :param certificates: The certificates to match against. + :return: A mapping of certificate digests to entries, without duplicates, not all entries may be present. + """ + certs: list[CCCertificate] = list(filter(lambda cert: cert.scheme == scheme, certificates)) + matchers: Sequence[CCSchemeMatcher] = [CCSchemeMatcher(entry, scheme) for entry in entries] + return cls._match_certs(matchers, certs, config.cc_matching_threshold) # type: ignore diff --git a/src/sec_certs/model/cpe_matching.py b/src/sec_certs/model/cpe_matching.py index 1a02008b..754ba7a9 100644 --- a/src/sec_certs/model/cpe_matching.py +++ b/src/sec_certs/model/cpe_matching.py @@ -10,6 +10,13 @@ from rapidfuzz import fuzz from sec_certs import cert_rules, constants from sec_certs.sample.cpe import CPE +from sec_certs.utils.strings import ( + discard_trademark_symbols, + fully_sanitize_string, + lemmatize_product_name, + load_spacy_model, + standardize_version_in_cert_name, +) from sec_certs.utils.tqdm import tqdm logger = logging.getLogger(__name__) @@ -27,11 +34,10 @@ class CPEClassifier: vendors_: set[str] def __init__(self, match_threshold: int = 80, n_max_matches: int = 10, spacy_model_to_use: str = "en_core_web_sm"): - import spacy self.match_threshold = match_threshold self.n_max_matches = n_max_matches - self.nlp = spacy.load(spacy_model_to_use, disable=["parser", "ner"]) + self.nlp = load_spacy_model(spacy_model_to_use) def fit(self, X: list[CPE], y: list[str] | None = None) -> CPEClassifier: """ @@ -106,9 +112,9 @@ class CPEClassifier: :param bool relax_title: See step 7 above, defaults to False :return Optional[Set[str]]: Set of matching CPE uris, None if no matches found """ - lemmatized_product_name = self._lemmatize_product_name(product_name) + lemmatized_product_name = lemmatize_product_name(self.nlp, product_name) candidate_vendors = self._get_candidate_list_of_vendors( - CPEClassifier._discard_trademark_symbols(vendor).lower() if vendor else vendor + discard_trademark_symbols(vendor).lower() if vendor else vendor ) candidates = self._get_candidate_cpe_matches(candidate_vendors, versions) candidates = self._filter_candidates_by_platform(candidates, product_name) @@ -208,15 +214,15 @@ class CPEClassifier: """ if relax_title: sanitized_title = ( - CPEClassifier._fully_sanitize_string(cpe.title) + fully_sanitize_string(cpe.title) if cpe.title - else CPEClassifier._fully_sanitize_string( + else fully_sanitize_string( cpe.vendor + " " + cpe.item_name + " " + cpe.version + " " + cpe.update + " " + cpe.target_hw ) ) else: if cpe.title: - sanitized_title = CPEClassifier._fully_sanitize_string(cpe.title) + sanitized_title = fully_sanitize_string(cpe.title) else: return 0 @@ -224,12 +230,12 @@ class CPEClassifier: if len(sanitized_title) < 5: return 0 - sanitized_item_name = CPEClassifier._fully_sanitize_string(cpe.item_name) + sanitized_item_name = fully_sanitize_string(cpe.item_name) sanitized_cpe_stripped_manufacturer = re.sub(r"\b" + rf"{cpe.vendor}" + r"\b", "", sanitized_title) - standard_version_product_name = self._standardize_version_in_cert_name(product_name, versions) + standard_version_product_name = standardize_version_in_cert_name(product_name, versions) # The expression below is currently unused, it could assist with some matches though - # cert_stripped = CPEClassifier._strip_manufacturer_and_version(product_name, candidate_vendors, versions) + # cert_stripped = strip_manufacturer_and_version(product_name, candidate_vendors, versions) # On some ratings, we require 100 match regardless of the treshold in settings. ratings = [ @@ -252,34 +258,6 @@ class CPEClassifier: return max(ratings) - @staticmethod - def _fully_sanitize_string(string: str) -> str: - return CPEClassifier._replace_special_chars_with_space( - CPEClassifier._discard_trademark_symbols(string.lower()) - ).strip() - - @staticmethod - def _replace_special_chars_with_space(string: str) -> str: - return re.sub(r"[^a-zA-Z0-9 \n\.]", " ", string) - - @staticmethod - def _discard_trademark_symbols(string: str) -> str: - return string.replace("®", "").replace("™", "") - - @staticmethod - def _strip_manufacturer_and_version(string: str, manufacturers: set[str] | None, versions: set[str]) -> str: - to_strip = versions | manufacturers if manufacturers else versions - for x in to_strip: - string = string.lower().replace(CPEClassifier._replace_special_chars_with_space(x.lower()), " ").strip() - return string - - @staticmethod - def _standardize_version_in_cert_name(string: str, detected_versions: set[str]) -> str: - for ver in detected_versions: - version_regex = r"(" + r"(\bversion)\s*" + ver + r"+) | (\bv\s*" + ver + r"+)" - string = re.sub(version_regex, " " + ver + " ", string, flags=re.IGNORECASE) - return string - def _process_manufacturer(self, manufacturer: str, result: set) -> set[str]: tokenized = manufacturer.split() if tokenized[0] in self.vendors_: @@ -392,8 +370,3 @@ class CPEClassifier: if candidate_vendor_version_pairs else [] ) - - def _lemmatize_product_name(self, product_name: str) -> str: - if not product_name: - return product_name - return " ".join([token.lemma_ for token in self.nlp(CPEClassifier._fully_sanitize_string(product_name))]) diff --git a/src/sec_certs/model/fips_matching.py b/src/sec_certs/model/fips_matching.py new file mode 100644 index 00000000..50079611 --- /dev/null +++ b/src/sec_certs/model/fips_matching.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import typing +from datetime import date +from typing import Iterable, Mapping, Sequence + +from sec_certs.configuration import config +from sec_certs.model.matching import AbstractMatcher +from sec_certs.sample.fips import FIPSCertificate +from sec_certs.utils.strings import fully_sanitize_string + +if typing.TYPE_CHECKING: + from sec_certs.sample.fips_iut import IUTEntry, IUTSnapshot + from sec_certs.sample.fips_mip import MIPEntry, MIPSnapshot + + +class FIPSProcessMatcher(AbstractMatcher[FIPSCertificate]): + """ + A heuristic matcher between entries on the FIPS IUT/MIP lists and + the FIPS certificates. + """ + + def __init__(self, entry: MIPEntry | IUTEntry, date: date | None = None): + self.entry = entry + self._prepare(date) + + def _prepare(self, date): + self._date = date or getattr(self.entry, "status_since", None) or getattr(self.entry, "iut_date", None) + self._product = fully_sanitize_string(self.entry.module_name) + self._vendor = fully_sanitize_string(self.entry.vendor_name) + self._standard = self.entry.standard + + def match(self, cert: FIPSCertificate) -> float: + """ + Compute the match of this matcher to the certificate, a float from 0 to 100. + + :param cert: The certificate to match against. + :return: The match score. + """ + # We want to match the same standard. + if cert.web_data.standard != self._standard: + return 0 + # We need to have something to match to. + if cert.name is None or cert.manufacturer is None: + return 0 + # We can't match to a cert that predates us (MIP or IUT always predates the cert). + if cert.web_data.validation_history and not any( + validation_entry.date > self._date for validation_entry in cert.web_data.validation_history + ): + return 0 + # If we match exactly, return early. + cert_name = fully_sanitize_string(cert.name) + cert_manufacturer = fully_sanitize_string(cert.manufacturer) + if self._product == cert_name and self._vendor == cert_manufacturer: + return 99 + + # Fuzzy match at the end with some penalization. + product_rating = self._compute_match(self._product, cert_name) + vendor_rating = self._compute_match(self._vendor, cert_manufacturer) + return max((0, product_rating * 0.5 + vendor_rating * 0.5 - 2)) + + @classmethod + def match_snapshot( + cls, snapshot: IUTSnapshot | MIPSnapshot, certificates: Iterable[FIPSCertificate] + ) -> Mapping[IUTEntry | MIPEntry, FIPSCertificate | None]: + """ + Match a whole snapshot of IUT/MIP entries to a FIPS certificate dataset. + + :param snapshot: The snapshot to match the entries of. + :param certificates: The certificates to match against. + :return: A mapping of certificate digests to entries, without duplicates, not all entries may be present. + """ + certs: list[FIPSCertificate] = list(certificates) + matchers: Sequence[FIPSProcessMatcher] = [ + FIPSProcessMatcher(entry, snapshot.timestamp.date()) for entry in snapshot + ] + # mypy is ridiculous + return cls._match_certs(matchers, certs, config.fips_matching_threshold) # type: ignore diff --git a/src/sec_certs/model/matching.py b/src/sec_certs/model/matching.py new file mode 100644 index 00000000..b298e316 --- /dev/null +++ b/src/sec_certs/model/matching.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from heapq import heappop, heappush +from typing import Any, Generic, Sequence, TypeVar + +from rapidfuzz import fuzz + +from sec_certs.sample.certificate import Certificate + +CertSubType = TypeVar("CertSubType", bound=Certificate) + + +class AbstractMatcher(Generic[CertSubType], ABC): + entry: Any + + @abstractmethod + def match(self, cert: CertSubType) -> float: + raise NotImplementedError + + def _compute_match(self, one: str, other: str) -> float: + return max( + [ + fuzz.token_set_ratio(one, other), + fuzz.partial_token_sort_ratio(one, other, score_cutoff=100), + fuzz.partial_ratio(one, other, score_cutoff=100), + ] + ) + + @staticmethod + def _match_certs(matchers: Sequence["AbstractMatcher"], certs: list[CertSubType], threshold: float): + scores: list[tuple[float, int, int]] = [] + matched_is: set[int] = set() + matched_js: set[int] = set() + for i, cert in enumerate(certs): + for j, matcher in enumerate(matchers): + score = matcher.match(cert) + triple = (100 - score, i, j) + heappush(scores, triple) + results = {} + for triple in (heappop(scores) for _ in range(len(scores))): + inv_score, i, j = triple + # Do not match already matched entries/certs. + if i in matched_is or j in matched_js: + continue + # Compute the actual score from the inverse. + score = 100 - inv_score + # Do not match if we are below threshold, all the following will be as well. + if score < threshold: + break + # Match cert dgst to entry + cert = certs[i] + entry = matchers[j].entry + results[cert.dgst] = entry + return results diff --git a/src/sec_certs/sample/cc.py b/src/sec_certs/sample/cc.py index 9f73aefa..c89f67c6 100644 --- a/src/sec_certs/sample/cc.py +++ b/src/sec_certs/sample/cc.py @@ -437,6 +437,7 @@ class CCCertificate( extracted_sars: set[SAR] | None = field(default=None) 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) @property def serialized_attributes(self) -> list[str]: diff --git a/src/sec_certs/sample/cc_certificate_id.py b/src/sec_certs/sample/cc_certificate_id.py index 428ca73b..3e28dfc8 100644 --- a/src/sec_certs/sample/cc_certificate_id.py +++ b/src/sec_certs/sample/cc_certificate_id.py @@ -16,13 +16,13 @@ class CertificateId: def _canonical_fr(self) -> str: new_cert_id = self.clean rules = [ - "(?:Rapport de certification|Certification Report) ([0-9]+[/-_][0-9]+(?:v[1-9])?(?:[_/-][MSR][0-9]+)?)", - "(?:ANSS[Ii]|DCSSI)(?:-CC)?[- ]([0-9]+[/-_][0-9]+(?:v[1-9])?(?:[_/-][MSR][0-9]+)?)", - "([0-9]+[/-_][0-9]+(?:v[1-9])?(?:[_/-][MSR][0-9]+)?)", + "(?:Rapport de certification|Certification Report) ([0-9]+[/-_][0-9]+(?:[vV][1-9])?(?:[_/-][MSR][0-9]+)?)", + "(?:ANSS[Ii]|DCSSI)(?:-CC)?[- ]([0-9]+[/-_][0-9]+(?:[vV][1-9])?(?:[_/-][MSR][0-9]+)?)", + "([0-9]+[/-_][0-9]+(?:[vV][1-9])?(?:[_/-][MSR][0-9]+)?)", ] for rule in rules: if match := re.match(rule, new_cert_id): - return "ANSSI-CC-" + match.group(1).replace("_", "/") + return "ANSSI-CC-" + match.group(1).replace("_", "/").replace("V", "v") return new_cert_id diff --git a/src/sec_certs/sample/cc_scheme.py b/src/sec_certs/sample/cc_scheme.py new file mode 100644 index 00000000..ceaf5860 --- /dev/null +++ b/src/sec_certs/sample/cc_scheme.py @@ -0,0 +1,1573 @@ +from __future__ import annotations + +import hashlib +import tempfile +import warnings +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from pathlib import Path +from typing import Any, Callable, ClassVar, Iterable +from urllib.parse import urljoin + +import requests +import tabula +from bs4 import BeautifulSoup, NavigableString, Tag +from requests import Response +from urllib3.connectionpool import InsecureRequestWarning + +from sec_certs import constants +from sec_certs.serialization.json import ComplexSerializableType +from sec_certs.utils.sanitization import sanitize_navigable_string as sns +from sec_certs.utils.tqdm import tqdm + +__all__ = [ + "get_australia_in_evaluation", + "get_canada_certified", + "get_canada_in_evaluation", + "get_france_certified", + "get_germany_certified", + "get_india_certified", + "get_india_archived", + "get_italy_certified", + "get_italy_in_evaluation", + "get_japan_certified", + "get_japan_archived", + "get_japan_in_evaluation", + "get_malaysia_certified", + "get_malaysia_in_evaluation", + "get_netherlands_certified", + "get_netherlands_in_evaluation", + "get_norway_certified", + "get_norway_archived", + "get_korea_certified", + "get_korea_suspended", + "get_korea_archived", + "get_singapore_certified", + "get_singapore_in_evaluation", + "get_singapore_archived", + "get_spain_certified", + "get_sweden_certified", + "get_sweden_in_evaluation", + "get_sweden_archived", + "get_turkey_certified", + "get_usa_certified", + "get_usa_in_evaluation", + "get_usa_archived", + "EntryType", + "CCScheme", +] + + +def _get(url: str, session, **kwargs) -> Response: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=InsecureRequestWarning) + conn = session if session else requests + resp = conn.get(url, headers={"User-Agent": "seccerts.org"}, verify=False, **kwargs) + resp.raise_for_status() + return resp + + +def _get_page(url: str, session=None) -> BeautifulSoup: + return BeautifulSoup(_get(url, session).content, "html5lib") + + +def _get_hash(url: str, session=None) -> bytes: + resp = _get(url, session) + h = hashlib.sha256() + for chunk in resp.iter_content(): + h.update(chunk) + return h.digest() + + +def get_australia_in_evaluation(enhanced: bool = True) -> list[dict[str, Any]]: # noqa: C901 + """ + Get Australia "products in evaluation" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :return: The entries. + """ + soup = _get_page(constants.CC_AUSTRALIA_INEVAL_URL) + header = soup.find("h2", string="Products in evaluation") + table = header.find_next_sibling("table") + results = [] + for tr in tqdm(table.find_all("tr"), desc="Get AU scheme in evaluation."): + tds = tr.find_all("td") + if not tds: + continue + cert: dict[str, Any] = { + "vendor": sns(tds[0].text), + "product": sns(tds[1].text), + "url": urljoin(constants.CC_AUSTRALIA_BASE_URL, tds[1].find("a")["href"]), + "level": sns(tds[2].text), + } + if enhanced: + e: dict[str, Any] = {} + cert_page = _get_page(cert["url"]) + article = cert_page.find("article", attrs={"role": "article"}) + blocks = article.find("div").find_all("div", class_="flex", recursive=False) + for h2 in blocks[0].find_all("h2"): + val = sns(h2.find_next_sibling("span").text) + h_text = sns(h2.text) + if not h_text: + continue + if "Version:" in h_text: + e["version"] = val + elif "Product type:" in h_text: + e["product_type"] = val + elif "Product status:" in h_text: + e["product_status"] = val + elif "Assurance level:" in h_text: + e["assurance_level"] = val + sides = blocks[1].find_all("div", recursive=False) + for div in sides[0].find_all("div", recursive=False): + h2 = div.find("h2") + h_text = sns(h2.text) + if not h_text: + continue + if "epl-vendor-token" in div.get("class"): + vendor_address = [h_text] + vendor_address.extend([sns(elem.text) for elem in h2.find_next_siblings("div")]) # type: ignore + e["vendor"] = "\n".join(vendor_address) + else: + val = sns(h2.find_next_sibling("span").text) + if "Evaluation Facility:" in h_text: + e["evaluation_facility"] = val + elif "Certification Progress:" in h_text: + e["certification_progress"] = val + elif "Estimated Approval" in h_text: + e["estimated_approval"] = val + e["contacts"] = [sns(p.text) for p in sides[1].find_all("p")] + e["description"] = sns(blocks[2].find("span").text) + cert["enhanced"] = e + results.append(cert) + return results + + +def get_canada_certified() -> list[dict[str, Any]]: + """ + Get Canada "certified product" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_CANADA_CERTIFIED_URL) + tbody = soup.find("table").find("tbody") + results = [] + for tr in tqdm(tbody.find_all("tr"), desc="Get CA scheme certified."): + tds = tr.find_all("td") + if not tds: + continue + cert = { + "product": sns(tds[0].text), + "vendor": sns(tds[1].text), + "level": sns(tds[2].text), + "certification_date": sns(tds[3].text), + } + results.append(cert) + return results + + +def get_canada_in_evaluation() -> list[dict[str, Any]]: + """ + Get Canada "products in evaluation" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_CANADA_INEVAL_URL) + tbody = soup.find("table").find("tbody") + results = [] + for tr in tqdm(tbody.find_all("tr"), desc="Get CA scheme in evaluation."): + tds = tr.find_all("td") + if not tds: + continue + cert = { + "product": sns(tds[0].text), + "vendor": sns(tds[1].text), + "level": sns(tds[2].text), + "cert_lab": sns(tds[3].text), + } + results.append(cert) + return results + + +def get_france_certified(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: # noqa: C901 + """ + Get French "certified product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + base_soup = _get_page(constants.CC_ANSSI_CERTIFIED_URL) + category_nav = base_soup.find("ul", class_="nav-categories") + results = [] + for li in tqdm(category_nav.find_all("li"), desc="Get FR scheme certified."): + a = li.find("a") + url = a["href"] + category_name = sns(a.text) + soup = _get_page(urljoin(constants.CC_ANSSI_BASE_URL, url)) + table = soup.find("table", class_="produits-liste cc") + if not table: + continue + tbody = table.find("tbody") + for tr in tqdm(tbody.find_all("tr")): + tds = tr.find_all("td") + if not tds: + continue + cert: dict[str, Any] = { + "product": sns(tds[0].text), + "vendor": sns(tds[1].text), + "level": sns(tds[2].text), + "id": sns(tds[3].text), + "certification_date": sns(tds[4].text), + "category": category_name, + "url": urljoin(constants.CC_ANSSI_BASE_URL, tds[0].find("a")["href"]), + } + if enhanced: + e: dict[str, Any] = {} + cert_page = _get_page(cert["url"]) + ref = cert_page.find("div", class_="ref-date") + for ref_li in ref.find_all("li"): + title, value = (sns(span.text) for span in ref_li.find_all("span", recursive=False)) + if not title: + continue + if "Référence" in title: + e["id"] = value + elif "Date de certification" in title: + e["certification_date"] = value + elif "Date de fin de validité" in title: + e["expiration_date"] = value + details = cert_page.find("div", class_="details") + for detail_li in details.find_all("li"): + title, value = (sns(span.text) for span in detail_li.find_all("span", recursive=False)) + if not title: + continue + if "Catégorie" in title: + e["category"] = value + elif "Référentiel" in title: + e["cc_version"] = value + elif "Niveau" in title: + e["level"] = value + elif "Augmentations" in title: + e["augmentations"] = value + elif "Profil de protection" in title: + e["protection_profile"] = value + elif "Développeur" in title: + e["developer"] = value + elif "Centre d'évaluation" in title: + e["evaluation_facility"] = value + elif "Accords de reconnaissance" in title: + e["recognition"] = value + e["description"] = sns(cert_page.find("div", class_="box-produit-descriptif").text) + links = cert_page.find("div", class_="box-produit-telechargements") + for link_li in links.find_all("li"): + a = link_li.find("a") + href = urljoin(constants.CC_ANSSI_BASE_URL, a["href"]) + title = sns(a.text) + if not title: + continue + if "Rapport de certification" in title: + e["report_link"] = href + if artifacts: + e["report_hash"] = _get_hash(href).hex() + elif "Security target" in title: + e["target_link"] = href + if artifacts: + e["target_hash"] = _get_hash(href).hex() + elif "Certificat" in title: + e["cert_link"] = href + if artifacts: + e["cert_hash"] = _get_hash(href).hex() + cert["enhanced"] = e + results.append(cert) + return results + + +def get_germany_certified(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: # noqa: C901 + """ + Get German "certified product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + base_soup = _get_page(constants.CC_BSI_CERTIFIED_URL) + category_nav = base_soup.find("ul", class_="no-bullet row") + results = [] + for li in tqdm(category_nav.find_all("li"), desc="Get DE scheme certified."): + a = li.find("a") + url = a["href"] + category_name = sns(a.text) + soup = _get_page(urljoin(constants.CC_BSI_BASE_URL, url)) + content = soup.find("div", class_="content").find("div", class_="column") + for table in tqdm(content.find_all("table")): + tbody = table.find("tbody") + header = table.find_parent("div", class_="wrapperTable").find_previous_sibling("h2") + for tr in tqdm(tbody.find_all("tr")): + tds = tr.find_all("td") + if len(tds) != 4: + continue + cert: dict[str, Any] = { + "cert_id": sns(tds[0].text), + "product": sns(tds[1].text), + "vendor": sns(tds[2].text), + "certification_date": sns(tds[3].text), + "category": category_name, + "url": urljoin(constants.CC_BSI_BASE_URL, tds[0].find("a")["href"]), + } + if enhanced: + e: dict[str, Any] = {} + cert_page = _get_page(cert["url"]) + content = cert_page.find("div", id="content").find("div", class_="column") + head = content.find("h1", class_="c-intro__headline") + e["product"] = sns(head.next_sibling.text) + details = content.find("table") + for details_tr in details.find_all("tr"): + details_tds = details_tr.find_all("td") + title = sns(details_tds[0].find("span", attrs={"lang": "en-GB"}).text) + if not title: + continue + value = sns(details_tds[1].text) + if "Applicant" in title: + e["applicant"] = value + elif "Evaluation Facility" in title: + e["evaluation_facility"] = value + elif "Assurance" in title: + e["assurance_level"] = value + elif "Protection Profile" in title: + e["protection_profile"] = value + elif "Certification Date" in title: + e["certification_date"] = value + elif "valid until" in title: + e["expiration_date"] = value + links = content.find("ul") + if links: + # has multiple entries/recertifications + e["entries"] = [] + for link_li in links.find_all("li"): + first_child = next(iter(link_li.children)) + if isinstance(first_child, Tag): + link_id = sns(first_child.text) + elif isinstance(first_child, NavigableString): + link_id = sns(first_child.text).split(" ")[0] # type: ignore + else: + link_id = None + entry = {"id": link_id} + en_spans = link_li.find_all("span", attrs={"lang": "en-GB"}) + if en_spans: + entry["description"] = sns(en_spans[-1].text) + # TODO: Could parse the links to documents here + e["entries"].append(entry) + doc_links = content.find_all("a", title=lambda title: cert["cert_id"] in title) + for doc_link in doc_links: + href = urljoin(constants.CC_BSI_BASE_URL, doc_link["href"]) + title = sns(doc_link["title"]) + if not title: + continue + if "Certification Report" in title: + e["report_link"] = href + if artifacts: + e["report_hash"] = _get_hash(href).hex() + elif "Security Target" in title: + e["target_link"] = href + if artifacts: + e["target_hash"] = _get_hash(href).hex() + elif "Certificate" in title: + e["cert_link"] = href + if artifacts: + e["cert_hash"] = _get_hash(href).hex() + description = content.find("div", attrs={"lang": "en"}) + if description: + e["description"] = sns(description.text) + cert["enhanced"] = e + if header is not None: + cert["subcategory"] = sns(header.text) + results.append(cert) + return results + + +def _fix_india_link(link: str) -> str: + return link.replace("/index.php", "") + + +def get_india_certified() -> list[dict[str, Any]]: + """ + Get Indian "certified product" entries. + + :return: The entries. + """ + pages = {0} + seen_pages = set() + results = [] + while pages: + page = pages.pop() + seen_pages.add(page) + url = constants.CC_INDIA_CERTIFIED_URL + f"?page={page}" + soup = _get_page(url) + + # Update pages + pager = soup.find("ul", class_="pager__items") + for li in pager.find_all("li"): + try: + new_page = int(li.text) - 1 + except Exception: + continue + if new_page not in seen_pages: + pages.add(new_page) + + # Parse table + tbody = soup.find("div", class_="view-content").find("table").find("tbody") + for tr in tbody.find_all("tr"): + tds = tr.find_all("td") + if not tds: + continue + report_a = tds[6].find("a") + target_a = tds[7].find("a") + cert_a = tds[8].find("a") + cert = { + "serial_number": sns(tds[0].text), + "product": sns(tds[1].text), + "sponsor": sns(tds[2].text), + "developer": sns(tds[3].text), + "level": sns(tds[4].text), + "issuance_date": sns(tds[5].text), + "report_link": urljoin(constants.CC_INDIA_BASE_URL, _fix_india_link(report_a["href"])), + "report_name": sns(report_a.text), + "target_link": urljoin(constants.CC_INDIA_BASE_URL, _fix_india_link(target_a["href"])), + "target_name": sns(target_a.text), + "cert_link": urljoin(constants.CC_INDIA_BASE_URL, _fix_india_link(cert_a["href"])), + "cert_name": sns(cert_a.text), + } + results.append(cert) + return results + + +def get_india_archived() -> list[dict[str, Any]]: + """ + Get Indian "archived product" entries. + + :return: The entries. + """ + pages = {0} + seen_pages = set() + results = [] + while pages: + page = pages.pop() + seen_pages.add(page) + url = constants.CC_INDIA_ARCHIVED_URL + f"?page={page}" + soup = _get_page(url) + + # Update pages + pager = soup.find("ul", class_="pager__items") + if pager: + for li in pager.find_all("li"): + try: + new_page = int(li.text) - 1 + except Exception: + continue + if new_page not in seen_pages: + pages.add(new_page) + + # Parse table + tbody = soup.find("div", class_="view-content").find("table").find("tbody") + for tr in tbody.find_all("tr"): + tds = tr.find_all("td") + if not tds: + continue + report_a = tds[5].find("a") + target_a = tds[6].find("a") + cert_a = tds[7].find("a") + cert = { + "serial_number": sns(tds[0].text), + "product": sns(tds[1].text), + "sponsor": sns(tds[2].text), + "developer": sns(tds[3].text), + "level": sns(tds[4].text), + "target_link": urljoin(constants.CC_INDIA_BASE_URL, _fix_india_link(target_a["href"])), + "target_name": sns(target_a.text), + "cert_link": urljoin(constants.CC_INDIA_BASE_URL, _fix_india_link(cert_a["href"])), + "cert_name": sns(cert_a.text), + "certification_date": sns(tds[8].text), + } + if report_a: + cert["report_link"] = urljoin(constants.CC_INDIA_BASE_URL, _fix_india_link(report_a["href"])) + cert["report_name"] = sns(report_a.text) + results.append(cert) + return results + + +def get_italy_certified() -> list[dict[str, Any]]: # noqa: C901 + """ + Get Italian "certified product" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_ITALY_CERTIFIED_URL) + div = soup.find("div", class_="certificati") + results = [] + for cert_div in div.find_all("div", recursive=False): + title = cert_div.find("h3").text + data_div = cert_div.find("div", class_="collapse") + cert = {"title": title} + for data_p in data_div.find_all("p"): + p_text = sns(data_p.text) + if not p_text or ":" not in p_text: + continue + p_name, p_data = p_text.split(":") + p_data = p_data + p_link = data_p.find("a") + if "Fornitore" in p_name: + cert["supplier"] = p_data + elif "Livello di garanzia" in p_name: + cert["level"] = p_data + elif "Data emissione certificato" in p_name: + cert["certification_date"] = p_data + elif "Data revisione" in p_name: + cert["revision_date"] = p_data + elif "Rapporto di Certificazione" in p_name and p_link: + cert["report_link_it"] = urljoin(constants.CC_ITALY_BASE_URL, p_link["href"]) + elif "Certification Report" in p_name and p_link: + cert["report_link_en"] = urljoin(constants.CC_ITALY_BASE_URL, p_link["href"]) + elif "Traguardo di Sicurezza" in p_name and p_link: + cert["target_link"] = urljoin(constants.CC_ITALY_BASE_URL, p_link["href"]) + elif "Nota su" in p_name and p_link: + cert["vulnerability_note_link"] = urljoin(constants.CC_ITALY_BASE_URL, p_link["href"]) + elif "Nota di chiarimento" in p_name and p_link: + cert["clarification_note_link"] = urljoin(constants.CC_ITALY_BASE_URL, p_link["href"]) + results.append(cert) + return results + + +def get_italy_in_evaluation() -> list[dict[str, Any]]: + """ + Get Italian "product in evaluation" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_ITALY_INEVAL_URL) + div = soup.find("div", class_="valutazioni") + results = [] + for cert_div in div.find_all("div", recursive=False): + title = cert_div.find("h3").text + data_div = cert_div.find("div", class_="collapse") + cert = {"title": title} + for data_p in data_div.find_all("p"): + p_text = sns(data_p.text) + if not p_text or ":" not in p_text: + continue + p_name, p_data = p_text.split(":") + p_data = p_data + if "Committente" in p_name: + cert["client"] = p_data + elif "Livello di garanzia" in p_name: + cert["level"] = p_data + elif "Tipologia prodotto" in p_name: + cert["product_type"] = p_data + results.append(cert) + return results + + +def _get_japan(url, enhanced, artifacts) -> list[dict[str, Any]]: # noqa: C901 + soup = _get_page(url) + table = soup.find("table", class_="cert-table") + results = [] + trs = list(table.find_all("tr")) + for tr in trs: + tds = tr.find_all("td") + if not tds: + continue + if len(tds) == 6: + cert: dict[str, Any] = { + "cert_id": sns(tds[0].text), + "supplier": sns(tds[1].text), + "toe_overseas_name": sns(tds[2].text), + "claim": sns(tds[4].text), + } + cert_date = sns(tds[3].text) + toe_a = tds[2].find("a") + if toe_a and "href" in toe_a.attrs: + toe_link = urljoin(constants.CC_JAPAN_CERT_BASE_URL, toe_a["href"]) + else: + toe_link = None + if cert_date and "Assurance Continuity" in cert_date: + cert["revalidations"] = [{"date": cert_date.split("(")[0], "link": toe_link}] + else: + cert["certification_date"] = cert_date + cert["toe_overseas_link"] = toe_link + results.append(cert) + if len(tds) == 1: + cert = results[-1] + cert["toe_japan_name"] = sns(tds[0].text) + toe_a = tds[0].find("a") + if toe_a and "href" in toe_a.attrs: + cert["toe_japan_link"] = urljoin(constants.CC_JAPAN_CERT_BASE_URL, toe_a["href"]) + if len(tds) == 2: + cert = results[-1] + cert["certification_date"] = sns(tds[1].text) + toe_a = tds[0].find("a") + if toe_a and "href" in toe_a.attrs: + toe_link = urljoin(constants.CC_JAPAN_CERT_BASE_URL, toe_a["href"]) + else: + toe_link = None + cert["toe_overseas_link"] = toe_link + if enhanced: + for cert in results: + e: dict[str, Any] = {} + cert_link = cert.get("toe_overseas_link") or cert.get("toe_japan_link") + if not cert_link: + continue + cert_page = _get_page(cert_link) + main = cert_page.find("div", id="main") + left = main.find("div", id="left") + for dl in left.find_all("dl"): + dt = dl.find("dt") + title = sns(dt.text) + value = sns(dt.find_next_sibling().text) + if not title: + continue + if "Product Name" in title: + e["product"] = value + elif "Version of TOE" in title: + e["toe_version"] = value + elif "Product Type" in title: + e["product_type"] = value + elif "Certification Identification" in title: + e["cert_id"] = value + elif "Version of Common Criteria" in title: + e["cc_version"] = value + elif "Date" in title: + e["certification_date"] = value + elif "Conformance Claim" in title: + e["assurance_level"] = value + elif "PP Identifier" in title and value != "None": + e["protection_profile"] = value + right = main.find("div", id="right") + for dl in right.find_all("dl", recursive=False): + title = sns(dl.find("dt").text) + value = sns(dl.find("dd").text) + if not title: + continue + if "Vendor" in title: + e["vendor"] = value + elif "Evaluation Facility" in title: + e["evaluation_facility"] = value + pdfbox = main.find("div", id="pdfbox") + if pdfbox: + for li in pdfbox.find_all("li"): + li_a = li.find("a") + name = sns(li_a.text) + if not name: + continue + if "Report" in name: + e["report_link"] = urljoin(constants.CC_JAPAN_BASE_URL, li_a["href"]) + if artifacts: + e["report_hash"] = _get_hash(e["report_link"]).hex() + elif "Certificate" in name: + e["cert_link"] = urljoin(constants.CC_JAPAN_BASE_URL, li_a["href"]) + if artifacts: + e["cert_hash"] = _get_hash(e["cert_link"]).hex() + elif "Target" in name: + e["target_link"] = urljoin(constants.CC_JAPAN_BASE_URL, li_a["href"]) + if artifacts: + e["target_hash"] = _get_hash(e["target_link"]).hex() + e["description"] = sns(main.find("div", id="overviewsbox").text) + cert["enhanced"] = e + return results + + +def get_japan_certified(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Japanese "certified product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + japan_hw = _get_japan(constants.CC_JAPAN_CERTIFIED_HW_URL, enhanced, artifacts) + japan_sw = _get_japan(constants.CC_JAPAN_CERTIFIED_SW_URL, enhanced, artifacts) + return japan_sw + japan_hw + + +def get_japan_archived(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Japanese "archived product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_japan(constants.CC_JAPAN_ARCHIVED_SW_URL, enhanced, artifacts) + + +def get_japan_in_evaluation() -> list[dict[str, Any]]: + """ + Get Japanese "product in evaluation" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_JAPAN_INEVAL_URL) + table = soup.find("table") + results = [] + for tr in table.find_all("tr"): + tds = tr.find_all("td") + if not tds: + continue + toe_a = tds[1].find("a") + cert = { + "supplier": sns(tds[0].text), + "toe_name": sns(toe_a.text), + "toe_link": urljoin(constants.CC_JAPAN_BASE_URL, toe_a["href"]), + "claim": sns(tds[2].text), + } + results.append(cert) + return results + + +def get_malaysia_certified() -> list[dict[str, Any]]: + """ + Get Malaysian "certified product" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_MALAYSIA_CERTIFIED_URL) + sections = soup.find("div", attrs={"itemprop": "articleBody"}).find_all("section", class_="sppb-section") + results = [] + for section in sections: + table = section.find("table") + if table is None: + continue + heading = section.find("h5") + if heading is None: + continue + category_name = sns(heading.text) + tbody = table.find("tbody") + for tr in tbody.find_all("tr", recursive=False): + tds = tr.find_all("td", recursive=False) + if len(tds) != 6: + continue + cert = { + "category": category_name, + "level": sns(tds[0].text), + "cert_id": sns(tds[1].text), + "certification_date": sns(tds[2].text), + "product": sns(tds[3].text), + "developer": sns(tds[4].text), + } + results.append(cert) + return results + + +def get_malaysia_in_evaluation() -> list[dict[str, Any]]: + """ + Get Malaysian "product in evaluation" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_MALAYSIA_INEVAL_URL) + main_div = soup.find("div", attrs={"itemprop": "articleBody"}) + table = main_div.find("table") + results = [] + for tr in table.find_all("tr")[1:]: + tds = tr.find_all("td") + if len(tds) != 5: + continue + cert = { + "level": sns(tds[0].text), + "project_id": sns(tds[1].text), + "toe_name": sns(tds[2].text), + "developer": sns(tds[3].text), + "expected_completion": sns(tds[4].text), + } + results.append(cert) + return results + + +def get_netherlands_certified(artifacts: bool = False) -> list[dict[str, Any]]: # noqa: C901 + """ + Get Dutch "certified product" entries. + + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + soup = _get_page(constants.CC_NETHERLANDS_CERTIFIED_URL) + main_div = soup.select("body > main > div > div > div > div:nth-child(2) > div.col-lg-9 > div:nth-child(3)")[0] + rows = main_div.find_all("div", class_="row", recursive=False) + modals = main_div.find_all("div", class_="modal", recursive=False) + results = [] + for row, modal in zip(rows, modals): + row_entries = row.find_all("a") + modal_trs = modal.find_all("tr") + cert: dict[str, Any] = { + "manufacturer": sns(row_entries[0].text), + "product": sns(row_entries[1].text), + "scheme": sns(row_entries[2].text), + "cert_id": sns(row_entries[3].text), + } + for tr in modal_trs: + th_text = tr.find("th").text + td = tr.find("td") + if "Manufacturer website" in th_text: + cert["manufacturer_link"] = td.find("a")["href"] + elif "Assurancelevel" in th_text: + cert["level"] = sns(td.text) + elif "Certificate" in th_text: + cert["cert_link"] = urljoin(constants.CC_NETHERLANDS_BASE_URL, td.find("a")["href"]) + if artifacts: + cert["cert_hash"] = _get_hash(cert["cert_link"]).hex() + elif "Certificationreport" in th_text: + cert["report_link"] = urljoin(constants.CC_NETHERLANDS_BASE_URL, td.find("a")["href"]) + if artifacts: + cert["report_hash"] = _get_hash(cert["report_link"]).hex() + elif "Securitytarget" in th_text: + cert["target_link"] = urljoin(constants.CC_NETHERLANDS_BASE_URL, td.find("a")["href"]) + if artifacts: + cert["target_hash"] = _get_hash(cert["target_link"]).hex() + elif "Maintenance report" in th_text: + cert["maintenance_link"] = urljoin(constants.CC_NETHERLANDS_BASE_URL, td.find("a")["href"]) + if artifacts: + cert["maintenance_hash"] = _get_hash(cert["maintenance_link"]).hex() + results.append(cert) + return results + + +def get_netherlands_in_evaluation() -> list[dict[str, Any]]: + """ + Get Dutch "product in evaluation" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_NETHERLANDS_INEVAL_URL) + table = soup.find("table") + results = [] + for tr in table.find_all("tr")[1:]: + tds = tr.find_all("td") + cert = { + "developer": sns(tds[0].text), + "product": sns(tds[1].text), + "category": sns(tds[2].text), + "level": sns(tds[3].text), + "certification_id": sns(tds[4].text), + } + results.append(cert) + return results + + +def _get_norway(url: str, enhanced: bool, artifacts: bool) -> list[dict[str, Any]]: # noqa: C901 + soup = _get_page(url) + results = [] + for tr in soup.find_all("tr", class_="certified-product"): + tds = tr.find_all("td") + cert: dict[str, Any] = { + "product": sns(tds[0].text), + "url": tds[0].find("a")["href"], + "category": sns(tds[1].find("p", class_="value").text), + "developer": sns(tds[2].find("p", class_="value").text), + "certification_date": sns(tds[3].find("time").text), + } + if enhanced: + e: dict[str, Any] = {} + cert_page = _get_page(cert["url"]) + content = cert_page.find("div", class_="main-content") + body = content.find("div", class_="articleelement") + if body: + e["description"] = sns(body.text) + specs = content.find("div", class_="specifications") + for row in specs.find_all("div", class_="row"): + title = sns(row.find("div", class_="label").text) + value = sns(row.find("div", class_="value").text) + if not title: + continue + if "Certificate No." in title: + e["id"] = value + elif "Mutual Recognition" in title: + e["mutual_recognition"] = value + elif "Product" in title: + e["product"] = value + elif "Category" in title: + e["category"] = value + elif "Sponsor" in title: + e["sponsor"] = value + elif "Developer" in title: + e["developer"] = value + elif "Evaluation Facility" in title: + e["evaluation_facility"] = value + elif "Certification Date" in title: + e["certification_date"] = value + elif "Evaluation Level" in title: + e["level"] = value + elif "Protection Profile" in title: + e["protection_profile"] = value + docs = content.find("div", class_="documents").find("div", class_="card-body") + e["documents"] = {} + for doc_collection in docs.find_all("div", class_="document-collection"): + head = sns(doc_collection.find("div", class_="header").text) + links = doc_collection.find_all("li") + if not head: + continue + if "Certificates" in head: + doc_type = "cert" + elif "Security targets" in head: + doc_type = "target" + elif "Certification reports" in head: + doc_type = "report" + elif "Maintenance report" in head: + doc_type = "maintenance" + else: + continue + entries = [] + for link in links: + a = link.find("a") + entry = {"href": urljoin(constants.CC_NORWAY_BASE_URL, a["href"])} + if artifacts: + entry["hash"] = _get_hash(entry["href"]).hex() + entries.append(entry) + e["documents"][doc_type] = entries + cert["enhanced"] = e + results.append(cert) + return results + + +def get_norway_certified(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Norwegian "certified product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_norway(constants.CC_NORWAY_CERTIFIED_URL, enhanced, artifacts) + + +def get_norway_archived(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Norwegian "archived product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_norway(constants.CC_NORWAY_ARCHIVED_URL, enhanced, artifacts) + + +def _get_korea(product_class: int, enhanced: bool, artifacts: bool) -> list[dict[str, Any]]: # noqa: C901 + session = requests.session() + session.get(constants.CC_KOREA_EN_URL) + # Get base page + url = constants.CC_KOREA_CERTIFIED_URL + f"?product_class={product_class}" + soup = _get_page(url, session=session) + seen_pages = set() + pages = {1} + results = [] + while pages: + page = pages.pop() + csrf = soup.find("form", id="fm").find("input", attrs={"name": "csrf"})["value"] + resp = session.post(url, data={"csrf": csrf, "selectPage": page, "product_class": product_class}) + soup = BeautifulSoup(resp.content, "html5lib") + tbody = soup.find("table", class_="cpl").find("tbody") + for tr in tbody.find_all("tr"): + tds = tr.find_all("td") + if len(tds) != 6: + continue + link = tds[0].find("a") + id = link["id"].split("-")[1] + cert: dict[str, Any] = { + "product": sns(tds[0].text), + "cert_id": sns(tds[1].text), + "product_link": constants.CC_KOREA_PRODUCT_URL.format(id), + "vendor": sns(tds[2].text), + "level": sns(tds[3].text), + "category": sns(tds[4].text), + "certification_date": sns(tds[5].text), + } + if enhanced: + e: dict[str, Any] = {} + if not cert["product_link"]: + continue + cert_page = _get_page(cert["product_link"], session) + main = cert_page.find("div", class_="mainContent") + table = main.find("table", class_="shortenedWidth") + v = e + for tr in table.find_all("tr"): + th = tr.find("th") + td = tr.find("td") + if not th: + mus = e.setdefault("maintenance_update", []) + v = {"name": sns(td.text)} + mus.append(v) + continue + title = sns(th.text) + value = sns(td.text) + a = td.find("a") + if not title: + continue + if "Product Name" in title: + v["product"] = value + elif "Common Criteria" in title: + v["cc_version"] = value + elif "Date of Certification" in title or "Date issued" in title: + v["certification_date"] = value + elif "EvaluationAssurance Level" in title: + v["assurance_level"] = value + elif "Expiry Date" in title: + v["expiration_date"] = value + elif "Type of Product" in title: + v["product_type"] = value + elif "Certification No." in title: + v["cert_id"] = value + elif "Protection Profile" in title: + v["protection_profile"] = value + elif "Developer" in title: + v["developer"] = value + elif "Certificate Holder" in title: + v["holder"] = value + elif "Certificate" in title and a: + v["cert_link"] = urljoin(constants.CC_KOREA_BASE_URL, a["href"]) + if artifacts: + v["cert_hash"] = _get_hash(v["cert_link"], session).hex() + elif "Security Target" in title and a: + v["target_link"] = urljoin(constants.CC_KOREA_BASE_URL, a["href"]) + if artifacts: + v["target_hash"] = _get_hash(v["target_link"], session).hex() + elif "Certification Report" in title and a: + v["report_link"] = urljoin(constants.CC_KOREA_BASE_URL, a["href"]) + if artifacts: + v["report_hash"] = _get_hash(v["report_link"], session).hex() + elif "Maintenance Report" in title and a: + v["maintenance_link"] = urljoin(constants.CC_KOREA_BASE_URL, a["href"]) + if artifacts: + v["maintenance_hash"] = _get_hash(v["maintenance_link"], session).hex() + cert["enhanced"] = e + results.append(cert) + seen_pages.add(page) + page_links = soup.find("div", class_="paginate").find_all("a", class_="number_off") + for page_link in page_links: + try: + new_page = int(page_link.text) + if new_page not in seen_pages: + pages.add(new_page) + except Exception: + pass + return results + + +def get_korea_certified(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Korean "certified product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_korea(product_class=1, enhanced=enhanced, artifacts=artifacts) + + +def get_korea_suspended(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Korean "suspended product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_korea(product_class=2, enhanced=enhanced, artifacts=artifacts) + + +def get_korea_archived(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Korean "product in evaluation" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_korea(product_class=4, enhanced=enhanced, artifacts=artifacts) + + +def _get_singapore(url: str, artifacts: bool) -> list[dict[str, Any]]: + soup = _get_page(url) + page_id = str(soup.find("input", id="CurrentPageId").value) + page = 1 + api_call = requests.post( + constants.CC_SINGAPORE_API_URL, + data={ + "PassSortFilter": False, + "currentPageId": page_id, + "page": page, + "limit": 15, + "ProductDeveloperName": "", + }, + ) + api_json = api_call.json() + total = api_json["total"] + results: list[dict[str, Any]] = [] + while len(results) != total: + for obj in api_json["objects"]: + cert: dict[str, Any] = { + "level": obj["assuranceLevel"], + "product": obj["productName"], + "vendor": obj["productDeveloper"], + "url": urljoin(constants.CC_SINGAPORE_BASE_URL, obj["productUrl"]), + "certification_date": obj["dateOfIssuance"], + "expiration_date": obj["dateOfExpiry"], + "category": obj["productCategory"]["title"], + "cert_title": obj["certificate"]["title"], + "cert_link": urljoin(constants.CC_SINGAPORE_BASE_URL, obj["certificate"]["mediaUrl"]), + "report_title": obj["certificationReport"]["title"], + "report_link": urljoin(constants.CC_SINGAPORE_BASE_URL, obj["certificationReport"]["mediaUrl"]), + "target_title": obj["securityTarget"]["title"], + "target_link": urljoin(constants.CC_SINGAPORE_BASE_URL, obj["securityTarget"]["mediaUrl"]), + } + if artifacts: + cert["cert_hash"] = _get_hash(cert["cert_link"]).hex() + cert["report_hash"] = _get_hash(cert["report_link"]).hex() + cert["target_hash"] = _get_hash(cert["target_link"]).hex() + results.append(cert) + page += 1 + api_call = requests.post( + constants.CC_SINGAPORE_API_URL, + data={ + "PassSortFilter": False, + "currentPageId": page_id, + "page": page, + "limit": 15, + "ProductDeveloperName": "", + }, + ) + api_json = api_call.json() + return results + + +def get_singapore_certified(artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Singaporean "certified product" entries. + + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_singapore(constants.CC_SINGAPORE_CERTIFIED_URL, artifacts) + + +def get_singapore_in_evaluation() -> list[dict[str, Any]]: + """ + Get Singaporean "product in evaluation" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_SINGAPORE_INEVAL_URL) + blocks = soup.find_all("div", class_="sfContentBlock") + for block in blocks: + table = block.find("table") + if table: + break + else: + raise ValueError("Cannot find table.") + results = [] + for tr in table.find_all("tr")[1:]: + tds = tr.find_all("td") + cert = { + "name": sns(tds[0].text), + "vendor": sns(tds[1].text), + "level": sns(tds[2].text), + } + results.append(cert) + return results + + +def get_singapore_archived(artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Singaporean "archived product" entries. + + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_singapore(constants.CC_SINGAPORE_ARCHIVED_URL, artifacts) + + +def get_spain_certified() -> list[dict[str, Any]]: + """ + Get Spanish "certified product" entries. + + :return: The entries. + """ + soup = _get_page(constants.CC_SPAIN_CERTIFIED_URL) + tbody = soup.find("table", class_="djc_items_table").find("tbody") + results = [] + for tr in tbody.find_all("tr", recursive=False): + tds = tr.find_all("td") + cert = { + "product": sns(tds[0].text), + "product_link": urljoin(constants.CC_SPAIN_BASE_URL, tds[0].find("a")["href"]), + "category": sns(tds[1].text), + "manufacturer": sns(tds[2].text), + "certification_date": sns(tds[3].find("td", class_="djc_value").text), + } + results.append(cert) + return results + + +def _get_sweden(url: str, enhanced: bool, artifacts: bool) -> list[dict[str, Any]]: # noqa: C901 + soup = _get_page(url) + nav = soup.find("main").find("nav", class_="component-nav-box__list") + results = [] + for link in nav.find_all("a"): + cert: dict[str, Any] = { + "product": sns(link.text), + "url": urljoin(constants.CC_SWEDEN_BASE_URL, link["href"]), + } + if enhanced: + e: dict[str, Any] = {} + if not cert["url"]: + continue + cert_page = _get_page(cert["url"]) + content = cert_page.find("section", class_="container-article") + head = content.find("h1") + e["title"] = sns(head.text) + table = content.find("table") + if table: + for tr in table.find_all("tr"): + tds = tr.find_all("td") + if len(tds) != 2: + continue + title = sns(tds[0].text) + value = sns(tds[1].text) + a = tds[1].find("a") + if not title: + continue + if "Certifierings ID" in title: + e["cert_id"] = value + elif "Giltighet" in title: + e["mutual_recognition"] = value + elif "Produktnamn" in title: + e["product"] = value + elif "Produktkategori" in title: + e["category"] = value + elif "Assuranspaket" in title: + e["assurance_level"] = value + elif "Certifieringsdatum" in title: + e["certification_date"] = value + elif "Sponsor" in title: + e["sponsor"] = value + elif "Utvecklare" in title: + e["developer"] = value + elif "Evalueringsföretag" in title: + e["evaluation_facility"] = value + elif "Security Target" in title and a: + e["target_link"] = urljoin(constants.CC_SWEDEN_BASE_URL, a["href"]) + if artifacts: + e["target_hash"] = _get_hash(e["target_link"]).hex() + elif "Certifieringsrapport" in title and a: + e["report_link"] = urljoin(constants.CC_SWEDEN_BASE_URL, a["href"]) + if artifacts: + e["report_hash"] = _get_hash(e["report_hash"]).hex() + elif "Certifikat" in title and a: + e["cert_link"] = urljoin(constants.CC_SWEDEN_BASE_URL, a["href"]) + if artifacts: + e["cert_hash"] = _get_hash(e["cert_link"]).hex() + cert["enhanced"] = e + results.append(cert) + return results + + +def get_sweden_certified(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Swedish "certified product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_sweden(constants.CC_SWEDEN_CERTIFIED_URL, enhanced, artifacts) + + +def get_sweden_in_evaluation(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Swedish "product in evaluation" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_sweden(constants.CC_SWEDEN_INEVAL_URL, enhanced, artifacts) + + +def get_sweden_archived(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: + """ + Get Swedish "archived product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + return _get_sweden(constants.CC_SWEDEN_ARCHIVED_URL, enhanced, artifacts) + + +def get_turkey_certified() -> list[dict[str, Any]]: + """ + Get Turkish "certified product" entries. + + :return: The entries. + """ + results = [] + with tempfile.TemporaryDirectory() as tmpdir: + pdf_path = Path(tmpdir) / "turkey.pdf" + resp = requests.get(constants.CC_TURKEY_ARCHIVED_URL) + if resp.status_code != requests.codes.ok: + raise ValueError(f"Unable to download: status={resp.status_code}") + with pdf_path.open("wb") as f: + f.write(resp.content) + dfs = tabula.read_pdf(str(pdf_path), pages="all") + for df in dfs: + for line in df.values: # type: ignore + cert = { + # TODO: Split item number and generate several dicts for a range they include. + "item_no": line[0], + "developer": line[1], + "product": line[2], + "cc_version": line[3], + "level": line[4], + "cert_lab": line[5], + "certification_date": line[6], + "expiration_date": line[7], + # TODO: Parse "Ongoing Evaluation" out of this field as well. + "archived": isinstance(line[9], str) and "Archived" in line[9], + } + results.append(cert) + return results + + +def get_usa_certified(enhanced: bool = True, artifacts: bool = False) -> list[dict[str, Any]]: # noqa: C901 + """ + Get American "certified product" entries. + + :param enhanced: Whether to enhance the results by following links (slower, more data). + :param artifacts: Whether to download and compute artifact hashes (way slower, even more data). + :return: The entries. + """ + # TODO: Information could be expanded by following the cc_claims (has links to protection profiles). + soup = _get_page(constants.CC_USA_CERTIFIED_URL) + tbody = soup.find("table", class_="tablesorter").find("tbody") + results = [] + for tr in tbody.find_all("tr"): + tds = tr.find_all("td") + vendor_span = tds[0].find("span", class_="b u") + product_link = tds[0].find("a") + scheme_img = tds[6].find("img") + # Only return the US certifications. + if scheme_img["title"] != "USA": + continue + cert: dict[str, Any] = { + "product": sns(product_link.text), + "vendor": sns(vendor_span.text), + "product_link": urljoin(constants.CC_USA_PRODUCT_URL, product_link["href"]), + "id": sns(tds[1].text), + "cc_claim": sns(tds[2].text), + "cert_lab": sns(tds[3].text), + "certification_date": sns(tds[4].text), + "assurance_maintenance_date": sns(tds[5].text), + } + if enhanced: + e: dict[str, Any] = {} + if not cert["product_link"]: + continue + cert_page = _get_page(cert["product_link"]) + details = cert_page.find("div", class_="txt2 lma") + for span in details.find_all("span"): + title = sns(span.text) + if not title: + continue + sibling = span.next_sibling + value = sns(sibling.text) + if "Certificate Date" in title: + e["certification_date"] = value + elif "Product Type" in title: + e["product_type"] = value + elif "Conformance Claim" in title: + e["cc_claim"] = value + elif "Validation Report Number" in title: + e["cert_id"] = value + elif "PP Identifier" in title: + e["protection_profile"] = sns(span.find_next_sibling("a").text) + elif "CC Testing Lab" in title: + e["evaluation_facility"] = sns(span.find_next_sibling("a").text) + links = cert_page.find_all("a", class_="pseudobtn1") + for link in links: + name = sns(link.text) + href = urljoin(constants.CC_USA_BASE_URL, sns(link["href"])) + if not name: + continue + if "CC Certificate" in name: + e["cert_link"] = href + if artifacts: + e["cert_hash"] = _get_hash(href).hex() + elif "Security Target" in name: + e["target_link"] = href + if artifacts: + e["target_hash"] = _get_hash(href).hex() + elif "Validation Report" in name: + e["report_link"] = href + if artifacts: + e["report_hash"] = _get_hash(href).hex() + elif "Assurance Activity" in name: + e["assurance_activity_link"] = href + if artifacts: + e["assurance_activity_hash"] = _get_hash(href).hex() + elif "Administrative Guide" in name: + guides = e.setdefault("administrative_guides", []) + guide = {"link": href} + guides.append(guide) + if artifacts: + guide["hash"] = _get_hash(href).hex() + cert["enhanced"] = e + results.append(cert) + return results + + +def get_usa_in_evaluation() -> list[dict[str, Any]]: + """ + Get American "product in evaluation" entries. + + :return: The entries. + """ + # TODO: Information could be expanded by following the cc_claims (has links to protection profiles). + soup = _get_page(constants.CC_USA_INEVAL_URL) + tbody = soup.find("table", class_="tablesorter").find("tbody") + results = [] + for tr in tbody.find_all("tr"): + tds = tr.find_all("td") + vendor_span = tds[0].find("span", class_="b u") + product_name = None + for child in tds[0].children: + if isinstance(child, NavigableString): + product_name = sns(child) + break + cert = { + "vendor": sns(vendor_span.text), + "id": sns(tds[1].text), + "cc_claim": sns(tds[2].text), + "cert_lab": sns(tds[3].text), + "kickoff_date": sns(tds[4].text), + } + if product_name: + cert["product"] = product_name + results.append(cert) + return results + + +def get_usa_archived() -> list[dict[str, Any]]: + """ + Get American "archived product" entries. + + :return: The entries. + """ + # TODO: Information could be expanded by following the cc_claims (has links to protection profiles). + soup = _get_page(constants.CC_USA_ARCHIVED_URL) + tbody = soup.find("table", class_="tablesorter").find("tbody") + results = [] + for tr in tbody.find_all("tr"): + tds = tr.find_all("td") + scheme_img = tds[5].find("img") + # Only return the US certifications. + if scheme_img["title"] != "USA": + continue + vendor_span = tds[0].find("span", class_="b u") + product_name = None + for child in tds[0].children: + if isinstance(child, NavigableString): + product_name = sns(child) + break + cert = { + "vendor": sns(vendor_span.text), + "id": sns(tds[1].text), + "cc_claim": sns(tds[2].text), + "cert_lab": sns(tds[3].text), + "certification_date": sns(tds[4].text), + } + if product_name: + cert["product"] = product_name + results.append(cert) + return results + + +class EntryType(Enum): + Certified = "CERTIFIED" + InEvaluation = "INEVALUATION" + Archived = "ARCHIVED" + + +@dataclass +class CCScheme(ComplexSerializableType): + """ + Dataclass for data extracted from a CCScheme website, so more like a + "CCSchemeWebDump" but that classname is not so nice. + + Contains the country (scheme) code a timestamp of extraction and + several lists of entries: certified, in-evaluation and archived. + It may only contain some lists of entries as the scheme might only publish + them. + """ + + country: str + timestamp: datetime + lists: dict[EntryType, Any] + + methods: ClassVar[dict[str, dict[EntryType, Callable]]] = { + "AU": {EntryType.InEvaluation: get_australia_in_evaluation}, + "CA": {EntryType.InEvaluation: get_canada_in_evaluation, EntryType.Certified: get_canada_certified}, + "FR": {EntryType.Certified: get_france_certified}, + "DE": {EntryType.Certified: get_germany_certified}, + "IN": {EntryType.Certified: get_india_certified, EntryType.Archived: get_india_archived}, + "IT": {EntryType.Certified: get_italy_certified, EntryType.InEvaluation: get_italy_in_evaluation}, + "JP": { + EntryType.InEvaluation: get_japan_in_evaluation, + EntryType.Certified: get_japan_certified, + EntryType.Archived: get_japan_archived, + }, + "MY": {EntryType.Certified: get_malaysia_certified, EntryType.InEvaluation: get_malaysia_in_evaluation}, + "NL": {EntryType.Certified: get_netherlands_certified, EntryType.InEvaluation: get_netherlands_in_evaluation}, + "NO": {EntryType.Certified: get_norway_certified, EntryType.Archived: get_norway_archived}, + "KO": {EntryType.Certified: get_korea_certified, EntryType.Archived: get_korea_archived}, + "SG": { + EntryType.InEvaluation: get_singapore_in_evaluation, + EntryType.Certified: get_singapore_certified, + EntryType.Archived: get_singapore_archived, + }, + "ES": {EntryType.Certified: get_spain_certified}, + "SE": { + EntryType.InEvaluation: get_sweden_in_evaluation, + EntryType.Certified: get_sweden_certified, + EntryType.Archived: get_sweden_archived, + }, + "TR": {EntryType.Certified: get_turkey_certified}, + "US": { + EntryType.InEvaluation: get_usa_in_evaluation, + EntryType.Certified: get_usa_certified, + EntryType.Archived: get_usa_archived, + }, + } + + @classmethod + def from_dict(cls, dct): + return cls( + dct["country"], + datetime.fromisoformat(dct["timestamp"]), + {EntryType(entry_type): entries for entry_type, entries in dct["lists"].items()}, + ) + + def to_dict(self): + return { + "country": self.country, + "timestamp": self.timestamp.isoformat(), + "lists": {entry_type.value: entries for entry_type, entries in self.lists.items()}, + } + + @classmethod + def from_web(cls, scheme: str, entry_types: Iterable[EntryType]) -> CCScheme: + if not (scheme_lists := cls.methods.get(scheme)): + raise ValueError("Unknown scheme.") + entries = {} + timestamp = datetime.now() + for each_type in entry_types: + if not (method := scheme_lists.get(each_type)): + raise ValueError("Wrong entry_type for scheme.") + entries[each_type] = method() + return cls(scheme, timestamp, entries) diff --git a/src/sec_certs/sample/fips_mip.py b/src/sec_certs/sample/fips_mip.py index f1ef05c8..89c46d01 100644 --- a/src/sec_certs/sample/fips_mip.py +++ b/src/sec_certs/sample/fips_mip.py @@ -4,6 +4,7 @@ import logging from dataclasses import dataclass from datetime import date, datetime from enum import Enum +from functools import total_ordering from pathlib import Path from tempfile import NamedTemporaryFile from typing import Iterator, Mapping @@ -20,25 +21,32 @@ from sec_certs.utils.helpers import to_utc logger = logging.getLogger(__name__) +@total_ordering class MIPStatus(Enum): IN_REVIEW = "In Review" REVIEW_PENDING = "Review Pending" COORDINATION = "Coordination" FINALIZATION = "Finalization" + def __lt__(self, other): + if self.__class__ == other.__class__: + mb = list(MIPStatus.__members__.keys()) + return mb.index(self.name) < mb.index(other.name) + raise NotImplementedError -@dataclass(frozen=True) + +@dataclass(frozen=True, order=True) class MIPEntry(ComplexSerializableType): module_name: str vendor_name: str standard: str - status: MIPStatus | None + status: MIPStatus status_since: date | None - def to_dict(self) -> dict[str, str | MIPStatus | None | date | None]: + def to_dict(self) -> dict[str, str | MIPStatus | date | None]: return { **self.__dict__, - "status": self.status.value if self.status else None, + "status": self.status.value, "status_since": self.status_since.isoformat() if self.status_since else None, } @@ -48,12 +56,32 @@ class MIPEntry(ComplexSerializableType): dct["module_name"], dct["vendor_name"], dct["standard"], - MIPStatus(dct["status"]) if dct["status"] else None, + MIPStatus(dct["status"]), date.fromisoformat(dct["status_since"]) if dct.get("status_since") else None, ) @dataclass +class MIPFlow(ComplexSerializableType): + module_name: str + vendor_name: str + standard: str + state_changes: list[tuple[date, MIPStatus]] + + def to_dict(self) -> dict[str, str | list]: + return {**self.__dict__, "state_changes": [(dt.isoformat(), status.value) for dt, status in self.state_changes]} + + @classmethod + def from_dict(cls, dct: Mapping) -> MIPFlow: + return cls( + dct["module_name"], + dct["vendor_name"], + dct["standard"], + [(date.fromisoformat(dt), MIPStatus(status)) for dt, status in dct["state_changes"]], + ) + + +@dataclass class MIPSnapshot(ComplexSerializableType): entries: set[MIPEntry] timestamp: datetime @@ -95,7 +123,6 @@ class MIPSnapshot(ComplexSerializableType): entries = set() for tr in lines: tds = tr.find_all("td") - status = None if "mip-highlight" in tds[-1]["class"]: status = MIPStatus.FINALIZATION elif "mip-highlight" in tds[-2]["class"]: @@ -104,6 +131,8 @@ class MIPSnapshot(ComplexSerializableType): status = MIPStatus.REVIEW_PENDING elif "mip-highlight" in tds[-4]["class"]: status = MIPStatus.IN_REVIEW + else: + raise ValueError("Cannot parse MIP status line.") entries.add(MIPEntry(str(tds[0].string), str(tds[1].string), str(tds[2].string), status, None)) return entries @@ -123,7 +152,7 @@ class MIPSnapshot(ComplexSerializableType): return { MIPEntry( str(line[0].string), - str(" ".join(line[1].find_all(text=True, recursive=False)).strip()), + str(" ".join(line[1].find_all(string=True, recursive=False)).strip()), str(line[2].string), MIPStatus(str(line[3].string)), None, @@ -137,7 +166,7 @@ class MIPSnapshot(ComplexSerializableType): entries = set() for line in (tr.find_all("td") for tr in lines): module_name = str(line[0].string) - vendor_name = str(" ".join(line[1].find_all(text=True, recursive=False)).strip()) + vendor_name = str(" ".join(line[1].find_all(string=True, recursive=False)).strip()) standard = str(line[2].string) status_line = FIPS_MIP_STATUS_RE.match(str(line[3].string)) if status_line is None: diff --git a/src/sec_certs/serialization/__init__.py b/src/sec_certs/serialization/__init__.py index e69de29b..faa56704 100644 --- a/src/sec_certs/serialization/__init__.py +++ b/src/sec_certs/serialization/__init__.py @@ -0,0 +1 @@ +"""This package provides tols for serialization of datasets and certificates.""" diff --git a/src/sec_certs/utils/__init__.py b/src/sec_certs/utils/__init__.py index e69de29b..45b35aea 100644 --- a/src/sec_certs/utils/__init__.py +++ b/src/sec_certs/utils/__init__.py @@ -0,0 +1 @@ +"""This package provides utilities used throught the framework.""" diff --git a/src/sec_certs/utils/strings.py b/src/sec_certs/utils/strings.py new file mode 100644 index 00000000..785b1026 --- /dev/null +++ b/src/sec_certs/utils/strings.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import re +from functools import lru_cache + +import spacy + + +@lru_cache +def load_spacy_model(spacy_model_to_load: str = "en_core_web_sm"): + return spacy.load(spacy_model_to_load, disable=["parser", "ner"]) + + +def fully_sanitize_string(string: str) -> str: + return replace_special_chars_with_space(discard_trademark_symbols(string.lower())).strip() + + +def replace_special_chars_with_space(string: str) -> str: + return re.sub(r"[^a-zA-Z0-9 \n\.]", " ", string) + + +def discard_trademark_symbols(string: str) -> str: + return string.replace("®", "").replace("™", "") + + +def strip_manufacturer_and_version(string: str, manufacturers: set[str] | None, versions: set[str]) -> str: + to_strip = versions | manufacturers if manufacturers else versions + for x in to_strip: + string = string.lower().replace(replace_special_chars_with_space(x.lower()), " ").strip() + return string + + +def standardize_version_in_cert_name(string: str, detected_versions: set[str]) -> str: + for ver in detected_versions: + version_regex = r"(" + r"(\bversion)\s*" + ver + r"+) | (\bv\s*" + ver + r"+)" + string = re.sub(version_regex, " " + ver + " ", string, flags=re.IGNORECASE) + return string + + +def lemmatize_product_name(nlp, product_name: str) -> str: + if not product_name: + return product_name + return " ".join([token.lemma_ for token in nlp(fully_sanitize_string(product_name))]) diff --git a/tests/cc/test_cc_analysis.py b/tests/cc/test_cc_analysis.py index fb2633d9..d0618266 100644 --- a/tests/cc/test_cc_analysis.py +++ b/tests/cc/test_cc_analysis.py @@ -9,7 +9,7 @@ import tests.data.cc.analysis import tests.data.common from sec_certs.cert_rules import SARS_IMPLIED_FROM_EAL -from sec_certs.dataset import CCDataset +from sec_certs.dataset.cc import CCDataset from sec_certs.dataset.cpe import CPEDataset from sec_certs.dataset.cve import CVEDataset from sec_certs.sample.cc import CCCertificate diff --git a/tests/cc/test_cc_dataset.py b/tests/cc/test_cc_dataset.py index d9fe79e3..bbfea14b 100644 --- a/tests/cc/test_cc_dataset.py +++ b/tests/cc/test_cc_dataset.py @@ -8,7 +8,7 @@ import pytest import tests.data.cc.dataset from sec_certs import constants -from sec_certs.dataset import CCDataset +from sec_certs.dataset.cc import CCDataset from sec_certs.sample.cc import CCCertificate diff --git a/tests/cc/test_cc_schemes.py b/tests/cc/test_cc_schemes.py index 078a3190..24467709 100644 --- a/tests/cc/test_cc_schemes.py +++ b/tests/cc/test_cc_schemes.py @@ -1,99 +1,217 @@ +from urllib.parse import urlparse + import pytest +from requests import RequestException + +import sec_certs.sample.cc_scheme as CCSchemes +from sec_certs.dataset.cc import CCDataset +from sec_certs.model.cc_matching import CCSchemeMatcher +from sec_certs.sample.cc import CCCertificate + -from sec_certs.dataset import CCSchemeDataset +def absolute_urls(results): + for result in results: + for key, value in result.items(): + if "url" in key or "link" in key and value is not None: + parsed = urlparse(value) + assert bool(parsed.netloc) + return True -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_australia(): - assert len(CCSchemeDataset.get_australia_in_evaluation()) != 0 + ineval = CCSchemes.get_australia_in_evaluation() + assert len(ineval) != 0 + assert absolute_urls(ineval) -@pytest.mark.xfail(reason="May fail due to server errors.") -def test_canada(): - assert len(CCSchemeDataset.get_canada_certified()) != 0 - assert len(CCSchemeDataset.get_canada_in_evaluation()) != 0 +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) +@pytest.fixture +def canada_certified(): + return CCSchemes.get_canada_certified() -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) +@pytest.mark.slow +def test_canada(canada_certified): + assert len(canada_certified) != 0 + assert absolute_urls(canada_certified) + ineval = CCSchemes.get_canada_in_evaluation() + assert len(ineval) != 0 + assert absolute_urls(ineval) + + +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) +@pytest.mark.slow def test_anssi(): - assert len(CCSchemeDataset.get_france_certified()) != 0 + certified = CCSchemes.get_france_certified() + assert len(certified) != 0 + assert absolute_urls(certified) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) +@pytest.mark.slow def test_bsi(): - assert len(CCSchemeDataset.get_germany_certified()) != 0 + certified = CCSchemes.get_germany_certified() + assert len(certified) != 0 + assert absolute_urls(certified) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_india(): - assert len(CCSchemeDataset.get_india_certified()) != 0 - assert len(CCSchemeDataset.get_india_archived()) != 0 + certified = CCSchemes.get_india_certified() + assert len(certified) != 0 + assert absolute_urls(certified) + archived = CCSchemes.get_india_archived() + assert len(archived) != 0 + assert absolute_urls(archived) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_italy(): - assert len(CCSchemeDataset.get_italy_certified()) != 0 - assert len(CCSchemeDataset.get_italy_in_evaluation()) != 0 + certified = CCSchemes.get_italy_certified() + assert len(certified) != 0 + assert absolute_urls(certified) + ineval = CCSchemes.get_italy_in_evaluation() + assert len(ineval) != 0 + assert absolute_urls(ineval) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_japan(): - assert len(CCSchemeDataset.get_japan_certified()) != 0 - assert len(CCSchemeDataset.get_japan_archived()) != 0 - assert len(CCSchemeDataset.get_japan_in_evaluation()) != 0 + certified = CCSchemes.get_japan_certified() + assert len(certified) != 0 + assert absolute_urls(certified) + archived = CCSchemes.get_japan_archived() + assert len(archived) != 0 + assert absolute_urls(archived) + ineval = CCSchemes.get_japan_in_evaluation() + assert len(ineval) != 0 + assert absolute_urls(ineval) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_malaysia(): - assert len(CCSchemeDataset.get_malaysia_certified()) != 0 - assert len(CCSchemeDataset.get_malaysia_in_evaluation()) != 0 + certified = CCSchemes.get_malaysia_certified() + assert len(certified) != 0 + assert absolute_urls(certified) + ineval = CCSchemes.get_malaysia_in_evaluation() + assert len(ineval) != 0 + assert absolute_urls(ineval) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_netherlands(): - assert len(CCSchemeDataset.get_netherlands_certified()) != 0 - assert len(CCSchemeDataset.get_netherlands_in_evaluation()) != 0 + certified = CCSchemes.get_netherlands_certified() + assert len(certified) != 0 + # assert absolute_urls(certified) + ineval = CCSchemes.get_netherlands_in_evaluation() + assert len(ineval) != 0 + assert absolute_urls(ineval) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_norway(): - assert len(CCSchemeDataset.get_norway_certified()) != 0 - assert len(CCSchemeDataset.get_norway_archived()) != 0 + certified = CCSchemes.get_norway_certified() + assert len(certified) != 0 + assert absolute_urls(certified) + archived = CCSchemes.get_norway_archived() + assert len(archived) != 0 + assert absolute_urls(archived) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_korea(): - assert len(CCSchemeDataset.get_korea_certified()) != 0 - CCSchemeDataset.get_korea_suspended() - assert len(CCSchemeDataset.get_korea_archived()) != 0 + certified = CCSchemes.get_korea_certified() + assert len(certified) != 0 + assert absolute_urls(certified) + archived = CCSchemes.get_korea_archived() + assert len(archived) != 0 + assert absolute_urls(archived) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_singapore(): - assert len(CCSchemeDataset.get_singapore_certified()) != 0 - assert len(CCSchemeDataset.get_singapore_archived()) != 0 - assert len(CCSchemeDataset.get_singapore_in_evaluation()) != 0 + certified = CCSchemes.get_singapore_certified() + assert len(certified) != 0 + assert absolute_urls(certified) + archived = CCSchemes.get_singapore_archived() + assert len(archived) != 0 + assert absolute_urls(archived) + ineval = CCSchemes.get_singapore_in_evaluation() + assert len(ineval) != 0 + assert absolute_urls(ineval) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_spain(): - assert len(CCSchemeDataset.get_spain_certified()) != 0 + certified = CCSchemes.get_spain_certified() + assert len(certified) != 0 + assert absolute_urls(certified) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_sweden(): - assert len(CCSchemeDataset.get_sweden_certified()) != 0 - assert len(CCSchemeDataset.get_sweden_in_evaluation()) != 0 - assert len(CCSchemeDataset.get_sweden_archived()) != 0 + certified = CCSchemes.get_sweden_certified() + assert len(certified) != 0 + assert absolute_urls(certified) + archived = CCSchemes.get_sweden_archived() + assert len(archived) != 0 + assert absolute_urls(archived) + ineval = CCSchemes.get_sweden_in_evaluation() + assert len(ineval) != 0 + assert absolute_urls(ineval) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_turkey(): - assert len(CCSchemeDataset.get_turkey_certified()) != 0 + certified = CCSchemes.get_turkey_certified() + assert len(certified) != 0 + assert absolute_urls(certified) -@pytest.mark.xfail(reason="May fail due to server errors.") +@pytest.mark.xfail(reason="May fail due to server errors.", raises=RequestException) def test_usa(): - assert len(CCSchemeDataset.get_usa_certified()) != 0 - assert len(CCSchemeDataset.get_usa_in_evaluation()) != 0 - assert len(CCSchemeDataset.get_usa_archived()) != 0 + certified = CCSchemes.get_usa_certified() + assert len(certified) != 0 + assert absolute_urls(certified) + archived = CCSchemes.get_usa_archived() + assert len(archived) != 0 + assert absolute_urls(archived) + ineval = CCSchemes.get_usa_in_evaluation() + assert len(ineval) != 0 + assert absolute_urls(ineval) + + +def test_single_match(cert_one: CCCertificate): + entry = { + "product": "NetIQ Identity Manager 4.7", + "url": "https://www.fmv.se/verksamhet/ovrig-verksamhet/csec/certifikat-utgivna-av-csec/netiq-identity-manager-4.7/", + "enhanced": { + "title": "NetIQ Identity Manager 4.7", + "cert_id": "CSEC2018013", + "mutual_recognition": "CCRA, SOGIS-MRA, EA-MLA", + "product": "NetIQ Identity Manager 4.7Software Version: Identity Applications (RBPM) 4.7.3.0.1109, Identity Manager Engine 4.7.3.0.AE, Identity Reporting Module 6.5.0. F14508F, Sentinel Log Management for Identity Governance and Administration 8.2.2.0_5415, One SSO Provider (OSP) 6.3.3.0, Self Service Password Reset (SSPR) 4.4.0.2 B366 r39762", + "category": "Identity Manager", + "target_link": "https://www.fmv.se/globalassets/csec/netiq-identity-manager-4.7/st---netiq-identity-manager-4.7.pdf", + "assurance_level": "EAL3 + ALC_FLR.2", + "certification_date": "2020-06-15", + "report_link": "https://www.fmv.se/globalassets/csec/netiq-identity-manager-4.7/certification-report---netiq-identity-manager-4.7.pdf", + "cert_link": "https://www.fmv.se/globalassets/csec/netiq-identity-manager-4.7/certifikat-ccra---netiq-identity-manager-4.7.pdf", + "sponsor": "NetIQ Corporation", + "developer": "NetIQ Corporation", + "evaluation_facility": "Combitech AB and EWA-Canada", + }, + } + matcher = CCSchemeMatcher(entry, "SE") + assert matcher.match(cert_one) > 95 + + +def test_matching(toy_dataset: CCDataset, canada_certified): + matches = CCSchemeMatcher.match_all(canada_certified, "CA", toy_dataset) + assert len(matches) == 1 + + +def test_process_dataset(toy_dataset: CCDataset): + toy_dataset.process_schemes(True, only_schemes={"CA"}) + assert toy_dataset["8a5e6bcda602920c"].heuristics.scheme_data is not None diff --git a/tests/data/cc/certificate/fictional_cert.json b/tests/data/cc/certificate/fictional_cert.json index 5d5a0499..7cf04d2d 100644 --- a/tests/data/cc/certificate/fictional_cert.json +++ b/tests/data/cc/certificate/fictional_cert.json @@ -89,7 +89,8 @@ "directly_referencing": null, "indirectly_referenced_by": null, "indirectly_referencing": null - } + }, + "scheme_data": 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 fd21471b..8a087e2b 100644 --- a/tests/data/cc/dataset/auxiliary_datasets/maintenances/maintenance_updates.json +++ b/tests/data/cc/dataset/auxiliary_datasets/maintenances/maintenance_updates.json @@ -70,7 +70,8 @@ }, "extracted_sars": null, "direct_transitive_cves": null, - "indirect_transitive_cves": null + "indirect_transitive_cves": null, + "scheme_data": null }, "related_cert_digest": "8a5e6bcda602920c", "maintenance_date": "2019-08-26" diff --git a/tests/data/cc/dataset/toy_dataset.json b/tests/data/cc/dataset/toy_dataset.json index dec802f7..b425235e 100644 --- a/tests/data/cc/dataset/toy_dataset.json +++ b/tests/data/cc/dataset/toy_dataset.json @@ -93,7 +93,8 @@ "directly_referencing": null, "indirectly_referenced_by": null, "indirectly_referencing": null - } + }, + "scheme_data": null } }, { @@ -180,7 +181,8 @@ "directly_referencing": null, "indirectly_referenced_by": null, "indirectly_referencing": null - } + }, + "scheme_data": null } }, { @@ -275,7 +277,8 @@ "directly_referencing": null, "indirectly_referenced_by": null, "indirectly_referencing": null - } + }, + "scheme_data": null } } ] diff --git a/tests/fips/test_fips_certificate.py b/tests/fips/test_fips_certificate.py index 207ae46d..99b1d9b7 100644 --- a/tests/fips/test_fips_certificate.py +++ b/tests/fips/test_fips_certificate.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import shutil from importlib import resources diff --git a/tests/fips/test_fips_dataset.py b/tests/fips/test_fips_dataset.py index 7a4bc712..ed7808be 100644 --- a/tests/fips/test_fips_dataset.py +++ b/tests/fips/test_fips_dataset.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import shutil from importlib import resources diff --git a/tests/fips/test_fips_iut.py b/tests/fips/test_fips_iut.py index 3412f6f2..cf1434b0 100644 --- a/tests/fips/test_fips_iut.py +++ b/tests/fips/test_fips_iut.py @@ -1,11 +1,16 @@ +from __future__ import annotations + +import datetime from importlib import resources from pathlib import Path import pytest import tests.data.fips.iut -from sec_certs.dataset import IUTDataset -from sec_certs.sample import IUTSnapshot +from sec_certs.dataset.fips import FIPSDataset +from sec_certs.dataset.fips_iut import IUTDataset +from sec_certs.model.fips_matching import FIPSProcessMatcher +from sec_certs.sample.fips_iut import IUTEntry, IUTSnapshot @pytest.fixture(scope="module") @@ -39,3 +44,23 @@ def test_iut_snapshot_from_web(): def test_iut_snapshot_from_web_latest(): assert IUTSnapshot.from_web_latest() + + +def test_iut_matching(processed_dataset: FIPSDataset): + entry = IUTEntry( + module_name="Red Hat Enterprise Linux 7.1 OpenSSL Module", + vendor_name="Red Hat(R), Inc.", + standard="FIPS 140-2", + iut_date=datetime.date(2014, 1, 1), + ) + matcher = FIPSProcessMatcher(entry) + scores = [matcher.match(cert) for cert in processed_dataset] + assert len(list(filter(lambda x: x > 90, scores))) == 1 + + +def test_iut_snapshot_match(processed_dataset: FIPSDataset, data_dump_path: Path): + snapshot = IUTSnapshot.from_dump(data_dump_path) + # Move snapshot date back so that there are matches + snapshot.timestamp = datetime.datetime(2014, 1, 2) + matches = FIPSProcessMatcher.match_snapshot(snapshot, processed_dataset) + assert matches diff --git a/tests/fips/test_fips_mip.py b/tests/fips/test_fips_mip.py index 807b83af..7fb473c2 100644 --- a/tests/fips/test_fips_mip.py +++ b/tests/fips/test_fips_mip.py @@ -1,11 +1,16 @@ +from __future__ import annotations + +import datetime from importlib import resources from pathlib import Path import pytest import tests.data.fips.mip -from sec_certs.dataset import MIPDataset -from sec_certs.sample import MIPSnapshot +from sec_certs.dataset.fips import FIPSDataset +from sec_certs.dataset.fips_mip import MIPDataset +from sec_certs.model.fips_matching import FIPSProcessMatcher +from sec_certs.sample.fips_mip import MIPEntry, MIPSnapshot, MIPStatus @pytest.fixture(scope="module") @@ -25,8 +30,9 @@ def test_mip_dataset_from_dumps(data_dir: Path): assert len(dset) == 3 -def test_mip_dataset_from_dataset_latest(): - assert MIPDataset.from_web_latest() +def test_mip_flows(): + dset = MIPDataset.from_web_latest() + assert dset.compute_flows() def test_mip_snapshot_from_dump(data_dump_path: Path): @@ -39,3 +45,24 @@ def test_from_web(): def test_from_web_latest(): assert MIPSnapshot.from_web_latest() + + +def test_mip_matching(processed_dataset: FIPSDataset): + entry = MIPEntry( + module_name="Red Hat Enterprise Linux 7.1 OpenSSL Module", + vendor_name="Red Hat(R), Inc.", + standard="FIPS 140-2", + status=MIPStatus.IN_REVIEW, + status_since=datetime.date(2014, 1, 1), + ) + matcher = FIPSProcessMatcher(entry) + scores = [matcher.match(cert) for cert in processed_dataset] + assert len(list(filter(lambda x: x > 90, scores))) == 1 + + +def test_mip_snapshot_match(processed_dataset: FIPSDataset, data_dump_path: Path): + snapshot = MIPSnapshot.from_dump(data_dump_path) + # Move snapshot date back so that there are matches + snapshot.timestamp = datetime.datetime(2014, 1, 2) + matches = FIPSProcessMatcher.match_snapshot(snapshot, processed_dataset) + assert matches |
