|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +''' |
| 4 | +Created on Mar 29, 2019 |
| 5 | +
|
| 6 | +@author: gsnyder |
| 7 | +
|
| 8 | +Retrieve components marked as commercial and having CVE's |
| 9 | +
|
| 10 | +Warning: This program is single-threaded to minimize the load on the system so it can take |
| 11 | +a very long time to run. |
| 12 | +
|
| 13 | +''' |
| 14 | + |
| 15 | +import argparse |
| 16 | +import csv |
| 17 | +from datetime import datetime |
| 18 | +import json |
| 19 | +import logging |
| 20 | +import sys |
| 21 | + |
| 22 | +from blackduck.HubRestApi import HubInstance |
| 23 | + |
| 24 | +parser = argparse.ArgumentParser("Find components marked as commercial, and with vulnerabilities, in the Black Duck KB and write them to an Excel file, one row per vulnerability") |
| 25 | +parser.add_argument("-f", "--file", default="has_commercial_components.csv", help="The output file name (default: has_commercial_components.csv) to use when capturing all the components marked commercial from the Black Duck KB that have vulnerabilities") |
| 26 | +parser.add_argument("-l", "--limit", type=int, default=100, help="The number of components to return with each call to the REST API (default: 100)") |
| 27 | +parser.add_argument("-t", "--total", type=int, default=99999, help="The total number of components to retrieve") |
| 28 | +args = parser.parse_args() |
| 29 | + |
| 30 | +logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', stream=sys.stderr, level=logging.DEBUG) |
| 31 | +logging.getLogger("requests").setLevel(logging.WARNING) |
| 32 | +logging.getLogger("urllib3").setLevel(logging.WARNING) |
| 33 | + |
| 34 | +hub = HubInstance() |
| 35 | + |
| 36 | +components_url = hub.get_apibase() + "/search/components" |
| 37 | + |
| 38 | +offset = 0 |
| 39 | +total_hits = 0 |
| 40 | + |
| 41 | +# loop to page through the results from the KB until there are none left |
| 42 | +while total_hits < args.total: |
| 43 | + logging.debug("Retrieving components with has_commercial=true AND has_cves=true from offset {}, limit {}".format( |
| 44 | + offset, args.limit)) |
| 45 | + find_commercial_url = components_url + "?q=has_commercial:true&filter=has_cves:true&limit={}&offset={}".format( |
| 46 | + args.limit, offset) |
| 47 | + |
| 48 | + logging.debug("Executing GET on {}".format(find_commercial_url)) |
| 49 | + results = hub.execute_get(find_commercial_url).json().get('items', []) |
| 50 | + |
| 51 | + if results: |
| 52 | + offset += args.limit |
| 53 | + hits = results[0]['hits'] |
| 54 | + total_hits += len(hits) |
| 55 | + logging.debug("Found {} hits, total hits now {}".format(len(hits), total_hits)) |
| 56 | + |
| 57 | + rows = [] |
| 58 | + for hit in hits: |
| 59 | + number_versions = int(hit['fields']['release_count'][0]) |
| 60 | + component_name = hit['fields']['name'][0] |
| 61 | + component_url = hit['component'] |
| 62 | + component_description = hit['fields']['description'][0] |
| 63 | + if number_versions < 1000: |
| 64 | + component = hub.execute_get(hit['component']).json() |
| 65 | + versions_url = hub.get_link(component, "versions") |
| 66 | + versions = hub.execute_get(versions_url).json().get('items', []) |
| 67 | + logging.debug("Found {} versions for component {}".format(len(versions), component['name'])) |
| 68 | + for version in versions: |
| 69 | + version_name = version['versionName'] |
| 70 | + version_url =version['_meta']['href'] |
| 71 | + vuln_url = hub.get_link(version, "vulnerabilities") |
| 72 | + vulns = hub.execute_get(vuln_url).json().get('items', []) |
| 73 | + logging.debug("Found {} vulnerabilities for version {}".format( |
| 74 | + len(vulns), version_name)) |
| 75 | + for vuln in vulns: |
| 76 | + logging.debug("Adding {}".format(vuln['name'])) |
| 77 | + row_data = { |
| 78 | + "Component Name": component_name, |
| 79 | + "Component URL": component_url, |
| 80 | + "Description": component_description, |
| 81 | + "Version": version_name, |
| 82 | + "Version URL": version_url, |
| 83 | + "Vuln": vuln['name'], |
| 84 | + "Vulnerability URL": vuln['_meta']['href'], |
| 85 | + "Vuln Description": vuln['description'], |
| 86 | + "Vuln Severity": vuln['severity'], |
| 87 | + "Vuln CWE URL": hub.get_link(vuln, "cwes"), |
| 88 | + "Vuln Published Date": vuln['publishedDate'], |
| 89 | + "Vuln Updated Date": vuln['updatedDate'], |
| 90 | + } |
| 91 | + |
| 92 | + # |
| 93 | + # Expand CVSS2 and CVSS3 data into separate columns so they can be used (in Excel) |
| 94 | + # to filter, sort, etc |
| 95 | + # |
| 96 | + cvss2 = {} |
| 97 | + if 'temporalMetrics' in vuln['cvss2']: |
| 98 | + # expand temporal metrics |
| 99 | + cvss2_temporal_metrics = {"cvss2_temporal_"+k:v for (k,v) in vuln['cvss2']['temporalMetrics'].items()} |
| 100 | + cvss2.update(cvss2_temporal_metrics) |
| 101 | + # remove the redundant info |
| 102 | + del vuln['cvss2']['temporalMetrics'] |
| 103 | + cvss2.update({"cvss2_"+k:str(v) for (k,v) in vuln['cvss2'].items()}) |
| 104 | + row_data.update(cvss2) |
| 105 | + |
| 106 | + cvss3 = {} |
| 107 | + if 'cvss3' in vuln: |
| 108 | + if 'temporalMetrics' in vuln['cvss3']: |
| 109 | + # expand temporal metrics |
| 110 | + cvss3_temporal_metrics = {"cvss3_temporal_"+k:v for (k,v) in vuln['cvss3']['temporalMetrics'].items()} |
| 111 | + cvss3.update(cvss3_temporal_metrics) |
| 112 | + # remove the redundant info |
| 113 | + del vuln['cvss3']['temporalMetrics'] |
| 114 | + cvss3 = {"cvss3_"+k:str(v) for (k,v) in vuln['cvss3'].items()} |
| 115 | + row_data.update(cvss3) |
| 116 | + rows.append(row_data) |
| 117 | + |
| 118 | + if len(hits) < args.limit: |
| 119 | + # at the end? |
| 120 | + logging.debug("Looks like we are at the end, breaking loop") |
| 121 | + break |
| 122 | + else: |
| 123 | + logging.debug("No results, exiting loop") |
| 124 | + break |
| 125 | + |
| 126 | +logging.debug("Saving {} hits to has_commercial_components.csv".format(total_hits)) |
| 127 | +all_columns = set() |
| 128 | +for row in rows: |
| 129 | + all_columns = all_columns.union(row.keys()) |
| 130 | + |
| 131 | +# Relying on spelling of keys/column names to put them into a 'nice' order |
| 132 | +# when they are written out to CSV using DictWriter |
| 133 | +all_columns = sorted(all_columns) |
| 134 | + |
| 135 | +with open(args.file, "w", newline="") as csvfile: |
| 136 | + writer = csv.DictWriter(csvfile, fieldnames=all_columns) |
| 137 | + writer.writeheader() |
| 138 | + for row in rows: |
| 139 | + writer.writerow(row) |
| 140 | + |
0 commit comments