From 34a544dbeeb1379962395bd12e0bef621e8b254d Mon Sep 17 00:00:00 2001 From: SukhveerS <78963782+Rexbeast2@users.noreply.github.com> Date: Sat, 19 Aug 2023 23:26:12 +0530 Subject: [PATCH] feat: epss percentile filter (#3244) Co-authored-by: Terri Oda --- README.md | 2 ++ cve_bin_tool/cli.py | 11 ++++++++ cve_bin_tool/cve_scanner.py | 25 ++++++++++++++--- doc/MANUAL.md | 7 +++++ test/test_cli.py | 54 +++++++++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 79c8e7e3c6..d161006f54 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,8 @@ Output: specify multiple output formats by using comma (',') as a separator note: don't use spaces between comma (',') and the output formats. -c CVSS, --cvss CVSS minimum CVSS score (as integer in range 0 to 10) to report (default: 0) + --epss-percentile + minimum EPSS percentile of CVE range between 0 to 100 to report (default: 0) -S {low,medium,high,critical}, --severity {low,medium,high,critical} minimum CVE severity to report (default: low) --no-0-cve-report only produce report when CVEs are found diff --git a/cve_bin_tool/cli.py b/cve_bin_tool/cli.py index 00927598ad..4f1da31845 100644 --- a/cve_bin_tool/cli.py +++ b/cve_bin_tool/cli.py @@ -266,6 +266,12 @@ def main(argv=None): help="minimum CVE severity to report (default: low)", default="low", ) + output_group.add_argument( + "--epss-percentile", + action="store", + help="minimum epss percentile of CVE range between 0 to 100 to report (default: 0)", + default=0, + ) output_group.add_argument( "--no-0-cve-report", action="store_true", @@ -563,6 +569,10 @@ def main(argv=None): if int(args["cvss"]) > 0: score = int(args["cvss"]) + epss_percentile = 0 + if float(args["epss_percentile"]) > 0: + epss_percentile = float(args["epss_percentile"]) / 100 + config_generate = set(args["generate_config"].split(",")) config_generate = [config_type.strip() for config_type in config_generate] for config_type in config_generate: @@ -863,6 +873,7 @@ def main(argv=None): with CVEScanner( score=score, + epss_percentile=epss_percentile, check_exploits=args["exploits"], exploits_list=cvedb_orig.get_exploits_list(), disabled_sources=disabled_sources, diff --git a/cve_bin_tool/cve_scanner.py b/cve_bin_tool/cve_scanner.py index 777d519d7b..2056aa592a 100644 --- a/cve_bin_tool/cve_scanner.py +++ b/cve_bin_tool/cve_scanner.py @@ -40,6 +40,7 @@ class CVEScanner: def __init__( self, score: int = 0, + epss_percentile: float = 0.0, logger: Logger = None, error_mode: ErrorMode = ErrorMode.TruncTrace, check_exploits: bool = False, @@ -49,6 +50,7 @@ def __init__( self.logger = logger or LOGGER.getChild(self.__class__.__name__) self.error_mode = error_mode self.score = score + self.epss_percentile = epss_percentile self.products_with_cve = 0 self.products_without_cve = 0 self.all_cve_data = defaultdict(CVEData) @@ -68,6 +70,8 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData): # being reported if self.score > 10: return + if self.epss_percentile > 100: + return if product_info.vendor == "UNKNOWN": # Add product @@ -257,16 +261,24 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData): row_dict["cvss_version"] or row["cvss_version"] ) # executing query to get metric for CVE - metric_result = self.metric((row["cve_number"],)) + metric_result = self.metric( + (row["cve_number"],), self.epss_percentile + ) # row_dict doesnt have metric as key. As it based on result from query on cve_severity table # declaring row_dict[metric] row_dict["metric"] = {} - # # looping for result of query for metrics. + # looping for result of query for metrics. for key, value in metric_result.items(): row_dict["metric"][key] = [ value[0], value[1], ] + # checking if epss percentile filter is applied + if self.epss_percentile: + # if epss filter is applied and condition is failed to satisfy row_dict["metric"] will be empty + if not row_dict["metric"]: + # continue to not include that particular cve + continue self.logger.debug( f'metrics found in CVE {row_dict["cve_number"]} is {row_dict["metric"]}' ) @@ -358,7 +370,7 @@ def affected(self): for cve_data in self.all_cve_data ) - def metric(self, cve_number): + def metric(self, cve_number, epss_percentile): """The query needs to be executed separately because if it is executed using the same cursor, the search stops. We need to create a separate connection and cursor for the query to be executed independently. Finally, the function should return a dictionary with the metrics of a given CVE. @@ -376,6 +388,13 @@ def metric(self, cve_number): # looping for result of query for metrics. for result in metric_result: metric_name, metric_score, metric_field = result + # if metric is EPSS if metric field must represent EPSS percentile + if metric_name == "EPSS": + # comparing if EPSS percentile found in CVE is less then EPSS percentile return + if float(metric_field) < epss_percentile: + cur.close() + conn.close() + return met met[metric_name] = [ metric_score, metric_field, diff --git a/doc/MANUAL.md b/doc/MANUAL.md index bb69236cbb..9bbee623f6 100644 --- a/doc/MANUAL.md +++ b/doc/MANUAL.md @@ -39,6 +39,7 @@ - [--html-theme HTML_THEME](#--html-theme-html_theme) - [-f {csv,json,console,html}, --format {csv,json,console,html}](#-f-csvjsonconsolehtml---format-csvjsonconsolehtml) - [-c CVSS, --cvss CVSS](#-c-cvss---cvss-cvss) + - [--epss-percentile](#epss-percentile) - [-S {low,medium,high,critical}, --severity {low,medium,high,critical}](#-s-lowmediumhighcritical---severity-lowmediumhighcritical) - [-A \[\-\\], --available-fix \[\-\\]](#-a-distro_name-distro_version_name---available-fix-distro_name-distro_version_name) - [-b \[\-\\], --backport-fix \[\-\\]](#-b-distro_name-distro_version_name---backport-fix-distro_name-distro_version_name) @@ -126,6 +127,8 @@ which is useful if you're trying the latest code from specify multiple output formats by using comma (',') as a separator note: don't use spaces between comma (',') and the output formats. -c CVSS, --cvss CVSS minimum CVSS score (as integer in range 0 to 10) to report (default: 0) + --epss-percentile minimum EPSS percentile of CVE range between 0 to 100 to report + (default: 0) -S {low,medium,high,critical}, --severity {low,medium,high,critical} minimum CVE severity to report (default: low) --no-0-cve-report only produce report when CVEs are found @@ -930,6 +933,10 @@ Note: Please don't use spaces between comma (',') and the output formats. This option specifies the minimum CVSS score (as integer in range 0 to 10) of the CVE to report. The default value is 0 which results in all CVEs being reported. +### --epss-percentile + +this option specifies the minimum EPSS percentile of CVE range between 0 to 100 to report. The default value is 0 which results in all CVEs being reported. + ### -S {low,medium,high,critical}, --severity {low,medium,high,critical} This option specifies the minimum CVE severity to report. The default value is low which results in all CVEs being reported. diff --git a/test/test_cli.py b/test/test_cli.py index 5a02a390fa..61596fea2f 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -484,6 +484,60 @@ def test_CVSS_score(self, capsys, caplog): my_test_filename_pathlib.unlink() caplog.clear() + def test_EPSS_percentile(self, capsys, caplog): + """scan with EPSS percentile to ensure only CVEs above score threshold are reported + Checks cannot placed on epss percentile value as the value changes everyday + """ + + my_test_filename = "epss_percentile.csv" + my_test_filename_pathlib = Path(my_test_filename) + + # Check command line parameters. Less than 0 result in default behaviour. + if my_test_filename_pathlib.exists(): + my_test_filename_pathlib.unlink() + with caplog.at_level(logging.DEBUG): + main( + [ + "cve-bin-tool", + "-x", + "--epss-percentile", + "-1", + "-f", + "csv", + "-o", + my_test_filename, + str(Path(self.tempdir) / CURL_7_20_0_RPM), + ] + ) + # Verify that some CVEs with a severity of Medium are reported + # Checks cannot placed on epss percentile value as the value changes everyday. + assert self.check_string_in_file(my_test_filename, "MEDIUM") + caplog.clear() + + # Check command line parameters. >10 results in no CVEs being reported (Maximum CVSS score is 10) + if my_test_filename_pathlib.exists(): + my_test_filename_pathlib.unlink() + with caplog.at_level(logging.DEBUG): + main( + [ + "cve-bin-tool", + "-x", + "--epss-percentile", + "110", + "-f", + "csv", + "-o", + my_test_filename, + str(Path(self.tempdir) / CURL_7_20_0_RPM), + ] + ) + # Verify that no CVEs are reported + with open(my_test_filename_pathlib) as fd: + assert not fd.read().split("\n")[1] + caplog.clear() + if my_test_filename_pathlib.exists(): + my_test_filename_pathlib.unlink() + @pytest.mark.skip(reason="Needs database rebuild. Temporary fix.") def test_SBOM(self, caplog): # check sbom file option