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
diff --git a/meta_lists/priority_explanations.yaml b/meta_lists/priority_explanations.yaml
0new file mode 1006440new file mode 100644
index 0000000..868d501
--- /dev/null
+++ b/meta_lists/priority_explanations.yaml
@@ -0,0 +1,7 @@
1# standardized priority reason text
2# tag/keyword based, so that we can add the tags as part of triage and
3# re-triage to make it easier to generate.
4---
5kernel-debugfs: |
6 Exploitation requires write access to debugfs entries, which are
7 restricted to root by default on Ubuntu kernels.
diff --git a/scripts/active_edit b/scripts/active_edit
index a0e0efa..cb4beb4 100755
--- a/scripts/active_edit
+++ b/scripts/active_edit
@@ -33,6 +33,7 @@ parser.add_option("-e", "--embargoed", dest="embargoed", help="This is an embarg
33parser.add_option("-y", "--yes", dest="autoconfirm", help="Do not ask for confirmation", action="store_true")33parser.add_option("-y", "--yes", dest="autoconfirm", help="Do not ask for confirmation", action="store_true")
34parser.add_option("-P", "--public", dest="public_date", help="Record date the CVE went public (default to today in UTC)", metavar="YYYY-MM-DD")34parser.add_option("-P", "--public", dest="public_date", help="Record date the CVE went public (default to today in UTC)", metavar="YYYY-MM-DD")
35parser.add_option("--priority", help="Record a priority for the CVE", default=None)35parser.add_option("--priority", help="Record a priority for the CVE", default=None)
36parser.add_option("-R", "--priority-reason", help="Record a priority reason for the CVE", default=None)
36parser.add_option("-C", "--cvss", help="CVSS3.1 rating", metavar="CVSS:3.1/AV:_/AC:_/PR:_/UI:_/S:_/C:_/I:_/A:_")37parser.add_option("-C", "--cvss", help="CVSS3.1 rating", metavar="CVSS:3.1/AV:_/AC:_/PR:_/UI:_/S:_/C:_/I:_/A:_")
37parser.add_option("-d", "--description", help="Description", default=None)38parser.add_option("-d", "--description", help="Description", default=None)
38(options, args) = parser.parse_args()39(options, args) = parser.parse_args()
@@ -164,17 +165,18 @@ def add_pkg(p, fp, fixed, parent, embargoed, break_fixes):
164165
165def create_or_update_cve(cve, packages, priority=None, bug_urls=None,166def create_or_update_cve(cve, packages, priority=None, bug_urls=None,
166 ref_urls=None, public_date=None, desc=None,167 ref_urls=None, public_date=None, desc=None,
167 cvss=None, embargoed=False, breakfix=False):168 cvss=None, embargoed=False, breakfix=False,
168169 priority_reason=None):
169 pkgs = []170 pkgs = []
170 break_fixes = []171 break_fixes = []
171 fixed = {}172 fixed = {}
172 # parse optional fixed_in release and version from package name173 # parse optional fixed_in release and version from package name
173 for p in packages:174 if packages:
174 tmp_p = p.split(',')175 for p in packages:
175 pkg = tmp_p[0]176 tmp_p = p.split(',')
176 pkgs.append(pkg)177 pkg = tmp_p[0]
177 fixed[pkg] = tmp_p[1:]178 pkgs.append(pkg)
179 fixed[pkg] = tmp_p[1:]
178180
179 update = False181 update = False
180 try:182 try:
@@ -219,8 +221,10 @@ def create_or_update_cve(cve, packages, priority=None, bug_urls=None,
219 if not embargoed and not public_date:221 if not embargoed and not public_date:
220 public_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")222 public_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
221223
222 if update:224 if update and not priority_reason:
223 mode = "a"225 mode = "a"
226 elif update and priority_reason:
227 mode = "r+"
224 else:228 else:
225 mode = "x"229 mode = "x"
226 with open(dst, mode, encoding="utf-8") as fp:230 with open(dst, mode, encoding="utf-8") as fp:
@@ -245,8 +249,10 @@ def create_or_update_cve(cve, packages, priority=None, bug_urls=None,
245 for url in (bug_urls if bug_urls else []):249 for url in (bug_urls if bug_urls else []):
246 print(" %s" % url, file=fp)250 print(" %s" % url, file=fp)
247 priority_text = priority if priority else "untriaged"251 priority_text = priority if priority else "untriaged"
248 if priority in cve_lib.PRIORITY_REASON_REQUIRED:252 if priority in cve_lib.PRIORITY_REASON_REQUIRED and not priority_reason:
249 priority_text += "\n XXX-Reason-XXX"253 priority_text += "\n XXX-Reason-XXX"
254 elif priority and priority_reason:
255 priority_text += f"\n {priority_reason}"
250 print('Priority: %s' % priority_text, file=fp)256 print('Priority: %s' % priority_text, file=fp)
251 print('Discovered-by:', file=fp)257 print('Discovered-by:', file=fp)
252 print('Assigned-to:', file=fp)258 print('Assigned-to:', file=fp)
@@ -257,14 +263,22 @@ def create_or_update_cve(cve, packages, priority=None, bug_urls=None,
257263
258 for p in pkgs:264 for p in pkgs:
259 add_pkg(p, fp, fixed, None, embargoed, break_fixes)265 add_pkg(p, fp, fixed, None, embargoed, break_fixes)
266 if priority_reason:
267 metadata_to_update = "Priority:"
268 update_file_metadata(metadata_to_update, priority_reason, fp)
260269
261270
271def update_file_metadata(metadata, value, cve_fp):
272 cve_content = cve_fp.readlines()
273 for cve_line_number, cve_line in enumerate (cve_content):
274 if cve_line.startswith(metadata):
275 # TODO: check length + wrap lines + existing reason
276 cve_content[cve_line_number] += f" {value}\n"
277 cve_fp.seek(0)
278 cve_fp.writelines(cve_content)
262279
263pkg_db = cve_lib.load_package_db()
264280
265if not options.pkgs:281pkg_db = cve_lib.load_package_db()
266 parser.print_help()
267 sys.exit(1)
268282
269if not options.cve:283if not options.cve:
270 parser.print_help()284 parser.print_help()
@@ -290,5 +304,5 @@ if not pat.search(cve):
290 print("Bad CVE entry. Should be CVE-XXXX-XXXX\n", file=sys.stderr)304 print("Bad CVE entry. Should be CVE-XXXX-XXXX\n", file=sys.stderr)
291 sys.exit(1)305 sys.exit(1)
292306
293create_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)307create_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)
294sys.exit(0)308sys.exit(0)
diff --git a/scripts/cve_lib.py b/scripts/cve_lib.py
index 95c8f65..7aeb4dd 100755
--- a/scripts/cve_lib.py
+++ b/scripts/cve_lib.py
@@ -29,6 +29,9 @@ from uct.config import read_uct_config
29from functools import reduce29from functools import reduce
30from functools import lru_cache30from functools import lru_cache
3131
32CVE_FILTER_NAME = "cve_filter_name"
33CVE_FILTER_ARGS = "cve_filter_args"
34
32def set_cve_dir(path):35def set_cve_dir(path):
33 '''Return a path with CVEs in it. Specifically:36 '''Return a path with CVEs in it. Specifically:
34 - if 'path' has CVEs in it, return path37 - if 'path' has CVEs in it, return path
@@ -2576,12 +2579,52 @@ def load_all(cves, uems, rcves=[]):
2576 table.setdefault(cve, info)2579 table.setdefault(cve, info)
2577 return table2580 return table
25782581
2582# Get CVE date information only, in the format of "%Y-%m-%d"
2583def parse_cve_pub_date(cve_public_date_from_file):
2584 cve_public_date = None
2585 date_only = cve_public_date_from_file.split()[0]
2586 if date_only != "unknown":
2587 try:
2588 cve_public_date = datetime.datetime.strptime(date_only, "%Y-%m-%d")
2589 except ValueError as e:
2590 print(f"WARN: Invalid CVE PublicDate: {date_only}. {e}")
2591 return cve_public_date
2592
2593
2594def cve_published_since(cve_info, published_since):
2595 cve_public_date = parse_cve_pub_date(cve_info['PublicDate'])
2596 return cve_public_date and cve_public_date > published_since
2597
2598
2599def cve_published_until(cve_info, published_until):
2600 cve_public_date = parse_cve_pub_date(cve_info['PublicDate'])
2601 return cve_public_date and cve_public_date < published_until
2602
2603
2604def cve_published_in_range(cve_info, published_since, published_until):
2605 return cve_published_since(cve_info, published_since) and cve_published_until(cve_info, published_until)
25792606
2580# supported options2607
2608def cve_affecting_pkgs(cve_info, packages):
2609 # removing whitespaces, if they exist before cchecking
2610 cve_affected_packages = [x.strip(' ') for x in cve_info['pkgs']]
2611 packages_in_filter = [x.strip(' ') for x in packages]
2612 return [pkg for pkg in packages_in_filter if pkg in cve_affected_packages]
2613
2614
2615def cve_in_list(cve_info, cve_list):
2616 cve_id = cve_info["id"]
2617 return cve_id in cve_list
2618
2619
2620
2621# supported options for opt argument
2581# pkgfamily = rename linux-source-* packages to "linux", or "xen-*" to "xen"2622# pkgfamily = rename linux-source-* packages to "linux", or "xen-*" to "xen"
2582# packages = list of packages to pay attention to2623# packages = list of packages to pay attention to
2583# debug = bool, display debug information2624# debug = bool, display debug information
2584def load_table(cves, uems, opt=None, rcves=[], icves=[]):2625
2626# filters = list of functions and , only load CVEs matching the filters criteria
2627def load_table(cves, uems, opt=None, rcves=[], icves=[], filters=None):
2585 table = dict()2628 table = dict()
2586 priority = dict()2629 priority = dict()
2587 listcves = []2630 listcves = []
@@ -2599,6 +2642,21 @@ def load_table(cves, uems, opt=None, rcves=[], icves=[]):
2599 cvedir = ignored_dir2642 cvedir = ignored_dir
2600 cvefile = os.path.join(cvedir, cve)2643 cvefile = os.path.join(cvedir, cve)
2601 info = load_cve(cvefile)2644 info = load_cve(cvefile)
2645 # Allowing to filter cves based on a list of filter functions
2646 # with their corresponding args
2647 filters_matched = True
2648 for filter in filters:
2649 filter_name = filter[CVE_FILTER_NAME]
2650 filter_args = filter[CVE_FILTER_ARGS]
2651 # always adding cve_info to any filter function its needed
2652 info["id"] = cve
2653 filter_args["cve_info"] = info
2654 if not filter_name(**filter_args):
2655 filters_matched = False
2656 break
2657 if not filters_matched:
2658 # Skipping CVEs which have not matched provided filters, if any
2659 continue
2602 cveinfo[cve] = info2660 cveinfo[cve] = info
26032661
2604 # Allow for Priority overrides2662 # Allow for Priority overrides
diff --git a/scripts/detect_priorities_mismatches.py b/scripts/detect_priorities_mismatches.py
2605new file mode 1006442663new file mode 100644
index 0000000..0d249da
--- /dev/null
+++ b/scripts/detect_priorities_mismatches.py
@@ -0,0 +1,321 @@
1#!/usr/bin/python3
2import argparse
3import os
4import subprocess
5import sys
6import tempfile
7from datetime import datetime
8import cve_lib
9
10ACTION_ADD_REASON = "add-reason"
11ACTION_EDIT_REASON = "edit-reason"
12ACTION_SKIP = "skip"
13
14EXPECTED_CVE_DESCRIPTIONS = (
15 "UNSUPPORTED", # ** UNSUPPORTED WHEN ASSIGNED **
16 "DISPUTED", # ** DISPUTED **
17 "REJECT", # ** REJECT **
18 "Note:", # **Note:** This is only exploitable
19 "This", # *This bug only affects
20 "info", # *info` actually points
21 "not" #*not* get upgraded
22)
23
24def get_cvss_base_severity(cvss_information):
25 base_severity = None
26 for cvss in cvss_information:
27 if cvss['source'] == "nvd":
28 cvss_base_severity = cvss['baseSeverity']
29 if cvss_base_severity != "NONE":
30 base_severity = cvss_base_severity
31 return base_severity
32
33
34def display_command_file_usage(output, line_prefix=''):
35 output.write(f'{line_prefix} The following commands can be used in this file:\n')
36 output.write(f'{line_prefix} \n')
37 output.write(f'{line_prefix}"* Add a CVE Ubuntu priority reason to the tracker:"\n')
38 output.write(f'{line_prefix}" <CVE> {ACTION_ADD_REASON} <REASON> ..."\n')
39 output.write(f'{line_prefix}"* Add a CVE Ubuntu priority reason in your editor:"\n')
40 output.write(f'{line_prefix}" <CVE> {ACTION_EDIT_REASON} ..."\n')
41 output.write(f'{line_prefix}"* Temporarily skip over a CVE:"\n')
42 output.write(f'{line_prefix}" <CVE> {ACTION_SKIP}\n"\n')
43 output.flush()
44
45
46def process_command_file(cves_output_content):
47 cves_output_content.seek(0)
48 line_number = 0
49 cves = set()
50 skipped_cves = set()
51 cves_to_add_reason_automatically = dict ()
52 invalid_lines = dict()
53 for line in cves_output_content.readlines():
54 line_number += 1
55 line = line.strip()
56
57 # We only want to action on lines starting with *
58 if not line or not line.startswith('*'):
59 continue
60
61 line_content = line.split()
62 try:
63 # action lines should start with * prefix, followed by a CVE number and desired action
64 (cve, action) = (line_content[1].strip().upper(), line_content[2])
65 except IndexError:
66 invalid_lines[str(line_number)] = [line, "Invalid line format"]
67 continue
68
69 # Ignore lines that look like action lines but are expected cve descriptions
70 if cve.startswith(EXPECTED_CVE_DESCRIPTIONS):
71 continue
72
73 if not cve.startswith('CVE-'):
74 # The first arg should look like a CVE ID
75 invalid_lines[str(line_number)] = [line, "Invalid CVE ID format"]
76 continue
77
78 if cve in cves:
79 invalid_lines[str(line_number)] = [line, f"Operation over {cve} was already performed"]
80 continue
81
82 cves.add(cve)
83 # This action sets the priority reason to the file without opening an editor
84 if action == ACTION_ADD_REASON:
85 # If the priority reason should be added, it must be present
86 try:
87 reason = " ".join(line_content[3:])
88 except IndexError:
89 invalid_lines[str(line_number)] = [line, f"Reason was not specified for {cve}"]
90 continue
91 cves_to_add_reason_automatically[cve] = [line, reason]
92
93 # This action opens an editor to edit the CVE
94 elif action == ACTION_EDIT_REASON:
95 try:
96 # find_cve will through ValueError exception if cve file is not found
97 cve_path= cve_lib.find_cve(cve)
98 except ValueError as e:
99 invalid_lines[str(line_number)] = [line, f"Unable to find cve file for {cve}: {e}"]
100 continue
101 cves_to_add_reason_in_editor[cve] = {"path": cve_path}
102
103 elif action == ACTION_SKIP:
104 # If the CVE should be skipped, no arguments are allowed
105 if len(line_content) > 3:
106 invalid_lines[str(line_number)] = [line, "Invalid skip command"]
107 continue
108 skipped_cves.add(cve)
109
110 else:
111 # The second arg must be a known action
112 invalid_lines[str(line_number)] = [line, f"Unknown action {action}"]
113 return invalid_lines, cves_to_add_reason_automatically, cves_to_add_reason_in_editor, skipped_cves
114
115
116def active_edit_priority_reason(cve, reason):
117 # TODO: check for script dir location
118 # TODO: check reason input
119 success = True
120 cmd = ['./scripts/active_edit', '-c', cve, '-R', reason]
121 try:
122 subprocess.run(cmd, check=True)
123 except subprocess.CalledProcessError as e:
124 output(f"ERROR while updating CVE file. {e}")
125 success = False
126 return success
127
128
129def report(errors, cves_to_add_reason_automatically, cves_to_add_reason_in_editor, skipped_cves, total, temp_file, interactive, keep_tmp_file):
130 print("\n============================ Summary =============================")
131 print(f"{total} CVEs needing priority reason")
132 print(f"{len(skipped_cves) + len(cves_to_add_reason_automatically) + len(cves_to_add_reason_in_editor)} CVEs processed:")
133 print(f" - {len(skipped_cves)} skipped CVE(s)")
134 print(f" - {len(cves_to_add_reason_automatically)} CVE(s) automatically updated with their corresponding reasons")
135 if not interactive:
136 print(f" - {len(cves_to_add_reason_in_editor)} CVE(s) opened in editor for manual edit")
137 print(f" - {len(errors)} issue(s)")
138 for error in errors:
139 if not interactive:
140 # In non-interactive mode, we have more information to report
141 print(f" - #{error}: {errors[error][1]}: {errors[error][0]}")
142 else:
143 print(f" - {error}: Unable to edit file.")
144 if not interactive and keep_tmp_file:
145 print(f"\n\ntempfile preserved at: {temp_file}")
146
147
148def setup_tmp_file(keep_tmp_file, tmp_file_content_prefix):
149 tmp_file_prefix="cves-needing-priority-reason."
150 # By default tmpfile is deleted
151 # We might want to preserve it for troubleshooting
152 delete_tmp_file = True
153 if keep_tmp_file:
154 delete_tmp_file = False
155 output = tempfile.NamedTemporaryFile(prefix=tmp_file_prefix, mode='w+', delete=delete_tmp_file)
156 display_command_file_usage(output, tmp_file_content_prefix)
157 return output
158
159
160# TODO: move this to other cve_lib filter
161def get_cves_with_mismatches(full_cves_information):
162 cves_with_mismatches = dict()
163 cves_with_cvss_severity = 0
164 for cve in full_cves_information:
165 ubuntu_priority = full_cves_information[cve]['Priority'][0]
166 cvss_base_severity = get_cvss_base_severity(full_cves_information[cve]['CVSS'])
167 if cvss_base_severity:
168 cves_with_cvss_severity +=1
169 ubuntu_priority_reason = full_cves_information[cve]['Priority'][1]
170 if cvss_base_severity.lower() != ubuntu_priority and not ubuntu_priority_reason:
171 cves_with_mismatches[cve] = full_cves_information[cve]
172 return cves_with_mismatches, cves_with_cvss_severity
173
174
175def check_date(date):
176 try:
177 parsed_date = datetime.strptime(date, "%Y-%m-%d")
178 except ValueError as e:
179 output.write(f"ERROR: invalid date argument value: {date}. {e}")
180 sys.exit(1)
181 return parsed_date
182
183
184def spawn_editor(path):
185 editor = os.getenv('EDITOR', 'vi')
186 subprocess.call([editor, path])
187
188
189if __name__ == "__main__":
190 parser = argparse.ArgumentParser()
191 parser.add_argument(
192 "--published-since",
193 help="Report CVEs published only since the date specified. Format: YYYY-MM-DD",
194 )
195 parser.add_argument(
196 "--published-until",
197 help="Report CVEs published only until the date specified. Format: YYYY-MM-DD",
198 )
199 parser.add_argument(
200 "--packages",
201 help="Report CVEs only affecting the list of comma separated packages. Format: pkg1,pkg2",
202 )
203 parser.add_argument(
204 "--interactive",
205 action ='store_true',
206 )
207 parser.add_argument(
208 "--keep-tmp-file",
209 action ='store_true',
210 )
211 args = parser.parse_args()
212
213 interactive_mode = False
214 original_output = sys.stdout
215 keep_tmp_file = args.keep_tmp_file
216 if args.interactive:
217 # In interactive mode, content is print to std out
218 output = sys.stdout
219 interactive_mode = True
220 else:
221 # In non-interactive mode, content is printed to a tmpfile
222 tmp_file_content_prefix = '# '
223 tmp_file_action_prefix = "* "
224 output = setup_tmp_file(keep_tmp_file, tmp_file_content_prefix)
225
226 cve_table_filters = []
227 since_date = None
228 until_date = None
229 if args.published_since:
230 since_date = check_date(args.published_since)
231 cve_table_filters.append({cve_lib.CVE_FILTER_NAME: cve_lib.cve_published_since, cve_lib.CVE_FILTER_ARGS: {"published_since": since_date}})
232
233 if args.published_until:
234 until_date = check_date(args.published_until)
235 cve_table_filters.append({cve_lib.CVE_FILTER_NAME: cve_lib.cve_published_until, cve_lib.CVE_FILTER_ARGS: {"published_until": until_date}})
236
237 if args.packages:
238 packages = args.packages.split(",")
239 cve_table_filters.append({cve_lib.CVE_FILTER_NAME: cve_lib.cve_affecting_pkgs, cve_lib.CVE_FILTER_ARGS: {"packages": packages}})
240
241 try:
242 print("\n==== Loading CVEs from UCT ====\n")
243 all_cves, embargoed_cves, retired_cves, ignored_cves = cve_lib.get_all_cve_list()
244 (
245 _,
246 _,
247 _,
248 _,
249 full_cves_information,
250 ) = cve_lib.load_table(all_cves, embargoed_cves, None, retired_cves, ignored_cves, cve_table_filters)
251 except ValueError as e:
252 # cve_lib.load_cve() can raises value error on CVE issues
253 print(f"ERROR: unable to load CVEs: {e}")
254 sys.exit(1)
255
256 cves_to_add_reason_automatically = dict()
257 cves_to_add_reason_in_editor = dict ()
258 skipped_cves = set()
259 errors = set()
260 total_cves_to_process = 0
261 tmp_file_name = None
262 cves_with_mismatches, cves_with_cvss_severity = get_cves_with_mismatches(full_cves_information)
263 if cves_with_mismatches:
264 total_cves_to_process = len(cves_with_mismatches)
265
266 output.write(f"\n==== Listing {total_cves_to_process} CVEs (out of {cves_with_cvss_severity}) with Ubuntu Priority different than CVSS base severity ====")
267
268 for index, cve in enumerate(cves_with_mismatches):
269 # Provide basic CVE metadata
270 output.write(f"\n\n=========== {index + 1}/{total_cves_to_process}: {cve} ===========")
271 output.write(f"{cves_with_mismatches[cve]['Description']}")
272 output.write(f"\n- Public Date: {cves_with_mismatches[cve]['PublicDate']}")
273 output.write(f"\n- Ubuntu Priority: {cves_with_mismatches[cve]['Priority'][0]} || CVSS Severity: {get_cvss_base_severity(cves_with_mismatches[cve]['CVSS'])}")
274 output.write(f"\n- Affected packages: {', '.join(cves_with_mismatches[cve]['pkgs'].keys())}")
275 output.write(f"\n- Tags: {', '.join(cves_with_mismatches[cve]['tags'])}")
276 output.write(f"\n- References: {cves_with_mismatches[cve]['References']}")
277 output.write(f"\n- Bugs: {cves_with_mismatches[cve]['Bugs']}")
278
279 # In interactive mode, ask and perform action on every CVE
280 if interactive_mode:
281 output.write("\n\nAdd priority reason? \nY]es, S]kip for now, or Q]uit]")
282 confirm = input()
283 if confirm.lower() in ('Y', 'y', 'yes', 'YES'):
284 reason = input("Please add Ubuntu priority reason: ")
285 if not active_edit_priority_reason(cve, reason):
286 errors.add(cve)
287 continue
288 # In interactive mode, we only add to cves_to_add_reason_automatically
289 # for later summary purposes.
290 cves_to_add_reason_automatically[cve] = []
291 elif confirm.lower() in ('Q', 'q', 'quit', 'Quit', 'QUIT'):
292 output.write("Quiting...\n")
293 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)
294 sys.exit(0)
295 elif confirm.lower() in ('S', 's', 'skip', 'Skip', 'SKIP'):
296 output.write(f"Skipping {cve}\n")
297 skipped_cves.add(cve)
298 else:
299 output.write("\n Invalid option, skipping {cve}\n")
300 skipped_cves.add(cve)
301 # In non-interactive mode, add cve info to file. Actions will be executed later
302 else:
303 output.write(f"\n\n{tmp_file_content_prefix}{tmp_file_action_prefix}{cve} {ACTION_ADD_REASON}")
304 output.write(f"\n{tmp_file_content_prefix}{tmp_file_action_prefix}{cve} {ACTION_EDIT_REASON}")
305 output.write(f"\n{tmp_file_content_prefix}{tmp_file_action_prefix}{cve} {ACTION_SKIP}")
306 if not interactive_mode:
307 output.flush()
308 tmp_file_name = output.name
309 # Show tmp file content in configured editor
310 spawn_editor(output.name)
311 errors, cves_to_add_reason_automatically, cves_to_add_reason_in_editor, skipped_cves = process_command_file(output)
312 for cve in cves_to_add_reason_automatically:
313 reason = cves_to_add_reason_automatically[cve][1]
314 if not active_edit_priority_reason(cve, reason):
315 file_line = cves_to_add_reason_automatically[cve][0]
316 errors[cve]=[file_line, "Unable to add priority reason automatically."]
317 for cve in cves_to_add_reason_in_editor:
318 spawn_editor(cves_to_add_reason_in_editor[cve]["path"])
319 # restore original sys.stdout
320 sys.stdout = original_output
321 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)
0\ No newline at end of file322\ No newline at end of file
diff --git a/scripts/report_priority_offset_spike.py b/scripts/report_priority_offset_spike.py
1new file mode 100644323new file mode 100644
index 0000000..506249f
--- /dev/null
+++ b/scripts/report_priority_offset_spike.py
@@ -0,0 +1,218 @@
1#!/usr/bin/python3
2
3import argparse
4import subprocess
5import sys
6from datetime import datetime
7import cve_lib
8import os
9
10# this is needed to consume google docs apis wrapper
11USM = os.environ.get(
12 "USM", os.path.expandvars("$HOME/git-pulls/ubuntu-security-metrics")
13)
14sys.path.append(f"{USM}")
15
16from metrics.data_sources.google_document import (
17 connect_to_spreadsheet,
18 write_to_spreadsheet,
19 append_row_data
20)
21
22PRIORITY_OFFSET = {
23 'critical': 'high' ,
24 'high': 'medium',
25 'medium': 'low',
26 'low': 'negligible'
27}
28
29PRIORITY_BUMP= {
30 'high': 'critical',
31 'medium': 'high',
32 'low': 'medium',
33 'negligible': 'low'
34}
35
36HIGHER_LEVELS = {
37 'critical' : ['high', 'medium','low'],
38 'high' : ['medium','low'],
39 'medium' : ['low'],
40 'low' : [],
41}
42
43def get_cvss_base_severity(cvss_information):
44 base_severity = None
45 for cvss in cvss_information:
46 if cvss['source'] == "nvd":
47 cvss_base_severity = cvss['baseSeverity']
48 if cvss_base_severity != "NONE":
49 base_severity = cvss_base_severity
50 return base_severity
51
52
53if __name__ == "__main__":
54 parser = argparse.ArgumentParser()
55 parser.add_argument(
56 "--published-since",
57 help="Report CVEs published only since the date specified. Format: YYYY-MM-DD",
58 )
59 args = parser.parse_args()
60
61 since_date = None
62 if args.published_since:
63 try:
64 since_date = datetime.strptime(args.published_since, "%Y-%m-%d")
65 except ValueError as e:
66 print(f"ERROR: invalid since date argument value: {args.published_since}. {e}")
67 sys.exit(1)
68
69 try:
70 print("\n==== Loading CVEs from UCT ====\n")
71 all_cves, embargoed_cves, retired_cves, ignored_cves = cve_lib.get_all_cve_list()
72 (
73 _,
74 _,
75 _,
76 _,
77 full_cves_information,
78 ) = cve_lib.load_table(all_cves, embargoed_cves, None, retired_cves, ignored_cves, since_date)
79 except ValueError as e:
80 # cve_lib.load_cve() can raises value error on CVE issues
81 print(f"ERROR: unable to load CVEs: {e}")
82 sys.exit(1)
83
84 cves_with_mismatches = dict()
85 cves_with_cvss_severity = 0
86 #cves_with_mismatches = dict()
87 cves_with_mismatches = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0}
88 total_differences = 0
89 cves_with_same = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0}
90 total_same = 0
91 cves_diff = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0}
92 total_diff = 0
93
94 creds_file = "/home/emitorino/register-automation-1338127a145f.json"
95 sheet = connect_to_spreadsheet(creds_file)
96 rows_data = []
97 row = 2
98 missing_cvss = 0
99 equals = 0
100 higher = 0
101 lower = 0
102 for cve in full_cves_information:
103 original_ubuntu_priority = full_cves_information[cve]['Priority'][0]
104 ubuntu_priority = PRIORITY_BUMP[original_ubuntu_priority]
105 print (f"Bumping {original_ubuntu_priority} to {ubuntu_priority}")
106 cvss_base_severity = get_cvss_base_severity(full_cves_information[cve]['CVSS'])
107 needs_explanation = "NO"
108 cve_data = []
109 if cvss_base_severity:
110 cves_with_cvss_severity +=1
111 cvss_base_severity = cvss_base_severity.lower()
112 cve_data = [cve, cvss_base_severity, original_ubuntu_priority, ubuntu_priority]
113 # if cvss_base_severity == ubuntu_priority:
114 # cves_with_same[cvss_base_severity] +=1
115 # total_same +=1
116 ubuntu_priority_reason = full_cves_information[cve]['Priority'][1]
117 # if cvss_base_severity.lower() != ubuntu_priority and not ubuntu_priority_reason:
118 # cves_with_mismatches[cve] = full_cves_information[cve]
119 if cvss_base_severity in PRIORITY_OFFSET:
120 if ubuntu_priority != cvss_base_severity:
121 cves_with_mismatches[cvss_base_severity] +=1
122 total_differences +=1
123 if ubuntu_priority in HIGHER_LEVELS[cvss_base_severity]:
124 needs_explanation = "YES"
125 lower += 1
126 else:
127 needs_explanation = "Maybe?"
128 higher += 1
129 else:
130 equals += 1
131 # if cvss_base_severity != ubuntu_priority:
132 # cves_diff[cvss_base_severity] +=1
133 # total_diff +=1
134 # if cvss_base_severity == ubuntu_priority:
135 # cves_with_same[cvss_base_severity] +=1
136 # total_same +=1
137 else:
138 print(f"ERROR: {ubuntu_priority} not in {PRIORITY_OFFSET.keys()}")
139 sys.exit(1)
140 cve_data.append(needs_explanation)
141 else:
142 missing_cvss+=1
143 cve_data = [cve, "None", original_ubuntu_priority, ubuntu_priority, needs_explanation]
144 row = append_row_data(
145 row,
146 rows_data,
147 "{}!A{}:E{}".format("Sheet2", row, row),
148 cve_data
149 )
150
151 print(total_differences)
152 print(cves_with_mismatches)
153 print("ALL CVES")
154 print(len(full_cves_information))
155 print("Missing CVSS")
156 print(missing_cvss)
157 print("NOW LOWER")
158 print(lower)
159 print("NOW HIGER")
160 print(higher)
161 print("NOW EQUALS")
162 print(equals)
163 write_to_spreadsheet(sheet, "1S1L4H5Kfkpgt_j0O2Gk6i8R2e78CFrpaCPyLbRz_No0", rows_data, rows_range=None, batch_update=True)
164 # row = 1
165 # range = 0
166 # delta = 60
167 # for c in data[range, range + delta]:
168 # values = [c]
169 # range = "{}!A{}:E{}".format("Sheet1", row, row)
170 # write_to_spreadsheet(
171 # sheet, "", values, rows_range=range, batch_update=False
172 # )
173 # row +=1
174 # print(total_same)
175 # print(cves_with_same)
176 # print(total_diff)
177 # print(cves_diff)
178 # if cves_with_mismatches:
179 # total_cves_to_process = len(cves_with_mismatches)
180 # print(f"\n==== Listing {total_cves_to_process} CVEs (out of {len(full_cves_information)}) with Ubuntu Priority different than CVSS base severity ====")
181 # for index, cve in enumerate(cves_with_mismatches):
182 # print(f"\n=========== {index + 1}/{total_cves_to_process}: {cve} ===========")
183 # print(cves_with_mismatches[cve]['Description'])
184 # print(f"Ubuntu Priority: {cves_with_mismatches[cve]['Priority'][0]}")
185 # print(f"CVSS Severity: {get_cvss_base_severity(cves_with_mismatches[cve]['CVSS'])}")
186 # print(f"Affected packages: {', '.join(cves_with_mismatches[cve]['pkgs'].keys())}")
187 # print(f"Tags: {', '.join(cves_with_mismatches[cve]['tags'])}")
188 # print(f"References: {cves_with_mismatches[cve]['References']}")
189 # print(f"Bugs: {cves_with_mismatches[cve]['Bugs']}")
190
191 # if cves_with_mismatches:
192 # total_cves_to_process = len(cves_with_mismatches)
193 # print(f"\n==== Listing {total_cves_to_process} CVEs (out of {cves_with_cvss_severity}) with Ubuntu Priority different than CVSS base severity ====")
194 # for index, cve in enumerate(cves_with_mismatches):
195 # print(f"\n=========== {index + 1}/{total_cves_to_process}: {cve} ===========")
196 # print(cves_with_mismatches[cve]['Description'])
197 # print(f"Ubuntu Priority: {cves_with_mismatches[cve]['Priority'][0]}")
198 # print(f"CVSS Severity: {get_cvss_base_severity(cves_with_mismatches[cve]['CVSS'])}")
199 # print(f"Affected packages: {', '.join(cves_with_mismatches[cve]['pkgs'].keys())}")
200 # print(f"Tags: {', '.join(cves_with_mismatches[cve]['tags'])}")
201 # print(f"References: {cves_with_mismatches[cve]['References']}")
202 # print(f"Bugs: {cves_with_mismatches[cve]['Bugs']}")
203 # print("Add priority reason? [Y/N/Q(quit)]")
204 # confirm = input()
205 # if confirm.lower() in ('Y', 'y', 'yes', 'YES'):
206 # reason = input("Please add Ubuntu priority reason: ")
207 # # TODO: check for script dir location
208 # cmd = ['./scripts/active_edit', '-c', cve, '-R', reason]
209 # try:
210 # subprocess.run(cmd, check=True)
211 # except subprocess.CalledProcessError as e:
212 # print(f"ERROR while updating CVE file. {e}")
213
214 # elif confirm.lower() in ('Q', 'q', 'quit', 'Quit', 'QUIT'):
215 # print("Quiting...")
216 # sys.exit(0)
217 # elif confirm.lower() in ('N', 'n', 'no', 'NO'):
218 # print(f"Skipping {cve}")

Subscribers

People subscribed via source and target branches