diff --git a/Dockerfile b/Dockerfile index cf91e72..f637d9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,8 @@ USER user WORKDIR $APP_DIR COPY src/requirements.txt . -COPY src/sbom-dt-dd.py . +COPY src/sbom_dt_dd.py . +COPY src/sbom_dt_dd_cli.py . COPY src/converter.py . COPY src/entrypoint.sh . COPY dependencytrack-client/ ./dependencytrack-client diff --git a/src/converter.py b/src/converter.py index 9e5eba4..e1fec9c 100644 --- a/src/converter.py +++ b/src/converter.py @@ -47,7 +47,7 @@ def __converterClassifierToComponentType(classifier): -def minimalSbomFormatConverter(minimalSbom, classifier): +def minimalSbomFormatConverter(minimalSbom): logger.info(f"Minimal input: {minimalSbom}") lc_factory = LicenseFactory() diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 1f1ce3c..9daa7c1 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -7,5 +7,4 @@ PYTHONPATH="$PYTHONPATH:/opt/app/dependencytrack-client" PYTHONPATH="$PYTHONPATH:/opt/app/defectdojo-client" export PYTHONPATH -exec python /opt/app/sbom-dt-dd.py "$@" - +exec python /opt/app/sbom_dt_dd_cli.py "$@" diff --git a/src/requirements.txt b/src/requirements.txt index da63dad..7a40941 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -2,4 +2,6 @@ regex==2024.11.6 loguru==0.7.3 PyYAML==6.0.2 cyclonedx-python-lib==10.4.1 - +fastapi==0.116.1 +gunicorn==23.0.0 +uvicorn==0.35.0 diff --git a/src/sbom-dt-dd.py b/src/sbom-dt-dd.py deleted file mode 100644 index 9803055..0000000 --- a/src/sbom-dt-dd.py +++ /dev/null @@ -1,263 +0,0 @@ -import os -from loguru import logger -import argparse -import subprocess -import json - -import datetime -from dateutil.relativedelta import relativedelta - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'defectdojo-client')) -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'dependencytrack-client')) - -import defectdojo_api -from defectdojo_api.rest import ApiException as DefectDojoApiException - -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=[]): - try: - logger.info(f"Calling {ApiClass=}.{EndpointMethod=} with {RequestClass=})") - if VERBOSE: - logger.debug(f"{additionalParams=}, {requestParams=}") - instance = ApiClass(apiClient) - if RequestClass: - request = RequestClass(**requestParams) - response = EndpointMethod(instance, *additionalParams, request) - else: - response = EndpointMethod(instance, *additionalParams) - logger.info(f"Response is {response}") - return response - except Exception as e: - logger.error(f"Caught error {e} with {str(e)}") - raise MyLocalException(e) - -def generateSBOM(target='.', name='dummyName', version='0.0.0'): - try: - result = subprocess.run( - ["syft", "scan", target, "-o", "cyclonedx-json", "--source-name", name, "--source-version", version], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - sbom = json.loads(result.stdout) - return sbom - except subprocess.CalledProcessError as e: - logger.error(f"SBOM scanner failed: {e.stderr}") - raise MyLocalException(e) - -# ---- main starts here with preparation of config ----------------------------------------------------------------------- - -try: - DTRACK_API_URL = os.environ["DTRACK_API_URL"] - DTRACK_TOKEN = os.environ["DTRACK_TOKEN"] - DEFECTDOJO_URL = os.environ["DEFECTDOJO_URL"] - DEFECTDOJO_TOKEN = os.environ["DEFECTDOJO_TOKEN"] -except KeyError as e: - raise Exception(f"Env variable {e} is shall be set") - - -parser = argparse.ArgumentParser(description='sbom-dt-dd glue logic') -parser.add_argument('--name', '-n', - help='Project Name', - required=False, - default=''), -parser.add_argument('--version', '-v', - help='Project Version', - required=False, - default='') -parser.add_argument('--description', '-d', - help='Project Description', - required=False, - default='') -parser.add_argument('--type', '-t', - help='Product Type from DefectDojo', - type=int, - required=True) -parser.add_argument('--classifier', '-c', - help='Project Classifier from DependencyTrack', - choices=['APPLICATION', 'FRAMEWORK', 'LIBRARY', 'CONTAINER', 'OPERATING_SYSTEM', 'DEVICE', - 'FIRMWARE', 'FILE', 'PLATFORM', 'DEVICE_DRIVER', 'MACHINE_LEARNING_MODEL', 'DATA'], - required=False, - default='') -parser.add_argument('--uploadsbom', '-U', - help='Upload a already existing SBOM instead of generating it. Give the SBOM file at -F instead of a target', - required=False, - action='store_true', - default=False) -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('--overwritemetadata', '-O', - help='Overwrite name, version, description and classifier with data from minimal SBOM', - 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) -parser.add_argument('--reimport', '-R', - help='Import the SBOM for an existing project/product once again', - required=False, - action='store_true', - default=False) -parser.add_argument('--verbose', '-V', - help='A lot of debug output', - required=False, - action='store_true', - default=False) -args = parser.parse_args() -projectName = args.name -projectVersion = args.version -projectDescription = args.description -productType = args.type -projectClassifier = args.classifier -reImport = args.reimport - -uploadSbomFlag = args.uploadsbom -if uploadSbomFlag: - sbomFileName = args.sbomfile - minimalSbomFormat = args.minimalsbomformat -else: - target = args.target - -if minimalSbomFormat: - overwriteMetadata = args.overwritemetadata - -if not overwriteMetadata and not (projectName and projectVersion and projectClassifier and projectDescription): - raise MyLocalException("If overwriteMetadata is not selected, projectName, projectVersion, projectClassifier and projectDescription must be set.") - -VERBOSE = args.verbose - - -# ---- main starts here -------------------------------------------------------------------------------------------------- - -if uploadSbomFlag: - # ------- read uploaded SBOM ------------- - 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, nameFromMinimalSbom, versionFromMinimalSbom, classifierFromMinimalSbom, descriptionFromMinimalSbom) = minimalSbomFormatConverter(sbom, projectClassifier) - logger.info("Converted") - if overwriteMetadata: - projectName = nameFromMinimalSbom - projectVersion = versionFromMinimalSbom - projectClassifier = classifierFromMinimalSbom - projectDescription = descriptionFromMinimalSbom - logger.info("Done.") -else: - # ------- generate SBOM ------------ - logger.info(f"Generating SBOM for {target}") - sbomJson = generateSBOM(target, projectName, projectVersion) - sbom = json.dumps(sbomJson) - logger.info("Done.") - - - -# ------- create product and engagement in DefectDojo ------- -if not reImport: - # in case of a reimport no modification on DefectDojo are required - defectdojo_configuration = defectdojo_api.Configuration( - host = DEFECTDOJO_URL - ) - defectdojo_configuration.api_key['tokenAuth'] = DEFECTDOJO_TOKEN - defectdojo_configuration.api_key_prefix['tokenAuth'] = 'Token' - - with defectdojo_api.ApiClient(defectdojo_configuration) as defectdojo_api_client: - print("Create product in DefectDojo") - productName = f"{projectName}:{projectVersion}" - product_response = \ - executeApiCall( - defectdojo_api_client, - defectdojo_api.ProductsApi, - defectdojo_api.ProductsApi.products_create, - defectdojo_api.ProductRequest, - { 'name': productName, 'description': projectDescription, 'prod_type': productType }, - [] - ) - - product_id = product_response.id - print(f"{product_id=}") - - print("Create engagement in DefectDojo") - start_time = datetime.date.today() - end_time = start_time + relativedelta(years=10) - engagementName = f"{productName} DTrack Link" - engagement_response = \ - executeApiCall( - defectdojo_api_client, - defectdojo_api.EngagementsApi, - defectdojo_api.EngagementsApi.engagements_create, - defectdojo_api.EngagementRequest, - { 'name': engagementName, 'target_start': start_time, 'target_end': end_time, 'status': 'In Progress', 'product': product_id }, - [] - ) - - engagement_id = engagement_response.id - print(f"{engagement_id=}") - - -# ------- create project in DependencyTrack, connect project to engagement in DefectDojo, upload SBOM -------- -dependencytrack_configuration = dependencytrack_api.Configuration( - host = f"{DTRACK_API_URL}/api" -) -dependencytrack_configuration.debug = False -dependencytrack_configuration.api_key['ApiKeyAuth'] = DTRACK_TOKEN - -with dependencytrack_api.ApiClient(dependencytrack_configuration) as dependencytrack_api_client: - if not reImport: - # in case of a reimport it is not necessary to create the project - project_response = \ - executeApiCall( - dependencytrack_api_client, - dependencytrack_api.ProjectApi, - dependencytrack_api.ProjectApi.create_project, - dependencytrack_api.Project, - { 'name': projectName, 'version': projectVersion, 'classifier': projectClassifier, 'uuid': "", 'last_bom_import': 0 }, - [] - ) - - project_uuid = project_response.uuid - print(f"{project_uuid=}") - - properties = [ - { 'group_name': "integrations", 'property_name': "defectdojo.engagementId", - 'property_value': str(engagement_id), 'property_type': "STRING" }, - { 'group_name': "integrations", 'property_name': "defectdojo.doNotReactivate", - 'property_value': "true", 'property_type': "BOOLEAN" }, - { 'group_name': "integrations", 'property_name': "defectdojo.reimport", - 'property_value': "true", 'property_type': "BOOLEAN" } - ] - for property in properties: - executeApiCall( - dependencytrack_api_client, - dependencytrack_api.ProjectPropertyApi, - dependencytrack_api.ProjectPropertyApi.create_property1, - dependencytrack_api.ProjectProperty, - property, - [ project_uuid ] - ) - - bom_response = \ - executeApiCall( - dependencytrack_api_client, - dependencytrack_api.BomApi, - dependencytrack_api.BomApi.upload_bom, - None, - None, - [ None, False, projectName, projectVersion, None, None, None, None, True, sbom ] - ) -