Skip to content

Commit

Permalink
feat: disable metrics by default
Browse files Browse the repository at this point in the history
Since commit 06b55f7, to get metrics, a
database query is made for every single CVE. This behavior dramatically
increases processing time. For example, 3 seconds was needed before this
change to scan apache2-bin_2.4.10-10+deb8u12_amd64.deb.tar.gz. After
this change, 36 seconds is needed to extract the metrics of the 60 CVEs
related to apache 2.4.10.

Time increase exponentially with the number of CVEs so the increase is
even greater when parsing a directory (e.g. an IoT firmware dump).

I assume that most users are not that interested with those metrics and
would prefer to get a quick result so only retrieve metrics if the user
asks it through the new --metrics option.

Signed-off-by: Fabrice Fontaine <[email protected]>
  • Loading branch information
ffontaine committed Dec 17, 2023
1 parent 098a96f commit 22dae3d
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 69 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ Output:
specify multiple output formats by using comma (',') as a separator
note: don't use spaces between comma (',') and the output formats.
<a href="https://github.com/intel/cve-bin-tool/blob/main/doc/MANUAL.md#-c-cvss---cvss-cvss">-c CVSS, --cvss CVSS</a> minimum CVSS score (as integer in range 0 to 10) to report (default: 0)
<a>--metrics</a>
retrieve CVE metrics (e.g., EPSS) (default: False)
<a>--epss-percentile</a>
minimum EPSS percentile of CVE range between 0 to 100 to report (input value can also be floating point) (default: 0)
<a>--epss-probability</a>
Expand Down
8 changes: 8 additions & 0 deletions cve_bin_tool/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ def main(argv=None):
help="minimum CVE severity to report (default: low)",
default="low",
)
output_group.add_argument(
"--metrics",
action="store_true",
help="check for metrics (e.g., EPSS) from found cves",
default=False,
)
output_group.add_argument(
"--epss-percentile",
action="store",
Expand Down Expand Up @@ -893,6 +899,7 @@ def main(argv=None):

with CVEScanner(
score=score,
check_metrics=args["metrics"],
epss_percentile=epss_percentile,
epss_probability=epss_probability,
check_exploits=args["exploits"],
Expand Down Expand Up @@ -1017,6 +1024,7 @@ def main(argv=None):
merge_report=merged_reports,
affected_versions=args["affected_versions"],
exploits=args["exploits"],
metrics=args["metrics"],
detailed=args["detailed"],
vex_filename=args["vex"],
sbom_filename=args["sbom_output"],
Expand Down
53 changes: 30 additions & 23 deletions cve_bin_tool/cve_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class CVEScanner:
def __init__(
self,
score: int = 0,
check_metrics: bool = False,
epss_percentile: float = 0.0,
epss_probability: float = 0.0,
logger: Logger = None,
Expand All @@ -49,6 +50,7 @@ def __init__(
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
self.error_mode = error_mode
self.score = score
self.check_metrics = check_metrics
self.epss_percentile = epss_percentile
self.epss_probability = epss_probability
self.products_with_cve = 0
Expand Down Expand Up @@ -243,29 +245,34 @@ 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"],),
self.epss_percentile,
self.epss_probability,
)
# 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.
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 > 0.0 or self.epss_probability > 0.0:
# 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"]}'
)
if self.check_metrics:
metric_result = self.metric(
(row["cve_number"],),
self.epss_percentile,
self.epss_probability,
)
# 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.
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 > 0.0
or self.epss_probability > 0.0
):
# 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"]}'
)
cve = CVE(**row_dict)
cves.append(cve)

