|
| 1 | +''' |
| 2 | +Created on July 8, 2021 |
| 3 | +
|
| 4 | +@author: gsnyder |
| 5 | +
|
| 6 | +Generate SBOM for a given project-version |
| 7 | +
|
| 8 | +''' |
| 9 | + |
| 10 | +from blackduck import Client |
| 11 | + |
| 12 | +import argparse |
| 13 | +import json |
| 14 | +import logging |
| 15 | +import sys |
| 16 | +import time |
| 17 | + |
| 18 | +logging.basicConfig( |
| 19 | + level=logging.DEBUG, |
| 20 | + format="[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s" |
| 21 | +) |
| 22 | + |
| 23 | + |
| 24 | +# version_name_map = { |
| 25 | +# 'version': 'VERSION', |
| 26 | +# 'scans': 'CODE_LOCATIONS', |
| 27 | +# 'components': 'COMPONENTS', |
| 28 | +# 'vulnerabilities': 'SECURITY', |
| 29 | +# 'source':'FILES', |
| 30 | +# 'cryptography': 'CRYPTO_ALGORITHMS', |
| 31 | +# 'license_terms': 'LICENSE_TERM_FULFILLMENT', |
| 32 | +# 'component_additional_fields': 'BOM_COMPONENT_CUSTOM_FIELDS', |
| 33 | +# 'project_version_additional_fields': 'PROJECT_VERSION_CUSTOM_FIELDS', |
| 34 | +# 'vulnerability_matches': 'VULNERABILITY_MATCH' |
| 35 | +# } |
| 36 | + |
| 37 | +# all_reports = list(version_name_map.keys()) |
| 38 | + |
| 39 | +class FailedReportDownload(Exception): |
| 40 | + pass |
| 41 | + |
| 42 | + |
| 43 | +parser = argparse.ArgumentParser("A program to create an SBOM for a given project-version") |
| 44 | +parser.add_argument("bd_url", help="Hub server URL e.g. https://your.blackduck.url") |
| 45 | +parser.add_argument("token_file", help="containing access token") |
| 46 | +parser.add_argument("project_name") |
| 47 | +parser.add_argument("version_name") |
| 48 | +parser.add_argument("-z", "--zip_file_name", default="reports.zip") |
| 49 | +# parser.add_argument("-r", "--reports", |
| 50 | +# default=",".join(all_reports), |
| 51 | +# help=f"Comma separated list (no spaces) of the reports to generate - {list(version_name_map.keys())}. Default is all reports.") |
| 52 | +# parser.add_argument('--format', default='CSV', choices=["CSV"], help="Report format - only CSV available for now") |
| 53 | +parser.add_argument('-t', '--tries', default=4, type=int, help="How many times to retry downloading the report, i.e. wait for the report to be generated") |
| 54 | +parser.add_argument('-s', '--sleep_time', default=5, type=int, help="The amount of time to sleep in-between (re-)tries to download the report") |
| 55 | +parser.add_argument('--no-verify', dest='verify', action='store_false', help="disable TLS certificate verification") |
| 56 | + |
| 57 | +args = parser.parse_args() |
| 58 | + |
| 59 | +logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG) |
| 60 | +logging.getLogger("requests").setLevel(logging.WARNING) |
| 61 | +logging.getLogger("urllib3").setLevel(logging.WARNING) |
| 62 | +logging.getLogger("blackduck").setLevel(logging.WARNING) |
| 63 | + |
| 64 | + |
| 65 | +def download_report(bd_client, location, filename, retries=args.tries): |
| 66 | + report_id = location.split("/")[-1] |
| 67 | + if retries: |
| 68 | + logging.debug(f"Retrieving generated report from {location}") |
| 69 | + response = bd.session.get(location) |
| 70 | + report_status = response.json().get('status', 'Not Ready') |
| 71 | + if response.status_code == 200 and report_status == 'COMPLETED': |
| 72 | + response = bd.session.get(location + "/download.zip", headers={'Content-Type': 'application/zip', 'Accept':'application/zip'}) |
| 73 | + if response.status_code == 200: |
| 74 | + with open(filename, "wb") as f: |
| 75 | + f.write(response.content) |
| 76 | + logging.info(f"Successfully downloaded zip file to {filename} for report {report_id}") |
| 77 | + else: |
| 78 | + logging.error("Ruh-roh, not sure what happened here") |
| 79 | + else: |
| 80 | + logging.debug(f"Failed to retrieve report {report_id}, report status: {report_status}") |
| 81 | + logging.debug("Probably not ready yet, waiting 5 seconds then retrying...") |
| 82 | + time.sleep(args.sleep_time) |
| 83 | + retries -= 1 |
| 84 | + download_report(bd_client, location, filename, retries) |
| 85 | + else: |
| 86 | + raise FailedReportDownload(f"Failed to retrieve report {report_id} after multiple retries") |
| 87 | + |
| 88 | +with open(args.token_file, 'r') as tf: |
| 89 | + access_token = tf.readline().strip() |
| 90 | + |
| 91 | +bd = Client(base_url=args.bd_url, token=access_token, verify=args.verify) |
| 92 | + |
| 93 | +params = { |
| 94 | + 'q': [f"name:{args.project_name}"] |
| 95 | +} |
| 96 | +projects = [p for p in bd.get_resource('projects', params=params) if p['name'] == args.project_name] |
| 97 | +assert len(projects) == 1, f"There should be one, and only one project named {args.project_name}. We found {len(projects)}" |
| 98 | +project = projects[0] |
| 99 | + |
| 100 | +params = { |
| 101 | + 'q': [f"versionName:{args.version_name}"] |
| 102 | +} |
| 103 | +versions = [v for v in bd.get_resource('versions', project, params=params) if v['versionName'] == args.version_name] |
| 104 | +assert len(versions) == 1, f"There should be one, and only one version named {args.version_name}. We found {len(versions)}" |
| 105 | +version = versions[0] |
| 106 | + |
| 107 | +logging.debug(f"Found {project['name']}:{version['versionName']}") |
| 108 | + |
| 109 | +post_data = { |
| 110 | + 'reportFormat': "JSON", |
| 111 | + 'reportType': 'SBOM', |
| 112 | + 'sbomType': "SPDX_22" |
| 113 | +} |
| 114 | +sbom_reports_url = version['_meta']['href'] + "/sbom-reports" |
| 115 | + |
| 116 | +r = bd.session.post(sbom_reports_url, json=post_data) |
| 117 | +r.raise_for_status() |
| 118 | +location = r.headers.get('Location') |
| 119 | +assert location, "Hmm, this does not make sense. If we successfully created a report then there needs to be a location where we can get it from" |
| 120 | + |
| 121 | +logging.debug(f"Created SBOM report for project {args.project_name}, version {args.version_name} at location {location}") |
| 122 | +download_report(bd, location, args.zip_file_name) |
| 123 | + |
0 commit comments