Source code for ntia_conformance_checker.adapters.spdx2_adapter

# SPDX-FileCopyrightText: 2026 SPDX contributors
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0

"""SPDX 2.x specific data extraction adapter."""

from spdx_tools.spdx.model.document import Document
from spdx_tools.spdx.model.relationship import RelationshipType
from spdx_tools.spdx.model.spdx_no_assertion import SpdxNoAssertion
from spdx_tools.spdx.validation.validation_message import ValidationMessage

from .adapter_interface import SbomAdapter, is_blank_string


def _is_blank(value: object, noassertion: bool = False) -> bool:
    """
    Check whether an SPDX 2 field value should be treated as blank.

    Blank means ``None``, ``SpdxNoAssertion``, or a blank string per
    :func:`is_blank_string`. spdx-tools keeps some keywords as plain strings,
    e.g. lowercase ``noassertion`` in ``copyrightText``.

    Args:
        value: The field value to check.
        noassertion: Whether ``NOASSERTION`` strings count as blank.

    Returns:
        bool: True if the value is considered blank.
    """
    if value is None or isinstance(value, SpdxNoAssertion):
        return True
    return is_blank_string(value, noassertion)


[docs] class Spdx2Adapter(SbomAdapter): """Adapter for extracting data from SPDX 2.x documents.""" def __init__(self, doc: Document) -> None: self.doc = doc
[docs] def get_doc_spec_version(self) -> str | None: doc_creation_info = getattr(self.doc, "creation_info", None) if doc_creation_info: return getattr(doc_creation_info, "spdx_version", None) return None
[docs] def check_author(self) -> bool: # Note that the spdx-tools's parser will raise an SPDXParsingError # anyway, if the document does not contain a creator. # So in practice, this section should always return True doc_creation_info = getattr(self.doc, "creation_info", None) if doc_creation_info: return bool(getattr(doc_creation_info, "creators", [])) return False
[docs] def check_timestamp(self) -> bool: # Note that the spdx-tools's parser will raise an SPDXParsingError, # if the document does not contain a timestamp. # So in practice, this section should always return True. doc_creation_info = getattr(self.doc, "creation_info", None) if doc_creation_info: return bool(getattr(doc_creation_info, "created", None)) return False
[docs] def get_sbom_name(self) -> str: doc_creation_info = getattr(self.doc, "creation_info", None) if doc_creation_info: return getattr(doc_creation_info, "name", "") return ""
[docs] def get_components_without_names( self, reachable_ids: set[str] ) -> list[tuple[str, str]]: return [ (package.name or "", package.spdx_id or "") for package in getattr(self.doc, "packages", []) if package.spdx_id in reachable_ids and _is_blank(package.name) ]
[docs] def get_components_without_versions( self, reachable_ids: set[str] ) -> list[tuple[str, str]]: return [ (package.name or "", package.spdx_id or "") for package in getattr(self.doc, "packages", []) if package.spdx_id in reachable_ids and _is_blank(package.version) ]
[docs] def get_components_without_suppliers( self, reachable_ids: set[str] ) -> list[tuple[str, str]]: return [ (package.name or "", package.spdx_id or "") for package in getattr(self.doc, "packages", []) if package.spdx_id in reachable_ids and _is_blank(package.supplier, noassertion=True) ]
[docs] def get_components_without_identifiers( self, reachable_ids: set[str] ) -> list[tuple[str, str]]: return [ (package.name or "", package.spdx_id or "") for package in getattr(self.doc, "packages", []) if _is_blank(package.spdx_id) ]
[docs] def get_components_without_concluded_licenses( self, reachable_ids: set[str] ) -> list[tuple[str, str]]: # Note: concluded license is mandatory in SPDX-2.2 and SPDX-2.3 return [ (package.name or "", package.spdx_id or "") for package in getattr(self.doc, "packages", []) if package.spdx_id in reachable_ids and _is_blank(package.license_concluded, noassertion=True) ]
[docs] def check_dependency_relationships(self) -> bool: """In SPDX 2, this checks for a DESCRIBES relationship""" if not getattr(self.doc, "relationships", []): return False describes_relationships = [ rel for rel in self.doc.relationships if rel.relationship_type == RelationshipType.DESCRIBES ] # A set of all package spdx_ids for quick lookup spdx_id_set = {package.spdx_id for package in getattr(self.doc, "packages", [])} # Check if any of the "DESCRIBES" relationships describe a Package describes_package = any( rel.related_spdx_element_id in spdx_id_set for rel in describes_relationships ) return describes_package
[docs] def get_total_number_components(self) -> int: """In SPDX 2, this returns the total count of packages.""" return len(getattr(self.doc, "packages", []))
[docs] def get_sbom_types( self, conformance_messages: list[ValidationMessage] ) -> list[str]: # SBOM type is only available in SPDX 3 return []