aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJ08nY2023-04-21 15:33:18 +0200
committerJ08nY2023-04-21 15:33:18 +0200
commitc04a2fb105ed3196f0ccbfb55b9b5765a798d960 (patch)
tree5c55b732b7aa00fd67e911db02bb509756123721
parent0c181f6c2748b4dea5e82fe87a1b3d04c7363076 (diff)
parent5893352a41ce8e513dfbbb7c3e35cac3925966ea (diff)
downloadsec-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
-rw-r--r--.github/workflows/tests.yml4
-rw-r--r--docs/quickstart.md4
-rw-r--r--notebooks/fips/in_process.ipynb84
-rw-r--r--src/sec_certs/cli.py3
-rw-r--r--src/sec_certs/configuration.py13
-rw-r--r--src/sec_certs/constants.py41
-rw-r--r--src/sec_certs/dataset/__init__.py3
-rw-r--r--src/sec_certs/dataset/cc.py809
-rw-r--r--src/sec_certs/dataset/cc_scheme.py60
-rw-r--r--src/sec_certs/dataset/fips_algorithm.py2
-rw-r--r--src/sec_certs/dataset/fips_mip.py37
-rw-r--r--src/sec_certs/model/__init__.py11
-rw-r--r--src/sec_certs/model/cc_matching.py100
-rw-r--r--src/sec_certs/model/cpe_matching.py59
-rw-r--r--src/sec_certs/model/fips_matching.py78
-rw-r--r--src/sec_certs/model/matching.py55
-rw-r--r--src/sec_certs/sample/cc.py1
-rw-r--r--src/sec_certs/sample/cc_certificate_id.py8
-rw-r--r--src/sec_certs/sample/cc_scheme.py1573
-rw-r--r--src/sec_certs/sample/fips_mip.py45
-rw-r--r--src/sec_certs/serialization/__init__.py1
-rw-r--r--src/sec_certs/utils/__init__.py1
-rw-r--r--src/sec_certs/utils/strings.py43
-rw-r--r--tests/cc/test_cc_analysis.py2
-rw-r--r--tests/cc/test_cc_dataset.py2
-rw-r--r--tests/cc/test_cc_schemes.py218
-rw-r--r--tests/data/cc/certificate/fictional_cert.json3
-rw-r--r--tests/data/cc/dataset/auxiliary_datasets/maintenances/maintenance_updates.json3
-rw-r--r--tests/data/cc/dataset/toy_dataset.json9
-rw-r--r--tests/fips/test_fips_certificate.py2
-rw-r--r--tests/fips/test_fips_dataset.py2
-rw-r--r--tests/fips/test_fips_iut.py29
-rw-r--r--tests/fips/test_fips_mip.py35
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