bd-kernel-vulns 1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bd_kernel_vulns-1.0/LICENSE +21 -0
- bd_kernel_vulns-1.0/PKG-INFO +18 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/BOMClass.py +141 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/ComponentClass.py +199 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/ComponentListClass.py +32 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/ConfigClass.py +117 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/KernelSourceClass.py +35 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/VulnClass.py +261 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/VulnListClass.py +140 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/__init__.py +0 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/config.py +104 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/global_values.py +11 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns/main.py +67 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns.egg-info/PKG-INFO +18 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns.egg-info/SOURCES.txt +19 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns.egg-info/dependency_links.txt +1 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns.egg-info/entry_points.txt +2 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns.egg-info/requires.txt +4 -0
- bd_kernel_vulns-1.0/bd_kernel_vulns.egg-info/top_level.txt +1 -0
- bd_kernel_vulns-1.0/pyproject.toml +31 -0
- bd_kernel_vulns-1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Matthew Brady
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bd_kernel_vulns
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: bd_kernel_vulns - Script to process the Linux Kernel in a BD project to assess vulns applicability based on supplied list of kernel source files (or folders)
|
|
5
|
+
Author-email: Matthew Brady <mbrad@blackduck.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/matthewb66/bd_kernel_vulns
|
|
7
|
+
Project-URL: Issues, https://github.com/matthewb66/bd_kernel_vulns/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: blackduck>=1.1.3
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: aiohttp
|
|
17
|
+
Requires-Dist: asyncio
|
|
18
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# import config
|
|
2
|
+
from .ComponentListClass import ComponentList
|
|
3
|
+
# from ComponentClass import Component
|
|
4
|
+
from .VulnListClass import VulnList
|
|
5
|
+
# from . import global_values
|
|
6
|
+
# import logging
|
|
7
|
+
from blackduck import Client
|
|
8
|
+
import sys
|
|
9
|
+
# from tabulate import tabulate
|
|
10
|
+
# import aiohttp
|
|
11
|
+
import asyncio
|
|
12
|
+
import platform
|
|
13
|
+
# import re
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BOM:
|
|
17
|
+
def __init__(self, conf):
|
|
18
|
+
try:
|
|
19
|
+
self.complist = ComponentList()
|
|
20
|
+
self.vulnlist = VulnList()
|
|
21
|
+
self.bd = Client(
|
|
22
|
+
token=conf.bd_api,
|
|
23
|
+
base_url=conf.bd_url,
|
|
24
|
+
verify=(not conf.bd_trustcert), # TLS certificate verification
|
|
25
|
+
timeout=60
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
conf.logger.info(f"Working on project '{conf.bd_project}' version '{conf.bd_version}'")
|
|
29
|
+
|
|
30
|
+
self.bdver_dict = self.get_project(conf)
|
|
31
|
+
if not self.bd:
|
|
32
|
+
raise ValueError("Unable to create BOM object")
|
|
33
|
+
|
|
34
|
+
res = self.bd.list_resources(self.bdver_dict)
|
|
35
|
+
self.projver = res['href']
|
|
36
|
+
# thishref = f"{self.projver}/components"
|
|
37
|
+
#
|
|
38
|
+
# bom_arr = self.get_paginated_data(thishref, "application/vnd.blackducksoftware.bill-of-materials-6+json")
|
|
39
|
+
#
|
|
40
|
+
# for comp in bom_arr:
|
|
41
|
+
# if 'componentVersion' not in comp:
|
|
42
|
+
# continue
|
|
43
|
+
# # compver = comp['componentVersion']
|
|
44
|
+
#
|
|
45
|
+
# compclass = Component(comp['componentName'], comp['componentVersionName'], comp)
|
|
46
|
+
# self.complist.add(compclass)
|
|
47
|
+
#
|
|
48
|
+
except ValueError as v:
|
|
49
|
+
conf.logger.error(v)
|
|
50
|
+
sys.exit(-1)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
def get_paginated_data(self, url, accept_hdr):
|
|
54
|
+
headers = {
|
|
55
|
+
'accept': accept_hdr,
|
|
56
|
+
}
|
|
57
|
+
url = url + "?limit=1000"
|
|
58
|
+
res = self.bd.get_json(url, headers=headers)
|
|
59
|
+
if 'totalCount' in res and 'items' in res:
|
|
60
|
+
total_comps = res['totalCount']
|
|
61
|
+
else:
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
ret_arr = []
|
|
65
|
+
downloaded_comps = 0
|
|
66
|
+
while downloaded_comps < total_comps:
|
|
67
|
+
downloaded_comps += len(res['items'])
|
|
68
|
+
|
|
69
|
+
ret_arr += res['items']
|
|
70
|
+
|
|
71
|
+
newurl = f"{url}&offset={downloaded_comps}"
|
|
72
|
+
res = self.bd.get_json(newurl, headers=headers)
|
|
73
|
+
if 'totalCount' not in res or 'items' not in res:
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
return ret_arr
|
|
77
|
+
|
|
78
|
+
def get_project(self, conf):
|
|
79
|
+
params = {
|
|
80
|
+
'q': "name:" + conf.bd_project,
|
|
81
|
+
'sort': 'name',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ver_dict = None
|
|
85
|
+
projects = self.bd.get_resource('projects', params=params)
|
|
86
|
+
for p in projects:
|
|
87
|
+
if p['name'] == conf.bd_project:
|
|
88
|
+
versions = self.bd.get_resource('versions', parent=p, params=params)
|
|
89
|
+
for v in versions:
|
|
90
|
+
if v['versionName'] == conf.bd_version:
|
|
91
|
+
ver_dict = v
|
|
92
|
+
break
|
|
93
|
+
break
|
|
94
|
+
else:
|
|
95
|
+
conf.logger.error(f"Version '{conf.bd_version}' does not exist in project '{conf.bd_project}'")
|
|
96
|
+
sys.exit(2)
|
|
97
|
+
|
|
98
|
+
if ver_dict is None:
|
|
99
|
+
conf.logger.warning(f"Project '{conf.bd_project}' does not exist")
|
|
100
|
+
sys.exit(2)
|
|
101
|
+
|
|
102
|
+
return ver_dict
|
|
103
|
+
|
|
104
|
+
def get_vulns(self, conf):
|
|
105
|
+
vuln_url = f"{self.projver}/vulnerable-bom-components"
|
|
106
|
+
vuln_arr = self.get_paginated_data(vuln_url, "application/vnd.blackducksoftware.bill-of-materials-8+json")
|
|
107
|
+
self.vulnlist.add_comp_data(vuln_arr, conf)
|
|
108
|
+
|
|
109
|
+
def process_data_async(self, conf):
|
|
110
|
+
if platform.system() == "Windows":
|
|
111
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
112
|
+
|
|
113
|
+
self.vulnlist.add_vuln_data(asyncio.run(self.vulnlist.async_get_vuln_data(self.bd, conf)), conf)
|
|
114
|
+
|
|
115
|
+
def ignore_vulns_async(self):
|
|
116
|
+
if platform.system() == "Windows":
|
|
117
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
118
|
+
|
|
119
|
+
data = asyncio.run(self.vulnlist.async_ignore_vulns(self.bd))
|
|
120
|
+
return len(data)
|
|
121
|
+
|
|
122
|
+
def ignore_vulns(self, conf): # DEBUG
|
|
123
|
+
self.vulnlist.ignore_vulns(self.bd, conf)
|
|
124
|
+
|
|
125
|
+
def process_kernel_vulns(self, conf, kfiles):
|
|
126
|
+
self.vulnlist.process_kernel_vulns(conf, kfiles)
|
|
127
|
+
|
|
128
|
+
# def count_comps(self):
|
|
129
|
+
# return len(self.complist)
|
|
130
|
+
|
|
131
|
+
def count_vulns(self):
|
|
132
|
+
return self.vulnlist.count()
|
|
133
|
+
|
|
134
|
+
def count_in_kernel_vulns(self):
|
|
135
|
+
return self.vulnlist.count_in_kernel()
|
|
136
|
+
|
|
137
|
+
def count_not_in_kernel_vulns(self):
|
|
138
|
+
return self.vulnlist.count() - self.vulnlist.count_in_kernel()
|
|
139
|
+
|
|
140
|
+
def check_kernel_comp(self):
|
|
141
|
+
return self.complist.check_kernel()
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# import global_values
|
|
2
|
+
import re
|
|
3
|
+
# import global_values
|
|
4
|
+
# import logging
|
|
5
|
+
# from thefuzz import fuzz
|
|
6
|
+
from .VulnListClass import VulnList
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Component:
|
|
10
|
+
def __init__(self, name, version):
|
|
11
|
+
self.name = name
|
|
12
|
+
self.version = version
|
|
13
|
+
self.vulnlist = VulnList()
|
|
14
|
+
self.data = None
|
|
15
|
+
|
|
16
|
+
# def get_matchtypes(self):
|
|
17
|
+
# try:
|
|
18
|
+
# return self.data['matchTypes']
|
|
19
|
+
# except KeyError:
|
|
20
|
+
# return []
|
|
21
|
+
#
|
|
22
|
+
# def is_dependency(self):
|
|
23
|
+
# dep_types = ['FILE_DEPENDENCY_DIRECT', 'FILE_DEPENDENCY_TRANSITIVE']
|
|
24
|
+
# match_types = self.get_matchtypes()
|
|
25
|
+
# for m in dep_types:
|
|
26
|
+
# if m in match_types:
|
|
27
|
+
# return True
|
|
28
|
+
# return False
|
|
29
|
+
#
|
|
30
|
+
# def is_signature(self):
|
|
31
|
+
# sig_types = ['FILE_EXACT', 'FILE_SOME_FILES_MODIFIED', 'FILE_FILES_ADDED_DELETED_AND_MODIFIED',
|
|
32
|
+
# 'FILE_EXACT_FILE_MATCH']
|
|
33
|
+
# match_types = self.get_matchtypes()
|
|
34
|
+
# for m in sig_types:
|
|
35
|
+
# if m in match_types:
|
|
36
|
+
# return True
|
|
37
|
+
# return False
|
|
38
|
+
|
|
39
|
+
def is_ignored(self):
|
|
40
|
+
try:
|
|
41
|
+
return self.data['ignored']
|
|
42
|
+
except KeyError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
# def process_signatures(self):
|
|
46
|
+
# all_paths_ignoreable = True
|
|
47
|
+
# unmatched = False
|
|
48
|
+
# reason = ''
|
|
49
|
+
# for sigentry in self.sigentry_arr:
|
|
50
|
+
# ignore, reason = sigentry.filter_folders()
|
|
51
|
+
# if not ignore:
|
|
52
|
+
# all_paths_ignoreable = False
|
|
53
|
+
# else:
|
|
54
|
+
# self.sigentry_arr.remove(sigentry)
|
|
55
|
+
#
|
|
56
|
+
# if all_paths_ignoreable:
|
|
57
|
+
# # Ignore
|
|
58
|
+
# reason = f"Mark IGNORED - {reason}"
|
|
59
|
+
# self.reason = reason
|
|
60
|
+
# logging.debug(f"- Component {self.filter_name}/{self.version}: {reason}")
|
|
61
|
+
# self.set_ignore()
|
|
62
|
+
# else:
|
|
63
|
+
# # print(f"NOT Ignoring {self.name}/{self.version}")
|
|
64
|
+
# self.sig_match_result = 0
|
|
65
|
+
# set_reviewed = False
|
|
66
|
+
# ignore = True
|
|
67
|
+
# unmatched = True
|
|
68
|
+
# reason = f"No Action - component name '{self.oriname_arr}' not found in signature paths"
|
|
69
|
+
# for sigentry in self.sigentry_arr:
|
|
70
|
+
# # compname_found, compver_found,\
|
|
71
|
+
# # new_match_result = sigentry.search_component(self.filter_name, self.filter_version)
|
|
72
|
+
# compname_found, compver_found,\
|
|
73
|
+
# new_match_result = sigentry.search_component(self.oriname_arr, self.filter_version)
|
|
74
|
+
# logging.debug(f"Compname in path {compname_found}, Version in path {compver_found}, "
|
|
75
|
+
# f"Match result {new_match_result}, Path '{sigentry.path}'")
|
|
76
|
+
#
|
|
77
|
+
# if compver_found:
|
|
78
|
+
# self.compver_found = True
|
|
79
|
+
# ignore = False
|
|
80
|
+
# unmatched = False
|
|
81
|
+
# if compname_found:
|
|
82
|
+
# self.compname_found = True
|
|
83
|
+
# if global_values.version_match_reqd:
|
|
84
|
+
# if compver_found:
|
|
85
|
+
# set_reviewed = True
|
|
86
|
+
# ignore = False
|
|
87
|
+
# unmatched = False
|
|
88
|
+
# else:
|
|
89
|
+
# reason = f"No Action - component version {self.filter_version} not found
|
|
90
|
+
# (and required because --version_match_reqd set)"
|
|
91
|
+
# elif compname_found:
|
|
92
|
+
# set_reviewed = True
|
|
93
|
+
# ignore = False
|
|
94
|
+
# unmatched = False
|
|
95
|
+
# if new_match_result > self.sig_match_result:
|
|
96
|
+
# self.sig_match_result = new_match_result
|
|
97
|
+
# self.best_sigpath = sigentry.path
|
|
98
|
+
# # print(self.name, self.version, src['commentPath'])
|
|
99
|
+
# if set_reviewed:
|
|
100
|
+
# if self.compver_found:
|
|
101
|
+
# reason = f"Mark REVIEWED - Compname & version in path '{self.best_sigpath}'
|
|
102
|
+
# (Match result {self.sig_match_result})"
|
|
103
|
+
# elif self.compname_found:
|
|
104
|
+
# reason = f"Mark REVIEWED - Compname {self.oriname_arr} in path '{self.best_sigpath}'
|
|
105
|
+
# (Match result {self.sig_match_result})"
|
|
106
|
+
#
|
|
107
|
+
# logging.debug(f"- Component {self.name}/{self.version}: {reason}")
|
|
108
|
+
# self.set_reviewed()
|
|
109
|
+
# unmatched = False
|
|
110
|
+
# if ignore and global_values.ignore_no_path_matches:
|
|
111
|
+
# self.set_ignore()
|
|
112
|
+
# reason = f"Mark IGNORED - compname or version not found in paths & --ignore_no_path_matches set"
|
|
113
|
+
#
|
|
114
|
+
# self.reason = reason
|
|
115
|
+
# self.unmatched = unmatched
|
|
116
|
+
|
|
117
|
+
# @staticmethod
|
|
118
|
+
# def filter_name_string(name, logger):
|
|
119
|
+
# # Remove common words
|
|
120
|
+
# # - for, with, in, on,
|
|
121
|
+
# # Remove strings in brackets
|
|
122
|
+
# # Replace / with space
|
|
123
|
+
# ret_name = re.sub(r"\(.*\)", r"", name)
|
|
124
|
+
# for rep in [r" for ", r" with ", r" in ", r" on ", r" a ", r" the ", r" by ",
|
|
125
|
+
# r" and ", r"^apache | apache | apache$", r" bundle ", r" only | only$", r" from ",
|
|
126
|
+
# r" to ", r" - "]:
|
|
127
|
+
# ret_name = re.sub(rep, " ", ret_name, flags=re.IGNORECASE)
|
|
128
|
+
# ret_name = re.sub(r"[/@#:]", " ", ret_name)
|
|
129
|
+
# ret_name = re.sub(r" \w$| \w |^\w ", r" ", ret_name)
|
|
130
|
+
# ret_name = ret_name.replace("::", " ")
|
|
131
|
+
# ret_name = re.sub(r" +", r" ", ret_name)
|
|
132
|
+
# ret_name = re.sub(r"^ ", r"", ret_name)
|
|
133
|
+
# ret_name = re.sub(r" $", r"", ret_name)
|
|
134
|
+
#
|
|
135
|
+
# debug(f"filter_name_string(): Compname '{name}' replaced with '{ret_name}'")
|
|
136
|
+
# return ret_name.lower()
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def filter_version_string(version):
|
|
140
|
+
# Remove +git*
|
|
141
|
+
# Remove -snapshot*
|
|
142
|
+
# Replace / with space
|
|
143
|
+
ret_version = re.sub(r"\+git.*", r"", version, flags=re.IGNORECASE)
|
|
144
|
+
ret_version = re.sub(r"-snapshot.*", r"", ret_version, flags=re.IGNORECASE)
|
|
145
|
+
ret_version = re.sub(r"/", r" ", ret_version)
|
|
146
|
+
ret_version = re.sub(r"^v", r"", ret_version, flags=re.IGNORECASE)
|
|
147
|
+
ret_version = re.sub(r"\+*", r"", ret_version, flags=re.IGNORECASE)
|
|
148
|
+
|
|
149
|
+
return ret_version.lower()
|
|
150
|
+
|
|
151
|
+
def get_compid(self):
|
|
152
|
+
try:
|
|
153
|
+
compurl = self.data['component']
|
|
154
|
+
return compurl.split('/')[-1]
|
|
155
|
+
except KeyError:
|
|
156
|
+
return ''
|
|
157
|
+
|
|
158
|
+
# def print_origins(self):
|
|
159
|
+
# try:
|
|
160
|
+
# for ori in self.data['origins']:
|
|
161
|
+
# print(f"Comp '{self.name}/{self.version}' Origin '{ori['externalId']}' Name '{ori['name']}'")
|
|
162
|
+
# except KeyError:
|
|
163
|
+
# print(f"Comp '{self.name}/{self.version}' No Origin")
|
|
164
|
+
#
|
|
165
|
+
# def get_origin_compnames(self):
|
|
166
|
+
# compnames_arr = []
|
|
167
|
+
# try:
|
|
168
|
+
# for ori_entry in self.data['origins']:
|
|
169
|
+
# ori = ori_entry['externalId']
|
|
170
|
+
# ori_ver = ori_entry['name']
|
|
171
|
+
# ori_string = ori.replace(f"{ori_ver}", '')
|
|
172
|
+
# arr = re.split(r"[:/#]", ori_string)
|
|
173
|
+
# new_name = arr[-2].lower()
|
|
174
|
+
# if new_name not in compnames_arr:
|
|
175
|
+
# logging.debug(
|
|
176
|
+
# f"Comp '{self.name}/{self.version}' Compname calculate from origin '{new_name}' -
|
|
177
|
+
# origin='{ori}'")
|
|
178
|
+
# compnames_arr.append(new_name)
|
|
179
|
+
# if self.filter_name.find(' ') == -1:
|
|
180
|
+
# # Single word component name
|
|
181
|
+
# if self.filter_name not in compnames_arr:
|
|
182
|
+
# compnames_arr.append(self.filter_name.lower())
|
|
183
|
+
# except (KeyError, IndexError):
|
|
184
|
+
# logging.debug(f"Comp '{self.name}/{self.version}' Compname calculate from compname only '{self.name}'")
|
|
185
|
+
# compnames_arr.append(self.filter_name.lower())
|
|
186
|
+
# return compnames_arr
|
|
187
|
+
#
|
|
188
|
+
# def get_sigpaths(self):
|
|
189
|
+
# data = ''
|
|
190
|
+
# count = 0
|
|
191
|
+
# for sigentry in self.sigentry_arr:
|
|
192
|
+
# data += f"{sigentry.get_sigpath()}\n"
|
|
193
|
+
# count += 1
|
|
194
|
+
# return data
|
|
195
|
+
|
|
196
|
+
def check_kernel(self):
|
|
197
|
+
if self.name == 'Linux Kernel':
|
|
198
|
+
return True
|
|
199
|
+
return False
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# from Component import Component
|
|
2
|
+
# import global_values
|
|
3
|
+
# import logging
|
|
4
|
+
# import requests
|
|
5
|
+
|
|
6
|
+
class ComponentList:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.components = []
|
|
9
|
+
|
|
10
|
+
def add(self, comp):
|
|
11
|
+
self.components.append(comp)
|
|
12
|
+
|
|
13
|
+
def count(self):
|
|
14
|
+
return len(self.components)
|
|
15
|
+
|
|
16
|
+
def count_ignored(self):
|
|
17
|
+
count = 0
|
|
18
|
+
for comp in self.components:
|
|
19
|
+
if comp.is_ignored():
|
|
20
|
+
count += 1
|
|
21
|
+
return count
|
|
22
|
+
|
|
23
|
+
def get_vulns(self):
|
|
24
|
+
for comp in self.components:
|
|
25
|
+
comp.get_vulns()
|
|
26
|
+
|
|
27
|
+
def check_kernel(self):
|
|
28
|
+
for comp in self.components:
|
|
29
|
+
if comp.is_kernel():
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
return False
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.bd_api = ''
|
|
9
|
+
self.bd_url = ''
|
|
10
|
+
self.bd_trustcert = False
|
|
11
|
+
|
|
12
|
+
self.bd_project = ''
|
|
13
|
+
self.bd_version = ''
|
|
14
|
+
self.logger = None
|
|
15
|
+
self.logfile = ''
|
|
16
|
+
self.kernel_source_file = ''
|
|
17
|
+
self.folders = False
|
|
18
|
+
self.debug = False
|
|
19
|
+
|
|
20
|
+
def get_cli_args(self):
|
|
21
|
+
parser = argparse.ArgumentParser(description='Black Duck vulns', prog='bd_vulns')
|
|
22
|
+
|
|
23
|
+
# parser.add_argument("projfolder", nargs="?", help="Yocto project folder to analyse", default=".")
|
|
24
|
+
|
|
25
|
+
parser.add_argument("--blackduck_url", type=str, help="Black Duck server URL (REQUIRED)", default="")
|
|
26
|
+
parser.add_argument("--blackduck_api_token", type=str, help="Black Duck API token (REQUIRED)", default="")
|
|
27
|
+
parser.add_argument("--blackduck_trust_cert", help="Black Duck trust server cert", action='store_true')
|
|
28
|
+
parser.add_argument("-p", "--project", help="Black Duck project to process (REQUIRED)", default="")
|
|
29
|
+
parser.add_argument("-v", "--version", help="Black Duck project version to process (REQUIRED)", default="")
|
|
30
|
+
parser.add_argument("--debug", help="Debug logging mode", action='store_true')
|
|
31
|
+
parser.add_argument("--logfile", help="Logging output file", default="")
|
|
32
|
+
parser.add_argument("-k", "--kernel_source_file", help="Kernel source files list (REQUIRED)", default="")
|
|
33
|
+
parser.add_argument("--folders", help="Kernel Source file only contains folders to be used to map vulns",
|
|
34
|
+
action='store_true')
|
|
35
|
+
|
|
36
|
+
args = parser.parse_args()
|
|
37
|
+
|
|
38
|
+
terminate = False
|
|
39
|
+
if args.debug:
|
|
40
|
+
loglevel = logging.DEBUG
|
|
41
|
+
else:
|
|
42
|
+
loglevel = logging.INFO
|
|
43
|
+
# global_values.logging_level = loglevel
|
|
44
|
+
self.logfile = args.logfile
|
|
45
|
+
|
|
46
|
+
self.logger = self.setup_logger('kernel-vulns', loglevel)
|
|
47
|
+
|
|
48
|
+
self.logger.debug("ARGUMENTS:")
|
|
49
|
+
for arg in vars(args):
|
|
50
|
+
self.logger.debug(f"--{arg}={getattr(args, arg)}")
|
|
51
|
+
self.logger.debug('')
|
|
52
|
+
|
|
53
|
+
url = os.environ.get('BLACKDUCK_URL')
|
|
54
|
+
if args.blackduck_url != '':
|
|
55
|
+
self.bd_url = args.blackduck_url
|
|
56
|
+
elif url is not None:
|
|
57
|
+
self.bd_url = url
|
|
58
|
+
else:
|
|
59
|
+
self.logger.error("Black Duck URL not specified")
|
|
60
|
+
terminate = True
|
|
61
|
+
|
|
62
|
+
if args.project != "" and args.version != "":
|
|
63
|
+
self.bd_project = args.project
|
|
64
|
+
self.bd_version = args.version
|
|
65
|
+
else:
|
|
66
|
+
self.logger.error("Black Duck project/version not specified")
|
|
67
|
+
terminate = True
|
|
68
|
+
|
|
69
|
+
api = os.environ.get('BLACKDUCK_API_TOKEN')
|
|
70
|
+
if args.blackduck_api_token != '':
|
|
71
|
+
self.bd_api = args.blackduck_api_token
|
|
72
|
+
elif api is not None:
|
|
73
|
+
self.bd_api = api
|
|
74
|
+
else:
|
|
75
|
+
self.logger.error("Black Duck API Token not specified")
|
|
76
|
+
terminate = True
|
|
77
|
+
|
|
78
|
+
trustcert = os.environ.get('BLACKDUCK_TRUST_CERT')
|
|
79
|
+
if trustcert == 'true' or args.blackduck_trust_cert:
|
|
80
|
+
self.bd_trustcert = True
|
|
81
|
+
|
|
82
|
+
if args.kernel_source_file != '':
|
|
83
|
+
if not os.path.exists(args.kernel_source_file):
|
|
84
|
+
self.logger.error(f"Supplied kernel source list file '{args.kernel_source_file}' does not exist")
|
|
85
|
+
terminate = True
|
|
86
|
+
else:
|
|
87
|
+
self.kernel_source_file = args.kernel_source_file
|
|
88
|
+
else:
|
|
89
|
+
self.logger.error(f"Kernel source list file required (--kernel_source_list)")
|
|
90
|
+
terminate = True
|
|
91
|
+
|
|
92
|
+
if args.folders == 'true':
|
|
93
|
+
self.folders = True
|
|
94
|
+
|
|
95
|
+
if terminate:
|
|
96
|
+
return False
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def setup_logger(self, name: str, level) -> logging.Logger:
|
|
100
|
+
logger = logging.getLogger(name)
|
|
101
|
+
logger.setLevel(level)
|
|
102
|
+
|
|
103
|
+
if not logger.hasHandlers(): # Avoid duplicate handlers
|
|
104
|
+
formatter = logging.Formatter(
|
|
105
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
console_handler = logging.StreamHandler()
|
|
109
|
+
console_handler.setFormatter(formatter)
|
|
110
|
+
logger.addHandler(console_handler)
|
|
111
|
+
|
|
112
|
+
if self.logfile != '':
|
|
113
|
+
file_handler = logging.FileHandler(self.logfile)
|
|
114
|
+
file_handler.setFormatter(formatter)
|
|
115
|
+
logger.addHandler(file_handler)
|
|
116
|
+
|
|
117
|
+
return logger
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# import global_values
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KernelSource:
|
|
5
|
+
def __init__(self, conf):
|
|
6
|
+
self.file_arr = []
|
|
7
|
+
self.folders = conf.folders
|
|
8
|
+
try:
|
|
9
|
+
with open(conf.kernel_source_file) as klfile:
|
|
10
|
+
lines = klfile.readlines()
|
|
11
|
+
except FileExistsError:
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
for line in lines:
|
|
15
|
+
line = line.strip()
|
|
16
|
+
if not self.folders:
|
|
17
|
+
if line.endswith('.c') or line.endswith('.h'):
|
|
18
|
+
self.file_arr.append(line)
|
|
19
|
+
else:
|
|
20
|
+
self.file_arr.append(line)
|
|
21
|
+
|
|
22
|
+
def check_files(self, f_arr):
|
|
23
|
+
for f in f_arr:
|
|
24
|
+
for kf in self.file_arr:
|
|
25
|
+
if not self.folders:
|
|
26
|
+
if kf.endswith(f):
|
|
27
|
+
return True
|
|
28
|
+
else:
|
|
29
|
+
folder = f + '/'
|
|
30
|
+
if kf.find(folder) != -1:
|
|
31
|
+
return True
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def count(self):
|
|
35
|
+
return len(self.file_arr)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# from . import global_values
|
|
2
|
+
# import config
|
|
3
|
+
import re
|
|
4
|
+
import datetime
|
|
5
|
+
# import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Vuln:
|
|
9
|
+
def __init__(self, data, conf, id='', cve_data=None):
|
|
10
|
+
self.comp_vuln_data = data
|
|
11
|
+
self.bdsa_data = None
|
|
12
|
+
self.cve_data = cve_data
|
|
13
|
+
self.linked_cve_data = None
|
|
14
|
+
self.id = id
|
|
15
|
+
self.in_kernel = True
|
|
16
|
+
if self.id == '':
|
|
17
|
+
# self.id = self.get_id()
|
|
18
|
+
if 'vulnerability' in self.comp_vuln_data:
|
|
19
|
+
self.id = self.comp_vuln_data['vulnerability']['vulnerabilityId']
|
|
20
|
+
else:
|
|
21
|
+
conf.logger.error('Unable to determine vuln id')
|
|
22
|
+
|
|
23
|
+
def get_id(self):
|
|
24
|
+
return self.id
|
|
25
|
+
|
|
26
|
+
def status(self):
|
|
27
|
+
try:
|
|
28
|
+
return self.comp_vuln_data['vulnerability']['remediationStatus']
|
|
29
|
+
except KeyError:
|
|
30
|
+
return ''
|
|
31
|
+
|
|
32
|
+
def severity(self):
|
|
33
|
+
try:
|
|
34
|
+
return self.comp_vuln_data['vulnerability']['severity']
|
|
35
|
+
except KeyError:
|
|
36
|
+
return ''
|
|
37
|
+
|
|
38
|
+
def related_vuln(self):
|
|
39
|
+
try:
|
|
40
|
+
return self.comp_vuln_data['vulnerability']['relatedVulnerability'].split('/')[-1]
|
|
41
|
+
except KeyError:
|
|
42
|
+
return ''
|
|
43
|
+
|
|
44
|
+
def component(self):
|
|
45
|
+
try:
|
|
46
|
+
return f"{self.comp_vuln_data['componentName']}/{self.comp_vuln_data['componentVersionName']}"
|
|
47
|
+
except KeyError:
|
|
48
|
+
return ''
|
|
49
|
+
|
|
50
|
+
def get_linked_vuln(self):
|
|
51
|
+
# vuln_url = f"{bd.base_url}/api/vulnerabilities/{self.id()}"
|
|
52
|
+
# vuln_data = self.get_data(bd, vuln_url, "application/vnd.blackducksoftware.vulnerability-4+json")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
if self.get_vuln_source() == 'BDSA':
|
|
56
|
+
if self.comp_vuln_data['vulnerability']['relatedVulnerability'] != '':
|
|
57
|
+
cve = self.comp_vuln_data['vulnerability']['relatedVulnerability'].split("/")[-1]
|
|
58
|
+
return cve
|
|
59
|
+
|
|
60
|
+
# for x in self.comp_vuln_data['_meta']['links']:
|
|
61
|
+
# if x['rel'] == 'related-vulnerability':
|
|
62
|
+
# if x['label'] == 'NVD':
|
|
63
|
+
# cve = x['href'].split("/")[-1]
|
|
64
|
+
# return cve
|
|
65
|
+
# break
|
|
66
|
+
else:
|
|
67
|
+
return ''
|
|
68
|
+
except KeyError:
|
|
69
|
+
return ''
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def get_data(bd, url, accept_hdr):
|
|
73
|
+
headers = {
|
|
74
|
+
'accept': accept_hdr,
|
|
75
|
+
}
|
|
76
|
+
res = bd.get_json(url, headers=headers)
|
|
77
|
+
return res
|
|
78
|
+
|
|
79
|
+
def get_component(self):
|
|
80
|
+
try:
|
|
81
|
+
return self.comp_vuln_data['componentName']
|
|
82
|
+
except KeyError:
|
|
83
|
+
return ''
|
|
84
|
+
|
|
85
|
+
def vuln_url(self, bd):
|
|
86
|
+
return f"{bd.base_url}/api/vulnerabilities/{self.get_id()}"
|
|
87
|
+
|
|
88
|
+
def url(self):
|
|
89
|
+
try:
|
|
90
|
+
return self.comp_vuln_data['_meta']['href']
|
|
91
|
+
except KeyError:
|
|
92
|
+
return ''
|
|
93
|
+
|
|
94
|
+
def get_associated_vuln_url(self, bd):
|
|
95
|
+
return f"{bd.base_url}/api/vulnerabilities/{self.get_linked_vuln()}"
|
|
96
|
+
|
|
97
|
+
def is_ignored(self):
|
|
98
|
+
if self.comp_vuln_data['ignored']:
|
|
99
|
+
return True
|
|
100
|
+
if 'vulnerability' in self.comp_vuln_data:
|
|
101
|
+
if self.comp_vuln_data['vulnerability']['remediationStatus'] == 'IGNORED':
|
|
102
|
+
return True
|
|
103
|
+
else:
|
|
104
|
+
return False
|
|
105
|
+
else:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def add_data(self, data):
|
|
109
|
+
try:
|
|
110
|
+
if data['source'] == 'BDSA':
|
|
111
|
+
self.bdsa_data = data
|
|
112
|
+
elif data['source'] == 'NVD':
|
|
113
|
+
self.cve_data = data
|
|
114
|
+
except KeyError:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
def add_linked_cve_data(self, data):
|
|
118
|
+
self.linked_cve_data = data
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def find_sourcefile(sline):
|
|
122
|
+
pattern = r'[\w/\.-]+\.[ch]\b'
|
|
123
|
+
res = re.findall(pattern, sline)
|
|
124
|
+
arr = []
|
|
125
|
+
for str in res:
|
|
126
|
+
if str not in arr:
|
|
127
|
+
arr.append(str)
|
|
128
|
+
return arr
|
|
129
|
+
|
|
130
|
+
def get_vuln_source(self):
|
|
131
|
+
try:
|
|
132
|
+
if 'source' in self.comp_vuln_data and self.comp_vuln_data['source'] != '':
|
|
133
|
+
return self.comp_vuln_data['source']
|
|
134
|
+
elif self.get_id().startswith('BDSA-'):
|
|
135
|
+
return 'BDSA'
|
|
136
|
+
elif self.get_id().startswith('CVE-'):
|
|
137
|
+
return 'NVD'
|
|
138
|
+
else:
|
|
139
|
+
return ''
|
|
140
|
+
|
|
141
|
+
except KeyError:
|
|
142
|
+
return ''
|
|
143
|
+
|
|
144
|
+
def process_kernel_vuln(self, conf):
|
|
145
|
+
try:
|
|
146
|
+
sourcefiles = []
|
|
147
|
+
if self.get_vuln_source() == 'NVD':
|
|
148
|
+
sourcefiles = self.find_sourcefile(self.cve_data['description'])
|
|
149
|
+
if len(sourcefiles) == 0:
|
|
150
|
+
desc = self.cve_data['description'].replace('\n', ' ')
|
|
151
|
+
conf.logger.debug(f"CVE {self.get_id()} - Description: {desc}")
|
|
152
|
+
elif self.get_vuln_source() == 'BDSA':
|
|
153
|
+
sourcefiles = self.find_sourcefile(self.bdsa_data['description'])
|
|
154
|
+
if len(sourcefiles) == 0:
|
|
155
|
+
sourcefiles = self.find_sourcefile(self.bdsa_data['technicalDescription'])
|
|
156
|
+
if len(sourcefiles) == 0:
|
|
157
|
+
bdsa = self.bdsa_data['description'].replace('\n', ' ')
|
|
158
|
+
conf.logger.debug(f"BDSA {self.get_id()} - Description: {bdsa}")
|
|
159
|
+
tech = self.bdsa_data['technicalDescription'].replace('\n', ' ')
|
|
160
|
+
conf.logger.debug(f"BDSA {self.get_id()} - Technical Description: {tech}")
|
|
161
|
+
if self.linked_cve_data:
|
|
162
|
+
# No source file found - need to check for linked CVE
|
|
163
|
+
sourcefiles = self.find_sourcefile(self.linked_cve_data['description'])
|
|
164
|
+
cve = self.linked_cve_data['description'].replace('\n', ' ')
|
|
165
|
+
conf.logger.debug(f"Linked CVE Description: {cve}")
|
|
166
|
+
|
|
167
|
+
return sourcefiles
|
|
168
|
+
# print(f"{self.get_id()}: {sourcefile}")
|
|
169
|
+
except KeyError:
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
def is_kernel_vuln(self):
|
|
173
|
+
if self.comp_vuln_data['componentName'] == 'Linux Kernel':
|
|
174
|
+
return True
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def set_not_in_kernel(self):
|
|
178
|
+
self.in_kernel = False
|
|
179
|
+
|
|
180
|
+
def ignore_vuln(self, bd, logger):
|
|
181
|
+
try:
|
|
182
|
+
# vuln_name = comp['vulnerabilityWithRemediation']['vulnerabilityName']
|
|
183
|
+
x = datetime.datetime.now()
|
|
184
|
+
mydate = x.strftime("%x %X")
|
|
185
|
+
|
|
186
|
+
payload = self.comp_vuln_data
|
|
187
|
+
# payload['remediationJustification'] = "NO_CODE"
|
|
188
|
+
payload[
|
|
189
|
+
'comment'] = (f"Remediated by bd-vulns utility {mydate} - "
|
|
190
|
+
f"vuln refers to source file not compiled in kernel")
|
|
191
|
+
payload['remediationStatus'] = "IGNORED"
|
|
192
|
+
|
|
193
|
+
# result = hub.execute_put(comp['_meta']['href'], data=comp)
|
|
194
|
+
href = self.comp_vuln_data['_meta']['href']
|
|
195
|
+
# href = '/'.join(href.split('/')[3:])
|
|
196
|
+
r = bd.session.put(href, json=self.comp_vuln_data)
|
|
197
|
+
r.raise_for_status()
|
|
198
|
+
if r.status_code != 202:
|
|
199
|
+
raise Exception(f"PUT returned {r.status_code}")
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error("Unable to update vulnerabilities via API\n" + str(e))
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
async def async_get_vuln_data(self, bd, conf, session, token):
|
|
207
|
+
if conf.bd_trustcert:
|
|
208
|
+
ssl = False
|
|
209
|
+
else:
|
|
210
|
+
ssl = None
|
|
211
|
+
|
|
212
|
+
headers = {
|
|
213
|
+
# 'accept': "application/vnd.blackducksoftware.bill-of-materials-6+json",
|
|
214
|
+
'Authorization': f'Bearer {token}',
|
|
215
|
+
}
|
|
216
|
+
# resp = globals.bd.get_json(thishref, headers=headers)
|
|
217
|
+
async with session.get(self.vuln_url(bd), headers=headers, ssl=ssl) as resp:
|
|
218
|
+
result_data = await resp.json()
|
|
219
|
+
return self.get_id(), result_data
|
|
220
|
+
|
|
221
|
+
async def async_get_associated_vuln_data(self, bd, conf, session, token):
|
|
222
|
+
if conf.bd_trustcert:
|
|
223
|
+
ssl = False
|
|
224
|
+
else:
|
|
225
|
+
ssl = None
|
|
226
|
+
|
|
227
|
+
headers = {
|
|
228
|
+
# 'accept': "application/vnd.blackducksoftware.bill-of-materials-6+json",
|
|
229
|
+
'Authorization': f'Bearer {token}',
|
|
230
|
+
}
|
|
231
|
+
# resp = globals.bd.get_json(thishref, headers=headers)
|
|
232
|
+
async with session.get(self.get_associated_vuln_url(bd), headers=headers, ssl=ssl) as resp:
|
|
233
|
+
result_data = await resp.json()
|
|
234
|
+
return self.get_id(), result_data
|
|
235
|
+
|
|
236
|
+
async def async_ignore_vuln(self, conf, session, token):
|
|
237
|
+
if conf.bd_trustcert:
|
|
238
|
+
ssl = False
|
|
239
|
+
else:
|
|
240
|
+
ssl = None
|
|
241
|
+
|
|
242
|
+
headers = {
|
|
243
|
+
# 'accept': "application/vnd.blackducksoftware.bill-of-materials-6+json",
|
|
244
|
+
'Authorization': f'Bearer {token}',
|
|
245
|
+
}
|
|
246
|
+
# resp = globals.bd.get_json(thishref, headers=headers)
|
|
247
|
+
x = datetime.datetime.now()
|
|
248
|
+
mydate = x.strftime("%x %X")
|
|
249
|
+
|
|
250
|
+
payload = self.comp_vuln_data
|
|
251
|
+
# payload['remediationJustification'] = "NO_CODE"
|
|
252
|
+
payload['comment'] = (f"Remediated by bd-vulns utility {mydate} - "
|
|
253
|
+
f"vuln refers to source file not compiled in kernel")
|
|
254
|
+
payload['remediationStatus'] = "IGNORED"
|
|
255
|
+
|
|
256
|
+
conf.logger.debug(f"{self.id} - {self.url()}")
|
|
257
|
+
async with session.put(self.url(), headers=headers, json=payload, ssl=ssl) as response:
|
|
258
|
+
res = response.status
|
|
259
|
+
|
|
260
|
+
# print(res)
|
|
261
|
+
return self.get_id(), res
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import asyncio
|
|
3
|
+
from .VulnClass import Vuln
|
|
4
|
+
# import config
|
|
5
|
+
from .KernelSourceClass import KernelSource
|
|
6
|
+
# from . import global_values
|
|
7
|
+
|
|
8
|
+
# logger = config.setup_logger('kernel-vulns')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VulnList:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.vulns = []
|
|
14
|
+
self.associated_vulns = []
|
|
15
|
+
|
|
16
|
+
def add_comp_data(self, data, conf):
|
|
17
|
+
conf.logger.debug(f"Vulnlist: processing {len(data)} vulns from compdata")
|
|
18
|
+
ignored = 0
|
|
19
|
+
for vulndata in data:
|
|
20
|
+
# if vulndata['ignored']:
|
|
21
|
+
# ignored += 1
|
|
22
|
+
# continue
|
|
23
|
+
vuln = Vuln(vulndata, conf.logger)
|
|
24
|
+
if not vuln:
|
|
25
|
+
conf.logger.error(f"Unable to process vuln entry {vulndata}")
|
|
26
|
+
continue
|
|
27
|
+
if vuln.is_ignored():
|
|
28
|
+
ignored += 1
|
|
29
|
+
continue
|
|
30
|
+
if vuln.is_kernel_vuln():
|
|
31
|
+
self.vulns.append(vuln)
|
|
32
|
+
conf.logger.debug(f"Skipped {ignored} ignored vulns")
|
|
33
|
+
|
|
34
|
+
def get_vuln(self, id):
|
|
35
|
+
for vuln in self.vulns:
|
|
36
|
+
if id == vuln.get_id():
|
|
37
|
+
return vuln
|
|
38
|
+
# global_values.logger.error(f"Unable to find vuln {id} in vuln list")
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def is_associated_vuln(self, id):
|
|
42
|
+
for vuln in self.associated_vulns:
|
|
43
|
+
if vuln == id:
|
|
44
|
+
return True
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def add_vuln_data(self, data, conf):
|
|
48
|
+
try:
|
|
49
|
+
conf.logger.debug(f"Vulnlist: adding {len(data)} vulns from asyncdata")
|
|
50
|
+
for id, entry in data.items():
|
|
51
|
+
# if id.startswith('CVE-'): # DEBUG
|
|
52
|
+
# global_values.logger.info(f"add_vuln_data(): Working on {id}")
|
|
53
|
+
if not self.is_associated_vuln(id):
|
|
54
|
+
vuln = self.get_vuln(id)
|
|
55
|
+
if vuln:
|
|
56
|
+
# vuln is in standard list (not a linked vuln)
|
|
57
|
+
if vuln.is_ignored():
|
|
58
|
+
continue
|
|
59
|
+
vuln.add_data(entry)
|
|
60
|
+
if vuln.get_vuln_source() == 'BDSA':
|
|
61
|
+
linked_vuln = vuln.get_linked_vuln()
|
|
62
|
+
if linked_vuln:
|
|
63
|
+
if linked_vuln in data.keys():
|
|
64
|
+
vuln.add_linked_cve_data(data[linked_vuln])
|
|
65
|
+
self.associated_vulns.append(id)
|
|
66
|
+
except KeyError as e:
|
|
67
|
+
conf.logger.error(f"add_vuln_data(): Key Error {e}")
|
|
68
|
+
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
def process_kernel_vulns(self, conf, kfiles: KernelSource):
|
|
72
|
+
for vuln in self.vulns:
|
|
73
|
+
if vuln.is_ignored() or vuln.get_id() in self.associated_vulns:
|
|
74
|
+
continue
|
|
75
|
+
files = vuln.process_kernel_vuln(conf)
|
|
76
|
+
if len(files) == 0 or kfiles.check_files(files):
|
|
77
|
+
conf.logger.debug(f"VULN IN KERNEL: {vuln.get_id()} - {files}")
|
|
78
|
+
else:
|
|
79
|
+
conf.logger.debug(f"VULN NOT IN KERNEL: {vuln.get_id()} - {files}")
|
|
80
|
+
vuln.set_not_in_kernel()
|
|
81
|
+
|
|
82
|
+
def count(self):
|
|
83
|
+
return len(self.vulns)
|
|
84
|
+
|
|
85
|
+
def count_in_kernel(self):
|
|
86
|
+
count = 0
|
|
87
|
+
for vuln in self.vulns:
|
|
88
|
+
if vuln.in_kernel:
|
|
89
|
+
count += 1
|
|
90
|
+
|
|
91
|
+
return count
|
|
92
|
+
|
|
93
|
+
# def remediate_vulns(self):
|
|
94
|
+
# for vuln in self.vulns:
|
|
95
|
+
|
|
96
|
+
def ignore_vulns(self, bd, conf): # DEBUG
|
|
97
|
+
for vuln in self.vulns:
|
|
98
|
+
vuln.ignore_vuln(bd, conf)
|
|
99
|
+
|
|
100
|
+
async def async_get_vuln_data(self, bd, conf):
|
|
101
|
+
token = bd.session.auth.bearer_token
|
|
102
|
+
|
|
103
|
+
async with aiohttp.ClientSession(trust_env=True) as session:
|
|
104
|
+
vuln_tasks = []
|
|
105
|
+
for vuln in self.vulns:
|
|
106
|
+
if vuln.is_ignored():
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
vuln_task = asyncio.ensure_future(vuln.async_get_vuln_data(bd, conf, session, token))
|
|
110
|
+
vuln_tasks.append(vuln_task)
|
|
111
|
+
|
|
112
|
+
if vuln.get_vuln_source() == 'BDSA':
|
|
113
|
+
linked_vuln = vuln.get_linked_vuln()
|
|
114
|
+
if linked_vuln != '':
|
|
115
|
+
lvuln = Vuln({}, conf, linked_vuln)
|
|
116
|
+
self.associated_vulns.append(lvuln)
|
|
117
|
+
vuln_task = asyncio.ensure_future(lvuln.async_get_vuln_data(bd, conf, session, token))
|
|
118
|
+
vuln_tasks.append(vuln_task)
|
|
119
|
+
|
|
120
|
+
vuln_data = dict(await asyncio.gather(*vuln_tasks))
|
|
121
|
+
await asyncio.sleep(0.250)
|
|
122
|
+
|
|
123
|
+
return vuln_data
|
|
124
|
+
|
|
125
|
+
async def async_ignore_vulns(self, bd):
|
|
126
|
+
token = bd.session.auth.bearer_token
|
|
127
|
+
|
|
128
|
+
async with aiohttp.ClientSession(trust_env=True) as session:
|
|
129
|
+
vuln_tasks = []
|
|
130
|
+
for vuln in self.vulns:
|
|
131
|
+
if vuln.is_ignored() or vuln.in_kernel:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
vuln_task = asyncio.ensure_future(vuln.async_ignore_vuln(bd, session, token))
|
|
135
|
+
vuln_tasks.append(vuln_task)
|
|
136
|
+
|
|
137
|
+
vuln_data = dict(await asyncio.gather(*vuln_tasks))
|
|
138
|
+
await asyncio.sleep(0.250)
|
|
139
|
+
|
|
140
|
+
return vuln_data
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
# from . import global_values
|
|
7
|
+
|
|
8
|
+
parser = argparse.ArgumentParser(description='Black Duck vulns', prog='bd_vulns')
|
|
9
|
+
|
|
10
|
+
# parser.add_argument("projfolder", nargs="?", help="Yocto project folder to analyse", default=".")
|
|
11
|
+
|
|
12
|
+
parser.add_argument("--blackduck_url", type=str, help="Black Duck server URL (REQUIRED)", default="")
|
|
13
|
+
parser.add_argument("--blackduck_api_token", type=str, help="Black Duck API token (REQUIRED)", default="")
|
|
14
|
+
parser.add_argument("--blackduck_trust_cert", help="Black Duck trust server cert", action='store_true')
|
|
15
|
+
parser.add_argument("-p", "--project", help="Black Duck project to process (REQUIRED)", default="")
|
|
16
|
+
parser.add_argument("-v", "--version", help="Black Duck project version to process (REQUIRED)", default="")
|
|
17
|
+
parser.add_argument("--debug", help="Debug logging mode", action='store_true')
|
|
18
|
+
parser.add_argument("--logfile", help="Logging output file", default="")
|
|
19
|
+
parser.add_argument("-k", "--kernel_source_file", help="Kernel source files list (REQUIRED)", default="")
|
|
20
|
+
parser.add_argument("--folders", help="Kernel Source file only contains folders to be used to map vulns",
|
|
21
|
+
action='store_true')
|
|
22
|
+
|
|
23
|
+
def check_args(args):
|
|
24
|
+
terminate = False
|
|
25
|
+
if args.debug:
|
|
26
|
+
loglevel = logging.DEBUG
|
|
27
|
+
else:
|
|
28
|
+
loglevel = logging.INFO
|
|
29
|
+
# global_values.logging_level = loglevel
|
|
30
|
+
global_values.logfile = args.logfile
|
|
31
|
+
|
|
32
|
+
global_values.logger = setup_logger('kernel-vulns', loglevel)
|
|
33
|
+
|
|
34
|
+
global_values.logger.debug("ARGUMENTS:")
|
|
35
|
+
for arg in vars(args):
|
|
36
|
+
global_values.logger.debug(f"--{arg}={getattr(args, arg)}")
|
|
37
|
+
global_values.logger.debug('')
|
|
38
|
+
|
|
39
|
+
url = os.environ.get('BLACKDUCK_URL')
|
|
40
|
+
if args.blackduck_url != '':
|
|
41
|
+
global_values.bd_url = args.blackduck_url
|
|
42
|
+
elif url is not None:
|
|
43
|
+
global_values.bd_url = url
|
|
44
|
+
else:
|
|
45
|
+
global_values.logger.error("Black Duck URL not specified")
|
|
46
|
+
terminate = True
|
|
47
|
+
|
|
48
|
+
if args.project != "" and args.version != "":
|
|
49
|
+
global_values.bd_project = args.project
|
|
50
|
+
global_values.bd_version = args.version
|
|
51
|
+
else:
|
|
52
|
+
global_values.logger.error("Black Duck project/version not specified")
|
|
53
|
+
terminate = True
|
|
54
|
+
|
|
55
|
+
api = os.environ.get('BLACKDUCK_API_TOKEN')
|
|
56
|
+
if args.blackduck_api_token != '':
|
|
57
|
+
global_values.bd_api = args.blackduck_api_token
|
|
58
|
+
elif api is not None:
|
|
59
|
+
global_values.bd_api = api
|
|
60
|
+
else:
|
|
61
|
+
global_values.logger.error("Black Duck API Token not specified")
|
|
62
|
+
terminate = True
|
|
63
|
+
|
|
64
|
+
trustcert = os.environ.get('BLACKDUCK_TRUST_CERT')
|
|
65
|
+
if trustcert == 'true' or args.blackduck_trust_cert:
|
|
66
|
+
global_values.bd_trustcert = True
|
|
67
|
+
|
|
68
|
+
if args.kernel_source_file != '':
|
|
69
|
+
if not os.path.exists(args.kernel_source_file):
|
|
70
|
+
global_values.logger.error(f"Supplied kernel source list file '{args.kernel_source_file}' does not exist")
|
|
71
|
+
terminate = True
|
|
72
|
+
else:
|
|
73
|
+
global_values.kernel_source_file = args.kernel_source_file
|
|
74
|
+
else:
|
|
75
|
+
global_values.logger.error(f"Kernel source list file required (--kernel_source_list)")
|
|
76
|
+
terminate = True
|
|
77
|
+
|
|
78
|
+
if args.folders == 'true':
|
|
79
|
+
global_values.folders = True
|
|
80
|
+
|
|
81
|
+
if terminate:
|
|
82
|
+
sys.exit(2)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def setup_logger(name: str, level) -> logging.Logger:
|
|
87
|
+
logger = logging.getLogger(name)
|
|
88
|
+
logger.setLevel(level)
|
|
89
|
+
|
|
90
|
+
if not logger.hasHandlers(): # Avoid duplicate handlers
|
|
91
|
+
formatter = logging.Formatter(
|
|
92
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
console_handler = logging.StreamHandler()
|
|
96
|
+
console_handler.setFormatter(formatter)
|
|
97
|
+
logger.addHandler(console_handler)
|
|
98
|
+
|
|
99
|
+
if global_values.logfile != '':
|
|
100
|
+
file_handler = logging.FileHandler(global_values.logfile)
|
|
101
|
+
file_handler.setFormatter(formatter)
|
|
102
|
+
logger.addHandler(file_handler)
|
|
103
|
+
|
|
104
|
+
return logger
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# from . import global_values
|
|
2
|
+
from .BOMClass import BOM
|
|
3
|
+
# from . import config
|
|
4
|
+
from .KernelSourceClass import KernelSource
|
|
5
|
+
from .ConfigClass import Config
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
# logger = config.setup_logger('kernel-vulns')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
conf = Config()
|
|
13
|
+
conf.get_cli_args()
|
|
14
|
+
|
|
15
|
+
process(conf)
|
|
16
|
+
# config.check_args(args)
|
|
17
|
+
|
|
18
|
+
sys.exit(0)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def process_kernel_vulns(blackduck_url, blackduck_api_token, kernel_source_file,
|
|
22
|
+
project, version, logger, blackduck_trust_cert=False, folders=''):
|
|
23
|
+
conf = Config()
|
|
24
|
+
conf.bd_url = blackduck_url
|
|
25
|
+
conf.bd_api = blackduck_api_token
|
|
26
|
+
conf.bd_project = project
|
|
27
|
+
conf.bd_version = version
|
|
28
|
+
conf.logger = logger
|
|
29
|
+
conf.bd_trustcert = blackduck_trust_cert
|
|
30
|
+
conf.folders = folders
|
|
31
|
+
conf.kernel_source_file = kernel_source_file
|
|
32
|
+
|
|
33
|
+
process(conf)
|
|
34
|
+
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def process(conf):
|
|
39
|
+
kfiles = KernelSource(conf)
|
|
40
|
+
conf.logger.debug(f"Read {kfiles.count()} source entries from kernel source file "
|
|
41
|
+
f"'{conf.kernel_source_file}'")
|
|
42
|
+
|
|
43
|
+
bom = BOM(conf)
|
|
44
|
+
if bom.check_kernel_comp():
|
|
45
|
+
conf.logger.warn("Linux Kernel not found in project - terminating")
|
|
46
|
+
sys.exit(-1)
|
|
47
|
+
|
|
48
|
+
bom.get_vulns(conf)
|
|
49
|
+
conf.logger.info(f"Found {bom.count_vulns()} kernel vulnerabilities from project")
|
|
50
|
+
|
|
51
|
+
# bom.print_vulns()
|
|
52
|
+
conf.logger.info("Get detailed data for vulnerabilities")
|
|
53
|
+
bom.process_data_async(conf)
|
|
54
|
+
|
|
55
|
+
conf.logger.info("Checking for kernel source file references in vulnerabilities")
|
|
56
|
+
bom.process_kernel_vulns(conf, kfiles)
|
|
57
|
+
|
|
58
|
+
conf.logger.info(f"Identified {bom.count_in_kernel_vulns()} in-scope kernel vulns "
|
|
59
|
+
f"({bom.count_not_in_kernel_vulns()} not in-scope)")
|
|
60
|
+
|
|
61
|
+
conf.logger.info(f"Ignored {bom.ignore_vulns_async()} vulns")
|
|
62
|
+
# bom.ignore_vulns()
|
|
63
|
+
conf.logger.info("Done")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == '__main__':
|
|
67
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bd_kernel_vulns
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: bd_kernel_vulns - Script to process the Linux Kernel in a BD project to assess vulns applicability based on supplied list of kernel source files (or folders)
|
|
5
|
+
Author-email: Matthew Brady <mbrad@blackduck.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/matthewb66/bd_kernel_vulns
|
|
7
|
+
Project-URL: Issues, https://github.com/matthewb66/bd_kernel_vulns/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: blackduck>=1.1.3
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: aiohttp
|
|
17
|
+
Requires-Dist: asyncio
|
|
18
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
pyproject.toml
|
|
3
|
+
bd_kernel_vulns/BOMClass.py
|
|
4
|
+
bd_kernel_vulns/ComponentClass.py
|
|
5
|
+
bd_kernel_vulns/ComponentListClass.py
|
|
6
|
+
bd_kernel_vulns/ConfigClass.py
|
|
7
|
+
bd_kernel_vulns/KernelSourceClass.py
|
|
8
|
+
bd_kernel_vulns/VulnClass.py
|
|
9
|
+
bd_kernel_vulns/VulnListClass.py
|
|
10
|
+
bd_kernel_vulns/__init__.py
|
|
11
|
+
bd_kernel_vulns/config.py
|
|
12
|
+
bd_kernel_vulns/global_values.py
|
|
13
|
+
bd_kernel_vulns/main.py
|
|
14
|
+
bd_kernel_vulns.egg-info/PKG-INFO
|
|
15
|
+
bd_kernel_vulns.egg-info/SOURCES.txt
|
|
16
|
+
bd_kernel_vulns.egg-info/dependency_links.txt
|
|
17
|
+
bd_kernel_vulns.egg-info/entry_points.txt
|
|
18
|
+
bd_kernel_vulns.egg-info/requires.txt
|
|
19
|
+
bd_kernel_vulns.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bd_kernel_vulns
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=67.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bd_kernel_vulns"
|
|
7
|
+
version = "1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Matthew Brady", email="mbrad@blackduck.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "bd_kernel_vulns - Script to process the Linux Kernel in a BD project to assess vulns applicability based on supplied list of kernel source files (or folders)"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"blackduck>=1.1.3",
|
|
21
|
+
"requests",
|
|
22
|
+
"aiohttp",
|
|
23
|
+
"asyncio"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/matthewb66/bd_kernel_vulns"
|
|
28
|
+
Issues = "https://github.com/matthewb66/bd_kernel_vulns/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
bd-scan-yocto-via-sbom = "bd_kernel_vulns:main.main"
|