From 565b04b8549066b760ad7eedb1f0e7e661aa1c6c Mon Sep 17 00:00:00 2001 From: olamberts Date: Thu, 14 Aug 2025 15:44:03 +0200 Subject: [PATCH 1/4] Initial CSAF commit --- pontos/csaf/__init__.py | 22 + pontos/csaf/_csaf.py | 347 +++++++++++ pontos/csaf/_enums.py | 56 ++ pontos/csaf/_utils.py | 17 + pontos/csaf/doc.md | 63 ++ pontos/csaf/models/__init__.py | 5 + pontos/csaf/models/relationship.py | 118 ++++ pontos/csaf/models/revision.py | 29 + pontos/csaf/models/vulnerability.py | 55 ++ tests/csaf/__init__.py | 0 tests/csaf/csaf_sample_1.json | 906 ++++++++++++++++++++++++++++ tests/csaf/test_csaf.py | 420 +++++++++++++ tests/csaf/test_relationship.py | 146 +++++ tests/csaf/test_vulnerability.py | 104 ++++ 14 files changed, 2288 insertions(+) create mode 100644 pontos/csaf/__init__.py create mode 100644 pontos/csaf/_csaf.py create mode 100644 pontos/csaf/_enums.py create mode 100644 pontos/csaf/_utils.py create mode 100644 pontos/csaf/doc.md create mode 100644 pontos/csaf/models/__init__.py create mode 100644 pontos/csaf/models/relationship.py create mode 100644 pontos/csaf/models/revision.py create mode 100644 pontos/csaf/models/vulnerability.py create mode 100644 tests/csaf/__init__.py create mode 100644 tests/csaf/csaf_sample_1.json create mode 100644 tests/csaf/test_csaf.py create mode 100644 tests/csaf/test_relationship.py create mode 100644 tests/csaf/test_vulnerability.py diff --git a/pontos/csaf/__init__.py b/pontos/csaf/__init__.py new file mode 100644 index 000000000..9c59baaf7 --- /dev/null +++ b/pontos/csaf/__init__.py @@ -0,0 +1,22 @@ +# ruff: noqa: I100 +from ._enums import ( + ProductStatus, + RelationshipCategory, + Remediation, + CsafBranchCategory, +) +from .models import Vulnerability, Relationship, Revision +from ._utils import iter_next_branches +from ._csaf import Csaf + +__all__ = [ + "CsafBranchCategory", + "RelationshipCategory", + "Remediation", + "ProductStatus", + "Vulnerability", + "Relationship", + "iter_next_branches", + "Csaf", + "Revision", +] diff --git a/pontos/csaf/_csaf.py b/pontos/csaf/_csaf.py new file mode 100644 index 000000000..c6a3d5efc --- /dev/null +++ b/pontos/csaf/_csaf.py @@ -0,0 +1,347 @@ +# Copyright (C) 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + + +import json +import logging +from typing import Dict, Iterable, List, Set, Tuple + +from black.trans import defaultdict + +from pontos.csaf import ( + Relationship, + RelationshipCategory, + Remediation, + Revision, + Vulnerability, + iter_next_branches, +) +from pontos.errors import PontosError + +logger = logging.getLogger(__name__) + + +class Csaf(dict): + """ + Main purpose: + 1. Increased accessibility of common data structures without having + to remember how exactly all the dictionary keys are called, + how the structures are nested + + 2. Provide parsing of common attributes (CVEs etc.) in their standard location + if they have a CSAF-dedicated field for it. + + 3. Provide extraction of various data in dependencies based on the "product ids". + These are the unique identifiers given to any specific OS/ package + version/ HW. + Product identifiers by themselves are just unique IDs specific to each document. + Specific vendors may use them in such a way, that they also encode semantic meaning, + but this model does NOT handle finding out any such meaning. It solely uses them + as generic identifiers (0,1,2,3). + + NOT purpose: + 1. Provide vendor-specific parsing. + Even more so, if it's not in official compliance to standard. + """ + + """Notes about content types: + Almost all values are strings, including "product_id" (cf. 3.1.8). + Thus, {0} shall never occur, but {"0"} instead and any + iterable of len != 0 will evaluate to true. + """ + + @property + def advisory_id(self) -> str: + return self.document["tracking"]["id"] + + def iter_middle_branches( + self, limit_to_categories: Set[str] | None = None + ) -> Iterable[Dict]: + if "branches" not in self.product_tree: + logger.warning( + "{}: product tree doesn't contain any branches.".format( + self.advisory_id + ) + ) + return + + for outer_branch in self.product_tree["branches"]: + if "branches" not in outer_branch: + logger.info( + "{} Outermost branch doesn't contain branches: {}".format( + self.advisory_id, json.dumps(outer_branch) + ) + ) + for br in iter_next_branches( + outer_branch, limit_to_categories=limit_to_categories + ): + yield br + + def iter_inner_product_branches(self) -> Iterable[Dict]: + """Provides all inner product branches - typically specific OSs/SW/HW versions. + + + Assumes that these lie at depth 3 in the product-branch tree. + """ + for second_layer_branch in self.iter_middle_branches(): + if "branches" not in second_layer_branch: + logger.debug( + "{} Middle branch doesn't contain branches: {}".format( + self.advisory_id, json.dumps(second_layer_branch) + ) + ) + continue + for br in iter_next_branches(second_layer_branch): + yield br + + def iter_products_with_cpe(self) -> Iterable[tuple[Dict, str]]: + """Provides all inner product branches that include a CPE for identification""" + for prod in self.iter_inner_product_branches(): + if "cpe" not in prod["product"].get( + "product_identification_helper", {} + ): + continue + yield prod, prod["product"]["product_identification_helper"]["cpe"] + + def get_product_tree_ids(self) -> Set[str]: + """Get those IDs assigned explicitly to unique products""" + res = { + prod["product"]["product_id"] + for prod in self.iter_inner_product_branches() + } + return res + + def get_cves_from_vulnerabilities_mentioned(self) -> Set[str]: + """Gets explicitly assigned CVEs, i.e., ignores any in descriptive fields.""" + cves = {vuln.cve for vuln in self.vulnerabilities} + return cves + + def iter_products_with_matching_id( + self, acceptable_ids: Set[str] + ) -> Iterable[Dict]: + """Retrieves complete product structures for these explicit IDs""" + for prod in self.iter_inner_product_branches(): + if prod["product"]["product_id"] in acceptable_ids: + yield prod + + def get_reference_urls( + self, allow_self_references: bool, allow_external_references: bool + ) -> Set[str]: + """Retrieve all URLs this document references to in its main section.""" + # only 'self', and 'external' are CSAF-compliant enum entries for the + # reference category + allowed_categories = set() + if allow_self_references: + allowed_categories.add("self") + if allow_external_references: + allowed_categories.add("external") + + if not allowed_categories: + raise PontosError( + "At least one reference category must be allowed." + ) + ref_urls = { + reference["url"] + for reference in self.document["references"] + if reference["category"] in allowed_categories + and "url" in reference + } + return ref_urls + + @property + def title(self) -> str: + return self.document["title"] + + @property + def initial_release_year(self) -> int: + initial_date = self.document["tracking"]["initial_release_date"] + year = initial_date.split("-")[0] + return int(year) + + @property + def vulnerabilities(self) -> List[Vulnerability]: + """Retrieves all vulnerabilities listed in a parsed format.""" + return [Vulnerability(v) for v in self.get("vulnerabilities", [])] + + @property + def contains_product_tree(self) -> bool: + # although *usually* contained, not required + # and e.g., Microsoft doesn't always use it + return "product_tree" in self + + @property + def product_tree(self) -> Dict: + # optional in CSAF, but most common + return self["product_tree"] + + @property + def relationships(self) -> List[Relationship]: + # not mandatory element to be populated, thus fine for an empty list to be returned + return [ + Relationship(relationship) + for relationship in self.product_tree.get("relationships", []) + ] + + @property + def revisions(self) -> List[Revision]: + return [ + Revision(revision) + for revision in self.document["tracking"].get( + "revision_history", [] + ) + ] + + @property + def document(self) -> Dict: + return self["document"] + + @property + def notes(self) -> Iterable[Dict]: + for note in self.document["notes"]: + yield note + + @property + def vulnerability_notes(self) -> Iterable[Dict]: + for vuln in self.vulnerabilities: + for note in vuln.iter_notes(): + yield note + + @property + def raw_references(self) -> Iterable[Dict]: + for reference in self.document["references"]: + yield reference + + @property + def has_vulnerabilities(self) -> bool: + return "vulnerabilities" in self + + def get_matching_relationships( + self, + restrict_to_categories: Set[RelationshipCategory] | None = None, + restrict_to_parent_ids: Set[str] | None = None, + apply_transitively: bool = False, + ) -> Tuple[List[Relationship], List[Relationship]]: + """Retrieves product relationships ('component of' etc.) based on product IDs + + Includes retrieval of the relationship ID + + Args: + restrict_to_categories: explicitly allowed kinds of relationships + restrict_to_parent_ids: explicitly allowed "parent" IDs (return keys). + apply_transitively: store relationship IDs whose parent matched & extract all + relationship children of that relationship product ID. + + Returns: + ([relationships with root as explicit parent], + [relationships with root as implicit parent]) + """ + + root_res = [] + transitive_res = [] + + remaining_relationships = set() + + for relationship in self.relationships: + if ( + not restrict_to_categories + or relationship.kind in restrict_to_categories + ) and ( + not restrict_to_parent_ids + or relationship.parent_id in restrict_to_parent_ids + ): + root_res.append(relationship) + # store those of future relevance + elif apply_transitively and ( + not restrict_to_categories + or relationship.kind in restrict_to_categories + ): + remaining_relationships.add(relationship) + + if apply_transitively: + next_allowed_parents = { + relationship.id for relationship in root_res + } + + while next_fitting_subset := { + relationship + for relationship in remaining_relationships + if relationship.parent_id in next_allowed_parents + }: + transitive_res.extend(list(next_fitting_subset)) + remaining_relationships -= next_fitting_subset + next_allowed_parents = { + relationship.id for relationship in next_fitting_subset + } + + return root_res, transitive_res + + def get_remediation_category_for_related_cves( + self, prod_id: str + ) -> Dict[str, Set[str | None]]: + """Retrieves the CSAF-proposed remediations possible + + Best-case is "vendor_fix" remediation. _Mostly_ this relates to a strict update path. + The update path may relate to a SW different to the one stated as vulnerable depending + on the vendor's SW dependency graph, publication and versioning cycle. + + Whether the SW-to-be-updated differs from the reported-as-vulnerable SW cannot be + assumed across all CSAF publishers and documents. + For some vendors they always match, for some they mostly match. + + Worst-case is "wont_fix" and no "mitigation" or "workaround' keys. In that case, + there exists no official solution to circumvent the CVE. Sometimes there still exists + an upgrade path via major versions though. + """ + res = defaultdict(set) + for vuln in self.vulnerabilities: + if prod_id in vuln.affected_product_ids: + found_remediation = False + for rem in vuln.iter_remediations(): + if prod_id in rem["product_ids"]: + res[vuln.cve].add(rem["category"]) + found_remediation = True + + if not found_remediation: + res[vuln.cve].add(None) + elif prod_id in vuln.confirmed_fixed_product_ids: + res[vuln.cve].add(Remediation.VENDOR_FIX) + + return dict(res) + + def get_all_cves_that_mention_one_of_the_product_ids_as_fixed( + self, restrict_to_product_ids: Set[str] | None = None + ) -> Set[str]: + """Only retrieves that subset of CVEs interested in. + + If the advisory contains a CVE only affecting products we do not support, then we should + ignore that CVE. + """ + res = set() + + if restrict_to_product_ids: + for vulnerability in self.vulnerabilities: + if ( + restrict_to_product_ids + & vulnerability.confirmed_fixed_product_ids + ): + res.add(vulnerability.cve) + else: + for vulnerability in self.vulnerabilities: + if vulnerability.confirmed_fixed_product_ids: + res.add(vulnerability.cve) + + return res + + def prod_has_vendor_fix_for_all_cves_affected_by( + self, prod_id: str + ) -> bool: + """Verifies that ALL CVEs we report on for this product are fixed by the vendor. + + Optional verification, but required for >= 1 vendor. + """ + for rem_cats in self.get_remediation_category_for_related_cves( + prod_id + ).values(): + if not rem_cats or Remediation.VENDOR_FIX not in rem_cats: + return False + return True diff --git a/pontos/csaf/_enums.py b/pontos/csaf/_enums.py new file mode 100644 index 000000000..0b2a5bf14 --- /dev/null +++ b/pontos/csaf/_enums.py @@ -0,0 +1,56 @@ +# Copyright (C) 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +from typing import Set + +from pontos.models import StrEnum + + +class Remediation(StrEnum): + VENDOR_FIX = "vendor_fix" + WORKAROUND = "workaround" + NONE_AVAILABLE = "none_available" + NO_FIX_PLANNED = "no_fix_planned" + MITIGATION = "mitigation" + + +class ProductStatus(StrEnum): + FIRST_AFFECTED = "first_affected" + FIRST_FIXED = "first_fixed" + FIXED = "fixed" + KNOWN_AFFECTED = "known_affected" + KNOWN_NOT_AFFECTED = "known_not_affected" + LAST_AFFECTED = "last_affected" + RECOMMENDED = "recommended" + UNDER_INVESTIGATION = "under_investigation" + + @staticmethod + def all_confirmed_affected_keys() -> Set["ProductStatus"]: + return { + ProductStatus.FIRST_AFFECTED, + ProductStatus.KNOWN_AFFECTED, + ProductStatus.LAST_AFFECTED, + } + + @staticmethod + def all_explicitly_fixed_keys() -> Set["ProductStatus"]: + return { + ProductStatus.FIRST_FIXED, + ProductStatus.FIXED, + } + + +class RelationshipCategory(StrEnum): + DEFAULT_COMPONENT_OF = "default_component_of" + EXTERNAL_COMPONENT_OF = "external_component_of" + OPTIONAL_COMPONENT_OF = "optional_component_of" + INSTALLED_ON = "installed_on" + INSTALLED_WITH = "installed_with" + + +class CsafBranchCategory(StrEnum): + PRODUCT_VERSION_RANGE = "product_version_range" + PRODUCT_VERSION = "product_version" + PRODUCT_NAME = "product_name" + VENDOR = "vendor" + ARCHITECTURE = "architecture" diff --git a/pontos/csaf/_utils.py b/pontos/csaf/_utils.py new file mode 100644 index 000000000..6fcb729f6 --- /dev/null +++ b/pontos/csaf/_utils.py @@ -0,0 +1,17 @@ +# Copyright (C) 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import Dict, Iterable, Set + + +def iter_next_branches( + branch: Dict, limit_to_categories: Set[str] | None = None +) -> Iterable[Dict]: + for inner_branch in branch.get("branches", []): + if ( + limit_to_categories + and inner_branch["category"] not in limit_to_categories + ): + continue + yield inner_branch diff --git a/pontos/csaf/doc.md b/pontos/csaf/doc.md new file mode 100644 index 000000000..070a87827 --- /dev/null +++ b/pontos/csaf/doc.md @@ -0,0 +1,63 @@ + + + * [Data Formats](#data-formats) + * [Simple Types](#simple-types) + * [Date](#date) + + +# CSAF: Common Security Advisory Framework Version + +## Introduction +* CSAF 2.0 support +* JSON is the only CSAF-compliant format +* Specification available [here](https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html#) + +This implementation does not aim to be complete, since the specification allows for *many* + options we yet to have observe any vendor of relevance using them. + +Further, since every vendor populates CSAFs slightly differently, this implementation only aims to provide nice +retrieval interfaces of information and no true interpretation. For some uses cases and vendors, this makes +retrieving the actually necessary information almost trivially easy, for others it becomes a project of its own. +For example uses of this implementation, I refer to the `notus-generator` repository. + +## Core Components +A CSAF document contains 4 core components: +* `document` (publisher, references, tracking, ...): Metadata +* `product_tree` (specification of individual Software/OS/Hardware components, their versions etc. and providing of explicit unique IDs in this document): Technically optional, but most commonly used +* `relationships` (Forest data structure with products from the `product_tree` in the root and leaves.): Technically optional, usage varies +* `vulnerabilities` (Which CVE + Score + which components are affected/ what remediations (if any) exist or are planned.) + +Usage of the `product_tree` must carefully consider (i) the forest's height it is encoded as and how the branches the vendor is using at each height interplay (ii) Whether any explicit version data encoded is actually CSAF compliant (many vendors/publishers are *not* CSAF compliant in those fields.). +Several known vendors do not follow the following guideline of the CSAF specification: +> It is recommended to use the hierarchical structure of vendor -> product_name -> product_version whenever possible to support the identification and matching of products on the consumer side. + +For certain methods, the `Csaf` class assumes a tree of height 2, i.e., that the CSAF follows the above recommendation. + + +
+Usage fo the `relationships` must carefully consider whether it has height > 1 (i.e., any nodes and not just root and leaf nodes). It that case, transitive remapping of the relationships during usage may be necessary. + +Usage of `vulnerabilities` must carefully consider (i) whether the IDs referred to are only from the `product_tree` or from the `relationships`, (ii) what the remediation status of the resp. affected products is. +Further, a CSAF may simple explicitly document that _none_ of the listed products are vulnerable to a given CVE. + + + +## Data Formats + + +### Primitive Types +Almost all primitive values in the JSON are `strings`. I.e., even if a uses numeric identifiers, he encodes them as strings. +As a result `['0']` may commonly exist (and thus evaluates to `True`), but `[0]` (evaluating to `False`) could never be a correct encoding for such identifiers. + +### Simple Types +#### Date +* {YYYY}-{MM}-{DD}T{HH}:{MM}:{SS}Z +* Example: 2024-10-08T00:00:00Z + + +### Product Identification Helpers +To ease exact identification of a given product, a vendor *may* add the `product_identification_helper` +field and populate with one or more CPEs, PURLs, Model Numbers, SBOM URLs, Hashes, etc. +Note: Do not assume that the vendor is compliant in how they populate these field. +At least for one vendor we are aware that the CPE values they provide are not following the CPE specification when compared to "what the vendor means". +Similar non-compliancy occurrence holds true for, e.g., version fields. diff --git a/pontos/csaf/models/__init__.py b/pontos/csaf/models/__init__.py new file mode 100644 index 000000000..6a6eed958 --- /dev/null +++ b/pontos/csaf/models/__init__.py @@ -0,0 +1,5 @@ +from .relationship import Relationship +from .revision import Revision +from .vulnerability import Vulnerability + +__all__ = ["Relationship", "Vulnerability", "Revision"] diff --git a/pontos/csaf/models/relationship.py b/pontos/csaf/models/relationship.py new file mode 100644 index 000000000..eeb1d7a0b --- /dev/null +++ b/pontos/csaf/models/relationship.py @@ -0,0 +1,118 @@ +# Copyright (C) 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from collections import defaultdict +from copy import deepcopy +from typing import Dict, List, Set + +from pontos.csaf import RelationshipCategory + + +class Relationship(dict): + def __hash__(self): + # Should be unique for a given CSAF. + return hash((self.parent_id, self.child_id, self.id, self.kind)) + + def __eq__(self, other): + if not isinstance(other, (Relationship, dict)): + return False + return dict(self) == dict(other) + + @property + def parent_id(self) -> str: + return self["relates_to_product_reference"] + + @property + def kind(self) -> RelationshipCategory: + return RelationshipCategory(self["category"]) + + @property + def child_id(self) -> str: + return self["product_reference"] + + @property + def id(self) -> str: + return self["full_product_name"]["product_id"] + + @staticmethod + def create_combined_parent_to_child_mapping( + relationships: List["Relationship"], + ) -> Dict[str, Set[str]]: + """Creates the typical {product -> {packages}} map, but by ID""" + res = defaultdict(set) + for relationship in relationships: + res[relationship.parent_id].add(relationship.child_id) + return dict(res) + + @staticmethod + def build_root_to_leaf_map( + root_relationships: List["Relationship"], + inner_tree_relationships: List["Relationship"], + leaf_product_ids: Set[str], + ) -> Dict[str, set[str]]: + + if not root_relationships: + raise ValueError("Cannot build leaf to root map without roots.") + + elif not inner_tree_relationships: + # can directly read them from the root nodes. + return Relationship.create_combined_parent_to_child_mapping( + root_relationships + ) + + elif not leaf_product_ids: + raise ValueError( + "For a forest with inner nodes requires inputs which product " + "references shall be considered leafs for simplicity." + ) + + remaining_inner_relationships = set(deepcopy(inner_tree_relationships)) + + res = defaultdict(set) + + relationship_to_root = { + root.id: root.parent_id for root in root_relationships + } + current_parent_ids = set(relationship_to_root.keys()) + + # if some trees in the forest are trivial + leaf_to_root = { + root.child_id: root.parent_id + for root in root_relationships + if root.child_id in leaf_product_ids + } + + while remaining_inner_relationships: + + matching_inner_children = { + child + for child in remaining_inner_relationships + if child.parent_id in current_parent_ids + } + if not matching_inner_children: + break + + for child in matching_inner_children: + if child.parent_id not in relationship_to_root: + raise ValueError( + f"Orphaned Relationship with ID {child.id}" + ) + + root_id = relationship_to_root[child.parent_id] + + # Step 2 + if child.child_id in leaf_product_ids: + # otherwise, it is a complex relationship ID + leaf_to_root[child.child_id] = root_id + + current_parent_ids = {child.id for child in matching_inner_children} + + remaining_inner_relationships = ( + remaining_inner_relationships - matching_inner_children + ) + + for relationship_id, root_id in leaf_to_root.items(): + res[root_id].add(relationship_id) + + return dict(res) diff --git a/pontos/csaf/models/revision.py b/pontos/csaf/models/revision.py new file mode 100644 index 000000000..d59f43bc2 --- /dev/null +++ b/pontos/csaf/models/revision.py @@ -0,0 +1,29 @@ +# Copyright (C) 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + + +class Revision(dict): + # Defined in 3.2.1.12.6 + # shall only exist for non-pre-release document states + @property + def date(self) -> "date": + # string; format date-time + return self["date"] + + @property + def number(self) -> int: + # version_t type + # must not be '0' or '0.x.y' for any document + # in the final state + return self["number"] + + @property + def summary(self) -> str: + return self["summary"] + + @property + def legacy_version(self): + # designed to refer to version numbers of the human-readable + # equivalent SA + return self["legacy_version"] diff --git a/pontos/csaf/models/vulnerability.py b/pontos/csaf/models/vulnerability.py new file mode 100644 index 000000000..d4ee0ad64 --- /dev/null +++ b/pontos/csaf/models/vulnerability.py @@ -0,0 +1,55 @@ +# Copyright (C) 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import Dict, Iterable, Set + +from pontos.csaf import ProductStatus, Remediation + + +class Vulnerability(dict): + @property + def cve(self) -> str: + return self["cve"] + + @property + def affected_product_ids(self) -> Set[str]: + affected_keys = ProductStatus.all_confirmed_affected_keys() + active_keys = affected_keys & set(self["product_status"].keys()) + if not active_keys: + return set() + affected_prods = set.union( + *(set(self["product_status"][key]) for key in active_keys) + ) + return affected_prods + + def iter_notes(self) -> Iterable[Dict]: + if "notes" not in self: + return + for note in self["notes"]: + yield note + + def iter_remediations(self) -> Iterable[Dict]: + if "remediations" not in self: + return + for remediation in self["remediations"]: + yield remediation + + @property + def confirmed_fixed_product_ids(self) -> Set[str]: + res = set() + for remediation in self.iter_remediations(): + # It's explicitly possible for multiple remediations + # with the same category exist. + # E.g., if multiple products are supported and how to upgrade them differs + if remediation["category"] == Remediation.VENDOR_FIX: + # Either Product ID or Group ID + res |= set(remediation["product_ids"]) + + fixed_keys = ProductStatus.all_explicitly_fixed_keys() + active_keys = fixed_keys & set(self["product_status"].keys()) + if active_keys: + res |= set.union( + *(set(self["product_status"][key]) for key in active_keys) + ) + return res diff --git a/tests/csaf/__init__.py b/tests/csaf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/csaf/csaf_sample_1.json b/tests/csaf/csaf_sample_1.json new file mode 100644 index 000000000..5aa5f132a --- /dev/null +++ b/tests/csaf/csaf_sample_1.json @@ -0,0 +1,906 @@ +{ + "document": { + "aggregate_severity": { + "namespace": "https://access.redhat.com/security/updates/classification/", + "text": "Important" + }, + "category": "csaf_security_advisory", + "csaf_version": "2.0", + "distribution": { + "text": "Copyright © Red Hat, Inc. All rights reserved.", + "tlp": { + "label": "WHITE", + "url": "https://www.first.org/tlp/" + } + }, + "lang": "en", + "notes": [ + { + "category": "summary", + "text": "An update for openvswitch2.17 is now available for Fast Datapath for Red Hat Enterprise Linux 8.\n\nRed Hat Product Security has rated this update as having a security impact of Important. A Common Vulnerability Scoring System (CVSS) base score, which gives a detailed severity rating, is available for each vulnerability from the CVE link(s) in the References section.", + "title": "Topic" + }, + { + "category": "general", + "text": "Open vSwitch provides standard network bridging functions and support for the OpenFlow protocol for remote per-flow control of traffic.\n\nSecurity Fix(es):\n\n* openvswsitch: ovs-vswitch fails to recover after malformed geneve metadata packet (CVE-2023-3966)\n\nFor more details about the security issue(s), including the impact, a CVSS score, acknowledgments, and other related information, refer to the CVE page(s) listed in the References section.", + "title": "Details" + }, + { + "category": "legal_disclaimer", + "text": "This content is licensed under the Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/). If you distribute this content, or a modified version of it, you must provide attribution to Red Hat Inc. and provide a link to the original.", + "title": "Terms of Use" + } + ], + "publisher": { + "category": "vendor", + "contact_details": "https://access.redhat.com/security/team/contact/", + "issuing_authority": "Red Hat Product Security is responsible for vulnerability handling across all Red Hat products and services.", + "name": "Red Hat Product Security", + "namespace": "https://www.redhat.com" + }, + "references": [ + { + "category": "self", + "summary": "https://access.redhat.com/errata/RHSA-2024:1234", + "url": "https://access.redhat.com/errata/RHSA-2024:1234" + }, + { + "category": "external", + "summary": "https://access.redhat.com/security/updates/classification/#important", + "url": "https://access.redhat.com/security/updates/classification/#important" + }, + { + "category": "external", + "summary": "2178363", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=2178363" + }, + { + "category": "external", + "summary": "FD-3267", + "url": "https://issues.redhat.com/browse/FD-3267" + }, + { + "category": "external", + "summary": "FDP-308", + "url": "https://issues.redhat.com/browse/FDP-308" + }, + { + "category": "self", + "summary": "Canonical URL", + "url": "https://security.access.redhat.com/data/csaf/v2/advisories/2024/rhsa-2024_1234.json" + } + ], + "title": "Red Hat Security Advisory: openvswitch2.17 security update", + "tracking": { + "current_release_date": "2024-12-11T15:32:02+00:00", + "generator": { + "date": "2024-12-11T15:32:02+00:00", + "engine": { + "name": "Red Hat SDEngine", + "version": "4.2.3" + } + }, + "id": "RHSA-2024:1234", + "initial_release_date": "2024-03-07T18:20:50+00:00", + "revision_history": [ + { + "date": "2024-03-07T18:20:50+00:00", + "number": "1", + "summary": "Initial version" + }, + { + "date": "2024-03-07T18:20:50+00:00", + "number": "2", + "summary": "Last updated version" + }, + { + "date": "2024-12-11T15:32:02+00:00", + "number": "3", + "summary": "Last generated version" + } + ], + "status": "final", + "version": "3" + } + }, + "product_tree": { + "branches": [ + { + "branches": [ + { + "branches": [ + { + "category": "product_name", + "name": "Fast Datapath for Red Hat Enterprise Linux 8", + "product": { + "name": "Fast Datapath for Red Hat Enterprise Linux 8", + "product_id": "8Base-Fast-Datapath", + "product_identification_helper": { + "cpe": "cpe:/o:redhat:enterprise_linux:8::fastdatapath" + } + } + } + ], + "category": "product_family", + "name": "Fast Datapath" + }, + { + "branches": [ + { + "category": "product_version", + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "product": { + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "product_id": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/network-scripts-openvswitch2.17@2.17.0-148.el8fdp?arch=x86_64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "product": { + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "product_id": "openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17@2.17.0-148.el8fdp?arch=x86_64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.x86_64", + "product": { + "name": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.x86_64", + "product_id": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.x86_64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-devel@2.17.0-148.el8fdp?arch=x86_64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.x86_64", + "product": { + "name": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.x86_64", + "product_id": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.x86_64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-ipsec@2.17.0-148.el8fdp?arch=x86_64" + } + } + }, + { + "category": "product_version", + "name": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "product": { + "name": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "product_id": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/python3-openvswitch2.17@2.17.0-148.el8fdp?arch=x86_64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.x86_64", + "product": { + "name": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.x86_64", + "product_id": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.x86_64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-debugsource@2.17.0-148.el8fdp?arch=x86_64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64", + "product": { + "name": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64", + "product_id": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-debuginfo@2.17.0-148.el8fdp?arch=x86_64" + } + } + }, + { + "category": "product_version", + "name": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64", + "product": { + "name": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64", + "product_id": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/python3-openvswitch2.17-debuginfo@2.17.0-148.el8fdp?arch=x86_64" + } + } + } + ], + "category": "architecture", + "name": "x86_64" + }, + { + "branches": [ + { + "category": "product_version", + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "product": { + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "product_id": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/network-scripts-openvswitch2.17@2.17.0-148.el8fdp?arch=ppc64le" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "product": { + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "product_id": "openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17@2.17.0-148.el8fdp?arch=ppc64le" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.ppc64le", + "product": { + "name": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.ppc64le", + "product_id": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.ppc64le", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-devel@2.17.0-148.el8fdp?arch=ppc64le" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.ppc64le", + "product": { + "name": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.ppc64le", + "product_id": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.ppc64le", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-ipsec@2.17.0-148.el8fdp?arch=ppc64le" + } + } + }, + { + "category": "product_version", + "name": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "product": { + "name": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "product_id": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/python3-openvswitch2.17@2.17.0-148.el8fdp?arch=ppc64le" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.ppc64le", + "product": { + "name": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.ppc64le", + "product_id": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.ppc64le", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-debugsource@2.17.0-148.el8fdp?arch=ppc64le" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "product": { + "name": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "product_id": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-debuginfo@2.17.0-148.el8fdp?arch=ppc64le" + } + } + }, + { + "category": "product_version", + "name": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "product": { + "name": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "product_id": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/python3-openvswitch2.17-debuginfo@2.17.0-148.el8fdp?arch=ppc64le" + } + } + } + ], + "category": "architecture", + "name": "ppc64le" + }, + { + "branches": [ + { + "category": "product_version", + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "product": { + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "product_id": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/network-scripts-openvswitch2.17@2.17.0-148.el8fdp?arch=aarch64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "product": { + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "product_id": "openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17@2.17.0-148.el8fdp?arch=aarch64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.aarch64", + "product": { + "name": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.aarch64", + "product_id": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.aarch64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-devel@2.17.0-148.el8fdp?arch=aarch64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.aarch64", + "product": { + "name": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.aarch64", + "product_id": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.aarch64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-ipsec@2.17.0-148.el8fdp?arch=aarch64" + } + } + }, + { + "category": "product_version", + "name": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "product": { + "name": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "product_id": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/python3-openvswitch2.17@2.17.0-148.el8fdp?arch=aarch64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.aarch64", + "product": { + "name": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.aarch64", + "product_id": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.aarch64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-debugsource@2.17.0-148.el8fdp?arch=aarch64" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "product": { + "name": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "product_id": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-debuginfo@2.17.0-148.el8fdp?arch=aarch64" + } + } + }, + { + "category": "product_version", + "name": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "product": { + "name": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "product_id": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/python3-openvswitch2.17-debuginfo@2.17.0-148.el8fdp?arch=aarch64" + } + } + } + ], + "category": "architecture", + "name": "aarch64" + }, + { + "branches": [ + { + "category": "product_version", + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "product": { + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "product_id": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/network-scripts-openvswitch2.17@2.17.0-148.el8fdp?arch=s390x" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "product": { + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "product_id": "openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17@2.17.0-148.el8fdp?arch=s390x" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.s390x", + "product": { + "name": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.s390x", + "product_id": "openvswitch2.17-devel-0:2.17.0-148.el8fdp.s390x", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-devel@2.17.0-148.el8fdp?arch=s390x" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.s390x", + "product": { + "name": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.s390x", + "product_id": "openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.s390x", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-ipsec@2.17.0-148.el8fdp?arch=s390x" + } + } + }, + { + "category": "product_version", + "name": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "product": { + "name": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "product_id": "python3-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/python3-openvswitch2.17@2.17.0-148.el8fdp?arch=s390x" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.s390x", + "product": { + "name": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.s390x", + "product_id": "openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.s390x", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-debugsource@2.17.0-148.el8fdp?arch=s390x" + } + } + }, + { + "category": "product_version", + "name": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "product": { + "name": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "product_id": "openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-debuginfo@2.17.0-148.el8fdp?arch=s390x" + } + } + }, + { + "category": "product_version", + "name": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "product": { + "name": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "product_id": "python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/python3-openvswitch2.17-debuginfo@2.17.0-148.el8fdp?arch=s390x" + } + } + } + ], + "category": "architecture", + "name": "s390x" + }, + { + "branches": [ + { + "category": "product_version", + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.src", + "product": { + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.src", + "product_id": "openvswitch2.17-0:2.17.0-148.el8fdp.src", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17@2.17.0-148.el8fdp?arch=src" + } + } + } + ], + "category": "architecture", + "name": "src" + }, + { + "branches": [ + { + "category": "product_version", + "name": "openvswitch2.17-test-0:2.17.0-148.el8fdp.noarch", + "product": { + "name": "openvswitch2.17-test-0:2.17.0-148.el8fdp.noarch", + "product_id": "openvswitch2.17-test-0:2.17.0-148.el8fdp.noarch", + "product_identification_helper": { + "purl": "pkg:rpm/redhat/openvswitch2.17-test@2.17.0-148.el8fdp?arch=noarch" + } + } + } + ], + "category": "architecture", + "name": "noarch" + } + ], + "category": "vendor", + "name": "Red Hat" + } + ], + "relationships": [ + { + "category": "default_component_of", + "full_product_name": { + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64 as a component of Fast Datapath for Red Hat Enterprise Linux 8", + "product_id": "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64" + }, + "product_reference": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "relates_to_product_reference": "8Base-Fast-Datapath" + }, + { + "category": "optional_component_of", + "full_product_name": { + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.aarch64 as a component of Fast Datapath for Red Hat Enterprise Linux 8", + "product_id": "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.aarch64" + }, + "product_reference": "openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "relates_to_product_reference": "8Base-Fast-DatapathOtherParent" + }, + { + "category": "default_component_of", + "full_product_name": { + "name": "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64 as a component of postgresql:15:9020020230619032405:rhel9 as a component of Red Hat Enterprise Linux AppStream (v. 9)", + "product_id": "AppStream-9.2.0.Z.MAIN.EUS:postgresql:15:9020020230619032405:rhel9:pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64" + }, + "product_reference": "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64", + "relates_to_product_reference": "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64" + } + ] + }, + "vulnerabilities": [ + { + "acknowledgments": [ + { + "names": [ + "Haresh Khandelwal", + "Timothy Redaelli" + ], + "organization": "Red Hat", + "summary": "This issue was discovered by Red Hat." + } + ], + "cve": "CVE-2023-3966", + "cwe": { + "id": "CWE-248", + "name": "Uncaught Exception" + }, + "discovery_date": "2023-03-14T00:00:00+00:00", + "ids": [ + { + "system_name": "Red Hat Bugzilla ID", + "text": "2178363" + } + ], + "notes": [ + { + "category": "description", + "text": "A flaw was found in Open vSwitch where multiple versions are vulnerable to crafted Geneve packets, which may result in a denial of service and invalid memory accesses. Triggering this issue requires that hardware offloading via the netlink path is enabled.", + "title": "Vulnerability description" + }, + { + "category": "summary", + "text": "openvswsitch: ovs-vswitch fails to recover after malformed geneve metadata packet", + "title": "Vulnerability summary" + }, + { + "category": "general", + "text": "The CVSS score(s) listed for this vulnerability do not reflect the associated product's status, and are included for informational purposes to better understand the severity of this vulnerability.", + "title": "CVSS score applicability" + } + ], + "product_status": { + "fixed": [ + "only_fixed_for_one_cve", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64" + ] + }, + "references": [ + { + "category": "self", + "summary": "Canonical URL", + "url": "https://access.redhat.com/security/cve/CVE-2023-3966" + }, + { + "category": "external", + "summary": "RHBZ#2178363", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=2178363" + }, + { + "category": "external", + "summary": "https://www.cve.org/CVERecord?id=CVE-2023-3966", + "url": "https://www.cve.org/CVERecord?id=CVE-2023-3966" + }, + { + "category": "external", + "summary": "https://nvd.nist.gov/vuln/detail/CVE-2023-3966", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-3966" + } + ], + "release_date": "2024-02-08T00:00:00+00:00", + "remediations": [ + { + "category": "vendor_fix", + "date": "2024-03-07T18:20:50+00:00", + "details": "For details on how to apply this update, which includes the changes described in this advisory, refer to:\n\nhttps://access.redhat.com/articles/11258", + "product_ids": [ + "only_fixed_for_one_cve", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64" + ], + "restart_required": { + "category": "none" + }, + "url": "https://access.redhat.com/errata/RHSA-2024:1234" + } + ], + "scores": [ + { + "cvss_v3": { + "attackComplexity": "LOW", + "attackVector": "NETWORK", + "availabilityImpact": "HIGH", + "baseScore": 7.5, + "baseSeverity": "HIGH", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "privilegesRequired": "NONE", + "scope": "UNCHANGED", + "userInteraction": "NONE", + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "version": "3.1" + }, + "products": [ + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64" + ] + } + ], + "threats": [ + { + "category": "impact", + "details": "Important" + } + ], + "title": "openvswsitch: ovs-vswitch fails to recover after malformed geneve metadata packet" + }, + { + "acknowledgments": [ + { + "names": [ + "Alex Katz", + "Slawomir Kaplonski" + ], + "organization": "Red Hat", + "summary": "This issue was discovered by Red Hat." + } + ], + "cve": "CVE-2023-5366", + "cwe": { + "id": "CWE-345", + "name": "Insufficient Verification of Data Authenticity" + }, + "discovery_date": "2021-09-21T00:00:00+00:00", + "ids": [ + { + "system_name": "Red Hat Bugzilla ID", + "text": "2006347" + } + ], + "notes": [ + { + "category": "description", + "text": "A flaw was found in Open vSwitch that allows ICMPv6 Neighbor Advertisement packets between virtual machines to bypass OpenFlow rules. This issue may allow a local attacker to create specially crafted packets with a modified or spoofed target IP address field that can redirect ICMPv6 traffic to arbitrary IP addresses.", + "title": "Vulnerability description" + }, + { + "category": "summary", + "text": "openvswitch: openvswitch don't match packets on nd_target field", + "title": "Vulnerability summary" + }, + { + "category": "other", + "text": "Red Hat Enterprise Linux 7 provides the `openvswitch` package only through the unsupported Optional repository. Customers are advised to install Open vSwitch (OVS) from RHEL Fast Datapath instead.\nRed Hat OpenStack Platform 13/16 deployments are not affected because they use openvswitch directly from the Fast Datapath channel. A rhosp-openvswitch update will therefore not be provided at this time. Any updates will be distributed through that channel.", + "title": "Statement" + }, + { + "category": "general", + "text": "The CVSS score(s) listed for this vulnerability do not reflect the associated product's status, and are included for informational purposes to better understand the severity of this vulnerability.", + "title": "CVSS score applicability" + } + ], + "product_status": { + "known_affected": [ + "only_fixed_for_one_cve" + ], + "fixed": [ + "only_in_fixed_status", + "in_fixed_status_and_remediations", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64" + ] + }, + "references": [ + { + "category": "self", + "summary": "Canonical URL", + "url": "https://access.redhat.com/security/cve/CVE-2023-5366" + }, + { + "category": "external", + "summary": "RHBZ#2006347", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=2006347" + }, + { + "category": "external", + "summary": "https://www.cve.org/CVERecord?id=CVE-2023-5366", + "url": "https://www.cve.org/CVERecord?id=CVE-2023-5366" + }, + { + "category": "external", + "summary": "https://nvd.nist.gov/vuln/detail/CVE-2023-5366", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-5366" + }, + { + "category": "external", + "summary": "https://mail.openvswitch.org/pipermail/ovs-announce/2024-February/000342.html", + "url": "https://mail.openvswitch.org/pipermail/ovs-announce/2024-February/000342.html" + } + ], + "release_date": "2023-09-26T00:00:00+00:00", + "remediations": [ + { + "category": "vendor_fix", + "date": "2024-03-07T18:20:50+00:00", + "details": "For details on how to apply this update, which includes the changes described in this advisory, refer to:\n\nhttps://access.redhat.com/articles/11258", + "product_ids": [ + "only_in_remediations", + "in_fixed_status_and_remediations", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.src", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-test-0:2.17.0-148.el8fdp.noarch", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64" + ], + "restart_required": { + "category": "none" + }, + "url": "https://access.redhat.com/errata/RHSA-2024:1234" + } + ], + "scores": [ + { + "cvss_v3": { + "attackComplexity": "LOW", + "attackVector": "LOCAL", + "availabilityImpact": "NONE", + "baseScore": 5.5, + "baseSeverity": "MEDIUM", + "confidentialityImpact": "NONE", + "integrityImpact": "HIGH", + "privilegesRequired": "LOW", + "scope": "UNCHANGED", + "userInteraction": "NONE", + "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N", + "version": "3.1" + }, + "products": [ + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.src", + "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-debugsource-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-devel-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:openvswitch2.17-ipsec-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:openvswitch2.17-test-0:2.17.0-148.el8fdp.noarch", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:python3-openvswitch2.17-0:2.17.0-148.el8fdp.x86_64", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.aarch64", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.ppc64le", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.s390x", + "8Base-Fast-Datapath:python3-openvswitch2.17-debuginfo-0:2.17.0-148.el8fdp.x86_64" + ] + } + ], + "threats": [ + { + "category": "impact", + "details": "Moderate" + } + ], + "title": "openvswitch: openvswitch don't match packets on nd_target field" + } + ] +} \ No newline at end of file diff --git a/tests/csaf/test_csaf.py b/tests/csaf/test_csaf.py new file mode 100644 index 000000000..8c0a1fd90 --- /dev/null +++ b/tests/csaf/test_csaf.py @@ -0,0 +1,420 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ruff: noqa: E501 + +import json +import unittest +from pathlib import Path + +from pontos.csaf import Csaf, Relationship, RelationshipCategory, Remediation +from pontos.errors import PontosError + + +def get_advisory_content(idx: int = 1) -> Csaf: + file = Path(__file__).parent / f"csaf_sample_{idx}.json" + data = json.loads(file.read_text()) + return Csaf(data) + + +class RedHatTestCase(unittest.TestCase): + def test_advisory_id(self): + csaf = get_advisory_content() + expected_result = "RHSA-2024:1234" + self.assertEqual(csaf.advisory_id, expected_result) + + def test_reference_urls(self): + csaf = get_advisory_content() + self_urls = { + "https://access.redhat.com/errata/RHSA-2024:1234", + "https://security.access.redhat.com/data/csaf/v2/advisories/2024/rhsa-2024_1234.json", + } + + external_urls = { + "https://access.redhat.com/security/updates/classification/#important", + "https://bugzilla.redhat.com/show_bug.cgi?id=2178363", + "https://issues.redhat.com/browse/FD-3267", + "https://issues.redhat.com/browse/FDP-308", + } + + self.assertEqual( + csaf.get_reference_urls( + allow_self_references=True, allow_external_references=False + ), + self_urls, + ) + + self.assertEqual( + csaf.get_reference_urls( + allow_self_references=False, allow_external_references=True + ), + external_urls, + ) + + self.assertEqual( + csaf.get_reference_urls( + allow_self_references=True, allow_external_references=True + ), + external_urls | self_urls, + ) + with self.assertRaises(PontosError): + csaf.get_reference_urls( + allow_self_references=False, allow_external_references=False + ) + + def test_title(self): + csaf = get_advisory_content() + expected_result = ( + "Red Hat Security Advisory: openvswitch2.17 security update" + ) + self.assertEqual(csaf.title, expected_result) + + def test_initial_release_year(self): + csaf = get_advisory_content() + expected_result = 2024 + self.assertEqual(csaf.initial_release_year, expected_result) + + def test_contains_product_tree(self): + csaf = get_advisory_content() + self.assertTrue(csaf.contains_product_tree) + + def test_first_inner_product_branch(self): + csaf = get_advisory_content() + expected_res = { + "category": "product_name", + "name": "Fast Datapath for Red Hat Enterprise Linux 8", + "product": { + "name": "Fast Datapath for Red Hat Enterprise Linux 8", + "product_id": "8Base-Fast-Datapath", + "product_identification_helper": { + "cpe": "cpe:/o:redhat:enterprise_linux:8::fastdatapath" + }, + }, + } + + res = next(csaf.iter_inner_product_branches()) + + self.assertEqual(res, expected_res) + + def test_get_all_cves_from_vulnerabilities_mentioned(self): + csaf = get_advisory_content() + expected_result = {"CVE-2023-3966", "CVE-2023-5366"} + self.assertEqual( + csaf.get_cves_from_vulnerabilities_mentioned(), expected_result + ) + + def test_get_all_cves_that_mention_one_of_the_product_ids_as_fixed(self): + csaf = get_advisory_content() + expected_result = {"CVE-2023-3966", "CVE-2023-5366"} + self.assertEqual( + csaf.get_all_cves_that_mention_one_of_the_product_ids_as_fixed(), + expected_result, + ) + self.assertEqual( + csaf.get_all_cves_that_mention_one_of_the_product_ids_as_fixed( + restrict_to_product_ids={ + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + } + ), + expected_result, + ) + + expected_partial_result = { + "CVE-2023-5366", + } + self.assertEqual( + csaf.get_all_cves_that_mention_one_of_the_product_ids_as_fixed( + restrict_to_product_ids={ + "only_in_fixed_status", + } + ), + expected_partial_result, + ) + + def test_get_remediation_category_for_related_cves(self): + csaf = get_advisory_content() + expected_result = { + "CVE-2023-3966": {Remediation.VENDOR_FIX}, + "CVE-2023-5366": {Remediation.VENDOR_FIX}, + } + + res = csaf.get_remediation_category_for_related_cves( + "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64" + ) + + self.assertDictEqual(res, expected_result) + + expected_result = { + "CVE-2023-3966": {Remediation.VENDOR_FIX}, + "CVE-2023-5366": {None}, + } + res = csaf.get_remediation_category_for_related_cves( + "only_fixed_for_one_cve" + ) + + self.assertDictEqual(res, expected_result) + + def test_prod_has_vendor_fix_for_all_cves_affected_by(self): + csaf = get_advisory_content() + self.assertTrue( + csaf.prod_has_vendor_fix_for_all_cves_affected_by( + "only_in_fixed_status" + ) + ) + + self.assertFalse( + csaf.prod_has_vendor_fix_for_all_cves_affected_by( + "only_fixed_for_one_cve" + ) + ) + + def test_get_matching_relationships_no_transitivity(self): + csaf = get_advisory_content() + expected_res = ( + [ + Relationship( + { + "category": "default_component_of", + "full_product_name": { + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64 as a component of Fast Datapath for Red Hat Enterprise Linux 8", + "product_id": "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + }, + "product_reference": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "relates_to_product_reference": "8Base-Fast-Datapath", + } + ), + Relationship( + { + "category": "optional_component_of", + "full_product_name": { + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.aarch64 as a component of Fast Datapath for Red Hat Enterprise Linux 8", + "product_id": "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + }, + "product_reference": "openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "relates_to_product_reference": "8Base-Fast-DatapathOtherParent", + } + ), + Relationship( + { + "category": "default_component_of", + "full_product_name": { + "name": "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64 as a component of postgresql:15:9020020230619032405:rhel9 as a component of Red Hat Enterprise Linux AppStream (v. 9)", + "product_id": "AppStream-9.2.0.Z.MAIN.EUS:postgresql:15:9020020230619032405:rhel9:pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64", + }, + "product_reference": "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64", + "relates_to_product_reference": "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + } + ), + ], + [], + ) + + res = csaf.get_matching_relationships( + restrict_to_categories=None, + restrict_to_parent_ids=None, + apply_transitively=False, + ) + self.assertEqual(res, expected_res) + + expected_res = ( + [ + Relationship( + { + "category": "optional_component_of", + "full_product_name": { + "name": "openvswitch2.17-0:2.17.0-148.el8fdp.aarch64 as a component of Fast Datapath for Red Hat Enterprise Linux 8", + "product_id": "8Base-Fast-Datapath:openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + }, + "product_reference": "openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "relates_to_product_reference": "8Base-Fast-DatapathOtherParent", + } + ) + ], + [], + ) + + res = csaf.get_matching_relationships( + restrict_to_categories={RelationshipCategory.OPTIONAL_COMPONENT_OF}, + restrict_to_parent_ids=None, + apply_transitively=False, + ) + self.assertEqual(res, expected_res) + + res = csaf.get_matching_relationships( + restrict_to_categories=None, + restrict_to_parent_ids={ + "8Base-Fast-DatapathOtherParent", + }, + apply_transitively=False, + ) + self.assertEqual(res, expected_res) + + def test_get_matching_relationships_with_transitivity(self): + csaf = get_advisory_content() + expected_res = ( + [ + Relationship( + { + "category": "default_component_of", + "full_product_name": { + "name": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64 as a component of Fast Datapath for Red Hat Enterprise Linux 8", + "product_id": "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + }, + "product_reference": "network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + "relates_to_product_reference": "8Base-Fast-Datapath", + } + ), + ], + [ + Relationship( + { + "category": "default_component_of", + "full_product_name": { + "name": "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64 as a component of postgresql:15:9020020230619032405:rhel9 as a component of Red Hat Enterprise Linux AppStream (v. 9)", + "product_id": "AppStream-9.2.0.Z.MAIN.EUS:postgresql:15:9020020230619032405:rhel9:pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64", + }, + "product_reference": "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64", + "relates_to_product_reference": "8Base-Fast-Datapath:network-scripts-openvswitch2.17-0:2.17.0-148.el8fdp.aarch64", + } + ) + ], + ) + + res = csaf.get_matching_relationships( + restrict_to_categories=None, + restrict_to_parent_ids={ + "8Base-Fast-Datapath", + }, + apply_transitively=True, + ) + self.assertEqual(res, expected_res) + + +# def test_parse_insight(self): +# redhat = RedHat() +# advisory_content = get_advisory_content() +# insight, xrefs = redhat._parse_insight(advisory_content) +# expected_result = """The mod_nss module provides strong cryptography for the Apache HTTP Server via the Secure Sockets Layer (SSL) and Transport Layer Security (TLS) protocols, using the Network Security Services (NSS) security library. +# +# This update fixes the following bugs: +# +# * When the NSS library was not initialized and mod_nss tried to clear its SSL cache on start-up, mod_nss terminated unexpectedly when the NSS library was built with debugging enabled. With this update, mod_nss does not try to clear the SSL cache in the described scenario, thus preventing this bug. (BZ#691502)""" +# +# self.assertEqual(expected_result, insight) +# self.assertEqual(xrefs, []) +# +# def test_parse_cves(self): +# redhat = RedHat() +# advisory_content = get_advisory_content() +# cves = redhat._parse_cves(advisory_content) +# expected_result = ["CVE-2024-4973"] +# self.assertEqual(expected_result, cves) +# +# # def test_parse_advisory_id(self): +# # redhat = RedHat() +# # advisory_content = get_advisory_content() +# # advisory_id = redhat._parse_advisory_id(advisory_content) +# # expected_result = "RHBA-2024:1234" +# # self.assertEqual(expected_result, advisory_id) +# +# def test_get_packages(self): +# redhat = RedHat() +# advisory_content = get_advisory_content() +# advisory = redhat._parse_advisory_content(advisory_content)[0] +# packages = redhat._get_all_packages(advisory_content) +# product_packages = redhat._get_packages(advisory, packages, "6") +# expected_result = defaultdict(set) +# expected_result.update( +# { +# "Red Hat Enterprise Linux 6": { +# "mod_nss-0:1.0.8-13.el6.i686", +# "mod_nss-0:1.0.8-13.el6.ppc64", +# "mod_nss-0:1.0.8-13.el6.x86_64", +# "mod_nss-0:1.0.8-13.el6.s390x", +# } +# } +# ) +# self.assertEqual(expected_result, product_packages) +# +# def test_get_vulnerability_information(self): +# redhat = RedHat() +# redhat._get_full_xml = MagicMock(return_value=[get_advisory_content()][0]) +# redhat._parse_advisory = MagicMock(return_value=Advisory()) +# +# expected_result = VulnerabilityInformation() +# expected_result.advisories.add_advisory(Advisory()) +# expected_result.products.add_packages( +# "Red Hat Enterprise Linux 6", +# "1.3.6.1.4.1.25623.1.1.11.2024.1234", +# [ +# Package(full_name="mod_nss-0:1.0.8-13.el6.i686"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.ppc64"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.s390x"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.x86_64"), +# ], +# ) +# expected_result.products.add_packages( +# "Red Hat Enterprise Linux 7", +# "1.3.6.1.4.1.25623.1.1.11.2024.1234", +# [ +# Package(full_name="mod_nss-0:1.0.8-13.el6.i686"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.ppc64"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.s390x"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.x86_64"), +# ], +# ) +# expected_result.products.add_packages( +# "Red Hat Enterprise Linux 8", +# "1.3.6.1.4.1.25623.1.1.11.2024.1234", +# [ +# Package(full_name="mod_nss-0:1.0.8-13.el6.i686"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.ppc64"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.s390x"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.x86_64"), +# ], +# ) +# expected_result.products.add_packages( +# "Red Hat Enterprise Linux 9", +# "1.3.6.1.4.1.25623.1.1.11.2024.1234", +# [ +# Package(full_name="mod_nss-0:1.0.8-13.el6.i686"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.ppc64"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.s390x"), +# Package(full_name="mod_nss-0:1.0.8-13.el6.x86_64"), +# ], +# ) +# +# vulnerability_information = VulnerabilityInformation() +# result = redhat.get_vulnerability_information(vulnerability_information) +# self.assertEqual(expected_result, result) +# +# def test_parse_advisory(self): +# redhat = RedHat() +# advisory_content = get_advisory_content() +# all_packages = redhat._get_all_packages(advisory_content) +# +# advisory = redhat._parse_advisory( +# "1.3.6.1.4.1.25623.1.1.11.2024.1234", +# "RHSA-2024:1234", +# advisory_content, +# redhat._get_packages(advisory_content, all_packages, "6"), +# None, +# ) +# expected_advisory = Advisory() +# expected_advisory.oid = "1.3.6.1.4.1.25623.1.1.11.2024.1234" +# expected_advisory.title = "Red Hat Enterprise Linux: Security Advisory (RHSA-2024:1234)" +# expected_advisory.advisory_xref = "https://access.redhat.com/errata/RHSA-2024:1234" +# expected_advisory.advisory_id = "RHSA-2024:1234" +# expected_advisory.summary = ( +# "The remote host is missing an update for the " +# "'mod_nss' package(s) announced via the RHSA-2024:1234 advisory." +# ) +# expected_advisory.cves = ["CVE-2024-4973"] +# expected_advisory.insight = """The mod_nss module provides strong cryptography for the Apache HTTP Server via the Secure Sockets Layer (SSL) and Transport Layer Security (TLS) protocols, using the Network Security Services (NSS) security library. +# +# This update fixes the following bugs: +# +# * When the NSS library was not initialized and mod_nss tried to clear its SSL cache on start-up, mod_nss terminated unexpectedly when the NSS library was built with debugging enabled. With this update, mod_nss does not try to clear the SSL cache in the described scenario, thus preventing this bug. (BZ#691502)""" +# expected_advisory.affected = "'mod_nss' package(s) on Red Hat Enterprise Linux 6." +# self.assertEqual(expected_advisory, advisory) diff --git a/tests/csaf/test_relationship.py b/tests/csaf/test_relationship.py new file mode 100644 index 000000000..45c8a78d1 --- /dev/null +++ b/tests/csaf/test_relationship.py @@ -0,0 +1,146 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ruff: noqa: E501 + +import unittest + +from pontos.csaf import Relationship + + +class CSAFRelationshipTestCase(unittest.TestCase): + def test_create_combined_parent_to_child_mapping(self): + relationships = [ + Relationship( + { + "product_reference": "abc", + "relates_to_product_reference": "same_parent", + } + ), + Relationship( + { + "product_reference": "def", + "relates_to_product_reference": "same_parent", + } + ), + Relationship( + { + "product_reference": "abc", + "relates_to_product_reference": "different_parent", + } + ), + ] + expected_res = { + "same_parent": {"abc", "def"}, + "different_parent": {"abc"}, + } + res = Relationship.create_combined_parent_to_child_mapping( + relationships + ) + self.assertEqual(res, expected_res) + + def test_build_root_to_leaf_map_no_inner_relationships(self): + root_relationships = [ + Relationship( + { + "product_reference": "abc", + "relates_to_product_reference": "same_parent", + } + ), + Relationship( + { + "product_reference": "def", + "relates_to_product_reference": "same_parent", + "full_product_name": {"product_id": "childRel1"}, + } + ), + Relationship( + { + "product_reference": "abc", + "relates_to_product_reference": "different_parent", + "full_product_name": {"product_id": "childRel2"}, + } + ), + ] + expected_res = Relationship.create_combined_parent_to_child_mapping( + root_relationships + ) + res = Relationship.build_root_to_leaf_map(root_relationships, [], set()) + self.assertEqual(res, expected_res) + + res2 = Relationship.build_root_to_leaf_map( + root_relationships, [], {"1", "2"} + ) + self.assertEqual(res2, expected_res) + + def test_build_root_to_leaf_map_throws(self): + + root_relationships = [ + Relationship( + { + "product_reference": "abc", + "relates_to_product_reference": "same_parent", + } + ), + ] + with self.assertRaises(ValueError): + Relationship.build_root_to_leaf_map([], [], set()) + + with self.assertRaises(ValueError): + Relationship.build_root_to_leaf_map([], root_relationships, set()) + + def test_build_to_root_three_layered_forest(self): + root_relationships = [ + Relationship( + { + "category": "default_component_of", + "full_product_name": { + "name": "postgresql:15:9020020230619032405:rhel9 as a component of Red Hat Enterprise Linux AppStream (v. 9)", + "product_id": "AppStream-9.2.0.Z.MAIN.EUS:postgresql:15:9020020230619032405:rhel9", + }, + "product_reference": "postgresql:15:9020020230619032405:rhel9", + "relates_to_product_reference": "AppStream-9.2.0.Z.MAIN.EUS", + } + ) + ] + inner_relationships = [ + Relationship( + { + "category": "default_component_of", + "full_product_name": { + "name": "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64 as a component of postgresql:15:9020020230619032405:rhel9 as a component of Red Hat Enterprise Linux AppStream (v. 9)", + "product_id": "AppStream-9.2.0.Z.MAIN.EUS:postgresql:15:9020020230619032405:rhel9:pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64", + }, + "product_reference": "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64", + "relates_to_product_reference": "AppStream-9.2.0.Z.MAIN.EUS:postgresql:15:9020020230619032405:rhel9", + } + ), + Relationship( + { + "category": "default_component_of", + "full_product_name": { + "name": "postgresql-upgrade-devel-0:15.3-1.module+el9.2.0.z+19113+6f5d9d63.x86_64 as a component of postgresql:15:9020020230619032405:rhel9 as a component of Red Hat Enterprise Linux AppStream (v. 9)", + "product_id": "AppStream-9.2.0.Z.MAIN.EUS:postgresql:15:9020020230619032405:rhel9:postgresql-upgrade-devel-0:15.3-1.module+el9.2.0.z+19113+6f5d9d63.x86_64", + }, + "product_reference": "postgresql-upgrade-devel-0:15.3-1.module+el9.2.0.z+19113+6f5d9d63.x86_64", + "relates_to_product_reference": "AppStream-9.2.0.Z.MAIN.EUS:postgresql:15:9020020230619032405:rhel9", + } + ), + ] + + leaf_product_ids = { + "postgresql-upgrade-devel-0:15.3-1.module+el9.2.0.z+19113+6f5d9d63.x86_64", + "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64", + } + + expected_res = { + "AppStream-9.2.0.Z.MAIN.EUS": { + "postgresql-upgrade-devel-0:15.3-1.module+el9.2.0.z+19113+6f5d9d63.x86_64", + "pg_repack-0:1.4.8-1.module+el9.2.0+17405+aeb9ec60.aarch64", + }, + } + res = Relationship.build_root_to_leaf_map( + root_relationships, inner_relationships, leaf_product_ids + ) + self.assertDictEqual(res, expected_res) diff --git a/tests/csaf/test_vulnerability.py b/tests/csaf/test_vulnerability.py new file mode 100644 index 000000000..7960f398f --- /dev/null +++ b/tests/csaf/test_vulnerability.py @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ruff: noqa: E501 + +import unittest + +from pontos.csaf import Vulnerability + + +class CSAFVulnerabilityTestCase(unittest.TestCase): + def test_affected_product_ids(self): + vuln = Vulnerability( + { + "product_status": { + "first_affected": ["1", "2"], + "first_fixed": ["3", "4"], + "fixed": ["5", "6"], + "known_affected": ["7", "8"], + "known_not_affected": ["9", "10"], + "last_affected": ["11", "12"], + "recommended": ["13", "14"], + "under_investigation": ["15", "16"], + } + } + ) + exp_res = {"1", "2", "7", "8", "11", "12"} + + self.assertEqual(vuln.affected_product_ids, exp_res) + + def test_none_affected_product_ids(self): + vuln = Vulnerability( + { + "product_status": { + "first_fixed": ["3", "4"], + "fixed": ["5", "6"], + "known_not_affected": ["9", "10"], + "recommended": ["13", "14"], + "under_investigation": ["15", "16"], + } + } + ) + exp_res = set() + + self.assertEqual(vuln.affected_product_ids, exp_res) + + def test_partial_affected_product_ids(self): + + vuln = Vulnerability( + { + "product_status": { + "first_fixed": ["3", "4"], + "fixed": ["5", "6"], + "known_affected": ["7", "8"], + "known_not_affected": ["9", "10"], + "last_affected": ["11", "12"], + "recommended": ["13", "14"], + "under_investigation": ["15", "16"], + } + } + ) + exp_res = {"7", "8", "11", "12"} + self.assertEqual(vuln.affected_product_ids, exp_res) + + def test_confirmed_fixed_product_ids(self): + vuln = Vulnerability( + { + "remediations": [ + {"category": "vendor_fix", "product_ids": ["1", "2"]}, + {"category": "wont_fix", "product_ids": ["3", "4"]}, + ], + "product_status": { + "fixed": ["5"], + "first_fixed": ["6"], + "affected": ["7"], + }, + } + ) + exp_res = {"1", "2", "5", "6"} + res = vuln.confirmed_fixed_product_ids + self.assertEqual(res, exp_res) + + def test_notes_iter(self): + vuln = Vulnerability({"notes": [{"abc": None}, {"def": None}]}) + exp_res = [{"abc": None}, {"def": None}] + res = vuln.iter_notes() + self.assertNotIsInstance(res, list) + self.assertListEqual(list(res), exp_res) + + def test_empty_notes_iter(self): + vuln = Vulnerability({}) + self.assertEqual(list(vuln.iter_notes()), []) + + def test_remediations_iter(self): + vuln = Vulnerability({"remediations": [{"abc": None}, {"def": None}]}) + exp_res = [{"abc": None}, {"def": None}] + res = vuln.iter_remediations() + self.assertNotIsInstance(res, list) + self.assertListEqual(list(res), exp_res) + + def test_empty_remediations_iter(self): + vuln = Vulnerability({}) + self.assertEqual(list(vuln.iter_remediations()), []) From 82107968755e4ddb32e6961195b8ad88be7d20d3 Mon Sep 17 00:00:00 2001 From: olamberts Date: Thu, 14 Aug 2025 15:58:56 +0200 Subject: [PATCH 2/4] Backports type-hinting, I001 --- pontos/csaf/__init__.py | 3 +-- pontos/csaf/_csaf.py | 12 ++++++------ pontos/csaf/_utils.py | 4 ++-- pontos/csaf/models/revision.py | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pontos/csaf/__init__.py b/pontos/csaf/__init__.py index 9c59baaf7..feb9dbbe9 100644 --- a/pontos/csaf/__init__.py +++ b/pontos/csaf/__init__.py @@ -1,5 +1,4 @@ -# ruff: noqa: I100 -from ._enums import ( +from ._enums import ( # noqa: I001 ProductStatus, RelationshipCategory, Remediation, diff --git a/pontos/csaf/_csaf.py b/pontos/csaf/_csaf.py index c6a3d5efc..d85d2055d 100644 --- a/pontos/csaf/_csaf.py +++ b/pontos/csaf/_csaf.py @@ -5,7 +5,7 @@ import json import logging -from typing import Dict, Iterable, List, Set, Tuple +from typing import Dict, Iterable, List, Set, Tuple, Optional from black.trans import defaultdict @@ -55,7 +55,7 @@ def advisory_id(self) -> str: return self.document["tracking"]["id"] def iter_middle_branches( - self, limit_to_categories: Set[str] | None = None + self, limit_to_categories: Optional[Set[str]] = None ) -> Iterable[Dict]: if "branches" not in self.product_tree: logger.warning( @@ -217,8 +217,8 @@ def has_vulnerabilities(self) -> bool: def get_matching_relationships( self, - restrict_to_categories: Set[RelationshipCategory] | None = None, - restrict_to_parent_ids: Set[str] | None = None, + restrict_to_categories: Optional[Set[RelationshipCategory]] = None, + restrict_to_parent_ids: Optional[Set[str]] = None, apply_transitively: bool = False, ) -> Tuple[List[Relationship], List[Relationship]]: """Retrieves product relationships ('component of' etc.) based on product IDs @@ -277,7 +277,7 @@ def get_matching_relationships( def get_remediation_category_for_related_cves( self, prod_id: str - ) -> Dict[str, Set[str | None]]: + ) -> Dict[str, Set[Optional[str]]]: """Retrieves the CSAF-proposed remediations possible Best-case is "vendor_fix" remediation. _Mostly_ this relates to a strict update path. @@ -309,7 +309,7 @@ def get_remediation_category_for_related_cves( return dict(res) def get_all_cves_that_mention_one_of_the_product_ids_as_fixed( - self, restrict_to_product_ids: Set[str] | None = None + self, restrict_to_product_ids: Optional[Set[str]] = None ) -> Set[str]: """Only retrieves that subset of CVEs interested in. diff --git a/pontos/csaf/_utils.py b/pontos/csaf/_utils.py index 6fcb729f6..eb4eae1dd 100644 --- a/pontos/csaf/_utils.py +++ b/pontos/csaf/_utils.py @@ -2,11 +2,11 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Dict, Iterable, Set +from typing import Dict, Iterable, Set, Optional def iter_next_branches( - branch: Dict, limit_to_categories: Set[str] | None = None + branch: Dict, limit_to_categories: Optional[Set[str]] = None ) -> Iterable[Dict]: for inner_branch in branch.get("branches", []): if ( diff --git a/pontos/csaf/models/revision.py b/pontos/csaf/models/revision.py index d59f43bc2..dd7999f1c 100644 --- a/pontos/csaf/models/revision.py +++ b/pontos/csaf/models/revision.py @@ -7,7 +7,7 @@ class Revision(dict): # Defined in 3.2.1.12.6 # shall only exist for non-pre-release document states @property - def date(self) -> "date": + def date(self) -> str: # string; format date-time return self["date"] From 7ebf340c0a39bbd0192830e1a7897bdd83a4e790 Mon Sep 17 00:00:00 2001 From: olamberts Date: Thu, 14 Aug 2025 16:01:32 +0200 Subject: [PATCH 3/4] Fixes: I001 --- pontos/csaf/_csaf.py | 2 +- pontos/csaf/_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pontos/csaf/_csaf.py b/pontos/csaf/_csaf.py index d85d2055d..c4c9d2986 100644 --- a/pontos/csaf/_csaf.py +++ b/pontos/csaf/_csaf.py @@ -5,7 +5,7 @@ import json import logging -from typing import Dict, Iterable, List, Set, Tuple, Optional +from typing import Dict, Iterable, List, Optional, Set, Tuple from black.trans import defaultdict diff --git a/pontos/csaf/_utils.py b/pontos/csaf/_utils.py index eb4eae1dd..f3d2d1fc0 100644 --- a/pontos/csaf/_utils.py +++ b/pontos/csaf/_utils.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Dict, Iterable, Set, Optional +from typing import Dict, Iterable, Optional, Set def iter_next_branches( From 7d2a542c5d4c36330bf5ae8bfd40b864c22af4b7 Mon Sep 17 00:00:00 2001 From: olamberts Date: Mon, 18 Aug 2025 14:45:12 +0200 Subject: [PATCH 4/4] Fixes: CSAF Typing, interface-minimization --- pontos/csaf/_csaf.py | 56 ++++++++++++++++------------- pontos/csaf/_utils.py | 8 ++--- pontos/csaf/models/relationship.py | 28 ++++++++++----- pontos/csaf/models/vulnerability.py | 21 ++++++++--- 4 files changed, 71 insertions(+), 42 deletions(-) diff --git a/pontos/csaf/_csaf.py b/pontos/csaf/_csaf.py index c4c9d2986..163a781f6 100644 --- a/pontos/csaf/_csaf.py +++ b/pontos/csaf/_csaf.py @@ -5,7 +5,7 @@ import json import logging -from typing import Dict, Iterable, List, Optional, Set, Tuple +from typing import Any, Container, Dict, Iterable, List, Optional, Set, Tuple from black.trans import defaultdict @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) -class Csaf(dict): +class Csaf: """ Main purpose: 1. Increased accessibility of common data structures without having @@ -50,13 +50,16 @@ class Csaf(dict): iterable of len != 0 will evaluate to true. """ + def __init__(self, csaf: Dict[str, Any]): + self._data = csaf + @property def advisory_id(self) -> str: return self.document["tracking"]["id"] def iter_middle_branches( - self, limit_to_categories: Optional[Set[str]] = None - ) -> Iterable[Dict]: + self, limit_to_categories: Optional[Container[str]] = None + ) -> Iterable[Dict[str, Any]]: if "branches" not in self.product_tree: logger.warning( "{}: product tree doesn't contain any branches.".format( @@ -77,7 +80,7 @@ def iter_middle_branches( ): yield br - def iter_inner_product_branches(self) -> Iterable[Dict]: + def iter_inner_product_branches(self) -> Iterable[Dict[str, Any]]: """Provides all inner product branches - typically specific OSs/SW/HW versions. @@ -94,7 +97,7 @@ def iter_inner_product_branches(self) -> Iterable[Dict]: for br in iter_next_branches(second_layer_branch): yield br - def iter_products_with_cpe(self) -> Iterable[tuple[Dict, str]]: + def iter_products_with_cpe(self) -> Iterable[Tuple[Dict[str, Any], str]]: """Provides all inner product branches that include a CPE for identification""" for prod in self.iter_inner_product_branches(): if "cpe" not in prod["product"].get( @@ -117,8 +120,8 @@ def get_cves_from_vulnerabilities_mentioned(self) -> Set[str]: return cves def iter_products_with_matching_id( - self, acceptable_ids: Set[str] - ) -> Iterable[Dict]: + self, acceptable_ids: Container[str] + ) -> Iterable[Dict[str, Any]]: """Retrieves complete product structures for these explicit IDs""" for prod in self.iter_inner_product_branches(): if prod["product"]["product_id"] in acceptable_ids: @@ -127,7 +130,11 @@ def iter_products_with_matching_id( def get_reference_urls( self, allow_self_references: bool, allow_external_references: bool ) -> Set[str]: - """Retrieve all URLs this document references to in its main section.""" + """Retrieve all URLs this document references to in its main section. + + Notes: Multiple different URLs may be published for the same category. + + """ # only 'self', and 'external' are CSAF-compliant enum entries for the # reference category allowed_categories = set() @@ -161,18 +168,18 @@ def initial_release_year(self) -> int: @property def vulnerabilities(self) -> List[Vulnerability]: """Retrieves all vulnerabilities listed in a parsed format.""" - return [Vulnerability(v) for v in self.get("vulnerabilities", [])] + return [Vulnerability(v) for v in self._data.get("vulnerabilities", [])] @property def contains_product_tree(self) -> bool: # although *usually* contained, not required # and e.g., Microsoft doesn't always use it - return "product_tree" in self + return "product_tree" in self._data @property - def product_tree(self) -> Dict: + def product_tree(self) -> Dict[str, Any]: # optional in CSAF, but most common - return self["product_tree"] + return self._data["product_tree"] @property def relationships(self) -> List[Relationship]: @@ -192,33 +199,32 @@ def revisions(self) -> List[Revision]: ] @property - def document(self) -> Dict: - return self["document"] + def document(self) -> Dict[str, Any]: + return self._data["document"] - @property - def notes(self) -> Iterable[Dict]: + def iter_main_notes(self) -> Iterable[Dict[str, str]]: for note in self.document["notes"]: yield note - @property - def vulnerability_notes(self) -> Iterable[Dict]: + def iter_vulnerability_notes(self) -> Iterable[Dict[str, str]]: for vuln in self.vulnerabilities: for note in vuln.iter_notes(): yield note - @property - def raw_references(self) -> Iterable[Dict]: + def iter_raw_references(self) -> Iterable[Dict[str, str]]: for reference in self.document["references"]: yield reference @property def has_vulnerabilities(self) -> bool: - return "vulnerabilities" in self + return "vulnerabilities" in self._data def get_matching_relationships( self, - restrict_to_categories: Optional[Set[RelationshipCategory]] = None, - restrict_to_parent_ids: Optional[Set[str]] = None, + restrict_to_categories: Optional[ + Container[RelationshipCategory] + ] = None, + restrict_to_parent_ids: Optional[Container[str]] = None, apply_transitively: bool = False, ) -> Tuple[List[Relationship], List[Relationship]]: """Retrieves product relationships ('component of' etc.) based on product IDs @@ -227,7 +233,7 @@ def get_matching_relationships( Args: restrict_to_categories: explicitly allowed kinds of relationships - restrict_to_parent_ids: explicitly allowed "parent" IDs (return keys). + restrict_to_parent_ids: explicitly allowed "parent" IDs. apply_transitively: store relationship IDs whose parent matched & extract all relationship children of that relationship product ID. diff --git a/pontos/csaf/_utils.py b/pontos/csaf/_utils.py index f3d2d1fc0..b44a8d189 100644 --- a/pontos/csaf/_utils.py +++ b/pontos/csaf/_utils.py @@ -2,15 +2,15 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Dict, Iterable, Optional, Set +from typing import Any, Container, Dict, Iterable, Optional def iter_next_branches( - branch: Dict, limit_to_categories: Optional[Set[str]] = None -) -> Iterable[Dict]: + branch: Dict[str, Any], limit_to_categories: Optional[Container[str]] = None +) -> Iterable[Dict[str, Any]]: for inner_branch in branch.get("branches", []): if ( - limit_to_categories + limit_to_categories is not None and inner_branch["category"] not in limit_to_categories ): continue diff --git a/pontos/csaf/models/relationship.py b/pontos/csaf/models/relationship.py index eeb1d7a0b..33c4b3888 100644 --- a/pontos/csaf/models/relationship.py +++ b/pontos/csaf/models/relationship.py @@ -3,37 +3,47 @@ # SPDX-License-Identifier: GPL-3.0-or-later from collections import defaultdict +from collections.abc import Container from copy import deepcopy -from typing import Dict, List, Set +from typing import Any, Dict, List, Set from pontos.csaf import RelationshipCategory -class Relationship(dict): +class Relationship: + def __init__(self, relationship: Dict[str, Any]): + self._data = relationship + def __hash__(self): # Should be unique for a given CSAF. - return hash((self.parent_id, self.child_id, self.id, self.kind)) + return hash((self.parent_id, self.child_id, self.id)) def __eq__(self, other): if not isinstance(other, (Relationship, dict)): return False - return dict(self) == dict(other) + if isinstance(other, Relationship): + return self._data == other._data + return self._data == other + + @property + def product(self) -> Dict[str, Any]: + return self._data["full_product_name"] @property def parent_id(self) -> str: - return self["relates_to_product_reference"] + return self._data["relates_to_product_reference"] @property def kind(self) -> RelationshipCategory: - return RelationshipCategory(self["category"]) + return RelationshipCategory(self._data["category"]) @property def child_id(self) -> str: - return self["product_reference"] + return self._data["product_reference"] @property def id(self) -> str: - return self["full_product_name"]["product_id"] + return self._data["full_product_name"]["product_id"] @staticmethod def create_combined_parent_to_child_mapping( @@ -49,7 +59,7 @@ def create_combined_parent_to_child_mapping( def build_root_to_leaf_map( root_relationships: List["Relationship"], inner_tree_relationships: List["Relationship"], - leaf_product_ids: Set[str], + leaf_product_ids: Container[str], ) -> Dict[str, set[str]]: if not root_relationships: diff --git a/pontos/csaf/models/vulnerability.py b/pontos/csaf/models/vulnerability.py index d4ee0ad64..76c16c8e4 100644 --- a/pontos/csaf/models/vulnerability.py +++ b/pontos/csaf/models/vulnerability.py @@ -2,12 +2,25 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Dict, Iterable, Set +from collections.abc import Mapping +from typing import Any, Dict, Iterable, Set from pontos.csaf import ProductStatus, Remediation -class Vulnerability(dict): +class Vulnerability(Mapping): + def __init__(self, vulnerability: dict[str, Any]): + self._data = vulnerability + + def __getitem__(self, key: str) -> Any: + return self._data[key] + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + @property def cve(self) -> str: return self["cve"] @@ -23,13 +36,13 @@ def affected_product_ids(self) -> Set[str]: ) return affected_prods - def iter_notes(self) -> Iterable[Dict]: + def iter_notes(self) -> Iterable[Dict[str, str]]: if "notes" not in self: return for note in self["notes"]: yield note - def iter_remediations(self) -> Iterable[Dict]: + def iter_remediations(self) -> Iterable[Dict[str, Any]]: if "remediations" not in self: return for remediation in self["remediations"]: