diff --git a/mypy.ini b/mypy.ini index 8aa6bc3..10a0b87 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,8 +3,6 @@ warn_unused_configs = True disallow_incomplete_defs = True no_implicit_optional = True disable_error_code = attr-defined -[mypy-setuptools.*] -ignore_missing_imports = True [mypy-lxml.*] ignore_missing_imports = True [mypy-pefile.*] diff --git a/requirements.txt b/requirements.txt index 95c5e54..8244203 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ cbor2 lxml pefile +setuptools diff --git a/uswid/cli.py b/uswid/cli.py index fe9d553..5167857 100755 --- a/uswid/cli.py +++ b/uswid/cli.py @@ -9,7 +9,7 @@ from collections import defaultdict from datetime import datetime -from typing import Optional, List, Dict, Callable +from typing import Optional, List, Dict, Any import argparse import socket import json @@ -49,7 +49,7 @@ from uswid.container_utils import container_generate, container_roundtrip -def _detect_format(filepath: str) -> Optional[Callable]: +def _detect_format(filepath: str) -> Optional[Any]: if filepath.endswith("bom.json") or filepath.endswith("cdx.json"): return uSwidFormatCycloneDX() if filepath.endswith("spdx.json"): @@ -148,21 +148,21 @@ def _container_merge_from_filepath( if base.verbose: fixup_strs.append(f"Add VCS commit → {component.edition}") if not component.get_entity_by_role(uSwidEntityRole.TAG_CREATOR): - entity: uSwidEntity = uSwidEntity( + entity_tag: uSwidEntity = uSwidEntity( name=", ".join(vcs.get_sbom_authors()), roles=[uSwidEntityRole.TAG_CREATOR], ) - component.add_entity(entity) + component.add_entity(entity_tag) if base.verbose: - fixup_strs.append(f"Add VCS SBOM author → {entity.name}") + fixup_strs.append(f"Add VCS SBOM author → {entity_tag.name}") if not component.get_entity_by_role(uSwidEntityRole.SOFTWARE_CREATOR): - entity: uSwidEntity = uSwidEntity( + entity_creator: uSwidEntity = uSwidEntity( name=", ".join(vcs.get_authors()), roles=[uSwidEntityRole.SOFTWARE_CREATOR], ) - component.add_entity(entity) + component.add_entity(entity_creator) if base.verbose: - fixup_strs.append(f"Add VCS author → {entity.name}") + fixup_strs.append(f"Add VCS author → {entity_creator.name}") if fixup_strs: print(f"Fixup required in {filepath}:") for fixup_str in fixup_strs: diff --git a/uswid/component.py b/uswid/component.py index d7d1b0a..cc0ea01 100644 --- a/uswid/component.py +++ b/uswid/component.py @@ -69,7 +69,7 @@ def __str__(self): return self.name.lower() @staticmethod - def from_str(value: str) -> str: + def from_str(value: str) -> "uSwidComponentType": """converts a lowercase component type to a uSwidComponentType""" return uSwidComponentType[value.upper()] @@ -132,7 +132,7 @@ def __init__( """Status, with specific terms and conditions for its use, e.g. 'DO NOT SHIP'""" self.activation_status: Optional[str] = None - def add_source_filename(self, source_file: str): + def add_source_filename(self, source_file: str) -> None: """Adds a source filename, i.e. what file helped created this component""" if source_file not in self.source_filenames: self.source_filenames.append(source_file) @@ -267,7 +267,8 @@ def problems(self) -> List[uSwidProblem]: # link link_by_rel: Dict[uSwidLinkRel, uSwidLink] = {} for link in self.links: - link_by_rel[link.rel] = link + if link.rel: + link_by_rel[link.rel] = link problems += link.problems() if uSwidLinkRel.LICENSE not in link_by_rel: problems += [uSwidProblem("link", "Has no LICENSE", since="0.4.7")] diff --git a/uswid/enums.py b/uswid/enums.py index 4204629..533988d 100644 --- a/uswid/enums.py +++ b/uswid/enums.py @@ -11,6 +11,7 @@ class uSwidVersionScheme(IntEnum): """Represents an enumerated version scheme""" + UNKNOWN = 0 MULTIPARTNUMERIC = 1 MULTIPARTNUMERIC_SUFFIX = 2 ALPHANUMERIC = 3 diff --git a/uswid/format_coswid.py b/uswid/format_coswid.py index e002027..f534d58 100644 --- a/uswid/format_coswid.py +++ b/uswid/format_coswid.py @@ -147,25 +147,8 @@ def _save_link(self, link: uSwidLink) -> Dict[uSwidGlobalMap, Any]: data: Dict[uSwidGlobalMap, Any] = {} data[uSwidGlobalMap.HREF] = link.href - - # map back into a uSwidLinkRel if possible if link.rel: - LINK_MAP: Dict[str, uSwidLinkRel] = { - "license": uSwidLinkRel.LICENSE, - "compiler": uSwidLinkRel.COMPILER, - "ancestor": uSwidLinkRel.ANCESTOR, - "component": uSwidLinkRel.COMPONENT, - "feature": uSwidLinkRel.FEATURE, - "installation-media": uSwidLinkRel.INSTALLATION_MEDIA, - "package-installer": uSwidLinkRel.PACKAGE_INSTALLER, - "parent": uSwidLinkRel.PARENT, - "patches": uSwidLinkRel.PATCHES, - "requires": uSwidLinkRel.REQUIRES, - "see-also": uSwidLinkRel.SEE_ALSO, - "supersedes": uSwidLinkRel.SUPERSEDES, - "supplemental": uSwidLinkRel.SUPPLEMENTAL, - } - data[uSwidGlobalMap.REL] = LINK_MAP.get(link.rel, link.rel) + data[uSwidGlobalMap.REL] = link.rel return data def _save_hash(self, ihash: uSwidHash) -> Tuple[int, bytes]: @@ -235,7 +218,7 @@ def _save_component(self, component: uSwidComponent) -> bytes: data[uSwidGlobalMap.VERSION_SCHEME] = component.version_scheme # metadata section - metadata: Dict[uSwidGlobalMap, Any] = { + metadata: Dict[Union[uSwidGlobalMap, str], Any] = { uSwidGlobalMap.GENERATOR: component.generator } if component.type and component.type != uSwidComponentType.FIRMWARE: diff --git a/uswid/format_cyclonedx.py b/uswid/format_cyclonedx.py index a9edd39..8c6c58e 100644 --- a/uswid/format_cyclonedx.py +++ b/uswid/format_cyclonedx.py @@ -50,7 +50,7 @@ def _convert_str_to_version_scheme(version_scheme: str) -> uSwidVersionScheme: "alphanumeric": uSwidVersionScheme.ALPHANUMERIC, "decimal": uSwidVersionScheme.DECIMAL, "semver": uSwidVersionScheme.SEMVER, - }.get(version_scheme, None) + }.get(version_scheme, uSwidVersionScheme.UNKNOWN) def _convert_entity_to_dict(entity: uSwidEntity) -> Dict[str, Any]: @@ -174,11 +174,11 @@ def _load_component_internal( component.add_entity(entity) # we only have authors - entity = component.get_entity_by_role(uSwidEntityRole.MAINTAINER) - if entity and not component.get_entity_by_role( + entity_authors = component.get_entity_by_role(uSwidEntityRole.MAINTAINER) + if entity_authors and not component.get_entity_by_role( uSwidEntityRole.SOFTWARE_CREATOR ): - entity.add_role(uSwidEntityRole.SOFTWARE_CREATOR) + entity_authors.add_role(uSwidEntityRole.SOFTWARE_CREATOR) def load(self, blob: bytes, path: Optional[str] = None) -> uSwidContainer: try: @@ -216,32 +216,32 @@ def load(self, blob: bytes, path: Optional[str] = None) -> uSwidContainer: ) for dep in root.get("dependencies", []): - component = container._get_by_id(dep["ref"]) + component_ref = container._get_by_id(dep["ref"]) component_other = container._get_by_id(dep["dependsOn"]) - if not component: + if not component_ref: continue if not component_other: continue if component_other.tag_id == "compiler": - component.add_link( + component_ref.add_link( uSwidLink( rel=uSwidLinkRel.COMPILER, href=component_other.software_name ) ) else: - component.add_link( + component_ref.add_link( uSwidLink(rel=uSwidLinkRel.COMPONENT, href=component_other.tag_id) ) for ann in root.get("annotations", []): - component = container._get_by_id(ann["bom-ref"]) - if not component: + component_ref = container._get_by_id(ann["bom-ref"]) + if not component_ref: continue try: date = datetime.fromisoformat(ann["timestamp"]) except AttributeError: date = None - component.add_evidence(uSwidEvidence(date=date, device_id=ann["text"])) + component_ref.add_evidence(uSwidEvidence(date=date, device_id=ann["text"])) return container @@ -353,9 +353,9 @@ def _save_component(self, component: uSwidComponent) -> Dict[str, Any]: # save the source code VCS and colloquial version link_vcs = component.get_link_by_rel(uSwidLinkRel.SEE_ALSO) if link_vcs or component.edition: - vcs_data: Dict[str, str] = {"type": "vcs"} + vcs_data: Dict[str, Any] = {"type": "vcs"} - if link_vcs: + if link_vcs and link_vcs.href: vcs_data["url"] = link_vcs.href else: vcs_data["url"] = "https://NOASSERTION/" @@ -363,12 +363,10 @@ def _save_component(self, component: uSwidComponent) -> Dict[str, Any]: # set the correct hash algorithm automatically if component.edition: hash_tmp = uSwidHash(value=component.edition) - vcs_data["hashes"] = [ - { - "alg": _convert_hash_alg_to_str(hash_tmp.alg_id), - "content": hash_tmp.value, - } - ] + vcs_data_hash = {"content": hash_tmp.value} + if hash_tmp.alg_id: + vcs_data_hash["alg"] = _convert_hash_alg_to_str(hash_tmp.alg_id) + vcs_data["hashes"] = [vcs_data_hash] root["externalReferences"] = [vcs_data] # additional metadata, not yet standardized in cdx diff --git a/uswid/format_goswid.py b/uswid/format_goswid.py index 0e8167a..8bde35e 100644 --- a/uswid/format_goswid.py +++ b/uswid/format_goswid.py @@ -195,7 +195,7 @@ def _load_link(self, link: uSwidLink, node: Dict[str, str]) -> None: """Imports a uSwidLink goSWID section""" link.href = node.get("href") - link.rel = uSwidLinkRel.from_string(node.get("rel")) + link.rel = uSwidLinkRel.from_string(node.get("rel", "unknown")) def _load_evidence(self, evidence: uSwidEvidence, node: Dict[str, str]) -> None: """Imports a uSwidEvidence goSWID section""" diff --git a/uswid/link.py b/uswid/link.py index b7f3ccc..923439c 100644 --- a/uswid/link.py +++ b/uswid/link.py @@ -23,6 +23,7 @@ class uSwidLinkRel(IntEnum): LICENSE = -2 COMPILER = -1 + UNKNOWN = 0 ANCESTOR = 1 COMPONENT = 2 FEATURE = 3 @@ -46,6 +47,7 @@ def from_string(cls, value: str) -> "uSwidLinkRel": "license": uSwidLinkRel.LICENSE, "compiler": uSwidLinkRel.COMPILER, "ancestor": uSwidLinkRel.ANCESTOR, + "unknown": uSwidLinkRel.UNKNOWN, "component": uSwidLinkRel.COMPONENT, "feature": uSwidLinkRel.FEATURE, "installation-media": uSwidLinkRel.INSTALLATION_MEDIA, @@ -95,9 +97,9 @@ def __init__( @property def spdx_id(self) -> Optional[str]: """Returns the SPDX ID from the URL, if possible""" - if not self.href.startswith("https://spdx.org/licenses/"): - return None - return self.href[26:].replace(".html", "") + if self.href and self.href.startswith("https://spdx.org/licenses/"): + return self.href[26:].replace(".html", "") + return None @property def rel(self) -> Optional[uSwidLinkRel]: diff --git a/uswid/test_uswid.py b/uswid/test_uswid.py index 9ffbc5e..a8089fe 100644 --- a/uswid/test_uswid.py +++ b/uswid/test_uswid.py @@ -21,7 +21,7 @@ from .container import uSwidContainer from .errors import NotSupportedError -from .link import uSwidLink +from .link import uSwidLink, uSwidLinkRel from .entity import uSwidEntity, uSwidEntityRole from .enums import uSwidVersionScheme from .component import uSwidComponent @@ -226,7 +226,7 @@ def test_entity(self): def test_link(self): """Unit tests for uSwidLink""" # enumerated type - link = uSwidLink(href="http://test.com/", rel="see-also") + link = uSwidLink(href="http://test.com/", rel=uSwidLinkRel.SEE_ALSO) self.assertEqual(str(link), 'uSwidLink(rel="see-also",href="http://test.com/")') self.assertEqual( str(uSwidFormatCoswid()._save_link(link)), # type: ignore @@ -235,7 +235,7 @@ def test_link(self): ) # rel from IANA "Software Tag Link Relationship Values" registry - link = uSwidLink(href="http://test.com/", rel="license") + link = uSwidLink(href="http://test.com/", rel=uSwidLinkRel.LICENSE) self.assertEqual(str(link), 'uSwidLink(rel="license",href="http://test.com/")') self.assertEqual( str(uSwidFormatCoswid()._save_link(link)), # type: ignore