Skip to content

Commit d5f8478

Browse files
Vuln remediation scripts (#126)
* Added set_vulnerablity_remidation * Created vuln_redhat_remediate.py * Clean up * Clean up * Clean up * Started framework for vulnerability remediation on vendor provided input * Added remediation based on input file * Now fetches tags * Now outputs actual tags. * Updated comments * Added get_project_custom_fields to HubRestApi.py * Updated vuln_b atch_remediation to get remediation files from the project's custom fields. * Updated to process origin-exclusion list and output CSV formated results * Improved get_rhsa_opinion logic * Removed logic for patches, as KB has this info. * Debug code removed, version/date updated. * Added comments describing function. * Moving CSV examples to example folder. Co-authored-by: kumykov <[email protected]>
1 parent 9018182 commit d5f8478

File tree

6 files changed

+430
-4
lines changed

6 files changed

+430
-4
lines changed

blackduck/HubRestApi.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,10 +498,10 @@ def get_vulnerability_affected_projects(self, vulnerability):
498498
return response.json()
499499

500500
# TODO: Refactor this, i.e. use get_link method?
501-
def get_vulnerable_bom_components(self, version_obj, limit=9999):
501+
def get_vulnerable_bom_components(self, version_obj, limit=9999, offset=0):
502502
url = "{}/vulnerable-bom-components".format(version_obj['_meta']['href'])
503503
custom_headers = {'Accept': 'application/vnd.blackducksoftware.bill-of-materials-6+json'}
504-
param_string = self._get_parameter_string({'limit': limit})
504+
param_string = self._get_parameter_string({'limit': limit, 'offset': offset})
505505
url = "{}{}".format(url, param_string)
506506
response = self.execute_get(url, custom_headers=custom_headers)
507507
return response.json()
@@ -513,6 +513,14 @@ def get_component_remediation(self, bom_component):
513513
response = self.execute_get(url)
514514
return response.json()
515515

516+
def set_vulnerablity_remediation (self, vuln, remediation_status, remediation_comment):
517+
url = vuln['_meta']['href']
518+
update={}
519+
update['remediationStatus'] = remediation_status
520+
update['comment'] = remediation_comment
521+
response = self.execute_put(url, data=update)
522+
return response
523+
516524
##
517525
#
518526
# Lookup Black Duck (Hub) KB info given Protex KB info
@@ -806,6 +814,20 @@ def get_project_by_id(self, project_id, limit=100):
806814
response = requests.get(url, headers=headers, verify = not self.config['insecure'])
807815
jsondata = response.json()
808816
return jsondata
817+
818+
def get_project_custom_fields(self, project):
819+
headers = self.get_headers()
820+
url = self.get_link(project, "custom-fields")
821+
response = requests.get(url, headers=headers, verify = not self.config['insecure'])
822+
jsondata = response.json()
823+
return jsondata
824+
825+
def get_project_tags(self, project):
826+
headers = self.get_headers()
827+
url = self.get_tags_url(project)
828+
response = requests.get(url, headers=headers, verify = not self.config['insecure'])
829+
jsondata = response.json()
830+
return jsondata
809831

810832
def get_project_versions(self, project, limit=100, parameters={}):
811833
# paramstring = self.get_limit_paramstring(limit)

examples/get_project_tags.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727

2828
if 'totalCount' in projects and projects['totalCount'] == 1:
2929
project = projects['items'][0]
30+
tags = hub.get_project_tags(project)
3031
else:
31-
project = {'info': 'project {} not found'.format(args.project_name)}
32+
tags = {'info': 'project {} not found'.format(args.project_name)}
3233

33-
print(json.dumps(project))
34+
print(json.dumps(tags))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"ppc","IGNORED","Ignore PPC origins"
2+
"armv7hl","NEEDS_REVIEW","Review ARMV7HL origins"

examples/vuln_batch_remediation.py

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#!/usr/bin/python3
2+
# encoding: utf-8
3+
'''
4+
examples.vulnerability_remediation -- shortdesc
5+
examples.vulnerability_remediation is a description
6+
It defines classes_and_methods
7+
@author: user_name
8+
@copyright: 2020 organization_name. All rights reserved.
9+
@license: license
10+
@contact: user_email
11+
@deffield updated: Updated
12+
'''
13+
14+
'''
15+
This script updates vulnerablity remedation status for specific CVEs
16+
or specific origins in a Black Duck projec/version. The intent is to reduce the number of
17+
new vulnearblitites that need to be manually reviewed. The script only process
18+
vulnerablities that are currently have NEW remediation stautus. The script looks for two
19+
types of matches for vulnerablitites.
20+
They are:
21+
22+
o) Specific CVE - intended to apply remedation status for specific CVE
23+
o) Origin subtring - intended to apply remediation status for specific origins
24+
For example, origns for a particular processor architecture (PPC)
25+
26+
Each processing step can be turned on or off. At least one step must be run. Default
27+
is to run both.
28+
29+
The script get's it CVE and orign lists from CSV files. The CSV filenames are loaded
30+
from Custom Fields in the Black Duck project. This allows different groups of projects to
31+
use different remeidation settings. If a CVE remediation status should apply globally
32+
to all projects, Black Duck's global remediation feature should be used.
33+
34+
Here is an example of the CSV data for the CVE list:
35+
36+
"CVE-2016-1840","IGNORED","Applies only to Apple OS"
37+
"CVE-2019-15847","IGNORED","Applies to Power9 architecture"
38+
"CVE-2016-4606","IGNORED","Applies only to Apple OS"
39+
40+
The 1st column is used for exact matches to CVE ids on vulnerablitites.
41+
The 2nd column is the new remediation status. Thus must be a valid Black duck status.
42+
The 3rd column is a comment that will be added to the vulnerablity without change.
43+
44+
Here is an example of the CSV data for the origin exclusion list:
45+
46+
"ppc","IGNORED","Ignore PPC origins"
47+
"armv7hl","NEEDS_REVIEW","Review ARMV7HL origins"
48+
49+
The 1st column is used for substring match to OriginID
50+
The 2nd column is the new remediation status. Thus must be a valid Black duck status.
51+
The 3rd column is a comment that will be added to the vulnerablity without change.
52+
53+
If a vulnerablity matches both CVE and origin exclusion, the CVE remeditation is applied.
54+
The comment will be updated the value from both files.
55+
56+
Black Duck custom fields are used to hold the file names. The files are opened
57+
relative to the directory where the script is run. The default Custom Field labels
58+
the script looks for are:
59+
CVE Remediation List
60+
Origin Exclusion List
61+
The lables can be changed from the command line, if needed.
62+
63+
'''
64+
65+
import sys
66+
import os
67+
import json
68+
import csv
69+
import traceback
70+
71+
from argparse import ArgumentParser
72+
from argparse import RawDescriptionHelpFormatter
73+
74+
from blackduck.HubRestApi import HubInstance
75+
76+
77+
__all__ = []
78+
__version__ = 0.1
79+
__date__ = '2020-12-21'
80+
__updated__ = '2021-12-26'
81+
82+
83+
def load_remediation_input(remediation_file):
84+
with open(remediation_file, mode='r') as infile:
85+
reader = csv.reader(infile)
86+
return {rows[0]:[rows[1],rows[2]] for rows in reader}
87+
88+
def remediation_is_valid(vuln, remediation_data):
89+
vulnerability_name = vuln['vulnerabilityWithRemediation']['vulnerabilityName']
90+
# remediation_status = vuln['vulnerabilityWithRemediation']['remediationStatus']
91+
# remediation_comment = vuln['vulnerabilityWithRemediation'].get('remediationComment','')
92+
if vulnerability_name in remediation_data.keys():
93+
return remediation_data[vulnerability_name]
94+
else:
95+
return None
96+
97+
def origin_is_excluded (vuln, exclusion_data):
98+
if 'componentVersionOriginId' in vuln.keys():
99+
originId = vuln['componentVersionOriginId']
100+
for excludedOrigin in exclusion_data:
101+
if excludedOrigin in originId:
102+
return exclusion_data[excludedOrigin]
103+
return None
104+
else:
105+
return None
106+
107+
def find_custom_field_value (custom_fields, custom_field_label):
108+
for field in custom_fields['items']:
109+
if field['label'] == custom_field_label:
110+
if len(field['values']) > 0:
111+
return field['values'][0]
112+
else:
113+
print (f'Error: Custom Field \"{custom_field_label}\" is empty on Black Duck instance.')
114+
return None
115+
return None
116+
117+
def process_vulnerabilities(hub, vulnerable_components, remediation_data=None, exclusion_data=None):
118+
count = 0
119+
print('"Component Name","Component Version","Component OriginID","CVE","Reason","Remeidation Status","HTTP response code"')
120+
121+
for vuln in vulnerable_components['items']:
122+
if vuln['vulnerabilityWithRemediation']['remediationStatus'] == "NEW":
123+
if (remediation_data):
124+
remediation_action = remediation_is_valid(vuln, remediation_data)
125+
126+
if (exclusion_data):
127+
exclusion_action = origin_is_excluded(vuln, exclusion_data)
128+
129+
# If vuln has both a remdiation action and an origin exclusion action, set remdiation status
130+
# to the remdiation action. Append the exclusion action's comment to the overall comment.
131+
reason = 'CVE-list'
132+
if (remediation_action and exclusion_action):
133+
remediation_action[1] = exclusion_action[1] + '\n' + remediation_action[1]
134+
reason = 'CVE-list and origin-exclusion'
135+
elif (exclusion_action): # If only exclusion action found, use it to set remediation status
136+
remediation_action = exclusion_action
137+
reason = 'origin-exclusion'
138+
139+
if (remediation_action):
140+
resp = hub.set_vulnerablity_remediation(vuln, remediation_action[0],remediation_action[1])
141+
count += 1
142+
print ('\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"'.
143+
format(vuln['componentName'], vuln['componentVersionName'],
144+
vuln['componentVersionOriginId'],
145+
vuln['vulnerabilityWithRemediation']['vulnerabilityName'],
146+
reason, remediation_action[0], resp.status_code))
147+
print (f'Remediated {count} vulnerabilities.')
148+
149+
def main(argv=None): # IGNORE:C0111
150+
'''Command line options.'''
151+
152+
if argv is None:
153+
argv = sys.argv
154+
else:
155+
sys.argv.extend(argv)
156+
157+
program_name = os.path.basename(sys.argv[0])
158+
program_version = "v%s" % __version__
159+
program_build_date = str(__updated__)
160+
program_version_message = '%%(prog)s %s (%s)' % (program_version, program_build_date)
161+
program_shortdesc = __import__('__main__').__doc__.split("\n")[1]
162+
program_license = '''%s
163+
164+
Created by user_name on %s.
165+
Copyright 2020 Synopsys. All rights reserved.
166+
167+
Licensed under the Apache License 2.0
168+
http://www.apache.org/licenses/LICENSE-2.0
169+
170+
Distributed on an "AS IS" basis without warranties
171+
or conditions of any kind, either express or implied.
172+
173+
USAGE
174+
''' % (program_shortdesc, str(__date__))
175+
176+
try:
177+
# Setup argument parser
178+
parser = ArgumentParser(description=program_license, formatter_class=RawDescriptionHelpFormatter)
179+
parser.add_argument("projectname", help="Project nname")
180+
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")
183+
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')
184+
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')
185+
parser.add_argument('-V', '--version', action='version', version=program_version_message)
186+
187+
# Process arguments
188+
args = parser.parse_args()
189+
190+
projectname = args.projectname
191+
projectversion = args.projectversion
192+
process_cve_remediation = args.process_cve_remediation_list
193+
process_origin_exclulsion = args.process_origin_exclusion_list
194+
195+
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}"
196+
print (message)
197+
198+
if (process_cve_remediation == False) and (process_origin_exclulsion == False):
199+
print ('Error: Nothing to do, both --no-process-cve-remediation-list and --no-process-origin-exclusion-list set.')
200+
exit (1)
201+
202+
# Connect to Black Duck instance, retrive project, project version, and the project's custom fields.
203+
hub = HubInstance()
204+
project = hub.get_project_by_name(projectname)
205+
version = hub.get_project_version_by_name(projectname, projectversion)
206+
custom_fields = hub.get_project_custom_fields (project)
207+
208+
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}')
211+
remediation_data = load_remediation_input(cve_remediation_file)
212+
else:
213+
remediation_data = None
214+
215+
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}')
218+
exclusion_data = load_remediation_input(exclusion_list_file)
219+
else:
220+
exclusion_data = None
221+
222+
# Retrieve the vulnerabiltites for the project version
223+
vulnerable_components = hub.get_vulnerable_bom_components(version)
224+
225+
process_vulnerabilities(hub, vulnerable_components, remediation_data, exclusion_data)
226+
227+
return 0
228+
except Exception:
229+
### handle keyboard interrupt ###
230+
traceback.print_exc()
231+
return 0
232+
233+
if __name__ == "__main__":
234+
sys.exit(main())
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"CVE-2016-1840","IGNORED","Applies only to Apple OS"
2+
"CVE-2019-15847","IGNORED","Applies to Power9 architecture"
3+
"CVE-2016-4606","IGNORED","Applies only to Apple OS"
4+
"CVE-2014-9803","IGNORED","Android only"

0 commit comments

Comments
 (0)