This is an automated email from the ASF dual-hosted git repository.
arm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/main by this push:
new b36e820 Collate licenses as part of SBOM scoring and add to report
b36e820 is described below
commit b36e820784850205c62dba4846d58e7b2e7591da
Author: Alastair McFarlane <[email protected]>
AuthorDate: Mon Dec 22 12:26:12 2025 +0000
Collate licenses as part of SBOM scoring and add to report
---
atr/get/sbom.py | 57 +++++++++++++++++++++++++++++++++++++++++++++
atr/models/results.py | 6 +++++
atr/sbom/licenses.py | 3 +++
atr/sbom/models/licenses.py | 17 ++++++++++++++
atr/tasks/sbom.py | 8 +++++--
5 files changed, 89 insertions(+), 2 deletions(-)
diff --git a/atr/get/sbom.py b/atr/get/sbom.py
index 8f73db3..f2ea02c 100644
--- a/atr/get/sbom.py
+++ b/atr/get/sbom.py
@@ -113,6 +113,7 @@ async def report(session: web.Committer, project: str,
version: str, file_path:
_augment_section(block, release, task_result, latest_augment,
last_augmented_bom)
_conformance_section(block, task_result)
+ _license_section(block, task_result)
if task_result.vulnerabilities is not None:
vulnerabilities = [
@@ -192,6 +193,26 @@ def _conformance_section(block: htm.Block, task_result:
results.SBOMToolScore) -
block.p["No NTIA 2021 minimum data field conformance warnings or
errors found."]
+def _license_section(block: htm.Block, task_result: results.SBOMToolScore) ->
None:
+ block.h2["Licenses"]
+ warnings = []
+ errors = []
+ if task_result.license_warnings is not None:
+ warnings = [sbom.models.licenses.Issue.model_validate(json.loads(w))
for w in task_result.license_warnings]
+ if task_result.license_errors is not None:
+ errors = [sbom.models.licenses.Issue.model_validate(json.loads(e)) for
e in task_result.license_errors]
+ if warnings:
+ block.h3["Warnings"]
+ _license_table(block, warnings)
+
+ if errors:
+ block.h3["Errors"]
+ _license_table(block, errors)
+
+ if not (warnings or errors):
+ block.p["No license warnings or errors found."]
+
+
def _report_header(
block: htm.Block, is_release_candidate: bool, release: sql.Release,
task_result: results.SBOMToolScore
) -> None:
@@ -293,6 +314,24 @@ def _extract_vulnerability_severity(vuln:
osv.VulnerabilityDetails) -> str:
return "Unknown"
+def _license_table(block: htm.Block, items: list[sbom.models.licenses.Issue])
-> None:
+ warning_rows = [
+ htm.tr[
+ htm.td[
+ f"Category {category!s}"
+ if (len(components) == 0)
+ else htm.details[htm.summary[f"Category {category!s}"],
htm.div[_detail_table(components)]]
+ ],
+ htm.td[str(count)],
+ ]
+ for category, count, components in _license_tally(items)
+ ]
+ block.table(".table.table-sm.table-bordered.table-striped")[
+ htm.thead[htm.tr[htm.th["License Category"], htm.th["Count"]]],
+ htm.tbody[*warning_rows],
+ ]
+
+
def _missing_table(block: htm.Block, items:
list[sbom.models.conformance.Missing]) -> None:
warning_rows = [
htm.tr[
@@ -334,6 +373,24 @@ def _missing_tally(items:
list[sbom.models.conformance.Missing]) -> list[tuple[s
)
+def _license_tally(
+ items: list[sbom.models.licenses.Issue],
+) -> list[tuple[sbom.models.licenses.Category, int, list[str | None]]]:
+ counts: dict[sbom.models.licenses.Category, int] = {}
+ components: dict[sbom.models.licenses.Category, list[str | None]] = {}
+ for item in items:
+ key = item.category
+ counts[key] = counts.get(key, 0) + 1
+ if key not in components:
+ components[key] = [str(item)]
+ else:
+ components[key].append(str(item))
+ return sorted(
+ [(category, count, components.get(category, [])) for category, count
in counts.items()],
+ key=lambda kv: kv[0].value,
+ )
+
+
def _vulnerability_component_details_osv(block: htm.Block, component:
results.OSVComponent) -> None:
details_content = []
summary_element = htm.summary[
diff --git a/atr/models/results.py b/atr/models/results.py
index 123324f..b8e8c13 100644
--- a/atr/models/results.py
+++ b/atr/models/results.py
@@ -132,6 +132,12 @@ class SBOMToolScore(schema.Strict):
warnings: list[str] = schema.description("Warnings from the SBOM tool")
errors: list[str] = schema.description("Errors from the SBOM tool")
outdated: list[str] | str | None = schema.description("Outdated tool(s)
from the SBOM tool")
+ license_warnings: list[str] | None = schema.Field(
+ default=[], strict=False, description="License warnings found in the
SBOM"
+ )
+ license_errors: list[str] | None = schema.Field(
+ default=[], strict=False, description="License errors found in the
SBOM"
+ )
vulnerabilities: list[str] | None = schema.Field(
default=None, strict=False, description="Vulnerabilities found in the
SBOM"
)
diff --git a/atr/sbom/licenses.py b/atr/sbom/licenses.py
index 0d1d7ae..bfed06a 100644
--- a/atr/sbom/licenses.py
+++ b/atr/sbom/licenses.py
@@ -35,6 +35,7 @@ def check(
name = component.name or "unknown"
version = component.version
scope = component.scope
+ type = component.type
if not component.licenses:
continue
@@ -83,6 +84,7 @@ def check(
category=models.licenses.Category.X,
any_unknown=any_unknown,
scope=scope,
+ component_type=type,
)
)
elif got_warning:
@@ -94,6 +96,7 @@ def check(
category=models.licenses.Category.B,
any_unknown=False,
scope=scope,
+ component_type=type,
)
)
diff --git a/atr/sbom/models/licenses.py b/atr/sbom/models/licenses.py
index 3ddb0bc..d105006 100644
--- a/atr/sbom/models/licenses.py
+++ b/atr/sbom/models/licenses.py
@@ -18,6 +18,9 @@
from __future__ import annotations
import enum
+from typing import Any
+
+import pydantic
from .base import Strict
@@ -27,11 +30,25 @@ class Category(enum.Enum):
B = enum.auto()
X = enum.auto()
+ def __str__(self):
+ return self.name
+
class Issue(Strict):
component_name: str
component_version: str | None
+ component_type: str | None = None
license_expression: str
category: Category
any_unknown: bool = False
scope: str | None = None
+
+ @pydantic.field_validator("category", mode="before")
+ @classmethod
+ def _coerce_property(cls, value: Any) -> Category:
+ return value if isinstance(value, Category) else Category(value)
+
+ def __str__(self):
+ type_str = "Component" if self.component_type is None else
self.component_type
+ version_str = f"@{self.component_version}" if self.component_version
!= "UNKNOWN" else ""
+ return f"{type_str} {self.component_name}{version_str} declares
license {self.license_expression}"
diff --git a/atr/tasks/sbom.py b/atr/tasks/sbom.py
index 506939b..31598dc 100644
--- a/atr/tasks/sbom.py
+++ b/atr/tasks/sbom.py
@@ -220,8 +220,10 @@ async def score_tool(args: FileArgs) -> results.Results |
None:
bundle = sbom.utilities.path_to_bundle(pathlib.Path(full_path))
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 tools
+ # TODO: Could update the ATR version with a constant showing last change
to the augment/scan
+ # tools so we know if it's outdated
outdated = sbom.tool.plugin_outdated_version(bundle.bom)
+ license_warnings, license_errors = sbom.licenses.check(bundle.bom)
vulnerabilities = sbom.osv.vulns_from_bundle(bundle)
cli_errors = sbom.cyclonedx.validate_cli(bundle)
return results.SBOMToolScore(
@@ -234,6 +236,8 @@ async def score_tool(args: FileArgs) -> results.Results |
None:
warnings=[w.model_dump_json() for w in warnings],
errors=[e.model_dump_json() for e in errors],
outdated=[o.model_dump_json() for o in outdated] if outdated else None,
+ license_warnings=[w.model_dump_json() for w in license_warnings] if
license_warnings else None,
+ license_errors=[e.model_dump_json() for e in license_errors] if
license_errors else None,
vulnerabilities=[v.model_dump_json() for v in vulnerabilities],
atr_props=properties,
cli_errors=cli_errors,
@@ -284,7 +288,7 @@ async def _generate_cyclonedx_core(artifact_path: str,
output_path: str) -> dict
log.info(f"Using root directory: {extract_dir}")
# Run syft to generate the CycloneDX SBOM
- syft_command = ["syft", extract_dir, "-o", "cyclonedx-json",
"--base-path", f"{temp_dir!s}"]
+ syft_command = ["syft", extract_dir, "-o", "cyclonedx-json",
"--enrich", "all", "--base-path", f"{temp_dir!s}"]
log.info(f"Running syft: {' '.join(syft_command)}")
try:
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]