Expand Down
5 changes: 5 additions & 0 deletions cve_bin_tool/output_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ class OutputEngine:
detailed (bool)
vex_filename (str)
exploits (bool)
metrics (bool)
all_product_data
sbom_filename (str)
sbom_type (str)
Expand Down Expand Up @@ -660,6 +661,7 @@ def __init__(
detailed: bool = False,
vex_filename: str = "",
exploits: bool = False,
metrics: bool = False,
all_product_data=None,
sbom_filename: str = "",
sbom_type: str = "spdx",
Expand Down Expand Up @@ -687,6 +689,7 @@ def __init__(
self.detailed = detailed
self.vex_filename = vex_filename
self.exploits = exploits
self.metrics = metrics
self.all_product_data = all_product_data
self.sbom_filename = sbom_filename
self.sbom_type = sbom_type
Expand Down Expand Up @@ -725,6 +728,7 @@ def output_cves(self, outfile, output_type="console"):
self.merge_report,
self.affected_versions,
self.exploits,
self.metrics,
)
elif output_type == "html":
output_html(
Expand All @@ -748,6 +752,7 @@ def output_cves(self, outfile, output_type="console"):
self.time_of_last_update,
self.affected_versions,
self.exploits,
self.metrics,
self.all_product_data,
self.offline,
outfile,
Expand Down
96 changes: 50 additions & 46 deletions cve_bin_tool/output_engine/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def _output_console_nowrap(
time_of_last_update: datetime,
affected_versions: int,
exploits: bool = False,
metrics: bool = False,
all_product_data=None,
offline: bool = False,
width: int = None,
Expand Down Expand Up @@ -218,8 +219,9 @@ def _output_console_nowrap(
table.add_column("Source")
table.add_column("Severity")
table.add_column("Score (CVSS Version)")
table.add_column("EPSS probability")
table.add_column("EPSS percentile")
if metrics:
table.add_column("EPSS probability")
table.add_column("EPSS percentile")
if affected_versions != 0:
table.add_column("Affected Versions")

Expand All @@ -239,9 +241,10 @@ def _output_console_nowrap(
Text.styled(cve_data["source"], color),
Text.styled(cve_data["severity"], color),
Text.styled(cvss_text, color),
Text.styled(cve_data["epss_probability"], color),
Text.styled(cve_data["epss_percentile"], color),
]
if metrics:
cells.append(Text.styled(cve_data["epss_probability"], color))
cells.append(Text.styled(cve_data["epss_percentile"], color))
if affected_versions != 0:
cells.append(Text.styled(cve_data["affected_versions"], color))
table.add_row(*cells)
Expand Down Expand Up @@ -338,46 +341,47 @@ def validate_cell_length(cell_name, cell_type):
# Print the table to the console
console.print(table)

table = Table()
# Add Head Columns to the Table
table.add_column("CVE")
table.add_column("CVSS_version")
table.add_column("CVSS_score")
table.add_column("EPSS_probability")
table.add_column("EPSS_percentile")
color = "green"

cve_by_metrics: defaultdict[Remarks, list[dict[str, str]]] = defaultdict(list)
# group cve_data by its remarks and separately by paths
for product_info, cve_data in all_cve_data.items():
for cve in cve_data["cves"]:
probability = "-"
percentile = "-"
for metric, field in cve.metric.items():
if metric == "EPSS":
probability = round(field[0], 5)
percentile = field[1]
cve_by_metrics[cve.remarks].append(
{
"cve_number": cve.cve_number,
"cvss_version": str(cve.cvss_version),
"cvss_score": str(cve.score),
"epss_probability": str(probability),
"epss_percentile": str(percentile),
"severity": cve.severity,
}
)
if metrics:
table = Table()
# Add Head Columns to the Table
table.add_column("CVE")
table.add_column("CVSS_version")
table.add_column("CVSS_score")
table.add_column("EPSS_probability")
table.add_column("EPSS_percentile")
color = "green"

for remarks in sorted(cve_by_remarks):
color = remarks_colors[remarks]
for cve in cve_by_metrics[remarks]:
color = cve["severity"].split("-")[0].lower()
cells = [
Text.styled(cve["cve_number"], color),
Text.styled(cve["cvss_version"], color),
Text.styled(str(cve["cvss_score"]), color),
Text.styled(cve["epss_probability"], color),
Text.styled(cve["epss_percentile"], color),
]
table.add_row(*cells)
console.print(table)
cve_by_metrics: defaultdict[Remarks, list[dict[str, str]]] = defaultdict(list)
# group cve_data by its remarks and separately by paths
for product_info, cve_data in all_cve_data.items():
for cve in cve_data["cves"]:
probability = "-"
percentile = "-"
for metric, field in cve.metric.items():
if metric == "EPSS":
probability = round(field[0], 5)
percentile = field[1]
cve_by_metrics[cve.remarks].append(
{
"cve_number": cve.cve_number,
"cvss_version": str(cve.cvss_version),
"cvss_score": str(cve.score),
"epss_probability": str(probability),
"epss_percentile": str(percentile),
"severity": cve.severity,
}
)

for remarks in sorted(cve_by_remarks):
color = remarks_colors[remarks]
for cve in cve_by_metrics[remarks]:
color = cve["severity"].split("-")[0].lower()
cells = [
Text.styled(cve["cve_number"], color),
Text.styled(cve["cvss_version"], color),
Text.styled(str(cve["cvss_score"]), color),
Text.styled(cve["epss_probability"], color),
Text.styled(cve["epss_percentile"], color),
]
table.add_row(*cells)
console.print(table)
6 changes: 6 additions & 0 deletions test/test_output_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,7 @@ def test_output_console(self):
time_of_last_update = datetime.today()
affected_versions = 0
exploits = False
metrics = True
console = Console(file=self.mock_file)
outfile = None
all_product_data = None
Expand All @@ -940,6 +941,7 @@ def test_output_console(self):
time_of_last_update,
affected_versions,
exploits,
metrics,
all_product_data,
True,
120,
Expand Down Expand Up @@ -973,6 +975,7 @@ def test_output_console_affected_versions(self):
time_of_last_update = datetime.today()
affected_versions = 1
exploits = False
metrics = True
console = Console(file=self.mock_file)
outfile = None
all_product_data = None
Expand All @@ -983,6 +986,7 @@ def test_output_console_affected_versions(self):
time_of_last_update,
affected_versions,
exploits,
metrics,
all_product_data,
True,
120,
Expand Down Expand Up @@ -1022,6 +1026,7 @@ def test_output_console_outfile(self):
time_of_last_update = datetime.today()
affected_versions = 0
exploits = False
metrics = True
outfile = tmpf.name
all_product_data = None

Expand All @@ -1031,6 +1036,7 @@ def test_output_console_outfile(self):
time_of_last_update,
affected_versions,
exploits,
metrics,
all_product_data,
True,
120,
Expand Down

0 comments on commit 22dae3d

Please sign in to comment.