Merge ~emitorino/ubuntu-cve-tracker:detect_priority_mismatch into ubuntu-cve-tracker:master
- Git
- lp:~emitorino/ubuntu-cve-tracker
- detect_priority_mismatch
- Merge into master
Status: | Merged |
---|---|
Merged at revision: | 79bfd08f3f5051fc7719ab3b5e7fa6e950732f2d |
Proposed branch: | ~emitorino/ubuntu-cve-tracker:detect_priority_mismatch |
Merge into: | ubuntu-cve-tracker:master |
Diff against target: |
752 lines (+634/-16) 5 files modified
meta_lists/priority_explanations.yaml (+7/-0) scripts/active_edit (+28/-14) scripts/cve_lib.py (+60/-2) scripts/detect_priorities_mismatches.py (+321/-0) scripts/report_priority_offset_spike.py (+218/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Steve Beattie | Approve | ||
Ubuntu Security Team | Pending | ||
Review via email: mp+459935@code.launchpad.net |
Commit message
- scripts/
- scripts/
- cve_lib: allow to load table only for CVEs published after certain date
Description of the change
This new script can be run like:
$ python3 scripts/
It detects CVEs which their Ubuntu priority deviates from the CVSS score and after displaying basic information, it allow to set the Priority reason in an interactive mode. This requires that the cvss information in $UCT is updated (i.e. ./scripts/
The idea of this MP is to discuss this implementation. Further testing and checks are still required
Steve Beattie (sbeattie) wrote : | # |
Emilia Torino (emitorino) wrote : | # |
> Hey Emi, I gave this a try and this is a good direction for the workflow. I
> appreciated the addition of the number of CVEs that need a priority
> explanation, I tweaked it a bit to report that out of the total number of CVEs
> examined within the given time period, with the following diff:
>
> --- a/scripts/
> +++ b/scripts/
> @@ -59,7 +59,7 @@ if __name__ == "__main__":
>
> if cves_with_
> total_cves_
> - print(f"\n==== Listing {total_
> Priority different than CVSS base severity ====")
> + print(f"\n==== Listing {total_
> {len(full_
> severity
> ====")
> for index, cve in enumerate(
> print(f"
> ===========")
> print(cves_
>
> which results in output like so since the beginning of 2024 based on current
> UCT as of now:
>
> ==== Listing 246 CVEs (out of 477) with Ubuntu Priority different than CVSS
> base severity ====
Thanks for the diff. It's very insightful. I have applied it https:/
>
> In examining the CVEs that popped up, I found that I ended up opening up the
> CVE file to examine the reference links and bug reports to dig into more
> details about each CVE.
>
> Something you might consider is how check-cves reports the options available
> to the user at https:/
> tracker/
> verbose, but the ways of indicating which key to press and what the default is
> if one simply presses enter may be useful.
>
> I would like a way to generate a non-interactive report of CVEs that need
> action.
>
> One thing that might be nice is instead of having the published_since argument
> to load_table(), take a criteria function as an argument, where the function
> expects to be passed a CVE and return True or False to include it, allowing
> use of different criteria besides being published after a given date as
> options to load_table(). You could then have load_table_
> you wanted that the detect_
> invokes load_table() with a criteria function that looks for published dates
> after the passed date.
Thanks for the feedback! I will work on the suggestions.
Alex Murray (alexmurray) wrote : | # |
Thanks for this @emitorino - I wonder if we should put some of this logic into cve_lib itself (or perhaps some other "common" library) so that it can be shared with check-syntax since ideally we would have check-syntax enforce this check as well. Thoughts?
Emilia Torino (emitorino) wrote : | # |
> Thanks for this @emitorino - I wonder if we should put some of this logic into
> cve_lib itself (or perhaps some other "common" library) so that it can be
> shared with check-syntax since ideally we would have check-syntax enforce this
> check as well. Thoughts?
Hey Alex, sure thing! this was indeed recommended already by eslerm and pfsmorigo.
Once we are happy with the UX of this, I will definitely refactor and consider this. Thanks!
Steve Beattie (sbeattie) wrote : | # |
One thing we talked about was adding tags that could either be indicators for the priority assessing role, or could be expanded into the explanation itself. A really simple sample of the data store for this could look something like:
diff --git a/meta_
new file mode 100644
index 00000000000.
--- /dev/null
+++ b/meta_
@@ -0,0 +1,7 @@
+# standardized priority reason text
+# tag/keyword based, so that we can add the tags as part of triage and
+# re-triage to make it easier to generate.
+---
+kernel-debugfs: |
+ Exploitation requires write access to debugfs entries, which are
+ restricted to root by default on Ubuntu kernels.
Emilia Torino (emitorino) wrote : | # |
> One thing we talked about was adding tags that could either be indicators for
> the priority assessing role, or could be expanded into the explanation itself.
> A really simple sample of the data store for this could look something like:
>
> diff --git a/meta_
> b/meta_
> new file mode 100644
> index 00000000000.
> --- /dev/null
> +++ b/meta_
> @@ -0,0 +1,7 @@
> +# standardized priority reason text
> +# tag/keyword based, so that we can add the tags as part of triage and
> +# re-triage to make it easier to generate.
> +---
> +kernel-debugfs: |
> + Exploitation requires write access to debugfs entries, which are
> + restricted to root by default on Ubuntu kernels.
I also added:
1) the capability to filter CVEs published until a provided date https:/
2) The support for non-interactive (i.e. tmpfile) mode https:/
Steve Beattie (sbeattie) wrote : | # |
Thanks Emi. I went ahead and merged this branch, after trying to ensure that the changes to active_edit were safe (but hit LP: #2058614 in the existing code).
One issue I hit with the non-interactive mode is, in testing in particular the some recenet kernel CVEs, the descriptions include code related things, including C pointers, which the interpreter tried to parse as a command and failed. To reproduce:
$ ./scripts/
[... text elided ...]
=========== 12/18: CVE-2024-26589 ===========
In the Linux kernel, the following vulnerability has been resolved: bpf:
Reject variable offset alu on PTR_TO_FLOW_KEYS For PTR_TO_FLOW_KEYS,
check_flow_
variable offset ptr alu is not prohibited for this ptr kind. So the
variable offset is not checked. The following prog is accepted: func#0 @0
0: R1=ctx() R10=fp0 0: (bf) r6 = r1 ; R1=ctx() R6_w=ctx() 1: (79) r7 =
*(u64 *)(r6 +144) ; R6_w=ctx() R7_w=flow_keys() 2: (b7) r8 = 1024 ;
[... rest elided ...]
which results in:
=======
18 CVEs needing priority reason
0 CVEs processed:
- 0 skipped CVE(s)
- 0 CVE(s) automatically updated with their corresponding reasons
- 0 CVE(s) opened in editor for manual edit
- 1 issue(s)
- #425: Invalid CVE ID format: *(u64 *)(r6 +144) ; R6_w=ctx() R7_w=flow_keys() 2: (b7) r8 = 1024 ;
So likely we need to indent the description and any other fields that might have lines that start with '*'.
(I think this is why the experimental triage interface prefixes all the information as comments / '#'; then the processor can ignore any such lines, rather than worrying about individual fields of info colliding with actions.)
Preview Diff
1 | diff --git a/meta_lists/priority_explanations.yaml b/meta_lists/priority_explanations.yaml |
2 | new file mode 100644 |
3 | index 0000000..868d501 |
4 | --- /dev/null |
5 | +++ b/meta_lists/priority_explanations.yaml |
6 | @@ -0,0 +1,7 @@ |
7 | +# standardized priority reason text |
8 | +# tag/keyword based, so that we can add the tags as part of triage and |
9 | +# re-triage to make it easier to generate. |
10 | +--- |
11 | +kernel-debugfs: | |
12 | + Exploitation requires write access to debugfs entries, which are |
13 | + restricted to root by default on Ubuntu kernels. |
14 | diff --git a/scripts/active_edit b/scripts/active_edit |
15 | index a0e0efa..cb4beb4 100755 |
16 | --- a/scripts/active_edit |
17 | +++ b/scripts/active_edit |
18 | @@ -33,6 +33,7 @@ parser.add_option("-e", "--embargoed", dest="embargoed", help="This is an embarg |
19 | parser.add_option("-y", "--yes", dest="autoconfirm", help="Do not ask for confirmation", action="store_true") |
20 | parser.add_option("-P", "--public", dest="public_date", help="Record date the CVE went public (default to today in UTC)", metavar="YYYY-MM-DD") |
21 | parser.add_option("--priority", help="Record a priority for the CVE", default=None) |
22 | +parser.add_option("-R", "--priority-reason", help="Record a priority reason for the CVE", default=None) |
23 | parser.add_option("-C", "--cvss", help="CVSS3.1 rating", metavar="CVSS:3.1/AV:_/AC:_/PR:_/UI:_/S:_/C:_/I:_/A:_") |
24 | parser.add_option("-d", "--description", help="Description", default=None) |
25 | (options, args) = parser.parse_args() |
26 | @@ -164,17 +165,18 @@ def add_pkg(p, fp, fixed, parent, embargoed, break_fixes): |
27 | |
28 | def create_or_update_cve(cve, packages, priority=None, bug_urls=None, |
29 | ref_urls=None, public_date=None, desc=None, |
30 | - cvss=None, embargoed=False, breakfix=False): |
31 | - |
32 | + cvss=None, embargoed=False, breakfix=False, |
33 | + priority_reason=None): |
34 | pkgs = [] |
35 | break_fixes = [] |
36 | fixed = {} |
37 | # parse optional fixed_in release and version from package name |
38 | - for p in packages: |
39 | - tmp_p = p.split(',') |
40 | - pkg = tmp_p[0] |
41 | - pkgs.append(pkg) |
42 | - fixed[pkg] = tmp_p[1:] |
43 | + if packages: |
44 | + for p in packages: |
45 | + tmp_p = p.split(',') |
46 | + pkg = tmp_p[0] |
47 | + pkgs.append(pkg) |
48 | + fixed[pkg] = tmp_p[1:] |
49 | |
50 | update = False |
51 | try: |
52 | @@ -219,8 +221,10 @@ def create_or_update_cve(cve, packages, priority=None, bug_urls=None, |
53 | if not embargoed and not public_date: |
54 | public_date = datetime.now(timezone.utc).strftime("%Y-%m-%d") |
55 | |
56 | - if update: |
57 | + if update and not priority_reason: |
58 | mode = "a" |
59 | + elif update and priority_reason: |
60 | + mode = "r+" |
61 | else: |
62 | mode = "x" |
63 | with open(dst, mode, encoding="utf-8") as fp: |
64 | @@ -245,8 +249,10 @@ def create_or_update_cve(cve, packages, priority=None, bug_urls=None, |
65 | for url in (bug_urls if bug_urls else []): |
66 | print(" %s" % url, file=fp) |
67 | priority_text = priority if priority else "untriaged" |
68 | - if priority in cve_lib.PRIORITY_REASON_REQUIRED: |
69 | + if priority in cve_lib.PRIORITY_REASON_REQUIRED and not priority_reason: |
70 | priority_text += "\n XXX-Reason-XXX" |
71 | + elif priority and priority_reason: |
72 | + priority_text += f"\n {priority_reason}" |
73 | print('Priority: %s' % priority_text, file=fp) |
74 | print('Discovered-by:', file=fp) |
75 | print('Assigned-to:', file=fp) |
76 | @@ -257,14 +263,22 @@ def create_or_update_cve(cve, packages, priority=None, bug_urls=None, |
77 | |
78 | for p in pkgs: |
79 | add_pkg(p, fp, fixed, None, embargoed, break_fixes) |
80 | + if priority_reason: |
81 | + metadata_to_update = "Priority:" |
82 | + update_file_metadata(metadata_to_update, priority_reason, fp) |
83 | |
84 | |
85 | +def update_file_metadata(metadata, value, cve_fp): |
86 | + cve_content = cve_fp.readlines() |
87 | + for cve_line_number, cve_line in enumerate (cve_content): |
88 | + if cve_line.startswith(metadata): |
89 | + # TODO: check length + wrap lines + existing reason |
90 | + cve_content[cve_line_number] += f" {value}\n" |
91 | + cve_fp.seek(0) |
92 | + cve_fp.writelines(cve_content) |
93 | |
94 | -pkg_db = cve_lib.load_package_db() |
95 | |
96 | -if not options.pkgs: |
97 | - parser.print_help() |
98 | - sys.exit(1) |
99 | +pkg_db = cve_lib.load_package_db() |
100 | |
101 | if not options.cve: |
102 | parser.print_help() |
103 | @@ -290,5 +304,5 @@ if not pat.search(cve): |
104 | print("Bad CVE entry. Should be CVE-XXXX-XXXX\n", file=sys.stderr) |
105 | sys.exit(1) |
106 | |
107 | -create_or_update_cve(cve, pkgs, priority=options.priority, bug_urls=options.bug_urls, ref_urls=options.ref_urls, public_date=options.public_date, desc=options.description, cvss=options.cvss, embargoed=options.embargoed, breakfix=options.breakfix) |
108 | +create_or_update_cve(cve, pkgs, priority=options.priority, bug_urls=options.bug_urls, ref_urls=options.ref_urls, public_date=options.public_date, desc=options.description, cvss=options.cvss, embargoed=options.embargoed, breakfix=options.breakfix, priority_reason=options.priority_reason) |
109 | sys.exit(0) |
110 | diff --git a/scripts/cve_lib.py b/scripts/cve_lib.py |
111 | index 95c8f65..7aeb4dd 100755 |
112 | --- a/scripts/cve_lib.py |
113 | +++ b/scripts/cve_lib.py |
114 | @@ -29,6 +29,9 @@ from uct.config import read_uct_config |
115 | from functools import reduce |
116 | from functools import lru_cache |
117 | |
118 | +CVE_FILTER_NAME = "cve_filter_name" |
119 | +CVE_FILTER_ARGS = "cve_filter_args" |
120 | + |
121 | def set_cve_dir(path): |
122 | '''Return a path with CVEs in it. Specifically: |
123 | - if 'path' has CVEs in it, return path |
124 | @@ -2576,12 +2579,52 @@ def load_all(cves, uems, rcves=[]): |
125 | table.setdefault(cve, info) |
126 | return table |
127 | |
128 | +# Get CVE date information only, in the format of "%Y-%m-%d" |
129 | +def parse_cve_pub_date(cve_public_date_from_file): |
130 | + cve_public_date = None |
131 | + date_only = cve_public_date_from_file.split()[0] |
132 | + if date_only != "unknown": |
133 | + try: |
134 | + cve_public_date = datetime.datetime.strptime(date_only, "%Y-%m-%d") |
135 | + except ValueError as e: |
136 | + print(f"WARN: Invalid CVE PublicDate: {date_only}. {e}") |
137 | + return cve_public_date |
138 | + |
139 | + |
140 | +def cve_published_since(cve_info, published_since): |
141 | + cve_public_date = parse_cve_pub_date(cve_info['PublicDate']) |
142 | + return cve_public_date and cve_public_date > published_since |
143 | + |
144 | + |
145 | +def cve_published_until(cve_info, published_until): |
146 | + cve_public_date = parse_cve_pub_date(cve_info['PublicDate']) |
147 | + return cve_public_date and cve_public_date < published_until |
148 | + |
149 | + |
150 | +def cve_published_in_range(cve_info, published_since, published_until): |
151 | + return cve_published_since(cve_info, published_since) and cve_published_until(cve_info, published_until) |
152 | |
153 | -# supported options |
154 | + |
155 | +def cve_affecting_pkgs(cve_info, packages): |
156 | + # removing whitespaces, if they exist before cchecking |
157 | + cve_affected_packages = [x.strip(' ') for x in cve_info['pkgs']] |
158 | + packages_in_filter = [x.strip(' ') for x in packages] |
159 | + return [pkg for pkg in packages_in_filter if pkg in cve_affected_packages] |
160 | + |
161 | + |
162 | +def cve_in_list(cve_info, cve_list): |
163 | + cve_id = cve_info["id"] |
164 | + return cve_id in cve_list |
165 | + |
166 | + |
167 | + |
168 | +# supported options for opt argument |
169 | # pkgfamily = rename linux-source-* packages to "linux", or "xen-*" to "xen" |
170 | # packages = list of packages to pay attention to |
171 | # debug = bool, display debug information |
172 | -def load_table(cves, uems, opt=None, rcves=[], icves=[]): |
173 | + |
174 | +# filters = list of functions and , only load CVEs matching the filters criteria |
175 | +def load_table(cves, uems, opt=None, rcves=[], icves=[], filters=None): |
176 | table = dict() |
177 | priority = dict() |
178 | listcves = [] |
179 | @@ -2599,6 +2642,21 @@ def load_table(cves, uems, opt=None, rcves=[], icves=[]): |
180 | cvedir = ignored_dir |
181 | cvefile = os.path.join(cvedir, cve) |
182 | info = load_cve(cvefile) |
183 | + # Allowing to filter cves based on a list of filter functions |
184 | + # with their corresponding args |
185 | + filters_matched = True |
186 | + for filter in filters: |
187 | + filter_name = filter[CVE_FILTER_NAME] |
188 | + filter_args = filter[CVE_FILTER_ARGS] |
189 | + # always adding cve_info to any filter function its needed |
190 | + info["id"] = cve |
191 | + filter_args["cve_info"] = info |
192 | + if not filter_name(**filter_args): |
193 | + filters_matched = False |
194 | + break |
195 | + if not filters_matched: |
196 | + # Skipping CVEs which have not matched provided filters, if any |
197 | + continue |
198 | cveinfo[cve] = info |
199 | |
200 | # Allow for Priority overrides |
201 | diff --git a/scripts/detect_priorities_mismatches.py b/scripts/detect_priorities_mismatches.py |
202 | new file mode 100644 |
203 | index 0000000..0d249da |
204 | --- /dev/null |
205 | +++ b/scripts/detect_priorities_mismatches.py |
206 | @@ -0,0 +1,321 @@ |
207 | +#!/usr/bin/python3 |
208 | +import argparse |
209 | +import os |
210 | +import subprocess |
211 | +import sys |
212 | +import tempfile |
213 | +from datetime import datetime |
214 | +import cve_lib |
215 | + |
216 | +ACTION_ADD_REASON = "add-reason" |
217 | +ACTION_EDIT_REASON = "edit-reason" |
218 | +ACTION_SKIP = "skip" |
219 | + |
220 | +EXPECTED_CVE_DESCRIPTIONS = ( |
221 | + "UNSUPPORTED", # ** UNSUPPORTED WHEN ASSIGNED ** |
222 | + "DISPUTED", # ** DISPUTED ** |
223 | + "REJECT", # ** REJECT ** |
224 | + "Note:", # **Note:** This is only exploitable |
225 | + "This", # *This bug only affects |
226 | + "info", # *info` actually points |
227 | + "not" #*not* get upgraded |
228 | +) |
229 | + |
230 | +def get_cvss_base_severity(cvss_information): |
231 | + base_severity = None |
232 | + for cvss in cvss_information: |
233 | + if cvss['source'] == "nvd": |
234 | + cvss_base_severity = cvss['baseSeverity'] |
235 | + if cvss_base_severity != "NONE": |
236 | + base_severity = cvss_base_severity |
237 | + return base_severity |
238 | + |
239 | + |
240 | +def display_command_file_usage(output, line_prefix=''): |
241 | + output.write(f'{line_prefix} The following commands can be used in this file:\n') |
242 | + output.write(f'{line_prefix} \n') |
243 | + output.write(f'{line_prefix}"* Add a CVE Ubuntu priority reason to the tracker:"\n') |
244 | + output.write(f'{line_prefix}" <CVE> {ACTION_ADD_REASON} <REASON> ..."\n') |
245 | + output.write(f'{line_prefix}"* Add a CVE Ubuntu priority reason in your editor:"\n') |
246 | + output.write(f'{line_prefix}" <CVE> {ACTION_EDIT_REASON} ..."\n') |
247 | + output.write(f'{line_prefix}"* Temporarily skip over a CVE:"\n') |
248 | + output.write(f'{line_prefix}" <CVE> {ACTION_SKIP}\n"\n') |
249 | + output.flush() |
250 | + |
251 | + |
252 | +def process_command_file(cves_output_content): |
253 | + cves_output_content.seek(0) |
254 | + line_number = 0 |
255 | + cves = set() |
256 | + skipped_cves = set() |
257 | + cves_to_add_reason_automatically = dict () |
258 | + invalid_lines = dict() |
259 | + for line in cves_output_content.readlines(): |
260 | + line_number += 1 |
261 | + line = line.strip() |
262 | + |
263 | + # We only want to action on lines starting with * |
264 | + if not line or not line.startswith('*'): |
265 | + continue |
266 | + |
267 | + line_content = line.split() |
268 | + try: |
269 | + # action lines should start with * prefix, followed by a CVE number and desired action |
270 | + (cve, action) = (line_content[1].strip().upper(), line_content[2]) |
271 | + except IndexError: |
272 | + invalid_lines[str(line_number)] = [line, "Invalid line format"] |
273 | + continue |
274 | + |
275 | + # Ignore lines that look like action lines but are expected cve descriptions |
276 | + if cve.startswith(EXPECTED_CVE_DESCRIPTIONS): |
277 | + continue |
278 | + |
279 | + if not cve.startswith('CVE-'): |
280 | + # The first arg should look like a CVE ID |
281 | + invalid_lines[str(line_number)] = [line, "Invalid CVE ID format"] |
282 | + continue |
283 | + |
284 | + if cve in cves: |
285 | + invalid_lines[str(line_number)] = [line, f"Operation over {cve} was already performed"] |
286 | + continue |
287 | + |
288 | + cves.add(cve) |
289 | + # This action sets the priority reason to the file without opening an editor |
290 | + if action == ACTION_ADD_REASON: |
291 | + # If the priority reason should be added, it must be present |
292 | + try: |
293 | + reason = " ".join(line_content[3:]) |
294 | + except IndexError: |
295 | + invalid_lines[str(line_number)] = [line, f"Reason was not specified for {cve}"] |
296 | + continue |
297 | + cves_to_add_reason_automatically[cve] = [line, reason] |
298 | + |
299 | + # This action opens an editor to edit the CVE |
300 | + elif action == ACTION_EDIT_REASON: |
301 | + try: |
302 | + # find_cve will through ValueError exception if cve file is not found |
303 | + cve_path= cve_lib.find_cve(cve) |
304 | + except ValueError as e: |
305 | + invalid_lines[str(line_number)] = [line, f"Unable to find cve file for {cve}: {e}"] |
306 | + continue |
307 | + cves_to_add_reason_in_editor[cve] = {"path": cve_path} |
308 | + |
309 | + elif action == ACTION_SKIP: |
310 | + # If the CVE should be skipped, no arguments are allowed |
311 | + if len(line_content) > 3: |
312 | + invalid_lines[str(line_number)] = [line, "Invalid skip command"] |
313 | + continue |
314 | + skipped_cves.add(cve) |
315 | + |
316 | + else: |
317 | + # The second arg must be a known action |
318 | + invalid_lines[str(line_number)] = [line, f"Unknown action {action}"] |
319 | + return invalid_lines, cves_to_add_reason_automatically, cves_to_add_reason_in_editor, skipped_cves |
320 | + |
321 | + |
322 | +def active_edit_priority_reason(cve, reason): |
323 | + # TODO: check for script dir location |
324 | + # TODO: check reason input |
325 | + success = True |
326 | + cmd = ['./scripts/active_edit', '-c', cve, '-R', reason] |
327 | + try: |
328 | + subprocess.run(cmd, check=True) |
329 | + except subprocess.CalledProcessError as e: |
330 | + output(f"ERROR while updating CVE file. {e}") |
331 | + success = False |
332 | + return success |
333 | + |
334 | + |
335 | +def report(errors, cves_to_add_reason_automatically, cves_to_add_reason_in_editor, skipped_cves, total, temp_file, interactive, keep_tmp_file): |
336 | + print("\n============================ Summary =============================") |
337 | + print(f"{total} CVEs needing priority reason") |
338 | + print(f"{len(skipped_cves) + len(cves_to_add_reason_automatically) + len(cves_to_add_reason_in_editor)} CVEs processed:") |
339 | + print(f" - {len(skipped_cves)} skipped CVE(s)") |
340 | + print(f" - {len(cves_to_add_reason_automatically)} CVE(s) automatically updated with their corresponding reasons") |
341 | + if not interactive: |
342 | + print(f" - {len(cves_to_add_reason_in_editor)} CVE(s) opened in editor for manual edit") |
343 | + print(f" - {len(errors)} issue(s)") |
344 | + for error in errors: |
345 | + if not interactive: |
346 | + # In non-interactive mode, we have more information to report |
347 | + print(f" - #{error}: {errors[error][1]}: {errors[error][0]}") |
348 | + else: |
349 | + print(f" - {error}: Unable to edit file.") |
350 | + if not interactive and keep_tmp_file: |
351 | + print(f"\n\ntempfile preserved at: {temp_file}") |
352 | + |
353 | + |
354 | +def setup_tmp_file(keep_tmp_file, tmp_file_content_prefix): |
355 | + tmp_file_prefix="cves-needing-priority-reason." |
356 | + # By default tmpfile is deleted |
357 | + # We might want to preserve it for troubleshooting |
358 | + delete_tmp_file = True |
359 | + if keep_tmp_file: |
360 | + delete_tmp_file = False |
361 | + output = tempfile.NamedTemporaryFile(prefix=tmp_file_prefix, mode='w+', delete=delete_tmp_file) |
362 | + display_command_file_usage(output, tmp_file_content_prefix) |
363 | + return output |
364 | + |
365 | + |
366 | +# TODO: move this to other cve_lib filter |
367 | +def get_cves_with_mismatches(full_cves_information): |
368 | + cves_with_mismatches = dict() |
369 | + cves_with_cvss_severity = 0 |
370 | + for cve in full_cves_information: |
371 | + ubuntu_priority = full_cves_information[cve]['Priority'][0] |
372 | + cvss_base_severity = get_cvss_base_severity(full_cves_information[cve]['CVSS']) |
373 | + if cvss_base_severity: |
374 | + cves_with_cvss_severity +=1 |
375 | + ubuntu_priority_reason = full_cves_information[cve]['Priority'][1] |
376 | + if cvss_base_severity.lower() != ubuntu_priority and not ubuntu_priority_reason: |
377 | + cves_with_mismatches[cve] = full_cves_information[cve] |
378 | + return cves_with_mismatches, cves_with_cvss_severity |
379 | + |
380 | + |
381 | +def check_date(date): |
382 | + try: |
383 | + parsed_date = datetime.strptime(date, "%Y-%m-%d") |
384 | + except ValueError as e: |
385 | + output.write(f"ERROR: invalid date argument value: {date}. {e}") |
386 | + sys.exit(1) |
387 | + return parsed_date |
388 | + |
389 | + |
390 | +def spawn_editor(path): |
391 | + editor = os.getenv('EDITOR', 'vi') |
392 | + subprocess.call([editor, path]) |
393 | + |
394 | + |
395 | +if __name__ == "__main__": |
396 | + parser = argparse.ArgumentParser() |
397 | + parser.add_argument( |
398 | + "--published-since", |
399 | + help="Report CVEs published only since the date specified. Format: YYYY-MM-DD", |
400 | + ) |
401 | + parser.add_argument( |
402 | + "--published-until", |
403 | + help="Report CVEs published only until the date specified. Format: YYYY-MM-DD", |
404 | + ) |
405 | + parser.add_argument( |
406 | + "--packages", |
407 | + help="Report CVEs only affecting the list of comma separated packages. Format: pkg1,pkg2", |
408 | + ) |
409 | + parser.add_argument( |
410 | + "--interactive", |
411 | + action ='store_true', |
412 | + ) |
413 | + parser.add_argument( |
414 | + "--keep-tmp-file", |
415 | + action ='store_true', |
416 | + ) |
417 | + args = parser.parse_args() |
418 | + |
419 | + interactive_mode = False |
420 | + original_output = sys.stdout |
421 | + keep_tmp_file = args.keep_tmp_file |
422 | + if args.interactive: |
423 | + # In interactive mode, content is print to std out |
424 | + output = sys.stdout |
425 | + interactive_mode = True |
426 | + else: |
427 | + # In non-interactive mode, content is printed to a tmpfile |
428 | + tmp_file_content_prefix = '# ' |
429 | + tmp_file_action_prefix = "* " |
430 | + output = setup_tmp_file(keep_tmp_file, tmp_file_content_prefix) |
431 | + |
432 | + cve_table_filters = [] |
433 | + since_date = None |
434 | + until_date = None |
435 | + if args.published_since: |
436 | + since_date = check_date(args.published_since) |
437 | + cve_table_filters.append({cve_lib.CVE_FILTER_NAME: cve_lib.cve_published_since, cve_lib.CVE_FILTER_ARGS: {"published_since": since_date}}) |
438 | + |
439 | + if args.published_until: |
440 | + until_date = check_date(args.published_until) |
441 | + cve_table_filters.append({cve_lib.CVE_FILTER_NAME: cve_lib.cve_published_until, cve_lib.CVE_FILTER_ARGS: {"published_until": until_date}}) |
442 | + |
443 | + if args.packages: |
444 | + packages = args.packages.split(",") |
445 | + cve_table_filters.append({cve_lib.CVE_FILTER_NAME: cve_lib.cve_affecting_pkgs, cve_lib.CVE_FILTER_ARGS: {"packages": packages}}) |
446 | + |
447 | + try: |
448 | + print("\n==== Loading CVEs from UCT ====\n") |
449 | + all_cves, embargoed_cves, retired_cves, ignored_cves = cve_lib.get_all_cve_list() |
450 | + ( |
451 | + _, |
452 | + _, |
453 | + _, |
454 | + _, |
455 | + full_cves_information, |
456 | + ) = cve_lib.load_table(all_cves, embargoed_cves, None, retired_cves, ignored_cves, cve_table_filters) |
457 | + except ValueError as e: |
458 | + # cve_lib.load_cve() can raises value error on CVE issues |
459 | + print(f"ERROR: unable to load CVEs: {e}") |
460 | + sys.exit(1) |
461 | + |
462 | + cves_to_add_reason_automatically = dict() |
463 | + cves_to_add_reason_in_editor = dict () |
464 | + skipped_cves = set() |
465 | + errors = set() |
466 | + total_cves_to_process = 0 |
467 | + tmp_file_name = None |
468 | + cves_with_mismatches, cves_with_cvss_severity = get_cves_with_mismatches(full_cves_information) |
469 | + if cves_with_mismatches: |
470 | + total_cves_to_process = len(cves_with_mismatches) |
471 | + |
472 | + output.write(f"\n==== Listing {total_cves_to_process} CVEs (out of {cves_with_cvss_severity}) with Ubuntu Priority different than CVSS base severity ====") |
473 | + |
474 | + for index, cve in enumerate(cves_with_mismatches): |
475 | + # Provide basic CVE metadata |
476 | + output.write(f"\n\n=========== {index + 1}/{total_cves_to_process}: {cve} ===========") |
477 | + output.write(f"{cves_with_mismatches[cve]['Description']}") |
478 | + output.write(f"\n- Public Date: {cves_with_mismatches[cve]['PublicDate']}") |
479 | + output.write(f"\n- Ubuntu Priority: {cves_with_mismatches[cve]['Priority'][0]} || CVSS Severity: {get_cvss_base_severity(cves_with_mismatches[cve]['CVSS'])}") |
480 | + output.write(f"\n- Affected packages: {', '.join(cves_with_mismatches[cve]['pkgs'].keys())}") |
481 | + output.write(f"\n- Tags: {', '.join(cves_with_mismatches[cve]['tags'])}") |
482 | + output.write(f"\n- References: {cves_with_mismatches[cve]['References']}") |
483 | + output.write(f"\n- Bugs: {cves_with_mismatches[cve]['Bugs']}") |
484 | + |
485 | + # In interactive mode, ask and perform action on every CVE |
486 | + if interactive_mode: |
487 | + output.write("\n\nAdd priority reason? \nY]es, S]kip for now, or Q]uit]") |
488 | + confirm = input() |
489 | + if confirm.lower() in ('Y', 'y', 'yes', 'YES'): |
490 | + reason = input("Please add Ubuntu priority reason: ") |
491 | + if not active_edit_priority_reason(cve, reason): |
492 | + errors.add(cve) |
493 | + continue |
494 | + # In interactive mode, we only add to cves_to_add_reason_automatically |
495 | + # for later summary purposes. |
496 | + cves_to_add_reason_automatically[cve] = [] |
497 | + elif confirm.lower() in ('Q', 'q', 'quit', 'Quit', 'QUIT'): |
498 | + output.write("Quiting...\n") |
499 | + report(errors, cves_to_add_reason_automatically, cves_to_add_reason_in_editor, skipped_cves, total_cves_to_process, None, interactive_mode, keep_tmp_file) |
500 | + sys.exit(0) |
501 | + elif confirm.lower() in ('S', 's', 'skip', 'Skip', 'SKIP'): |
502 | + output.write(f"Skipping {cve}\n") |
503 | + skipped_cves.add(cve) |
504 | + else: |
505 | + output.write("\n Invalid option, skipping {cve}\n") |
506 | + skipped_cves.add(cve) |
507 | + # In non-interactive mode, add cve info to file. Actions will be executed later |
508 | + else: |
509 | + output.write(f"\n\n{tmp_file_content_prefix}{tmp_file_action_prefix}{cve} {ACTION_ADD_REASON}") |
510 | + output.write(f"\n{tmp_file_content_prefix}{tmp_file_action_prefix}{cve} {ACTION_EDIT_REASON}") |
511 | + output.write(f"\n{tmp_file_content_prefix}{tmp_file_action_prefix}{cve} {ACTION_SKIP}") |
512 | + if not interactive_mode: |
513 | + output.flush() |
514 | + tmp_file_name = output.name |
515 | + # Show tmp file content in configured editor |
516 | + spawn_editor(output.name) |
517 | + errors, cves_to_add_reason_automatically, cves_to_add_reason_in_editor, skipped_cves = process_command_file(output) |
518 | + for cve in cves_to_add_reason_automatically: |
519 | + reason = cves_to_add_reason_automatically[cve][1] |
520 | + if not active_edit_priority_reason(cve, reason): |
521 | + file_line = cves_to_add_reason_automatically[cve][0] |
522 | + errors[cve]=[file_line, "Unable to add priority reason automatically."] |
523 | + for cve in cves_to_add_reason_in_editor: |
524 | + spawn_editor(cves_to_add_reason_in_editor[cve]["path"]) |
525 | + # restore original sys.stdout |
526 | + sys.stdout = original_output |
527 | + report(errors, cves_to_add_reason_automatically, cves_to_add_reason_in_editor, skipped_cves, total_cves_to_process, tmp_file_name, interactive_mode, keep_tmp_file) |
528 | \ No newline at end of file |
529 | diff --git a/scripts/report_priority_offset_spike.py b/scripts/report_priority_offset_spike.py |
530 | new file mode 100644 |
531 | index 0000000..506249f |
532 | --- /dev/null |
533 | +++ b/scripts/report_priority_offset_spike.py |
534 | @@ -0,0 +1,218 @@ |
535 | +#!/usr/bin/python3 |
536 | + |
537 | +import argparse |
538 | +import subprocess |
539 | +import sys |
540 | +from datetime import datetime |
541 | +import cve_lib |
542 | +import os |
543 | + |
544 | +# this is needed to consume google docs apis wrapper |
545 | +USM = os.environ.get( |
546 | + "USM", os.path.expandvars("$HOME/git-pulls/ubuntu-security-metrics") |
547 | +) |
548 | +sys.path.append(f"{USM}") |
549 | + |
550 | +from metrics.data_sources.google_document import ( |
551 | + connect_to_spreadsheet, |
552 | + write_to_spreadsheet, |
553 | + append_row_data |
554 | +) |
555 | + |
556 | +PRIORITY_OFFSET = { |
557 | + 'critical': 'high' , |
558 | + 'high': 'medium', |
559 | + 'medium': 'low', |
560 | + 'low': 'negligible' |
561 | +} |
562 | + |
563 | +PRIORITY_BUMP= { |
564 | + 'high': 'critical', |
565 | + 'medium': 'high', |
566 | + 'low': 'medium', |
567 | + 'negligible': 'low' |
568 | +} |
569 | + |
570 | +HIGHER_LEVELS = { |
571 | + 'critical' : ['high', 'medium','low'], |
572 | + 'high' : ['medium','low'], |
573 | + 'medium' : ['low'], |
574 | + 'low' : [], |
575 | +} |
576 | + |
577 | +def get_cvss_base_severity(cvss_information): |
578 | + base_severity = None |
579 | + for cvss in cvss_information: |
580 | + if cvss['source'] == "nvd": |
581 | + cvss_base_severity = cvss['baseSeverity'] |
582 | + if cvss_base_severity != "NONE": |
583 | + base_severity = cvss_base_severity |
584 | + return base_severity |
585 | + |
586 | + |
587 | +if __name__ == "__main__": |
588 | + parser = argparse.ArgumentParser() |
589 | + parser.add_argument( |
590 | + "--published-since", |
591 | + help="Report CVEs published only since the date specified. Format: YYYY-MM-DD", |
592 | + ) |
593 | + args = parser.parse_args() |
594 | + |
595 | + since_date = None |
596 | + if args.published_since: |
597 | + try: |
598 | + since_date = datetime.strptime(args.published_since, "%Y-%m-%d") |
599 | + except ValueError as e: |
600 | + print(f"ERROR: invalid since date argument value: {args.published_since}. {e}") |
601 | + sys.exit(1) |
602 | + |
603 | + try: |
604 | + print("\n==== Loading CVEs from UCT ====\n") |
605 | + all_cves, embargoed_cves, retired_cves, ignored_cves = cve_lib.get_all_cve_list() |
606 | + ( |
607 | + _, |
608 | + _, |
609 | + _, |
610 | + _, |
611 | + full_cves_information, |
612 | + ) = cve_lib.load_table(all_cves, embargoed_cves, None, retired_cves, ignored_cves, since_date) |
613 | + except ValueError as e: |
614 | + # cve_lib.load_cve() can raises value error on CVE issues |
615 | + print(f"ERROR: unable to load CVEs: {e}") |
616 | + sys.exit(1) |
617 | + |
618 | + cves_with_mismatches = dict() |
619 | + cves_with_cvss_severity = 0 |
620 | + #cves_with_mismatches = dict() |
621 | + cves_with_mismatches = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0} |
622 | + total_differences = 0 |
623 | + cves_with_same = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0} |
624 | + total_same = 0 |
625 | + cves_diff = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0} |
626 | + total_diff = 0 |
627 | + |
628 | + creds_file = "/home/emitorino/register-automation-1338127a145f.json" |
629 | + sheet = connect_to_spreadsheet(creds_file) |
630 | + rows_data = [] |
631 | + row = 2 |
632 | + missing_cvss = 0 |
633 | + equals = 0 |
634 | + higher = 0 |
635 | + lower = 0 |
636 | + for cve in full_cves_information: |
637 | + original_ubuntu_priority = full_cves_information[cve]['Priority'][0] |
638 | + ubuntu_priority = PRIORITY_BUMP[original_ubuntu_priority] |
639 | + print (f"Bumping {original_ubuntu_priority} to {ubuntu_priority}") |
640 | + cvss_base_severity = get_cvss_base_severity(full_cves_information[cve]['CVSS']) |
641 | + needs_explanation = "NO" |
642 | + cve_data = [] |
643 | + if cvss_base_severity: |
644 | + cves_with_cvss_severity +=1 |
645 | + cvss_base_severity = cvss_base_severity.lower() |
646 | + cve_data = [cve, cvss_base_severity, original_ubuntu_priority, ubuntu_priority] |
647 | + # if cvss_base_severity == ubuntu_priority: |
648 | + # cves_with_same[cvss_base_severity] +=1 |
649 | + # total_same +=1 |
650 | + ubuntu_priority_reason = full_cves_information[cve]['Priority'][1] |
651 | + # if cvss_base_severity.lower() != ubuntu_priority and not ubuntu_priority_reason: |
652 | + # cves_with_mismatches[cve] = full_cves_information[cve] |
653 | + if cvss_base_severity in PRIORITY_OFFSET: |
654 | + if ubuntu_priority != cvss_base_severity: |
655 | + cves_with_mismatches[cvss_base_severity] +=1 |
656 | + total_differences +=1 |
657 | + if ubuntu_priority in HIGHER_LEVELS[cvss_base_severity]: |
658 | + needs_explanation = "YES" |
659 | + lower += 1 |
660 | + else: |
661 | + needs_explanation = "Maybe?" |
662 | + higher += 1 |
663 | + else: |
664 | + equals += 1 |
665 | + # if cvss_base_severity != ubuntu_priority: |
666 | + # cves_diff[cvss_base_severity] +=1 |
667 | + # total_diff +=1 |
668 | + # if cvss_base_severity == ubuntu_priority: |
669 | + # cves_with_same[cvss_base_severity] +=1 |
670 | + # total_same +=1 |
671 | + else: |
672 | + print(f"ERROR: {ubuntu_priority} not in {PRIORITY_OFFSET.keys()}") |
673 | + sys.exit(1) |
674 | + cve_data.append(needs_explanation) |
675 | + else: |
676 | + missing_cvss+=1 |
677 | + cve_data = [cve, "None", original_ubuntu_priority, ubuntu_priority, needs_explanation] |
678 | + row = append_row_data( |
679 | + row, |
680 | + rows_data, |
681 | + "{}!A{}:E{}".format("Sheet2", row, row), |
682 | + cve_data |
683 | + ) |
684 | + |
685 | + print(total_differences) |
686 | + print(cves_with_mismatches) |
687 | + print("ALL CVES") |
688 | + print(len(full_cves_information)) |
689 | + print("Missing CVSS") |
690 | + print(missing_cvss) |
691 | + print("NOW LOWER") |
692 | + print(lower) |
693 | + print("NOW HIGER") |
694 | + print(higher) |
695 | + print("NOW EQUALS") |
696 | + print(equals) |
697 | + write_to_spreadsheet(sheet, "1S1L4H5Kfkpgt_j0O2Gk6i8R2e78CFrpaCPyLbRz_No0", rows_data, rows_range=None, batch_update=True) |
698 | + # row = 1 |
699 | + # range = 0 |
700 | + # delta = 60 |
701 | + # for c in data[range, range + delta]: |
702 | + # values = [c] |
703 | + # range = "{}!A{}:E{}".format("Sheet1", row, row) |
704 | + # write_to_spreadsheet( |
705 | + # sheet, "", values, rows_range=range, batch_update=False |
706 | + # ) |
707 | + # row +=1 |
708 | + # print(total_same) |
709 | + # print(cves_with_same) |
710 | + # print(total_diff) |
711 | + # print(cves_diff) |
712 | + # if cves_with_mismatches: |
713 | + # total_cves_to_process = len(cves_with_mismatches) |
714 | + # print(f"\n==== Listing {total_cves_to_process} CVEs (out of {len(full_cves_information)}) with Ubuntu Priority different than CVSS base severity ====") |
715 | + # for index, cve in enumerate(cves_with_mismatches): |
716 | + # print(f"\n=========== {index + 1}/{total_cves_to_process}: {cve} ===========") |
717 | + # print(cves_with_mismatches[cve]['Description']) |
718 | + # print(f"Ubuntu Priority: {cves_with_mismatches[cve]['Priority'][0]}") |
719 | + # print(f"CVSS Severity: {get_cvss_base_severity(cves_with_mismatches[cve]['CVSS'])}") |
720 | + # print(f"Affected packages: {', '.join(cves_with_mismatches[cve]['pkgs'].keys())}") |
721 | + # print(f"Tags: {', '.join(cves_with_mismatches[cve]['tags'])}") |
722 | + # print(f"References: {cves_with_mismatches[cve]['References']}") |
723 | + # print(f"Bugs: {cves_with_mismatches[cve]['Bugs']}") |
724 | + |
725 | + # if cves_with_mismatches: |
726 | + # total_cves_to_process = len(cves_with_mismatches) |
727 | + # print(f"\n==== Listing {total_cves_to_process} CVEs (out of {cves_with_cvss_severity}) with Ubuntu Priority different than CVSS base severity ====") |
728 | + # for index, cve in enumerate(cves_with_mismatches): |
729 | + # print(f"\n=========== {index + 1}/{total_cves_to_process}: {cve} ===========") |
730 | + # print(cves_with_mismatches[cve]['Description']) |
731 | + # print(f"Ubuntu Priority: {cves_with_mismatches[cve]['Priority'][0]}") |
732 | + # print(f"CVSS Severity: {get_cvss_base_severity(cves_with_mismatches[cve]['CVSS'])}") |
733 | + # print(f"Affected packages: {', '.join(cves_with_mismatches[cve]['pkgs'].keys())}") |
734 | + # print(f"Tags: {', '.join(cves_with_mismatches[cve]['tags'])}") |
735 | + # print(f"References: {cves_with_mismatches[cve]['References']}") |
736 | + # print(f"Bugs: {cves_with_mismatches[cve]['Bugs']}") |
737 | + # print("Add priority reason? [Y/N/Q(quit)]") |
738 | + # confirm = input() |
739 | + # if confirm.lower() in ('Y', 'y', 'yes', 'YES'): |
740 | + # reason = input("Please add Ubuntu priority reason: ") |
741 | + # # TODO: check for script dir location |
742 | + # cmd = ['./scripts/active_edit', '-c', cve, '-R', reason] |
743 | + # try: |
744 | + # subprocess.run(cmd, check=True) |
745 | + # except subprocess.CalledProcessError as e: |
746 | + # print(f"ERROR while updating CVE file. {e}") |
747 | + |
748 | + # elif confirm.lower() in ('Q', 'q', 'quit', 'Quit', 'QUIT'): |
749 | + # print("Quiting...") |
750 | + # sys.exit(0) |
751 | + # elif confirm.lower() in ('N', 'n', 'no', 'NO'): |
752 | + # print(f"Skipping {cve}") |
Hey Emi, I gave this a try and this is a good direction for the workflow. I appreciated the addition of the number of CVEs that need a priority explanation, I tweaked it a bit to report that out of the total number of CVEs examined within the given time period, with the following diff:
--- a/scripts/ detect_ priorities_ mismatches. py detect_ priorities_ mismatches. py
+++ b/scripts/
@@ -59,7 +59,7 @@ if __name__ == "__main__":
if cves_with_ mismatches:
total_ cves_to_ process = len(cves_ with_mismatches ) cves_to_ process} CVEs with Ubuntu Priority different than CVSS base severity ====") cves_to_ process} CVEs (out of {len(full_ cves_informatio n)}) with Ubuntu Priority different than CVSS base severity cves_with_ mismatches) :
print( f"\n=== ======= = {index + 1}/{total_ cves_to_ process} : {cve} ===========")
print( cves_with_ mismatches[ cve]['Descripti on'])
- print(f"\n==== Listing {total_
+ print(f"\n==== Listing {total_
====")
for index, cve in enumerate(
which results in output like so since the beginning of 2024 based on current UCT as of now:
==== Listing 246 CVEs (out of 477) with Ubuntu Priority different than CVSS base severity ====
In examining the CVEs that popped up, I found that I ended up opening up the CVE file to examine the reference links and bug reports to dig into more details about each CVE.
Something you might consider is how check-cves reports the options available to the user at https:/ /git.launchpad. net/ubuntu- cve-tracker/ tree/scripts/ check-cves# n1101 ; I don't think it needs to be as verbose, but the ways of indicating which key to press and what the default is if one simply presses enter may be useful.
I would like a way to generate a non-interactive report of CVEs that need action.
One thing that might be nice is instead of having the published_since argument to load_table(), take a criteria function as an argument, where the function expects to be passed a CVE and return True or False to include it, allowing use of different criteria besides being published after a given date as options to load_table(). You could then have load_table_ published_ since() if you wanted that the detect_ priorities_ mismatch. py calls that under the hood invokes load_table() with a criteria function that looks for published dates after the passed date.