Merge ~emitorino/ubuntu-cve-tracker:detect_priority_mismatch into ubuntu-cve-tracker:master

Proposed by Emilia Torino
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)
Reviewer Review Type Date Requested Status
Steve Beattie Approve
Ubuntu Security Team Pending
Review via email: mp+459935@code.launchpad.net

Commit message

- scripts/detect_priorities_mismatches.py: add script to load CVEs and detect when the Ubuntu priority deviates from the CVSS score
- scripts/active_edit: support setting the priority reason only
- 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/detect_priorities_mismatches.py --published-since 2024-01-01

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/process_cves refresh-cvss is previously run). This is not a hard requirement, but ideal.

The idea of this MP is to discuss this implementation. Further testing and checks are still required

To post a comment you must log in.
Revision history for this message
Steve Beattie (sbeattie) 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/detect_priorities_mismatches.py
+++ b/scripts/detect_priorities_mismatches.py
@@ -59,7 +59,7 @@ if __name__ == "__main__":

     if cves_with_mismatches:
         total_cves_to_process = len(cves_with_mismatches)
- print(f"\n==== Listing {total_cves_to_process} CVEs with Ubuntu Priority different than CVSS base severity ====")
+ print(f"\n==== Listing {total_cves_to_process} CVEs (out of {len(full_cves_information)}) with Ubuntu Priority different than CVSS base severity
 ====")
         for index, cve in enumerate(cves_with_mismatches):
             print(f"\n=========== {index + 1}/{total_cves_to_process}: {cve} ===========")
             print(cves_with_mismatches[cve]['Description'])

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.

Revision history for this message
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/detect_priorities_mismatches.py
> +++ b/scripts/detect_priorities_mismatches.py
> @@ -59,7 +59,7 @@ if __name__ == "__main__":
>
> if cves_with_mismatches:
> total_cves_to_process = len(cves_with_mismatches)
> - print(f"\n==== Listing {total_cves_to_process} CVEs with Ubuntu
> Priority different than CVSS base severity ====")
> + print(f"\n==== Listing {total_cves_to_process} CVEs (out of
> {len(full_cves_information)}) with Ubuntu Priority different than CVSS base
> severity
> ====")
> for index, cve in enumerate(cves_with_mismatches):
> print(f"\n=========== {index + 1}/{total_cves_to_process}: {cve}
> ===========")
> print(cves_with_mismatches[cve]['Description'])
>
> 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://git.launchpad.net/~emitorino/ubuntu-cve-tracker/commit/?id=e99397b8cddba3e9eb89222ce1782fe625fd3da7

>
> 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.

Thanks for the feedback! I will work on the suggestions.

Revision history for this message
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?

Revision history for this message
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!

Revision history for this message
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_lists/priority_explanations.yaml b/meta_lists/priority_explanations.yaml
new file mode 100644
index 00000000000..868d5011dca
--- /dev/null
+++ b/meta_lists/priority_explanations.yaml
@@ -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.

Revision history for this message
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_lists/priority_explanations.yaml
> b/meta_lists/priority_explanations.yaml
> new file mode 100644
> index 00000000000..868d5011dca
> --- /dev/null
> +++ b/meta_lists/priority_explanations.yaml
> @@ -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.

Added in https://git.launchpad.net/~emitorino/ubuntu-cve-tracker/commit/?id=8c994d7350484b43b72b876a84764548185a00db

I also added:

1) the capability to filter CVEs published until a provided date https://git.launchpad.net/~emitorino/ubuntu-cve-tracker/commit/?id=0ada37ceebc83e7ad345e8a45f15bbd22e1082af. So this combined with --published-since can allow to filter by a range

2) The support for non-interactive (i.e. tmpfile) mode https://git.launchpad.net/~emitorino/ubuntu-cve-tracker/commit/?id=44f9c8be7a6aef70ce65bcec4caf46b35259469f

Revision history for this message
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/detect_priorities_mismatches.py --packages linux --published-since 2024-02-01
[... 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_keys_access() only uses fixed off for validation. However,
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:

============================ Summary =============================
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.)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/meta_lists/priority_explanations.yaml b/meta_lists/priority_explanations.yaml
2new file mode 100644
3index 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.
14diff --git a/scripts/active_edit b/scripts/active_edit
15index 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)
110diff --git a/scripts/cve_lib.py b/scripts/cve_lib.py
111index 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
201diff --git a/scripts/detect_priorities_mismatches.py b/scripts/detect_priorities_mismatches.py
202new file mode 100644
203index 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
529diff --git a/scripts/report_priority_offset_spike.py b/scripts/report_priority_offset_spike.py
530new file mode 100644
531index 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}")

Subscribers

People subscribed via source and target branches