From 9afa00f61fc8926f8637d5324adfdc45c53e3d78 Mon Sep 17 00:00:00 2001 From: Wolfgang Hottgenroth Date: Wed, 9 Jul 2025 11:26:50 +0200 Subject: [PATCH] add minimal sbom converter --- Dockerfile | 1 + src/converter.py | 96 ++++++++++++++++++++++++++++++++++++++++++++ src/requirements.txt | 2 + src/sbom-dt-dd.py | 13 ++++++ 4 files changed, 112 insertions(+) create mode 100644 src/converter.py diff --git a/Dockerfile b/Dockerfile index 7469b9d..cf91e72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,7 @@ WORKDIR $APP_DIR COPY src/requirements.txt . COPY src/sbom-dt-dd.py . +COPY src/converter.py . COPY src/entrypoint.sh . COPY dependencytrack-client/ ./dependencytrack-client COPY defectdojo-client/ ./defectdojo-client diff --git a/src/converter.py b/src/converter.py new file mode 100644 index 0000000..ed58687 --- /dev/null +++ b/src/converter.py @@ -0,0 +1,96 @@ +from loguru import logger +import yaml +import uuid +from packageurl import PackageURL + +from cyclonedx.builder.this import this_component as cdx_lib_component +from cyclonedx.factory.license import LicenseFactory +from cyclonedx.model.bom import Bom +from cyclonedx.model.component import Component, ComponentType +from cyclonedx.model.contact import OrganizationalEntity +from cyclonedx.model import XsUri +from cyclonedx.output.json import JsonV1Dot5 + +class MyLocalConverterException(Exception): pass + +def __converterClassifierToComponentType(classifier): + componentType = '' + match classifier: + case 'APPLICATION': + componentType = ComponentType.APPLICATION + case 'FRAMEWORK': + componentType = ComponentType.FRAMEWORK + case 'LIBRARY': + componentType = ComponentType.LIBRARY + case 'CONTAINER': + componentType = ComponentType.CONTAINER + case 'OPERATING_SYSTEM': + componentType = ComponentType.OPERATING_SYSTEM + case 'DEVICE': + componentType = ComponentType.DEVICE + case 'FIRMWARE': + componentType = ComponentType.FIRMWARE + case 'FILE': + componentType = ComponentType.FILE + case 'PLATFORM': + componentType = ComponentType.PLATFORM + case 'DEVICE_DRIVER': + componentType = ComponentType.DEVICE_DRIVER + case 'MACHINE_LEARNING_MODEL': + componentType = ComponentType.MACHINE_LEARNING_MODEL + case 'DATA': + componentType = ComponentType.DATA + case _: + raise MyLocalConverterException(f"No componentType for {classifier} found") + return componentType + + + +def minimalSbomFormatConverter(minimalSbom, classifier): + logger.info(f"Minimal input: {minimalSbom}") + + lc_factory = LicenseFactory() + + minimalSbomObject = yaml.safe_load(minimalSbom) + logger.debug(f"{minimalSbomObject=}") + + bom = Bom() + bom.metadata.tools.components.add(cdx_lib_component()) + bom.metadata.tools.components.add(Component( + name='sbom-dt-dd', + type=ComponentType.APPLICATION + )) + + bom.metadata.component = root_component = Component( + name=minimalSbomObject['product'], + type=__converterClassifierToComponentType(classifier), + version=minimalSbomObject['version'], + licenses=[lc_factory.make_from_string(minimalSbomObject['license'])], + supplier=OrganizationalEntity( + name=minimalSbomObject['supplier']['name'], + urls=[XsUri(minimalSbomObject['supplier']['url'])] + ), + bom_ref = f"urn:uuid:{uuid.uuid4()}" + ) + + for minimalComponentDescription in minimalSbomObject['components']: + component = Component( + type=ComponentType.LIBRARY, + name=minimalComponentDescription['name'], + version=minimalComponentDescription['version'], + licenses=[lc_factory.make_from_string(minimalComponentDescription['license'])], + bom_ref = f"urn:uuid:{uuid.uuid4()}" + ) + if 'cpe' in minimalComponentDescription: + component.cpe = minimalComponentDescription['cpe'] + if 'purl' in minimalComponentDescription: + component.purl = PackageURL.from_string(minimalComponentDescription['purl']) + bom.components.add(component) + bom.register_dependency(root_component, [component]) + + outputSbom = JsonV1Dot5(bom).output_as_string(indent=2) + logger.info(outputSbom) + + + raise Exception("Conversion aborted") + diff --git a/src/requirements.txt b/src/requirements.txt index 1d61f74..da63dad 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,3 +1,5 @@ regex==2024.11.6 loguru==0.7.3 +PyYAML==6.0.2 +cyclonedx-python-lib==10.4.1 diff --git a/src/sbom-dt-dd.py b/src/sbom-dt-dd.py index 1c2b3e6..965e671 100644 --- a/src/sbom-dt-dd.py +++ b/src/sbom-dt-dd.py @@ -12,6 +12,9 @@ from dateutil.relativedelta import relativedelta import dependencytrack_api from dependencytrack_api.rest import ApiException as DependencyTrackApiException +from converter import minimalSbomFormatConverter + + class MyLocalException(Exception): pass def executeApiCall(apiClient, ApiClass, EndpointMethod, RequestClass, requestParams, additionalParams=[]): @@ -84,6 +87,10 @@ parser.add_argument('--uploadsbom', '-U', parser.add_argument('--sbomfile', '-F', help='Filename of existing SBOM file to upload, use together with -U, do not use together with -T', required=False) +parser.add_argument('--minimalsbomformat', '-K', + help='SBOM file comes in dedicated minimal format and will be converted into cyclonedx before uploading', + action='store_true', + default=False) parser.add_argument('--target', '-T', help='Target to scan, either path name for sources or docker image tag', required=False) @@ -102,6 +109,7 @@ projectClassifier = args.classifier uploadSbomFlag = args.uploadsbom if uploadSbomFlag: sbomFileName = args.sbomfile + minimalSbomFormat = args.minimalsbomformat else: target = args.target @@ -115,6 +123,11 @@ if uploadSbomFlag: logger.info(f"Reading SBOM from file {sbomFileName}") with open(sbomFileName, 'r') as sbomFile: sbom = sbomFile.read() + logger.info("SBOM file read.") + if minimalSbomFormat: + logger.info("Start converting from minimal format into cyclonedx") + sbom = minimalSbomFormatConverter(sbom, projectClassifier) + logger.info("Converted") logger.info("Done.") else: # ------- generate SBOM ------------