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.
@@ -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,11 @@
1
+ bd_api = ''
2
+ bd_url = ''
3
+ bd_trustcert = False
4
+
5
+ bd_project = ''
6
+ bd_version = ''
7
+ logger = None
8
+ logfile = ''
9
+ kernel_source_file = ''
10
+ folders = False
11
+ debug = False
@@ -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,2 @@
1
+ [console_scripts]
2
+ bd-scan-yocto-via-sbom = bd_kernel_vulns:main.main
@@ -0,0 +1,4 @@
1
+ blackduck>=1.1.3
2
+ requests
3
+ aiohttp
4
+ asyncio
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+