This is an automated email from the ASF dual-hosted git repository.

arm pushed a commit to branch cyclonedx_xml
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit a87902183160f79a6582aa1122b0026560cf5106
Author: Alastair McFarlane <[email protected]>
AuthorDate: Wed Mar 25 17:51:46 2026 +0000

    #901 - add support for XML in sbom tooling
---
 atr/db/interaction.py                  |   2 +
 atr/sbom/cli.py                        |  23 ++++++--
 atr/sbom/conformance.py                |  27 +++++----
 atr/sbom/cyclonedx.py                  |   2 +-
 atr/sbom/licenses.py                   |  22 +++++---
 atr/sbom/models/bom.py                 | 100 ---------------------------------
 atr/sbom/models/bundle.py              |  10 ++--
 atr/sbom/osv.py                        |  36 +++---------
 atr/sbom/tool.py                       |  24 ++++----
 atr/sbom/utilities.py                  |  51 +++++++++++++----
 atr/tasks/sbom.py                      |  12 ++--
 typestubs/py_serializable/__init__.pyi |  94 +++++++++++++++++++++++++++++++
 12 files changed, 217 insertions(+), 186 deletions(-)

diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index 7f73f555..3af3ad6e 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -39,6 +39,8 @@ import atr.user as user
 import atr.util as util
 import atr.web as web
 
+# Infra-provided service account with permission to run ATR workflows
+# audit_guidance required actor for ATR distribution workflows; must not be 
used for project TP workflows.
 _GITHUB_TRUSTED_ROLE_NID: Final[int] = 254436773
 
 
diff --git a/atr/sbom/cli.py b/atr/sbom/cli.py
index a9cbd801..117036ac 100644
--- a/atr/sbom/cli.py
+++ b/atr/sbom/cli.py
@@ -20,6 +20,10 @@ import pathlib
 import sys
 
 import yyjson
+from cyclonedx.output import make_outputter
+from cyclonedx.schema import OutputFormat
+from cyclonedx.schema.schema import SchemaVersion1Dot7, SCHEMA_VERSIONS
+from cyclonedx.model.bom import Bom
 
 from . import models, osv
 from .conformance import ntia_2021_issues
@@ -57,10 +61,15 @@ def command_merge(bundle: models.bundle.Bundle) -> None:
     patch_ops = asyncio.run(bundle_to_ntia_patch(bundle))
     if patch_ops:
         patch_data = patch_to_data(patch_ops)
-        merged = bundle.doc.patch(yyjson.Document(patch_data))
-        print(merged.dumps())
+        output = bundle.doc.patch(yyjson.Document(patch_data))
+    else:
+        output = bundle.doc
+    if bundle.source_type == "json":
+        print(output.dumps())
     else:
-        print(bundle.doc.dumps())
+        print(make_outputter(
+            Bom.from_json(data=output.as_obj), OutputFormat.XML, 
bundle.spec_version
+        ).output_as_string(indent=2))
 
 
 def command_missing(bundle: models.bundle.Bundle) -> None:
@@ -154,11 +163,11 @@ def command_where(bundle: models.bundle.Bundle) -> None:
             case models.conformance.MissingComponentProperty():
                 components = bundle.bom.components
                 primary_component = bundle.bom.metadata.component if 
bundle.bom.metadata else None
-                if (error.index is not None) and (components is not None):
-                    print(components[error.index].model_dump_json(indent=2))
+                if (error.index is not None) and len(components) > 0:
+                    print(components[error.index].as_json(SchemaVersion1Dot7))
                     print()
                 elif primary_component is not None:
-                    print(primary_component.model_dump_json(indent=2))
+                    print(primary_component.as_json(SchemaVersion1Dot7))
                     print()
 
 
@@ -172,6 +181,8 @@ def main() -> None:  # noqa: C901
         sys.exit(1)
     path = pathlib.Path(sys.argv[2])
     bundle = path_to_bundle(path)
+    if not bundle:
+        raise RuntimeError("Could not load bundle")
     match sys.argv[1]:
         case "license":
             command_license(bundle)
