Skip to content

Commit

Permalink
- Simplified root package discovery
Browse files Browse the repository at this point in the history
- spdx_find_doc_and_root_package wraps spdx related code
- fixed isodate generator
- fixed annotator string
- build deps are generated as SPDXRef-Image- instead of SPDXRef-container-
- added test_main_input_sbom_spdx_with_packages

Signed-off-by: Jindrich Luza <[email protected]>
  • Loading branch information
midnightercz committed Dec 4, 2024
1 parent 0303928 commit 440b1e7
Show file tree
Hide file tree
Showing 9 changed files with 1,083 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,12 @@ def detect_sbom_format(sbom):
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"
)
parser.add_argument("--sbom", type=pathlib.Path, help="Path to the sbom file", required=True)
parser.add_argument(
"--sbom-type",
choices=["spdx", "cyclonedx"],
default="cyclonedx",
help="Type of the sbom file",
required=True,
)
parser.add_argument(
"--base-images-from-dockerfile",
type=pathlib.Path,
Expand All @@ -129,28 +123,93 @@ def parse_args():
return args


def map_relationships(relationships):
"""Map relationships of spdx element.
Method returns triplet containing root element, map of relations and inverse map of relations.
Root element is considered as element which is not listed as related document
in any of the relationships. Relationship map is dict of {key: value} where key is spdx
element and list of related elements is the value.
Inverse map is dict of {key: value} where key is related spdx element in the relation ship
and value is spdx element.
"""
def spdx_find_doc_and_root_package(relationships):
"""Find SPDX root package and document in the SBOM
relations_map = {}
relations_inverse_map = {}
:param relationships: (List) - List of relationships in the SBOM
for relation in relationships:
relations_map.setdefault(relation["spdxElementId"], []).append(relation["relatedSpdxElement"])
relations_inverse_map[relation["relatedSpdxElement"]] = relation["spdxElementId"]
Method scans relationships for relationshipType "DESCRIBES" and returns
relatedSpdxElement and spdxElementId which are SPDX root package and document.
In the case there's no relationship with relationshipType "DESCRIBES" ValueError is raised.
"""

parent_element = None
for parent_element in relations_map.keys():
if parent_element not in relations_inverse_map:
for relationship in relationships:
if relationship["relationshipType"] == "DESCRIBES":
root_package1 = relationship["relatedSpdxElement"]
doc = relationship["spdxElementId"]
break
return parent_element, relations_map, relations_inverse_map
else:
raise ValueError("No DESCRIBES relationship found in the SBOM")
return root_package1, doc


def spdx_create_dependency_package(component, annotation_date):
"""Create SPDX package for the base image component."""

# Calculate unique identifier SPDXID based on the component name and purl
# See: https://github.com/konflux-ci/architecture/blob/main/ADR/0044-spdx-support.md
SPDXID = f"SPDXRef-Image-{component['name']}-" + f"{hashlib.sha256(component['purl'].encode()).hexdigest()}"
package = {
"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": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": component["purl"],
}
],
# Annotations are used to provide cyclonedx custom properties
# as json string
# See: https://github.com/konflux-ci/architecture/blob/main/ADR/0044-spdx-support.md
"annotations": [
{
"annotator": "Tool: konflux:jsonencoded",
"annotationDate": annotation_date,
"annotationType": "OTHER",
"comment": json.dumps(
{"name": property["name"], "value": property["value"]},
separators=(",", ":"),
),
}
for property in component["properties"]
],
}
return package, SPDXID


def create_build_relationship(SPDXID, root_package1):
return {
"spdxElementId": SPDXID,
"relatedSpdxElement": root_package1,
"relationshipType": "BUILD_TOOL_OF",
}


def create_build_packages_and_relationships(sbom, base_images_sbom_components):
"""Create SPDX packages and relationships for base images components.
:param sbom: (Dict) - SBOM data
:param base_images_sbom_components: (List) - List of base images components
Method creates SPDX packages for base images components and relationships
"""

packages = []
relationships = []
root_package, doc = spdx_find_doc_and_root_package(sbom["relationships"])
annotation_date = datetime.datetime.now().isoformat()[:-7] + "Z"
for component in base_images_sbom_components:
# create dependency package for each base image
package, SPDXID = spdx_create_dependency_package(component, annotation_date)

packages.append(package)
# Add relationship for parsed base image components and root package
relationships.append(create_build_relationship(SPDXID, root_package))
return packages, relationships


def main():
Expand All @@ -174,86 +233,7 @@ def main():
else:
sbom.update({"formulation": [{"components": base_images_sbom_components}]})
else:
root_element1, map1, inverse_map1 = map_relationships(sbom["relationships"])

packages = []
relationships = []

# 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.
root_package1 = None
for r, contains in map1.items():
# 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:
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 root_package1:
root_package1 = "SPDXRef-DocumentRoot-Unknown-"
packages.append(
{
"SPDXID": "SPDXRef-DocumentRoot-Unknown-",
"name": "",
"downloadLocation": "NOASSERTION",
}
)
relationships.append(
{
"spdxElementId": root_element1 or sbom["SPDXID"],
"relatedSpdxElement": "SPDXRef-DocumentRoot-Unknown-",
"relationshipType": "DESCRIBES",
}
)

annotation_date = datetime.datetime.now().isoformat()
for component in base_images_sbom_components:
# Calculate unique identifier SPDXID based on the component name and purl
SPDXID = (
f"SPDXRef-{component['type']}-{component['name']}-"
+ f"{hashlib.sha256(component['purl'].encode()).hexdigest()}"
)
packages.append(
{
"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": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": component["purl"],
}
],
# Annotations are used to provide cyclonedx custom properties
# as json string
"annotations": [
{
"annotator": "Tool:konflux:jsonencoded",
"annotationDate": annotation_date,
"annotationType": "OTHER",
"comment": json.dumps(
{"name": property["name"], "value": property["value"]},
separators=(",", ":"),
),
}
for property in component["properties"]
],
}
)
# Add relationship for parsed base image components and "middle" element which wraps
# all spdx packages, but it's not spdx document itself.
relationships.append(
{
"spdxElementId": SPDXID,
"relatedSpdxElement": root_package1,
"relationshipType": "BUILD_TOOL_OF",
}
)
packages, relationships = create_build_packages_and_relationships(sbom, base_images_sbom_components)
# merge newly created packages for build tools with existing packages
sbom["packages"] = sbom.get("packages", []) + packages
# merge newly created relationships of the build tools with existing relationships
Expand Down
Loading

0 comments on commit 440b1e7

Please sign in to comment.