Skip to content

Commit 86d1849

Browse files
authored
Merge pull request #195 from gue-ni/omicron_scripts
Add script for creating CSV files for vuln_batch_remediation.py
2 parents 935744e + 9132248 commit 86d1849

File tree

2 files changed

+168
-24
lines changed

2 files changed

+168
-24
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'''
2+
Export the vulnerabilites from a project as CSV. Can be used to apply batch vulnerability
3+
remediation with vuln_batch_remediation.py
4+
5+
Output is in format:
6+
identifier, status, comment, componentName, componentVersion, description
7+
8+
The API token should be specified in a .env file or as environment variable.
9+
'''
10+
import re
11+
import os
12+
import sys
13+
import csv
14+
import logging
15+
import argparse
16+
from pprint import pprint
17+
from blackduck import Client
18+
from dotenv import load_dotenv
19+
20+
load_dotenv()
21+
22+
API_TOKEN = os.getenv('API_TOKEN')
23+
24+
logging.basicConfig(
25+
level=logging.INFO,
26+
format="[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s"
27+
)
28+
29+
def strip_newline(str):
30+
return str.replace('\r', '').replace('\n', ' ')
31+
32+
def match_component(selected_components, component):
33+
if (len(selected_components) == 0):
34+
return True
35+
36+
for selected in selected_components:
37+
if (re.search(selected, component, re.IGNORECASE)):
38+
return True
39+
40+
return False
41+
42+
def main():
43+
program_name = os.path.basename(sys.argv[0])
44+
parser = argparse.ArgumentParser(prog=program_name, usage="%(prog)s [options]", description="Automated Assessment")
45+
parser.add_argument("project", help="project name")
46+
parser.add_argument("version", help="project version, e.g. latest")
47+
parser.add_argument("--output", required=False,help="csv output path" )
48+
parser.add_argument("--base-url", required=False, help="base url", default="https://blackduck.omicron.at")
49+
parser.add_argument("--components", required=False, help="component names, comma seperated without space")
50+
args = parser.parse_args()
51+
52+
components = args.components.split(',') if args.components != None else []
53+
projectname = args.project
54+
projectversion = args.version
55+
output = args.output if args.output != None else "output.csv"
56+
57+
csv_file = open(output, mode='w', newline='', encoding='utf-8')
58+
csv_writer = csv.writer(csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
59+
60+
bd = Client(
61+
token=API_TOKEN,
62+
base_url=args.base_url,
63+
verify=False # TLS certificate verification
64+
)
65+
66+
for project in bd.get_resource('projects'):
67+
if (project['name'] == projectname):
68+
for version in bd.get_resource('versions', project):
69+
if (projectversion == None):
70+
pprint(version['versionName'])
71+
72+
else:
73+
if (version['versionName'] == projectversion):
74+
for vulnverable_component in bd.get_resource('vulnerable-components', version):
75+
componentName = vulnverable_component["componentName"]
76+
77+
if (match_component(components, componentName)):
78+
componentVersion = vulnverable_component["componentVersionName"]
79+
remediation = vulnverable_component['vulnerabilityWithRemediation']
80+
81+
status = remediation['remediationStatus']
82+
identifier = remediation['vulnerabilityName']
83+
description = strip_newline(remediation['description'])
84+
comment = strip_newline(remediation.get('remediationComment', ""))
85+
86+
row = [identifier, status, comment, componentName, componentVersion, description]
87+
csv_writer.writerow(row)
88+
break
89+
break
90+
91+
if __name__ == "__main__":
92+
main()

examples/vuln_batch_remediation.py

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
Each processing step can be turned on or off. At least one step must be run. Default
2727
is to run both.
2828
29-
The script get's it CVE and orign lists from CSV files. The CSV filenames are loaded
29+
The script can get's its CVE and orign lists from CSV files. The CSV filenames are loaded
3030
from Custom Fields in the Black Duck project. This allows different groups of projects to
3131
use different remeidation settings. If a CVE remediation status should apply globally
3232
to all projects, Black Duck's global remediation feature should be used.
3333
34+
The script can also get the CSV filenames from the command line arguments.
35+
3436
Here is an example of the CSV data for the CVE list:
3537
3638
"CVE-2016-1840","IGNORED","Applies only to Apple OS"
@@ -67,7 +69,7 @@
6769
import json
6870
import csv
6971
import traceback
70-
72+
from pprint import pprint
7173
from argparse import ArgumentParser
7274
from argparse import RawDescriptionHelpFormatter
7375

@@ -81,15 +83,20 @@
8183

8284

8385
def load_remediation_input(remediation_file):
84-
with open(remediation_file, mode='r') as infile:
86+
with open(remediation_file, mode='r', encoding="utf-8") as infile:
8587
reader = csv.reader(infile)
86-
return {rows[0]:[rows[1],rows[2]] for rows in reader}
88+
#return {rows[0]:[rows[1],rows[2]] for rows in reader}
89+
return {rows[0]:rows[1:] for rows in reader}
8790

8891
def remediation_is_valid(vuln, remediation_data):
8992
vulnerability_name = vuln['vulnerabilityWithRemediation']['vulnerabilityName']
90-
# remediation_status = vuln['vulnerabilityWithRemediation']['remediationStatus']
91-
# remediation_comment = vuln['vulnerabilityWithRemediation'].get('remediationComment','')
93+
remediation_status = vuln['vulnerabilityWithRemediation']['remediationStatus']
94+
remediation_comment = vuln['vulnerabilityWithRemediation'].get('remediationComment','')
95+
9296
if vulnerability_name in remediation_data.keys():
97+
remediation = remediation_data[vulnerability_name]
98+
if (remediation_status == remediation[0] and remediation_comment == remediation[1]):
99+
return None
93100
return remediation_data[vulnerability_name]
94101
else:
95102
return None
@@ -114,12 +121,31 @@ def find_custom_field_value (custom_fields, custom_field_label):
114121
return None
115122
return None
116123

117-
def process_vulnerabilities(hub, vulnerable_components, remediation_data=None, exclusion_data=None):
124+
125+
126+
def set_vulnerablity_remediation(hub, vuln, remediation_status, remediation_comment):
127+
url = vuln['_meta']['href']
128+
update={}
129+
update['remediationStatus'] = remediation_status
130+
update['comment'] = remediation_comment
131+
response = hub.execute_put(url, data=update)
132+
return response
133+
134+
def process_vulnerabilities(hub, vulnerable_components, remediation_data=None, exclusion_data=None, dry_run=False):
135+
136+
if (dry_run):
137+
print(f"Opening dry run output file: {dry_run}")
138+
csv_file = open(dry_run, mode='w', newline='', encoding='utf-8')
139+
csv_writer = csv.writer(csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
140+
118141
count = 0
119-
print('"Component Name","Component Version","Component OriginID","CVE","Reason","Remeidation Status","HTTP response code"')
142+
print('"Component Name","Component Version","CVE","Reason","Remeidation Status","HTTP response code"')
120143

121144
for vuln in vulnerable_components['items']:
122145
if vuln['vulnerabilityWithRemediation']['remediationStatus'] == "NEW":
146+
remediation_action = None
147+
exclusion_action = None
148+
123149
if (remediation_data):
124150
remediation_action = remediation_is_valid(vuln, remediation_data)
125151

@@ -137,14 +163,20 @@ def process_vulnerabilities(hub, vulnerable_components, remediation_data=None, e
137163
reason = 'origin-exclusion'
138164

139165
if (remediation_action):
140-
resp = hub.set_vulnerablity_remediation(vuln, remediation_action[0],remediation_action[1])
141-
count += 1
142-
print ('\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"'.
166+
if (dry_run):
167+
remediation_action.insert(0, vuln['vulnerabilityWithRemediation']['vulnerabilityName'])
168+
csv_writer.writerow(remediation_action)
169+
else:
170+
resp = set_vulnerablity_remediation(hub, vuln, remediation_action[0],remediation_action[1])
171+
count += 1
172+
173+
print ('\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"'.
143174
format(vuln['componentName'], vuln['componentVersionName'],
144-
vuln['componentVersionOriginId'],
145175
vuln['vulnerabilityWithRemediation']['vulnerabilityName'],
146-
reason, remediation_action[0], resp.status_code))
147-
print (f'Remediated {count} vulnerabilities.')
176+
reason, remediation_action[0], resp.status_code if not dry_run else ""))
177+
178+
179+
print (f'Remediated {count} vulnerabilities. {"(dry run)" if dry_run else ""}')
148180

149181
def main(argv=None): # IGNORE:C0111
150182
'''Command line options.'''
@@ -157,7 +189,7 @@ def main(argv=None): # IGNORE:C0111
157189
program_name = os.path.basename(sys.argv[0])
158190
program_version = "v%s" % __version__
159191
program_build_date = str(__updated__)
160-
program_version_message = '%%(prog)s %s (%s)' % (program_version, program_build_date)
192+
program_version_message = '%s %s (%s)' % (program_name, program_version, program_build_date)
161193
program_shortdesc = __import__('__main__').__doc__.split("\n")[1]
162194
program_license = '''%s
163195
@@ -178,8 +210,11 @@ def main(argv=None): # IGNORE:C0111
178210
parser = ArgumentParser(description=program_license, formatter_class=RawDescriptionHelpFormatter)
179211
parser.add_argument("projectname", help="Project nname")
180212
parser.add_argument("projectversion", help="Project vesrsion")
181-
parser.add_argument("--no-process-cve-remediation-list", dest='process_cve_remediation_list', action='store_false', help="Disbable processing CVE-Remediation-list")
182-
parser.add_argument("--no-process-origin-exclusion-list", dest='process_origin_exclusion_list', action='store_false', help="Disable processing Origin-Exclusion-List")
213+
parser.add_argument("--dry-run", dest="dry_run", nargs='?', const="dry_run.csv", help="dry run remediations and output to file")
214+
parser.add_argument("--remediation-list", dest="local_remediation_list", default=None, help="Filename of cve remediation list csv file")
215+
parser.add_argument("--origin-exclusion-list", dest="local_origin_exclusion_list", default=None, help="Filename of origin exclusion list csv file")
216+
parser.add_argument("--no-process-cve-remediation-list", dest='process_cve_remediation_list', action='store_false', help="Disable processing CVE-Remediation-list")
217+
parser.add_argument("--no-process-origin-exclusion-list", dest='process_origin_exclusion_list', default=None, action='store_false', help="Disable processing Origin-Exclusion-List")
183218
parser.add_argument("--cve-remediation-list-custom-field-label", default='CVE Remediation List', help='Label of Custom Field on Black Duck that contains remeidation list file name')
184219
parser.add_argument("--origin-exclusion-list-custom-field-label", default='Origin Exclusion List', help='Label of Custom Field on Black Duck that containts origin exclusion list file name')
185220
parser.add_argument('-V', '--version', action='version', version=program_version_message)
@@ -189,9 +224,15 @@ def main(argv=None): # IGNORE:C0111
189224

190225
projectname = args.projectname
191226
projectversion = args.projectversion
227+
local_cve_remediation_file = args.local_remediation_list
228+
local_origin_exclusion_file = args.local_origin_exclusion_list
192229
process_cve_remediation = args.process_cve_remediation_list
193230
process_origin_exclulsion = args.process_origin_exclusion_list
194-
231+
#dry_run = args.dry_run
232+
#dry_run_output = args.dry_run_output
233+
dry_run = args.dry_run
234+
print(args.dry_run)
235+
195236
message = f"{program_version_message}\n\n Project: {projectname}\n Version: {projectversion}\n Process origin exclusion list: {process_origin_exclulsion}\n Process CVE remediation list: {process_cve_remediation}"
196237
print (message)
197238

@@ -203,26 +244,37 @@ def main(argv=None): # IGNORE:C0111
203244
hub = HubInstance()
204245
project = hub.get_project_by_name(projectname)
205246
version = hub.get_project_version_by_name(projectname, projectversion)
206-
custom_fields = hub.get_project_custom_fields (project)
247+
248+
custom_fields = hub.get_cf_values(project)
207249

208250
if (process_cve_remediation):
209-
cve_remediation_file = find_custom_field_value (custom_fields, args.cve_remediation_list_custom_field_label)
210-
print (f' Opening: {args.cve_remediation_list_custom_field_label}:{cve_remediation_file}')
251+
if (local_cve_remediation_file):
252+
cve_remediation_file = local_cve_remediation_file
253+
print (f' Opening CVE remediation file: {cve_remediation_file}')
254+
else:
255+
cve_remediation_file = find_custom_field_value (custom_fields, args.cve_remediation_list_custom_field_label)
256+
print (f' Opening: {args.cve_remediation_list_custom_field_label}:{cve_remediation_file}')
257+
211258
remediation_data = load_remediation_input(cve_remediation_file)
212259
else:
213260
remediation_data = None
214261

215262
if (process_origin_exclulsion):
216-
exclusion_list_file = find_custom_field_value (custom_fields, args.origin_exclusion_list_custom_field_label)
217-
print (f' Opening: {args.origin_exclusion_list_custom_field_label}:{exclusion_list_file}')
263+
if local_origin_exclusion_file:
264+
exclusion_list_file = local_origin_exclusion_file
265+
print (f' Opening origin exclusion list: {exclusion_list_file}')
266+
else:
267+
exclusion_list_file = find_custom_field_value (custom_fields, args.origin_exclusion_list_custom_field_label)
268+
print (f' Opening: {args.origin_exclusion_list_custom_field_label}:{exclusion_list_file}')
218269
exclusion_data = load_remediation_input(exclusion_list_file)
219270
else:
220271
exclusion_data = None
272+
221273

222274
# Retrieve the vulnerabiltites for the project version
223275
vulnerable_components = hub.get_vulnerable_bom_components(version)
224276

225-
process_vulnerabilities(hub, vulnerable_components, remediation_data, exclusion_data)
277+
process_vulnerabilities(hub, vulnerable_components, remediation_data, exclusion_data, dry_run)
226278

227279
return 0
228280
except Exception:

0 commit comments

Comments
 (0)