diff --git a/atr/sbom/conformance.py b/atr/sbom/conformance.py
index 96b0f638..51a2ab2d 100644
--- a/atr/sbom/conformance.py
+++ b/atr/sbom/conformance.py
@@ -19,9 +19,14 @@ from __future__ import annotations
 
 import datetime
 import urllib.parse
+from typing import TYPE_CHECKING
 
 import aiohttp
 import yyjson
+from cyclonedx.model.component import Component
+
+if TYPE_CHECKING:
+    from cyclonedx.model.bom import Bom
 
 from . import constants, models
 from .maven import cache_read, cache_write
@@ -234,7 +239,7 @@ def assemble_metadata_timestamp(doc: yyjson.Document, 
patch_ops: models.patch.Pa
 
 
 def ntia_2021_issues(
-    bom_value: models.bom.Bom,
+    bom_value: Bom,
 ) -> tuple[list[models.conformance.Missing], list[models.conformance.Missing]]:
     # 1. Supplier
     # ECMA-424 1st edition says that this is the supplier of the primary 
component
@@ -271,12 +276,12 @@ def ntia_2021_issues(
     warnings: list[models.conformance.Missing] = []
     errors: list[models.conformance.Missing] = []
 
-    if bom_value.metadata is not None:
+    if bom_value.metadata:
         if bom_value.metadata.supplier is None:
             
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_SUPPLIER))
 
         if bom_value.metadata.component is not None:
-            if bom_value.metadata.component.name is None:
+            if not bom_value.metadata.component.name:
                 errors.append(
                     
models.conformance.MissingComponentProperty(property=models.conformance.ComponentProperty.NAME)
                 )
@@ -299,19 +304,17 @@ def ntia_2021_issues(
         else:
             
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_COMPONENT))
 
-        if bom_value.metadata.author is None:
+        if len(bom_value.metadata.authors) < 1:
             
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_AUTHOR))
-
-        if bom_value.metadata.timestamp is None:
-            
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA_TIMESTAMP))
     else:
         
errors.append(models.conformance.MissingProperty(property=models.conformance.Property.METADATA))
 
-    for index, component in enumerate(bom_value.components or []):
+    components: list[Component] = list(bom_value.components)
+    for index, component in enumerate(components):
         component_type = component.type
         component_friendly_name = component.name
