Skip to content

Commit

Permalink
- Added format autodetection
Browse files Browse the repository at this point in the history
- Renamed middle element to root package
- Added mandatory attributes to spdx elements

Signed-off-by: Jindrich Luza <[email protected]>
  • Loading branch information
midnightercz committed Nov 22, 2024
1 parent e2e1b4c commit 0303928
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ def get_base_images_sbom_components(base_images_digests, is_last_from_scratch):
return components


def detect_sbom_format(sbom):
if sbom.get("bomFormat") == "CycloneDX":
return "cyclonedx"
elif sbom.get("spdxVersion"):
return "spdx"
else:
raise ValueError("Unknown SBOM format")

def parse_args():
parser = argparse.ArgumentParser(
description="Updates the sbom file with base images data based on the provided files"
Expand Down Expand Up @@ -160,7 +168,7 @@ def main():
sbom = json.load(f)

base_images_sbom_components = get_base_images_sbom_components(base_images_digests, is_last_from_scratch)
if args.sbom_type == "cyclonedx":
if detect_sbom_format(sbom) == "cyclonedx":
if "formulation" in sbom:
sbom["formulation"].append({"components": base_images_sbom_components})
else:
Expand All @@ -171,24 +179,25 @@ def main():
packages = []
relationships = []

# Try to calculate middle element represeting the container image or directory, which was
# Try to calculate root package represeting the container image or directory, which was
# used to build the SBOM, based on the relationships maps.
# SPDX has relationsship ROOT-ID DESCRIBES MIDDLE-ID which express the fact the SBOM documents
# describes container image or directory represented by MIDDLE-ID package.
middle_element1 = None
root_package1 = None
for r, contains in map1.items():
# middle element is the one which contains another elements and is in relationship with
# the root element where it stand as relatedSpdxElement
# root package is the one which contains another elements and is in relationship with
# the document element where it stand as relatedSpdxElement
if contains and inverse_map1.get(r) == root_element1:
middle_element1 = r
# If not middle element is found then create one with ID "Uknown" as source for the SBOM
root_package1 = r
# If not root package is found then create one with ID "Uknown" as source for the SBOM
# is not known.
if not middle_element1:
middle_element1 = "SPDXRef-DocumentRoot-Unknown-"
if not root_package1:
root_package1 = "SPDXRef-DocumentRoot-Unknown-"
packages.append(
{
"SPDXID": "SPDXRef-DocumentRoot-Unknown-",
"name": "",
"downloadLocation": "NOASSERTION",
}
)
relationships.append(
Expand All @@ -210,6 +219,7 @@ def main():
{
"SPDXID": SPDXID,
"name": component["name"],
"downloadLocation": "NOASSERTION",
# See more info about external refs here:
# https://spdx.github.io/spdx-spec/v2.3/package-information/#7211-description
"externalRefs": [
Expand All @@ -223,7 +233,7 @@ def main():
# as json string
"annotations": [
{
"annotator": "konflux:jsonencoded",
"annotator": "Tool:konflux:jsonencoded",
"annotationDate": annotation_date,
"annotationType": "OTHER",
"comment": json.dumps(
Expand All @@ -240,7 +250,7 @@ def main():
relationships.append(
{
"spdxElementId": SPDXID,
"relatedSpdxElement": middle_element1,
"relatedSpdxElement": root_package1,
"relationshipType": "BUILD_TOOL_OF",
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ def test_main_input_sbom_does_not_contain_formulation(tmp_path, mocker):
# minimal input sbom file
sbom_file.write_text(
"""{
"bomFormat": "CycloneDX",
"project_name": "MyProject",
"version": "1.0",
"components": []
Expand Down Expand Up @@ -532,8 +533,9 @@ def test_main_input_sbom_spdx_minimal(tmp_path, mocker, isodate):
sbom_file.write_text(
"""{
"SPDXID": "SPDXRef-Document",
"project_name": "MyProject",
"version": "1.0",
"spdxVersion": "SPDX-2.3",
"name": "MyProject",
"documentNamespace": "http://example.com/uid-1234",
"packages": [],
"relationships": []
}"""
Expand Down Expand Up @@ -562,12 +564,14 @@ def test_main_input_sbom_spdx_minimal(tmp_path, mocker, isodate):
"packages": [
{
"SPDXID": "SPDXRef-DocumentRoot-Unknown-",
"downloadLocation": "NOASSERTION",
"name": "",
},
{
"SPDXID": "SPDXRef-container-quay.io/mkosiarc_rhtap/single-container-app-"
"9520a72cbb69edfca5cac88ea2a9e0e09142ec934952b9420d686e77765f002c",
"name": "quay.io/mkosiarc_rhtap/single-container-app",
"downloadLocation": "NOASSERTION",
"externalRefs": [
{
"referenceType": "purl",
Expand All @@ -579,7 +583,7 @@ def test_main_input_sbom_spdx_minimal(tmp_path, mocker, isodate):
],
"annotations": [
{
"annotator": "konflux:jsonencoded",
"annotator": "Tool:konflux:jsonencoded",
"annotationDate": "2021-07-01T00:00:00Z",
"annotationType": "OTHER",
"comment": '{"name":"konflux:container:is_builder_image:for_stage","value":"0"}',
Expand All @@ -590,6 +594,7 @@ def test_main_input_sbom_spdx_minimal(tmp_path, mocker, isodate):
"name": "registry.access.redhat.com/ubi8/ubi",
"SPDXID": "SPDXRef-container-registry.access.redhat.com/ubi8/ubi-"
"0f22256f634f8205fbd9c438c387ccf2d4859250e04104571c93fdb89a62bae1",
"downloadLocation": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
Expand All @@ -601,7 +606,7 @@ def test_main_input_sbom_spdx_minimal(tmp_path, mocker, isodate):
],
"annotations": [
{
"annotator": "konflux:jsonencoded",
"annotator": "Tool:konflux:jsonencoded",
"annotationDate": "2021-07-01T00:00:00Z",
"annotationType": "OTHER",
"comment": '{"name":"konflux:container:is_base_image","value":"true"}',
Expand Down Expand Up @@ -646,6 +651,7 @@ def test_main_input_sbom_does_not_contain_formulation_and_base_image_from_scratc
sbom_file.write_text(
"""{
"project_name": "MyProject",
"bomFormat": "CycloneDX",
"version": "1.0",
"components": []
}"""
Expand Down Expand Up @@ -720,6 +726,7 @@ def test_main_input_sbom_contains_formulation(tmp_path, mocker):
sbom_file.write_text(
"""
{
"bomFormat": "CycloneDX",
"project_name": "MyProject",
"version": "1.0",
"components": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
from packageurl import PackageURL


def detect_sbom_format(sbom):
if sbom.get("bomFormat") == "CycloneDX":
return "cyclonedx"
elif sbom.get("spdxVersion"):
return "spdx"
else:
raise ValueError("Unknown SBOM format")


def _is_syft_local_golang_component(component: dict) -> bool:
"""
Check if a Syft Golang reported component is a local replacement.
Expand Down Expand Up @@ -401,31 +410,32 @@ def map_relationships(relationships):
break
return parent_element, relations_map, relations_inverse_map

def calculate_middle_element(root_element, map, inverse_map):
"""Calculate middle element of the relationship.
Middle element is considered as element which is related to root element and is not root element.
def calculate_root_package(root_element, map, inverse_map):
"""Calculate root package from relationship map.
Root package is considered as package which contains other packages and
is described by the document itself.
"""
middle_element = None
root_package = None
for r, contains in map.items():
if contains and inverse_map.get(r) == root_element:
middle_element = r
return middle_element
root_package = r
return root_package

relationships = []

root_element1, map1, inverse_map1 = map_relationships(relationships1)
root_element2, map2, inverse_map2 = map_relationships(relationships2)
package_ids = [package["SPDXID"] for package in packages]

middle_element1 = calculate_middle_element(root_element1, map1, inverse_map1)
middle_element2 = calculate_middle_element(root_element2, map2, inverse_map2)
root_package1 = calculate_root_package(root_element1, map1, inverse_map1)
root_package2 = calculate_root_package(root_element2, map2, inverse_map2)

for relation in relationships2:
_relation = relation.copy()

# If relations is Root decribes middle element, skip it
if (
_relation["relatedSpdxElement"] == middle_element2
_relation["relatedSpdxElement"] == root_package2
and _relation["spdxElementId"] == root_element2
and _relation["relationshipType"] == "DESCRIBES"
):
Expand All @@ -436,10 +446,10 @@ def calculate_middle_element(root_element, map, inverse_map):
_relation["spdxElementId"] = root_element1
elif relation["relatedSpdxElement"] == root_element2:
_relation["relatedSpdxElement"] = root_element1
if _relation["spdxElementId"] == middle_element2:
_relation["spdxElementId"] = middle_element1
if _relation["relatedSpdxElement"] == middle_element2:
_relation["relatedSpdxElement"] = middle_element1
if _relation["spdxElementId"] == root_package2:
_relation["spdxElementId"] = root_package1
if _relation["relatedSpdxElement"] == root_package2:
_relation["relatedSpdxElement"] = root_package1

# include only relations to packages which exists in merged packages.
if _relation["relatedSpdxElement"] in package_ids:
Expand Down Expand Up @@ -482,15 +492,20 @@ def get_package_key(pkg):
return filtered_packages + cachi2_sbom["packages"]


def merge_sboms(cachi2_sbom_path: str, syft_sbom_path: str, format: str = "cyclonedx") -> str:
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:
cachi2_sbom = json.load(file)

with open(syft_sbom_path) as file:
syft_sbom = json.load(file)

if format == "cyclonedx":
format1 = detect_sbom_format(cachi2_sbom)
format2 = detect_sbom_format(syft_sbom)
if format1 != format2:
raise ValueError("SBOMs are in different formats")

if format1 == "cyclonedx":
syft_sbom["components"] = merge_components(syft_sbom, cachi2_sbom)
_merge_tools_metadata(syft_sbom, cachi2_sbom)
else:
Expand Down Expand Up @@ -527,6 +542,6 @@ def merge_sboms(cachi2_sbom_path: str, syft_sbom_path: str, format: str = "cyclo

args = parser.parse_args()

merged_sbom = merge_sboms(args.cachi2_sbom_path, args.syft_sbom_path, format=args.sbom_format)
merged_sbom = merge_sboms(args.cachi2_sbom_path, args.syft_sbom_path)

print(merged_sbom)
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def isodate() -> Generator:


def test_merge_sboms_spdx(data_dir: Path, isodate: Generator) -> None:
result = merge_sboms(f"{data_dir}/cachi2.bom.spdx.json", f"{data_dir}/syft.bom.spdx.json", format="spdx")
result = merge_sboms(f"{data_dir}/cachi2.bom.spdx.json", f"{data_dir}/syft.bom.spdx.json")

with open(f"{data_dir}/merged.bom.spdx.json") as file:
expected_sbom = json.load(file)
Expand All @@ -66,9 +66,7 @@ def test_merge_both_formats_equal(data_dir: Path, isodate: Generator) -> None:
"""Test that the merge result is the same for both formats."""

result_cdx = json.loads(merge_sboms(f"{data_dir}/cachi2.bom.json", f"{data_dir}/syft.bom.json"))
result_spdx = json.loads(
merge_sboms(f"{data_dir}/cachi2.bom.spdx.json", f"{data_dir}/syft.bom.spdx.json", format="spdx")
)
result_spdx = json.loads(merge_sboms(f"{data_dir}/cachi2.bom.spdx.json", f"{data_dir}/syft.bom.spdx.json"))
cdx_components = []
for component in result_cdx["components"]:
cdx_components.append(
Expand Down
Loading

0 comments on commit 0303928

Please sign in to comment.