Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the SBOM merge script to handle the newer metadata.tools format #448

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions tests/unit/data/sboms/merged.bom.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:43840228-3fae-4d88-95aa-96512d2510dd",
"specVersion": "1.5",
"serialNumber": "urn:uuid:4370d1ba-7643-4579-8313-bc715da2fa90",
"version": 1,
"metadata": {
"timestamp": "2023-05-03T18:19:41Z",
"tools": [
{
"vendor": "anchore",
"name": "syft",
"version": "0.47.0"
},
{
"vendor": "red hat",
"name": "cachi2"
}
],
"tools": {
"components": [
{
"type": "application",
"author": "anchore",
"name": "syft",
"version": "0.100.0"
},
{
"type": "application",
"author": "red hat",
"name": "cachi2"
}
]
},
"component": {
"bom-ref": "6b8edfe5f2756e0",
"type": "file",
Expand Down
22 changes: 13 additions & 9 deletions tests/unit/data/sboms/syft.bom.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:43840228-3fae-4d88-95aa-96512d2510dd",
"specVersion": "1.5",
"serialNumber": "urn:uuid:4370d1ba-7643-4579-8313-bc715da2fa90",
"version": 1,
"metadata": {
"timestamp": "2023-05-03T18:19:41Z",
"tools": [
{
"vendor": "anchore",
"name": "syft",
"version": "0.47.0"
}
],
"tools": {
"components": [
{
"type": "application",
"author": "anchore",
"name": "syft",
"version": "0.100.0"
}
]
},
"component": {
"bom-ref": "6b8edfe5f2756e0",
"type": "file",
Expand Down
116 changes: 116 additions & 0 deletions tests/unit/test_merge_syft_sbom.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import json
from pathlib import Path
from typing import Any

import pytest

from utils.merge_syft_sbom import merge_sboms

TOOLS_METADATA = {
"syft-cyclonedx-1.4": {
"name": "syft",
"vendor": "anchore",
"version": "0.47.0",
},
"syft-cyclonedx-1.5": {
"type": "application",
"author": "anchore",
"name": "syft",
"version": "0.100.0",
},
"cachi2-cyclonedx-1.4": {
"name": "cachi2",
"vendor": "red hat",
},
"cachi2-cyclonedx-1.5": {
"type": "application",
"author": "red hat",
"name": "cachi2",
},
}


def test_merge_sboms(data_dir: Path) -> None:
result = merge_sboms(f"{data_dir}/sboms/cachi2.bom.json", f"{data_dir}/sboms/syft.bom.json")
Expand All @@ -11,3 +37,93 @@ def test_merge_sboms(data_dir: Path) -> None:
expected_sbom = json.load(file)

assert json.loads(result) == expected_sbom


@pytest.mark.parametrize(
"syft_tools_metadata, expected_result",
[
(
[TOOLS_METADATA["syft-cyclonedx-1.4"]],
[
TOOLS_METADATA["syft-cyclonedx-1.4"],
TOOLS_METADATA["cachi2-cyclonedx-1.4"],
],
),
(
{
"components": [TOOLS_METADATA["syft-cyclonedx-1.5"]],
},
{
"components": [
TOOLS_METADATA["syft-cyclonedx-1.5"],
TOOLS_METADATA["cachi2-cyclonedx-1.5"],
],
},
),
],
)
def test_merging_tools_metadata(
syft_tools_metadata: str, expected_result: Any, tmpdir: Path
) -> None:
syft_sbom = {
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"metadata": {
"tools": syft_tools_metadata,
},
"components": [],
}

cachi2_sbom = {
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"metadata": {
"tools": [TOOLS_METADATA["cachi2-cyclonedx-1.4"]],
},
"components": [],
}

syft_sbom_path = f"{tmpdir}/syft.bom.json"
cachi2_sbom_path = f"{tmpdir}/cachi2.bom.json"

with open(syft_sbom_path, "w") as file:
json.dump(syft_sbom, file)

with open(cachi2_sbom_path, "w") as file:
json.dump(cachi2_sbom, file)

result = merge_sboms(cachi2_sbom_path, syft_sbom_path)

assert json.loads(result)["metadata"]["tools"] == expected_result


def test_invalid_tools_format(tmpdir: Path) -> None:
syft_sbom = {
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"metadata": {
"tools": "invalid",
},
"components": [],
}

cachi2_sbom = {
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"metadata": {
"tools": [TOOLS_METADATA["cachi2-cyclonedx-1.4"]],
},
"components": [],
}

syft_sbom_path = f"{tmpdir}/syft.bom.json"
cachi2_sbom_path = f"{tmpdir}/cachi2.bom.json"

with open(syft_sbom_path, "w") as file:
json.dump(syft_sbom, file)

with open(cachi2_sbom_path, "w") as file:
json.dump(cachi2_sbom, file)

with pytest.raises(RuntimeError):
merge_sboms(cachi2_sbom_path, syft_sbom_path)
38 changes: 35 additions & 3 deletions utils/merge_syft_sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,40 @@ def component_is_duplicated(component: dict[str, Any]) -> bool:
return component_is_duplicated


def _merge_tools_metadata(syft_sbom: dict[Any, Any], cachi2_sbom: dict[Any, Any]) -> None:
"""Merge the content of tools in the metadata section of the SBOM.

With CycloneDX 1.5, a new format for specifying tools was introduced, and the format from 1.4
was marked as deprecated.

This function aims to support both formats in the Syft SBOM. We're assuming the Cachi2 SBOM
was generated with the same version as this script, and it will be in the older format.
"""
syft_tools = syft_sbom["metadata"]["tools"]
cachi2_tools = cachi2_sbom["metadata"]["tools"]

if isinstance(syft_tools, dict):
components = []

for t in cachi2_tools:
components.append(
{
"author": t["vendor"],
"name": t["name"],
"type": "application",
}
)

syft_tools["components"].extend(components)
elif isinstance(syft_tools, list):
syft_tools.extend(cachi2_tools)
else:
raise RuntimeError(
"The .metadata.tools JSON key is in an unexpected format. "
f"Expected dict or list, got {type(syft_tools)}."
)


def merge_sboms(cachi2_sbom_path: str, syft_sbom_path: str) -> str:
"""Merge Cachi2 components into the Syft SBOM while removing duplicates."""
with open(cachi2_sbom_path) as file:
Expand All @@ -134,9 +168,7 @@ def merge_sboms(cachi2_sbom_path: str, syft_sbom_path: str) -> str:

syft_sbom["components"] = filtered_syft_components + cachi2_sbom["components"]

syft_sbom.get("metadata", {}).get("tools", []).extend(
cachi2_sbom.get("metadata", {}).get("tools", [])
)
_merge_tools_metadata(syft_sbom, cachi2_sbom)

return json.dumps(syft_sbom, indent=2)

Expand Down