-        if component_type is not None:
-            component_friendly_name = f"{component_type}: 
{component_friendly_name}"
+        if component_type:
+            component_friendly_name = f"{component_type.value}: 
{component_friendly_name}"
         if component.supplier is None:
             errors.append(
                 models.conformance.MissingComponentProperty(
@@ -321,7 +324,7 @@ def ntia_2021_issues(
                 )
             )
 
-        if component.name is None:
+        if not component.name:
             errors.append(
                 models.conformance.MissingComponentProperty(
                     property=models.conformance.ComponentProperty.NAME,
@@ -330,7 +333,7 @@ def ntia_2021_issues(
                 )
             )
 
-        if component.version is None:
+        if not component.version:
             errors.append(
                 models.conformance.MissingComponentProperty(
                     property=models.conformance.ComponentProperty.VERSION,
diff --git a/atr/sbom/cyclonedx.py b/atr/sbom/cyclonedx.py
index 35459798..f47473c4 100644
--- a/atr/sbom/cyclonedx.py
+++ b/atr/sbom/cyclonedx.py
@@ -39,7 +39,7 @@ def validate_cli(bundle_value: models.bundle.Bundle) -> 
list[str] | None:
         "validate",
         "--fail-on-errors",
         "--input-format",
-        "json",
+        bundle_value.source_type,
         "--input-file",
         bundle_value.path.as_posix(),
     ]
diff --git a/atr/sbom/licenses.py b/atr/sbom/licenses.py
index 470571c8..dd8ee692 100644
--- a/atr/sbom/licenses.py
+++ b/atr/sbom/licenses.py
@@ -17,19 +17,27 @@
 
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
+from cyclonedx.model.component import Component
+from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression
+
+if TYPE_CHECKING:
+    from cyclonedx.model.bom import Bom
+
 from . import constants, models
 from .spdx import license_expression_atoms
 
 
 def check(
-    bom_value: models.bom.Bom,
+    bom_value: Bom,
     include_all: bool = False,
 ) -> tuple[list[models.licenses.Issue], list[models.licenses.Issue], 
list[models.licenses.Issue]]:
     warnings: list[models.licenses.Issue] = []
     errors: list[models.licenses.Issue] = []
     good: list[models.licenses.Issue] = []
 
-    components = bom_value.components or []
+    components: list[Component] = list(bom_value.components)
     if bom_value.metadata and bom_value.metadata.component:
         components = [bom_value.metadata.component, *components]
 
@@ -45,16 +53,16 @@ def check(
         for license_choice in component.licenses:
             license_expr = None
 
-            if license_choice.expression:
-                license_expr = license_choice.expression
-            elif license_choice.license and license_choice.license.id:
-                license_expr = license_choice.license.id
+            if isinstance(license_choice, LicenseExpression):
+                license_expr = license_choice.value
+            elif isinstance(license_choice, DisjunctiveLicense):
+                license_expr = license_choice.id
 
             if not license_expr:
                 continue
 
             parse_failed = False
-            if license_choice.expression:
+            if isinstance(license_choice, LicenseExpression):
                 try:
                     atoms = license_expression_atoms(license_expr)
                 except ValueError:
diff --git a/atr/sbom/models/bom.py b/atr/sbom/models/bom.py
deleted file mode 100644
index e02dff26..00000000
--- a/atr/sbom/models/bom.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-from __future__ import annotations
-
-import pydantic
-
-from .base import Lax
-
-
-class Swid(Lax):
-    tag_id: str | None = pydantic.Field(default=None, alias="tagId")
-
-
-class Supplier(Lax):
-    name: str | None = None
-    url: list[str] | None = None
-
-
-class License(Lax):
-    id: str | None = None
-    name: str | None = None
-    url: str | None = None
-
-
-class LicenseChoice(Lax):
-    license: License | None = None
-    expression: str | None = None
-
-
-class Component(Lax):
-    bom_ref: str | None = pydantic.Field(default=None, alias="bom-ref")
-    name: str | None = None
-    version: str | None = None
-    supplier: Supplier | None = None
-    purl: str | None = None
-    cpe: str | None = None
-    swid: Swid | None = None
-    licenses: list[LicenseChoice] | None = None
-    scope: str | None = None
-    type: str | None = None
-
-
-class ToolComponent(Lax):
-    name: str | None = None
-    version: str | None = None
-    description: str | None = None
-    supplier: Supplier | None = None
-
-
-class ServiceComponent(Lax):
-    name: str | None = None
-    version: str | None = None
-    description: str | None = None
-    supplier: Supplier | None = None
-    authenticated: bool | None = None
-
-
-class Tool(Lax):
-    name: str | None = None
-    version: str | None = None
-    description: str | None = None
-
-
-class Tools(Lax):
-    components: list[ToolComponent] | None = None
-    services: list[ServiceComponent] | None = None
-
-
-class Metadata(Lax):
-    author: str | None = None
-    timestamp: str | None = None
-    supplier: Supplier | None = None
-    component: Component | None = None
-    tools: Tools | list[Tool] | None = None
-
-
-class Dependency(Lax):
-    ref: str
-    depends_on: list[str] | None = pydantic.Field(default=None, 
alias="dependsOn")
-
-
-class Bom(Lax):
-    metadata: Metadata | None = None
-    components: list[Component] | None = None
-    dependencies: list[Dependency] | None = None
diff --git a/atr/sbom/models/bundle.py b/atr/sbom/models/bundle.py
index 800e70e0..5ab3d83f 100644
--- a/atr/sbom/models/bundle.py
+++ b/atr/sbom/models/bundle.py
@@ -18,19 +18,21 @@
 from __future__ import annotations
 
 import dataclasses
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Literal
 
 if TYPE_CHECKING:
     import pathlib
 
     import yyjson
-
-    from .bom import Bom
+    from cyclonedx.model.bom import Bom
+    from cyclonedx.schema import SchemaVersion
 
 
 @dataclasses.dataclass
 class Bundle:
-    doc: yyjson.Document
+    source_type: Literal["json", "xml"]
+    spec_version: SchemaVersion
     bom: Bom
+    doc: yyjson.Document
     path: pathlib.Path
     text: str
diff --git a/atr/sbom/osv.py b/atr/sbom/osv.py
index 227c7816..cb042f4a 100644
--- a/atr/sbom/osv.py
+++ b/atr/sbom/osv.py
@@ -28,6 +28,7 @@ from .utilities import get_pointer, osv_severity_to_cdx
 if TYPE_CHECKING:
     import aiohttp
     import yyjson
+    from cyclonedx.model.component import Component
 
 _DEBUG: bool = os.environ.get("DEBUG_SBOM_TOOL") == "1"
 _OSV_API_BASE: str = "https://api.osv.dev/v1";
@@ -82,7 +83,7 @@ _SOURCE_DATABASE_NAMES = {
 
 
 async def scan_bundle(bundle: models.bundle.Bundle) -> 
tuple[list[models.osv.ComponentVulnerabilities], list[str]]:
-    components = bundle.bom.components or []
+    components = list(bundle.bom.components)
     queries, ignored = _scan_bundle_build_queries(components)
     if _DEBUG:
         print(f"[DEBUG] Scanning {len(queries)} components for 
vulnerabilities")
@@ -165,29 +166,6 @@ def _assemble_vulnerabilities(doc: yyjson.Document, 
patch_ops: models.patch.Patc
     )
 
 
-def _component_purl_with_version(component: models.bom.Component) -> str | 
None:
-    if component.purl is None:
-        return None
-    if component.version is None:
-        return None
-    version = component.version.strip()
-    if not version:
-        return None
-    purl = component.purl
-    split_index = len(purl)
-    question_index = purl.find("?")
-    if (question_index != -1) and (question_index < split_index):
-        split_index = question_index
-    hash_index = purl.find("#")
-    if (hash_index != -1) and (hash_index < split_index):
-        split_index = hash_index
-    if "@" in purl[:split_index]:
-        return purl
-    base = purl[:split_index]
-    suffix = purl[split_index:]
-    return f"{base}@{version}{suffix}"
-
-
 async def _fetch_vulnerabilities_for_batch(
     session: aiohttp.ClientSession,
     queries: list[dict[str, Any]],
@@ -256,18 +234,18 @@ async def _paginate_query(
 
 
 def _scan_bundle_build_queries(
-    components: list[models.bom.Component],
+    components: list[Component],
 ) -> tuple[list[tuple[str, dict[str, Any]]], list[str]]:
     queries: list[tuple[str, dict[str, Any]]] = []
     ignored = []
     for component in components:
-        purl_with_version = _component_purl_with_version(component)
+        purl_with_version = component.purl
         if purl_with_version is None:
             ignored.append(component.name)
             continue
-        query = {"package": {"purl": purl_with_version}}
-        if component.bom_ref is not None:
-            queries.append((component.bom_ref, query))
+        query = {"package": {"purl": str(purl_with_version)}}
+        if component.bom_ref:
+            queries.append((str(component.bom_ref), query))
     return queries, ignored
 
 
diff --git a/atr/sbom/tool.py b/atr/sbom/tool.py
index 08e98802..4ca1447b 100644
--- a/atr/sbom/tool.py
+++ b/atr/sbom/tool.py
@@ -17,7 +17,6 @@
 
 from __future__ import annotations
 
-import datetime
 from typing import TYPE_CHECKING, Any, Final
 
 import semver
@@ -27,6 +26,8 @@ from . import maven, models, utilities
 if TYPE_CHECKING:
     from collections.abc import Callable
 
+    from cyclonedx.model.bom import Bom, BomMetaData
+
 
 _KNOWN_TOOLS: Final[dict[str, models.tool.Tool]] = {
     # name in file: ( canonical name, friendly name, version callable )
@@ -59,22 +60,17 @@ def outdated_version_core(
     return None
 
 
-def plugin_outdated_version(bom_value: models.bom.Bom) -> 
list[models.tool.Outdated] | None:
-    if bom_value.metadata is None:
+def plugin_outdated_version(bom_value: Bom) -> list[models.tool.Outdated] | 
None:
+    if _metadata_is_empty(bom_value.metadata):
         return [models.tool.OutdatedMissingMetadata()]
-    timestamp = bom_value.metadata.timestamp
-    if timestamp is None:
-        # This quite often isn't available
-        # We could use the file mtime, but that's extremely heuristic
-        # return OutdatedMissingTimestamp()
-        timestamp = 
datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
+    timestamp = bom_value.metadata.timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
     tools: list[Any] = []
     tools_value = bom_value.metadata.tools
     if isinstance(tools_value, list):
         tools = tools_value
     elif tools_value:
-        tools = tools_value.components or []
-        services = tools_value.services or []
+        tools = list(tools_value.components)
+        services = list(tools_value.services)
         tools.extend(services)
     errors = []
     for tool in tools:
@@ -102,3 +98,9 @@ def version_parse(version_str: str) -> semver.VersionInfo | 
None:
         return semver.VersionInfo.parse(version_str.lstrip("v"))
     except ValueError:
         return None
+
+
+def _metadata_is_empty(metadata: BomMetaData) -> bool:
+    if metadata.component is None and metadata.supplier is None:
+        return True
+    return False
diff --git a/atr/sbom/utilities.py b/atr/sbom/utilities.py
index 5d4e94a7..96b0f4a8 100644
--- a/atr/sbom/utilities.py
+++ b/atr/sbom/utilities.py
@@ -17,10 +17,16 @@
 
 from __future__ import annotations
 
+import json
 import re
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Literal
 
 import cvss
+from cyclonedx.model import Property
+from cyclonedx.model.bom import Bom
+from cyclonedx.output import BaseOutput, make_outputter
+from cyclonedx.schema import OutputFormat, SchemaVersion
+from defusedxml import ElementTree
 
 if TYPE_CHECKING:
     import pathlib
@@ -111,14 +117,10 @@ def get_pointer(doc: yyjson.Document, path: str) -> Any | 
None:
         raise
 
 
-def get_props_from_bundle(bundle_value: models.bundle.Bundle) -> tuple[int, 
list[dict[str, str]]]:
-    version: int | None = get_pointer(bundle_value.doc, "/version")
-    if version is None:
-        version = 0
-    properties: list[dict[str, str]] | None = get_pointer(bundle_value.doc, 
"/properties")
-    if properties is None:
-        return version, []
-    return version, [p for p in properties if "asf:atr:" in p.get("name", "")]
+def get_props_from_bundle(bundle_value: models.bundle.Bundle) -> tuple[int, 
list[Property]]:
+    version: int = bundle_value.bom.version
+    properties = bundle_value.bom.properties
+    return version, [p for p in properties if "asf:atr:" in p.name]
 
 
 def osv_severity_to_cdx(severity: list[dict[str, Any]] | None, textual: str) 
-> list[dict[str, str | float]] | None:
@@ -138,10 +140,35 @@ def patch_to_data(patch_ops: models.patch.Patch) -> 
list[dict[str, Any]]:
     return [op.model_dump(by_alias=True, exclude_none=True) for op in 
patch_ops]
 
 
-def path_to_bundle(path: pathlib.Path) -> models.bundle.Bundle:
+def path_to_bundle(path: pathlib.Path) -> models.bundle.Bundle | None:
     text = path.read_text(encoding="utf-8")
-    bom = models.bom.Bom.model_validate_json(text)
-    return models.bundle.Bundle(doc=yyjson.Document(text), bom=bom, path=path, 
text=text)
+    bom: Bom | None = None
+    source_type: Literal["json", "xml"] | None = None
+    # Default to latest schema version
+    version_str: str | None = None
+    spec_version: SchemaVersion | None = None
+    if path.name.endswith(".json"):
+        bom_json: dict[str, Any] = json.loads(text)
+        bom = Bom.from_json(data=bom_json)
+        source_type = "json"
+        version_str = bom_json.get('specVersion', '1.7')
+        spec_version = SchemaVersion.from_version(version_str)
+    elif path.name.endswith(".xml"):
+        bom_xml = ElementTree.fromstring(text)
+        bom = Bom.from_xml(bom_xml)
+        tag = re.match(r"\{http://cyclonedx.org/schema/bom/(.+)}", bom_xml.tag)
+        spec_version = SchemaVersion.from_version(tag.group(1) or '1.7')
+        source_type = "xml"
+        if bom:
+            outputter: BaseOutput = make_outputter(
+                bom=bom, output_format=OutputFormat.JSON, 
schema_version=spec_version
+            )
+            text: str = outputter.output_as_string()
+    if not bom or not source_type or not spec_version:
+        raise ValueError("Error importing BOM")
+    return models.bundle.Bundle(
+        doc=yyjson.Document(text), bom=bom, path=path, text=text, 
source_type=source_type, spec_version=spec_version
+    )
 
 
 def _extract_cdx_score(type: str, score_str: str) -> dict[str, str | float]:
diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py
index c96e6a6e..273a4af6 100644
--- a/atr/tasks/sbom.py
+++ b/atr/tasks/sbom.py
@@ -83,12 +83,10 @@ class ScoreArgs(FileArgs):
 
 @checks.with_model(FileArgs)
 async def augment(args: FileArgs) -> results.Results | None:
-    project_str = str(args.project_key)
-    version_str = str(args.version_key)
     revision_str = str(args.revision_number)
     path_str = str(args.file_path)
 
-    base_dir = paths.get_unfinished_dir() / project_str / version_str / 
revision_str
+    base_dir = paths.get_unfinished_dir_for(args.project_key, 
args.version_key, args.revision_number)
     if not await aiofiles.os.path.isdir(base_dir):
         raise SBOMScoringError("Revision directory does not exist", 
{"base_dir": str(base_dir)})
     full_path = base_dir / path_str
@@ -97,6 +95,8 @@ async def augment(args: FileArgs) -> results.Results | None:
         raise SBOMScoringError("SBOM file does not exist", {"file_path": 
path_str})
     # Read from the old revision
     bundle = sbom.utilities.path_to_bundle(full_path)
+    if not bundle:
+        raise SBOMScoringError("Could not load bundle")
     patch_ops = await sbom.utilities.bundle_to_ntia_patch(bundle)
     new_full_path: pathlib.Path | None = None
     new_full_path_str: str | None = None
@@ -159,6 +159,8 @@ async def osv_scan(args: FileArgs) -> results.Results | 
None:
     if not (full_path_str.endswith(".cdx.json") and await 
aiofiles.os.path.isfile(full_path)):
         raise SBOMScanningError("SBOM file does not exist", {"file_path": 
path_str})
     bundle = sbom.utilities.path_to_bundle(full_path)
+    if not bundle:
+        raise SBOMScanningError("Could not load bundle")
     vulnerabilities, ignored = await sbom.osv.scan_bundle(bundle)
     patch_ops = await sbom.utilities.bundle_to_vuln_patch(bundle, 
vulnerabilities)
     components = []
@@ -259,6 +261,8 @@ async def score_tool(args: ScoreArgs) -> results.Results | 
None:
     if not (full_path_str.endswith(".cdx.json") and await 
aiofiles.os.path.isfile(full_path)):
         raise SBOMScoringError("SBOM file does not exist", {"file_path": 
path_str})
     bundle = sbom.utilities.path_to_bundle(full_path)
+    if not bundle:
+        raise SBOMScoringError("Could not load bundle")
     version, properties = sbom.utilities.get_props_from_bundle(bundle)
     warnings, errors = sbom.conformance.ntia_2021_issues(bundle.bom)
     # TODO: Could update the ATR version with a constant showing last change 
to the augment/scan
@@ -302,7 +306,7 @@ async def score_tool(args: ScoreArgs) -> results.Results | 
None:
         vulnerabilities=[v.model_dump_json() for v in vulnerabilities],
         prev_licenses=[w.model_dump_json() for w in prev_licenses] if 
prev_licenses else None,
         prev_vulnerabilities=[v.model_dump_json() for v in 
prev_vulnerabilities] if prev_vulnerabilities else None,
-        atr_props=properties,
+        atr_props=[{p.name: p.value or ""} for p in properties],
         cli_errors=cli_errors,
     )
 
diff --git a/typestubs/py_serializable/__init__.pyi 
b/typestubs/py_serializable/__init__.pyi
new file mode 100644
index 00000000..da458fc6
--- /dev/null
+++ b/typestubs/py_serializable/__init__.pyi
@@ -0,0 +1,94 @@
+from collections.abc import Callable
+from enum import Enum
+from io import TextIOBase
+from typing import Any, Iterable, Self, TypeVar, overload
+from xml.etree.ElementTree import Element
+
+_T = TypeVar("_T")
+_E = TypeVar("_E")
+_F = TypeVar("_F", bound=Callable[..., Any])
+
+
+class ViewType: ...
+
+
+class SerializationType(str, Enum):
+    JSON = "JSON"
+    XML = "XML"
+
+
+class XmlArraySerializationType(Enum): ...
+
+
+class XmlStringSerializationType(Enum): ...
+
+
+class _JsonSerializable:
+    @classmethod
+    def from_json(cls, data: dict[str, Any]) -> Self | None: ...
+    def as_json(self, view_: type[ViewType] | None = ...) -> str: ...
+
+
+class _XmlSerializable:
+    @classmethod
+    def from_xml(
+        cls,
+        data: TextIOBase | Element,
+        default_namespace: str | None = ...,
+    ) -> Self | None: ...
+    def as_xml(
+        self,
+        view_: type[ViewType] | None = ...,
+        as_string: bool = ...,
+        element_name: str | None = ...,
+        xmlns: str | None = ...,
+    ) -> Element | str: ...
+
+
+class ObjectMetadataLibrary: ...
+
+
+# Typed as a no-op to avoid the Union[Type[_T], Type[_JsonSerializable], 
Type[_XmlSerializable]]
+# that py_serializable produces using Union as a stand-in for the unsupported 
Intersection type.
+# The serialization methods are added to individual classes via their own 
stubs.
+@overload
+def serializable_class(
+    cls: None = ...,
+    *,
+    name: str | None = ...,
+    serialization_types: Iterable[SerializationType] | None = ...,
+    ignore_during_deserialization: Iterable[str] | None = ...,
+    ignore_unknown_during_deserialization: bool = ...,
+) -> Callable[[type[_T]], type[_T]]: ...
+@overload
+def serializable_class(
+    cls: type[_T],
+    *,
+    name: str | None = ...,
+    serialization_types: Iterable[SerializationType] | None = ...,
+    ignore_during_deserialization: Iterable[str] | None = ...,
+    ignore_unknown_during_deserialization: bool = ...,
+) -> type[_T]: ...
+
+
+@overload
+def serializable_enum(cls: None = ...) -> Callable[[type[_E]], type[_E]]: ...
+@overload
+def serializable_enum(cls: type[_E]) -> type[_E]: ...
+
+
+def type_mapping(type_: type) -> Callable[[_F], _F]: ...
+def include_none(
+    view_: type[ViewType] | None = ..., none_value: Any | None = ...
+) -> Callable[[_F], _F]: ...
+def json_name(name: str) -> Callable[[_F], _F]: ...
+def string_format(format_: str) -> Callable[[_F], _F]: ...
+def view(view_: type[ViewType]) -> Callable[[_F], _F]: ...
+def xml_attribute() -> Callable[[_F], _F]: ...
+def xml_array(
+    array_type: XmlArraySerializationType, child_name: str
+) -> Callable[[_F], _F]: ...
+def xml_string(string_type: XmlStringSerializationType) -> Callable[[_F], _F]: 
...
+def xml_name(name: str) -> Callable[[_F], _F]: ...
+def xml_sequence(sequence: int) -> Callable[[_F], _F]: ...
+def allow_none_value_for_dict(key: str) -> Callable[[_F], _F]: ...


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to