aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/sec_certs/dataset/fips_algorithm.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/sec_certs/dataset/fips_algorithm.py')
-rw-r--r--src/sec_certs/dataset/fips_algorithm.py124
1 files changed, 124 insertions, 0 deletions
diff --git a/src/sec_certs/dataset/fips_algorithm.py b/src/sec_certs/dataset/fips_algorithm.py
new file mode 100644
index 00000000..c48cff07
--- /dev/null
+++ b/src/sec_certs/dataset/fips_algorithm.py
@@ -0,0 +1,124 @@
+from __future__ import annotations
+
+import itertools
+import logging
+import re
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from typing import Iterator
+
+import pandas as pd
+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.serialization.json import ComplexSerializableType
+from sec_certs.utils import helpers
+
+logger = logging.getLogger(__name__)
+
+
+class FIPSAlgorithmDataset(JSONPathDataset, ComplexSerializableType):
+ def __init__(
+ self, algs: dict[str, FIPSAlgorithm] = dict(), json_path: str | Path = constants.DUMMY_NONEXISTING_PATH
+ ):
+ self.algs = algs
+ self.json_path = Path(json_path)
+ self.alg_number_to_algs: dict[str, set[FIPSAlgorithm]] = dict()
+
+ self._build_lookup_dicts()
+
+ @property
+ def serialized_attributes(self) -> list[str]:
+ return ["algs"]
+
+ def __iter__(self) -> Iterator[FIPSAlgorithm]:
+ yield from self.algs.values()
+
+ def __getitem__(self, item: str) -> FIPSAlgorithm:
+ return self.algs.__getitem__(item)
+
+ def __setitem__(self, key: str, value: FIPSAlgorithm) -> None:
+ self.algs.__setitem__(key, value)
+
+ def __len__(self) -> int:
+ return len(self.algs)
+
+ def __contains__(self, item: FIPSAlgorithm) -> bool:
+ if not isinstance(item, FIPSAlgorithm):
+ raise ValueError(f"{item} is not of FIPSAlgorithm class")
+ return item.dgst in self.algs.keys() and self.algs[item.dgst] == item
+
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, FIPSAlgorithmDataset) and self.algs == other.algs
+
+ @classmethod
+ def from_web(cls, json_path: str | Path = constants.DUMMY_NONEXISTING_PATH) -> FIPSAlgorithmDataset:
+ with TemporaryDirectory() as tmp_dir:
+ htmls = FIPSAlgorithmDataset.download_alg_list_htmls(Path(tmp_dir))
+ algs = set(itertools.chain.from_iterable(FIPSAlgorithmDataset.parse_algorithms_from_html(x) for x in htmls))
+ return cls({x.dgst: x for x in algs}, json_path)
+
+ @staticmethod
+ def download_alg_list_htmls(output_dir: Path) -> list[Path]:
+ first_page_path = output_dir / "page1.html"
+ ITEMS_PER_PAGE = "ipp=250"
+
+ res = helpers.download_file(constants.FIPS_ALG_SEARCH_URL + "1&" + ITEMS_PER_PAGE, first_page_path)
+ if res != constants.RESPONSE_OK:
+ res = helpers.download_file(constants.FIPS_ALG_SEARCH_URL + "1&" + ITEMS_PER_PAGE, first_page_path)
+ if res != constants.RESPONSE_OK:
+ logger.error(f"Could not build Algorithm dataset, got server response: {res}")
+ raise ValueError(f"Could not build Algorithm dataset, got server response: {res}")
+
+ n_pages = FIPSAlgorithmDataset.get_number_of_html_pages(first_page_path)
+
+ urls = [constants.FIPS_ALG_SEARCH_URL + str(i) + "&" + ITEMS_PER_PAGE for i in range(2, n_pages + 1)]
+ paths = [output_dir / f"page{i}.html" for i in range(2, n_pages + 1)]
+ responses = helpers.download_parallel(urls, paths, progress_bar_desc="Downloading FIPS Algorithm HTMLs")
+
+ failed_tuples = [
+ (url, path) for url, path, resp in zip(urls, paths, responses) if resp != constants.RESPONSE_OK
+ ]
+ if failed_tuples:
+ failed_urls, failed_paths = zip(*failed_tuples)
+ responses = helpers.download_parallel(failed_urls, failed_paths)
+ if any([x != constants.RESPONSE_OK for x in responses]):
+ raise ValueError("Failed to download the algorithms HTML data, the dataset won't be constructed.")
+
+ return paths
+
+ @staticmethod
+ def get_number_of_html_pages(html_path: Path) -> int:
+ with html_path.open("r") as handle:
+ soup = BeautifulSoup(handle, "html5lib")
+ return int(soup.select("span[data-total-pages]")[0].attrs["data-total-pages"])
+
+ @staticmethod
+ def parse_algorithms_from_html(html_path: Path) -> set[FIPSAlgorithm]:
+ df = pd.read_html(html_path)[0]
+ df["alg_type"] = df["Validation Number"].map(lambda x: re.sub(r"[0-9\s]", "", x))
+ df["alg_number"] = df["Validation Number"].map(lambda x: re.sub(r"[^0-9]", "", x))
+ df["alg"] = df.apply(
+ lambda row: FIPSAlgorithm(
+ row["alg_number"], row["alg_type"], row["Vendor"], row["Implementation"], row["Validation Date"]
+ ),
+ axis=1,
+ )
+ return set(df["alg"])
+
+ def to_pandas(self) -> pd.DataFrame:
+ df = pd.DataFrame([x.pandas_tuple for x in self], columns=FIPSAlgorithm.pandas_columns)
+ df = df.set_index("dgst")
+ return df
+
+ def _build_lookup_dicts(self) -> None:
+ for alg in self:
+ if alg.alg_number not in self.alg_number_to_algs:
+ self.alg_number_to_algs[alg.alg_number] = {alg}
+ else:
+ self.alg_number_to_algs[alg.alg_number].add(alg)
+
+ def get_algorithms_by_id(self, alg_number: str) -> set[FIPSAlgorithm]:
+ return self.alg_number_to_algs.get(alg_number, set())