Skip to content

Commit

Permalink
Support new tool metadata format in the merge syft sbom script
Browse files Browse the repository at this point in the history
CycloneDX 1.5 changes the way to define tools in the metadata section,
and marks the 1.4 way as deprecated.

Syft has adopted the newer format starting from version 0.99.0. This
makes the 'merge_syft_sbom.py' script to fail in case a newer Syft
SBOM is used.

This patch updates the script so both formats can be handled. It assumes
the Cachi2 SBOM is in the 1.4 format.

Signed-off-by: Bruno Pimentel <[email protected]>
  • Loading branch information
brunoapimentel committed Feb 15, 2024
1 parent 9351f07 commit dccda6b
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 25 deletions.
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

0 comments on commit dccda6b

Please sign in to comment.