This is an automated email from the ASF dual-hosted git repository. arm pushed a commit to branch sbom_version_changes in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit f2472bf62474cc64d2f137dd83d3942932cd51fc Author: Alastair McFarlane <[email protected]> AuthorDate: Fri Dec 19 14:23:30 2025 +0000 Update outdated tool scanners and add ATR tool metadata to the SBOM --- atr/get/sbom.py | 60 ++++++++++++------- atr/models/results.py | 11 +++- atr/sbom/__init__.py | 3 +- atr/sbom/cli.py | 2 +- atr/sbom/maven.py | 59 +------------------ atr/sbom/models/__init__.py | 14 +---- atr/sbom/models/bom.py | 10 ++++ atr/sbom/models/{maven.py => tool.py} | 0 atr/sbom/osv.py | 1 - atr/sbom/tool.py | 108 ++++++++++++++++++++++++++++++++++ atr/sbom/utilities.py | 97 ++++++++++++++++++++++++------ atr/tasks/sbom.py | 51 ++++++++-------- 12 files changed, 278 insertions(+), 138 deletions(-) diff --git a/atr/get/sbom.py b/atr/get/sbom.py index 4a588e1..ec9c49c 100644 --- a/atr/get/sbom.py +++ b/atr/get/sbom.py @@ -94,8 +94,6 @@ async def report(session: web.Committer, project: str, version: str, file_path: _conformance_section(block, task_result) - block.h2["Vulnerabilities"] - if task_result.vulnerabilities is not None: vulnerabilities = [ sbom.models.osv.CdxVulnAdapter.validate_python(json.loads(e)) for e in task_result.vulnerabilities @@ -107,31 +105,49 @@ async def report(session: web.Committer, project: str, version: str, file_path: block, project, version, file_path, task_result, vulnerabilities, osv_tasks, is_release_candidate ) - block.h2["Outdated tool"] - outdated = None - if task_result.outdated: - outdated = sbom.models.maven.OutdatedAdapter.validate_python(json.loads(task_result.outdated)) - if outdated: - if outdated.kind == "tool": - block.p[ - f"""The CycloneDX Maven Plugin is outdated. The used version is - {outdated.used_version} and the available version is - {outdated.available_version}.""" - ] - else: - block.p[ - f"""There was a problem with the SBOM detected when trying to - determine if the CycloneDX Maven Plugin is outdated: - {outdated.kind.upper()}.""" - ] - else: - block.p["No outdated tool found."] + _outdated_tool_section(block, task_result) _cyclonedx_cli_errors(block, task_result) return await template.blank("SBOM report", content=block.collect()) +def _outdated_tool_section(block: htm.Block, task_result: results.SBOMToolScore): + block.h2["Outdated tools"] + if task_result.outdated: + outdated = [] + if isinstance(task_result.outdated, str): + # Older version, only checked one tool + outdated = [sbom.models.tool.OutdatedAdapter.validate_python(json.loads(task_result.outdated))] + elif isinstance(task_result.outdated, list): + # Newer version, checked multiple tools + outdated = [sbom.models.tool.OutdatedAdapter.validate_python(json.loads(o)) for o in task_result.outdated] + if len(outdated) == 0: + block.p["No outdated tools found."] + for result in outdated: + if result.kind == "tool": + if "Apache Trusted Releases" in result.name: + block.p[ + f"""The last version of ATR used on this SBOM was + {result.used_version} but ATR is currently version + {result.available_version}.""" + ] + else: + block.p[ + f"""The {result.name} is outdated. The used version is + {result.used_version} and the available version is + {result.available_version}.""" + ] + else: + block.p[ + f"""There was a problem with the SBOM detected when trying to + determine if the {result.name} is outdated: + {result.kind.upper()}.""" + ] + else: + block.p["No outdated tools found."] + + def _conformance_section(block: htm.Block, task_result: results.SBOMToolScore) -> None: warnings = [sbom.models.conformance.MissingAdapter.validate_python(json.loads(w)) for w in task_result.warnings] errors = [sbom.models.conformance.MissingAdapter.validate_python(json.loads(e)) for e in task_result.errors] @@ -432,6 +448,8 @@ def _vulnerability_scan_section( in_progress_task = _vulnerability_scan_find_in_progress_task(osv_tasks, task_result.revision_number) + block.h2["Vulnerabilities"] + scans = [] if task_result.atr_props is not None: scans = [t.get("value", "") for t in task_result.atr_props if t.get("name", "") == "asf:atr:osv-scan"] diff --git a/atr/models/results.py b/atr/models/results.py index 166a4ed..123324f 100644 --- a/atr/models/results.py +++ b/atr/models/results.py @@ -58,6 +58,9 @@ class SBOMOSVScan(schema.Strict): project_name: str = schema.description("Project name") version_name: str = schema.description("Version name") revision_number: str = schema.description("Revision number") + bom_version: int | None = schema.Field( + default=None, strict=False, description="BOM Version produced with scan results" + ) file_path: str = schema.description("Relative path to the scanned SBOM file") new_file_path: str = schema.Field(default="", strict=False, description="Relative path to the updated SBOM file") components: list[OSVComponent] = schema.description("Components with vulnerabilities") @@ -103,6 +106,11 @@ class SbomQsReport(schema.Strict): class SBOMAugment(schema.Strict): kind: Literal["sbom_augment"] = schema.Field(alias="kind") path: str = schema.description("The path to the augmented SBOM file") + bom_version: int | None = schema.Field( + default=None, + strict=False, + description="BOM Version produced by the augment task, if any augmentations were applied", + ) class SBOMQsScore(schema.Strict): @@ -119,10 +127,11 @@ class SBOMToolScore(schema.Strict): project_name: str = schema.description("Project name") version_name: str = schema.description("Version name") revision_number: str = schema.description("Revision number") + bom_version: int | None = schema.Field(default=None, strict=False, description="BOM Version scanned") file_path: str = schema.description("Relative path to the scored SBOM file") warnings: list[str] = schema.description("Warnings from the SBOM tool") errors: list[str] = schema.description("Errors from the SBOM tool") - outdated: str | None = schema.description("Outdated tool from the SBOM tool") + outdated: list[str] | str | None = schema.description("Outdated tool(s) from the SBOM tool") vulnerabilities: list[str] | None = schema.Field( default=None, strict=False, description="Vulnerabilities found in the SBOM" ) diff --git a/atr/sbom/__init__.py b/atr/sbom/__init__.py index b560995..595ce27 100644 --- a/atr/sbom/__init__.py +++ b/atr/sbom/__init__.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from . import cli, conformance, constants, cyclonedx, licenses, maven, models, osv, sbomqs, spdx, utilities +from . import cli, conformance, constants, cyclonedx, licenses, maven, models, osv, sbomqs, spdx, tool, utilities __all__ = [ "cli", @@ -28,5 +28,6 @@ __all__ = [ "osv", "sbomqs", "spdx", + "tool", "utilities", ] diff --git a/atr/sbom/cli.py b/atr/sbom/cli.py index edc93fd..74647a4 100644 --- a/atr/sbom/cli.py +++ b/atr/sbom/cli.py @@ -25,8 +25,8 @@ from . import models, osv from .conformance import ntia_2021_issues from .cyclonedx import validate_cli, validate_py from .licenses import check -from .maven import plugin_outdated_version from .sbomqs import total_score +from .tool import plugin_outdated_version from .utilities import bundle_to_ntia_patch, bundle_to_vuln_patch, patch_to_data, path_to_bundle diff --git a/atr/sbom/maven.py b/atr/sbom/maven.py index a443414..5ff9bc5 100644 --- a/atr/sbom/maven.py +++ b/atr/sbom/maven.py @@ -17,14 +17,13 @@ from __future__ import annotations -import datetime import pathlib import tempfile from typing import Any, Final import yyjson -from . import constants, models +from . import constants _CACHE_PATH: Final[pathlib.Path] = pathlib.Path(tempfile.gettempdir()) / "sbomtool-cache.json" @@ -49,57 +48,6 @@ def cache_write(cache: dict[str, Any]) -> None: pass -def plugin_outdated_version(bom_value: models.bom.Bom) -> models.maven.Outdated | None: - if bom_value.metadata is None: - return models.maven.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") - tools = [] - tools_value = bom_value.metadata.tools - if isinstance(tools_value, list): - tools = tools_value - elif tools_value: - tools = tools_value.components or [] - for tool in tools: - names_or_descriptions = { - "cyclonedx maven plugin", - "cyclonedx-maven-plugin", - } - name_or_description = (tool.name or tool.description or "").lower() - if name_or_description not in names_or_descriptions: - continue - if tool.version is None: - return models.maven.OutdatedMissingVersion(name=name_or_description) - available_version = plugin_outdated_version_core(timestamp, tool.version) - if available_version is not None: - return models.maven.OutdatedTool( - name=name_or_description, - used_version=tool.version, - available_version=available_version, - ) - return None - - -def plugin_outdated_version_core(isotime: str, version: str) -> str | None: - expected_version = version_as_of(isotime) - if expected_version is None: - return None - if version == expected_version: - return None - expected_version_comparable = version_parse(expected_version) - version_comparable = version_parse(version) - # If the version used is less than the version available - if version_comparable < expected_version_comparable: - # Then note the version available - return expected_version - # Otherwise, the user is using the latest version - return None - - def version_as_of(isotime: str) -> str | None: # Given these mappings: # { @@ -115,8 +63,3 @@ def version_as_of(isotime: str) -> str | None: if isotime >= date: return version return None - - -def version_parse(version: str) -> tuple[int, int, int]: - parts = version.split(".") - return int(parts[0]), int(parts[1]), int(parts[2]) diff --git a/atr/sbom/models/__init__.py b/atr/sbom/models/__init__.py index fd44fb8..15cf734 100644 --- a/atr/sbom/models/__init__.py +++ b/atr/sbom/models/__init__.py @@ -17,16 +17,6 @@ from __future__ import annotations -from . import base, bom, bundle, conformance, licenses, maven, osv, patch, sbomqs +from . import base, bom, bundle, conformance, licenses, osv, patch, sbomqs, tool -__all__ = [ - "base", - "bom", - "bundle", - "conformance", - "licenses", - "maven", - "osv", - "patch", - "sbomqs", -] +__all__ = ["base", "bom", "bundle", "conformance", "licenses", "osv", "patch", "sbomqs", "tool"] diff --git a/atr/sbom/models/bom.py b/atr/sbom/models/bom.py index 3bc65f9..e02dff2 100644 --- a/atr/sbom/models/bom.py +++ b/atr/sbom/models/bom.py @@ -59,6 +59,15 @@ 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): @@ -69,6 +78,7 @@ class Tool(Lax): class Tools(Lax): components: list[ToolComponent] | None = None + services: list[ServiceComponent] | None = None class Metadata(Lax): diff --git a/atr/sbom/models/maven.py b/atr/sbom/models/tool.py similarity index 100% rename from atr/sbom/models/maven.py rename to atr/sbom/models/tool.py diff --git a/atr/sbom/osv.py b/atr/sbom/osv.py index 7008ef9..01609ba 100644 --- a/atr/sbom/osv.py +++ b/atr/sbom/osv.py @@ -108,7 +108,6 @@ def vulns_from_bundle(bundle: models.bundle.Bundle) -> list[models.osv.CdxVulner async def vuln_patch( - session: aiohttp.ClientSession, doc: yyjson.Document, components: list[models.osv.ComponentVulnerabilities], ) -> models.patch.Patch: diff --git a/atr/sbom/tool.py b/atr/sbom/tool.py new file mode 100644 index 0000000..739dafa --- /dev/null +++ b/atr/sbom/tool.py @@ -0,0 +1,108 @@ +# 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 datetime +from typing import TYPE_CHECKING, Any, Final + +from . import maven, models + +if TYPE_CHECKING: + from collections.abc import Callable + +try: + from atr import metadata + + atr_version = metadata.version +except ImportError: + metadata = None + atr_version = "cli" + +_KNOWN_TOOLS: Final[dict[str, tuple[str, str, Callable[[str], str | None]]]] = { + # name in file: ( canonical name, friendly name, version callable ) + "cyclonedx-maven-plugin": ("cyclonedx-maven-plugin", "CycloneDX Maven Plugin", maven.version_as_of), + "cyclonedx maven plugin": ("cyclonedx-maven-plugin", "CycloneDX Maven Plugin", maven.version_as_of), + "apache trusted releases": ("apache trusted releases", "Apache Trusted Releases platform", lambda _: atr_version), +} + + +def plugin_outdated_version(bom_value: models.bom.Bom) -> list[models.tool.Outdated] | None: + if bom_value.metadata is None: + 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") + 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.extend(services) + errors = [] + for tool in tools: + name_or_description = (tool.name or tool.description or "").lower() + if name_or_description not in _KNOWN_TOOLS: + continue + if tool.version is None: + errors.append(models.tool.OutdatedMissingVersion(name=name_or_description)) + _, name, version_func = _KNOWN_TOOLS[name_or_description] + available_version = outdated_version_core(timestamp, tool.version, version_func) + if available_version is not None: + errors.append( + models.tool.OutdatedTool( + name=name, + used_version=tool.version, + available_version=available_version, + ) + ) + return errors + + +def outdated_version_core(isotime: str, version: str, version_as_of: Callable[[str], str | None]) -> str | None: + expected_version = version_as_of(isotime) + if expected_version is None: + return None + if version == expected_version: + return None + expected_version_comparable = version_parse(expected_version) + version_comparable = version_parse(version) + if expected_version_comparable is None or version_comparable is None: + # Couldn't parse the version + return None + # If the version used is less than the version available + if version_comparable[0] < expected_version_comparable[0]: + # Then note the version available + return expected_version + # Otherwise, the user is using the latest version + return None + + +def version_parse(version_str: str) -> tuple[tuple[int, int, int], str] | None: + parts = version_str.lstrip("v").split(".") + if len(parts) == 2: + return (int(parts[0]), int(parts[1]), 0), "" + if len(parts) == 3: + return (int(parts[0]), int(parts[1]), int(parts[2])), "" + if len(parts) == 4: + return (int(parts[0]), int(parts[1]), int(parts[2])), parts[3] + return None diff --git a/atr/sbom/utilities.py b/atr/sbom/utilities.py index 36a2cf5..719938d 100644 --- a/atr/sbom/utilities.py +++ b/atr/sbom/utilities.py @@ -30,11 +30,39 @@ if TYPE_CHECKING: import aiohttp import yyjson -from . import models +try: + from atr import metadata + + version = metadata.version +except ImportError: + metadata = None + version = "cli" +from . import constants, models _SCORING_METHODS_OSV = {"CVSS_V2": "CVSSv2", "CVSS_V3": "CVSSv3", "CVSS_V4": "CVSSv4"} _SCORING_METHODS_CDX = {"CVSSv2": "CVSS_V2", "CVSSv3": "CVSS_V3", "CVSSv4": "CVSS_V4", "other": "Other"} _CDX_SEVERITIES = ["critical", "high", "medium", "low", "info", "none", "unknown"] +_TOOL_METADATA = { + "bom-ref": "tool:asf:atr", + "provider": { + "name": constants.conformance.THE_APACHE_SOFTWARE_FOUNDATION, + "url": ["https://apache.org/"], + }, + "name": "Apache Trusted Releases", + "authenticated": True, + "version": version, +} + + +def apply_patch( + reason: str, revision: str, bundle: models.bundle.Bundle, patch_ops: models.patch.Patch +) -> tuple[int, yyjson.Document]: + """Take a list of patch operations and apply them to the bundle. Returns the patched document, with the + task recorded in `properties` and the SBOM version number incremented as per the CDX spec.""" + _record_task(reason, revision, bundle.doc, patch_ops) + new_version = _increment_version(bundle.doc, patch_ops) + patch_data = patch_to_data(patch_ops) + return new_version, bundle.doc.patch(yyjson.Document(patch_data)) async def bundle_to_ntia_patch(bundle_value: models.bundle.Bundle) -> models.patch.Patch: @@ -52,8 +80,7 @@ async def bundle_to_vuln_patch( from .osv import vuln_patch # TODO: May not need session (copied from ntia patch) - async with aiohttp.ClientSession() as session: - patch_ops = await vuln_patch(session, bundle_value.doc, vulnerabilities) + patch_ops = await vuln_patch(bundle_value.doc, vulnerabilities) return patch_ops @@ -67,11 +94,14 @@ def get_pointer(doc: yyjson.Document, path: str) -> Any | None: raise -def get_atr_props_from_bundle(bundle_value: models.bundle.Bundle) -> list[dict[str, str]]: +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 [] - return [p for p in properties if "asf:atr:" in p.get("name", "")] + return version, [] + return version, [p for p in properties if "asf:atr:" in p.get("name", "")] def patch_to_data(patch_ops: models.patch.Patch) -> list[dict[str, Any]]: @@ -84,17 +114,6 @@ def path_to_bundle(path: pathlib.Path) -> models.bundle.Bundle: return models.bundle.Bundle(doc=yyjson.Document(text), bom=bom, path=path, text=text) -def record_task(task: str, revision: str, doc: yyjson.Document, patch_ops: models.patch.Patch) -> models.patch.Patch: - properties: list[dict[str, str]] | None = get_pointer(doc, "/properties") - operation = {"name": f"asf:atr:{task}", "value": revision} - if properties is None: - patch_ops.append(models.patch.AddOp(op="add", path="/properties", value=[operation])) - else: - properties.append(operation) - patch_ops.append(models.patch.ReplaceOp(op="replace", path="/properties", value=properties)) - return patch_ops - - def osv_severity_to_cdx(severity: list[dict[str, Any]] | None, textual: str) -> list[dict[str, str | float]] | None: if severity is not None: return [ @@ -155,6 +174,18 @@ def _extract_cdx_score(type: str, score_str: str) -> dict[str, str | float]: return {"severity": score_str} +def _increment_version(doc: yyjson.Document, patch_ops: models.patch.Patch) -> int: + version: int | None = get_pointer(doc, "/version") + if version is not None: + version = version + 1 + patch_ops.append(models.patch.ReplaceOp(op="replace", path="/version", value=version)) + else: + # This shouldn't happen, but we can handle it just in case + version = 1 + patch_ops.append(models.patch.AddOp(op="add", path="/version", value=version)) + return version + + def _map_severity(severity: str) -> str: sev = severity.lower() if sev in _CDX_SEVERITIES: @@ -164,3 +195,35 @@ def _map_severity(severity: str) -> str: if sev == "moderate": return "medium" return "unknown" + + +def _record_task(task: str, revision: str, doc: yyjson.Document, patch_ops: models.patch.Patch) -> models.patch.Patch: + properties: list[dict[str, str]] | None = get_pointer(doc, "/properties") + tools: dict[str, str] | None = get_pointer(doc, "/metadata/tools") + services: list[dict[str, str]] | None = get_pointer(doc, "/metadata/tools/services") + operation = {"name": f"asf:atr:{task}", "value": revision} + if properties is None: + patch_ops.append(models.patch.AddOp(op="add", path="/properties", value=[operation])) + else: + properties.append(operation) + patch_ops.append(models.patch.ReplaceOp(op="replace", path="/properties", value=properties)) + if tools is None: + tools = {} + patch_ops.append(models.patch.AddOp(op="add", path="/metadata/tools", value=tools)) + if services is None: + services = [] + patch_ops.append(models.patch.AddOp(op="add", path="/metadata/tools/services", value=services)) + try: + tool_index = [s.get("bom-ref", "") for s in services].index("tool:asf:atr") + except ValueError: + tool_index = -1 + if tool_index == -1: + patch_ops.append( + models.patch.AddOp(op="add", path=f"/metadata/tools/services/{len(services)}", value=_TOOL_METADATA) + ) + else: + # If we ever add more metadata to this, such as task-specific properties under the tool, this changes + patch_ops.append( + models.patch.ReplaceOp(op="replace", path=f"/metadata/tools/services/{tool_index}", value=_TOOL_METADATA) + ) + return patch_ops diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py index d07dd0d..506939b 100644 --- a/atr/tasks/sbom.py +++ b/atr/tasks/sbom.py @@ -23,7 +23,6 @@ from typing import Any, Final import aiofiles import aiofiles.os -import yyjson import atr.archives as archives import atr.config as config @@ -87,10 +86,9 @@ async def augment(args: FileArgs) -> results.Results | None: bundle = sbom.utilities.path_to_bundle(pathlib.Path(full_path)) patch_ops = await sbom.utilities.bundle_to_ntia_patch(bundle) new_full_path: str | None = None + new_version = None if patch_ops: - sbom.utilities.record_task("augment", args.revision_number, bundle.doc, patch_ops) - patch_data = sbom.utilities.patch_to_data(patch_ops) - merged = bundle.doc.patch(yyjson.Document(patch_data)) + new_version, merged = sbom.utilities.apply_patch("augment", args.revision_number, bundle, patch_ops) description = "SBOM augmentation through web interface" async with storage.write(args.asf_uid) as write: wacp = await write.as_project_committee_participant(args.project_name) @@ -110,6 +108,7 @@ async def augment(args: FileArgs) -> results.Results | None: return results.SBOMAugment( kind="sbom_augment", path=(new_full_path if new_full_path is not None else full_path), + bom_version=new_version, ) @@ -145,31 +144,29 @@ async def osv_scan(args: FileArgs) -> results.Results | None: components = [results.OSVComponent(purl=v.ref, vulnerabilities=v.vulnerabilities) for v in vulnerabilities] new_full_path: str | None = None - if patch_ops: - sbom.utilities.record_task("osv-scan", args.revision_number, bundle.doc, patch_ops) - patch_data = sbom.utilities.patch_to_data(patch_ops) - merged = bundle.doc.patch(yyjson.Document(patch_data)) - description = "SBOM vulnerability scan through web interface" - async with storage.write(args.asf_uid) as write: - wacp = await write.as_project_committee_participant(args.project_name) - async with wacp.revision.create_and_manage( - args.project_name, args.version_name, args.asf_uid or "unknown", description=description - ) as creating: - new_full_path = os.path.join(str(creating.interim_path), args.file_path) - # Write to the new revision - log.info(f"Writing updated SBOM to {new_full_path}") - await aiofiles.os.remove(new_full_path) - async with aiofiles.open(new_full_path, "w", encoding="utf-8") as f: - await f.write(merged.dumps()) - - if creating.new is None: - raise RuntimeError("Internal error: New revision not found") + new_version, merged = sbom.utilities.apply_patch("osv-scan", args.revision_number, bundle, patch_ops) + description = "SBOM vulnerability scan through web interface" + async with storage.write(args.asf_uid) as write: + wacp = await write.as_project_committee_participant(args.project_name) + async with wacp.revision.create_and_manage( + args.project_name, args.version_name, args.asf_uid or "unknown", description=description + ) as creating: + new_full_path = os.path.join(str(creating.interim_path), args.file_path) + # Write to the new revision + log.info(f"Writing updated SBOM to {new_full_path}") + await aiofiles.os.remove(new_full_path) + async with aiofiles.open(new_full_path, "w", encoding="utf-8") as f: + await f.write(merged.dumps()) + + if creating.new is None: + raise RuntimeError("Internal error: New revision not found") return results.SBOMOSVScan( kind="sbom_osv_scan", project_name=args.project_name, version_name=args.version_name, revision_number=args.revision_number, + bom_version=new_version, file_path=full_path, new_file_path=new_full_path or full_path, components=components, @@ -221,9 +218,10 @@ async def score_tool(args: FileArgs) -> results.Results | None: if not (full_path.endswith(".cdx.json") and os.path.isfile(full_path)): raise SBOMScoringError("SBOM file does not exist", {"file_path": args.file_path}) bundle = sbom.utilities.path_to_bundle(pathlib.Path(full_path)) - properties = sbom.utilities.get_atr_props_from_bundle(bundle) + version, properties = sbom.utilities.get_props_from_bundle(bundle) warnings, errors = sbom.conformance.ntia_2021_issues(bundle.bom) - outdated = sbom.maven.plugin_outdated_version(bundle.bom) + # TODO: Could update the ATR version with a constant showing last change to the augment/scan tools + outdated = sbom.tool.plugin_outdated_version(bundle.bom) vulnerabilities = sbom.osv.vulns_from_bundle(bundle) cli_errors = sbom.cyclonedx.validate_cli(bundle) return results.SBOMToolScore( @@ -231,10 +229,11 @@ async def score_tool(args: FileArgs) -> results.Results | None: project_name=args.project_name, version_name=args.version_name, revision_number=args.revision_number, + bom_version=version, file_path=args.file_path, warnings=[w.model_dump_json() for w in warnings], errors=[e.model_dump_json() for e in errors], - outdated=outdated.model_dump_json() if outdated else None, + outdated=[o.model_dump_json() for o in outdated] if outdated else None, vulnerabilities=[v.model_dump_json() for v in vulnerabilities], atr_props=properties, cli_errors=cli_errors, --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
