From d983a6a6640dbc2b324ca4efd2ced05e25a05d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannik=20Tannh=C3=A4user?= Date: Thu, 2 Apr 2026 14:40:27 +0200 Subject: [PATCH] Add section with ignored CVEs to the report Add ignored CVEs to the final html and json report. This includes the CVEs ignored via OSV ID and ignore scan path arguments (passed via command line). This enables better tracability and more transparency. --- vanir/detector_runner.py | 68 ++++++++++++++++++++++++++++++++-- vanir/detector_runner_test.py | 7 ++++ vanir/scanners/scanner_base.py | 5 ++- vanir/vulnerability_manager.py | 5 ++- 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/vanir/detector_runner.py b/vanir/detector_runner.py index 7710525..1228b62 100644 --- a/vanir/detector_runner.py +++ b/vanir/detector_runner.py @@ -15,6 +15,7 @@ offline_directory_scanner /test/source """ import collections +import copy import datetime import functools import inspect @@ -23,7 +24,7 @@ import os import sys import textwrap -from typing import Any, Mapping, Sequence, Type, TypeVar +from typing import Any, Mapping, Sequence, Set, Type, TypeVar from absl import app from absl import flags @@ -33,6 +34,7 @@ from vanir import detector_common_flags from vanir import osv_client from vanir import reporter +from vanir import vulnerability_manager from vanir.scanners import scanner_base @@ -160,6 +162,19 @@ + + Ignored CVEs + + {{ '
Ignored CVEs by OSV ID filter
' if ignored_cves['ignored_by_ID_filter']|length > 0 else '' }} +
{% for cve in ignored_cves['ignored_by_ID_filter'] %}{{ '%-15s' | format(cve) | replace(' ',' ')}} {% endfor %}
+        
+ {{ '
Ignored CVEs by path filter
' if ignored_cves['ignored_by_path_filter']|length > 0 else '' }} + {% for path in ignored_cves['ignored_by_path_filter'] %} +
{{ path }} {% for cve in ignored_cves['ignored_by_path_filter'][path] %}{{ '%-15s' | format(cve) | replace(' ',' ')}} {% endfor %}
+        
+ {% endfor %} + +

@@ -351,11 +366,20 @@ def _get_public_osv_url(osv_id: str) -> str: return 'Not published.' return osv_client.get_osv_url(osv_id) +def _get_cves_from_findings(findings: scanner_base.Findings, vuln_manager: vulnerability_manager)-> Set[str]: + """Generate CVE ID set based on findings. """ + signature_ids = [vuln.signature_id for vuln in findings] + cve_ids = set() + for signature_id in signature_ids: + cve_ids.update(vuln_manager.sign_id_to_cve_ids(signature_id)) + return cve_ids + def _generate_json_report( report_file_name: str, report_book: reporter.ReportBook, covered_cves: Sequence[str], + ignored_cves: Mapping[str, any], ) -> None: """Generates a JSON report based on the findings. @@ -363,6 +387,7 @@ def _generate_json_report( report_file_name: a JSON report file name to create. report_book: a report book instance containing all reports. covered_cves: a sequence of CVEs covered by this run. + ignored_cves: a mapping of CVEs ignored by this run. Returns: None @@ -392,6 +417,7 @@ def _generate_json_report( }) json_report['options'] = ' '.join(sys.argv[1:]) json_report['covered_cves'] = covered_cves + json_report['ignored_cves'] = ignored_cves json_report['missing_patches'] = missing_patches with open(report_file_name, 'w') as report_file: json.dump(json_report, report_file, indent=4) @@ -401,6 +427,7 @@ def _generate_html_report( report_file_name: str, report_book: reporter.ReportBook, covered_cves: Sequence[str], + ignored_cves: Mapping[str, any], stats: scanner_base.ScannedFileStats, ) -> None: """Generates a HTML file summarizing the report in a human-readable format. @@ -409,6 +436,7 @@ def _generate_html_report( report_file_name: a HTML report file name to create. report_book: a report book instance containing all reports. covered_cves: a sequence of CVEs covered by this run. + ignored_cves: a mapping of CVEs ignored by this run. stats: |ScannedFileStats| object with scan result stats. Returns: @@ -455,6 +483,7 @@ def _generate_html_report( html_report = template.render( report_file_name=report_file_name, covered_cves=covered_cves, + ignored_cves=ignored_cves, unpatched_cves=report_book.unpatched_cves, target_missing_patches=target_missing_patches, non_target_missing_patches=non_target_missing_patches, @@ -530,9 +559,28 @@ def main(argv: Sequence[str]) -> None: [scanner_base.ShortFunctionFilter()] + list(detector_common_flags.generate_finding_filters_from_flags()) ) + + # Apply findings filters and collect filtered out CVE IDs + ignored_paths = collections.defaultdict(list) findings = scanner_base.ShortFunctionFilter().filter(findings) + all_findings = copy.copy(findings) + all_findings_cve_ids = _get_cves_from_findings(all_findings, vuln_manager) + for finding_filter in finding_filters: findings = finding_filter.filter(findings) + # Collect CVE IDs for vulnerabilities filtered out by PathPrefixFilter + if isinstance(finding_filter, scanner_base.PathPrefixFilter): + findings_filtered = finding_filter.filter(all_findings) + filtered_cve_ids = _get_cves_from_findings(findings_filtered, vuln_manager) + cve_ids_ignored = list(all_findings_cve_ids - filtered_cve_ids) + if cve_ids_ignored: + excluded_path = finding_filter._prefix + for cve_id in cve_ids_ignored: + ignored_paths[excluded_path].append(cve_id) + + # sort CVE IDs in ignored paths dict + for path in ignored_paths: + ignored_paths[path] = sorted(ignored_paths[path]) report_book = reporter.ReportBook( reporter.generate_reports(findings), vuln_manager @@ -546,11 +594,25 @@ def main(argv: Sequence[str]) -> None: ) covered_cves = sorted(set(covered_cves)) + # Collect CVE IDs for vulnerabilities filtered out by OsvIdFilter + filtered_vuln_ids = [vuln.id for vuln in vuln_manager.get_vulnerabilities(ignore_filters=False)] + unfiltered_vuln_ids = [vuln.id for vuln in vuln_manager.get_vulnerabilities(ignore_filters=True)] + ignored_cve_ids = set() + for osv_id in detector_common_flags._OSV_ID_IGNORE_LIST.value: + if osv_id in unfiltered_vuln_ids and osv_id not in filtered_vuln_ids: + ignored_cve_ids.update(vuln_manager.osv_id_to_cve_ids(osv_id)) + + ignored_cves = {} + if ignored_cve_ids: + ignored_cves["ignored_by_ID_filter"] = sorted(ignored_cve_ids) + if ignored_paths: + ignored_cves["ignored_by_path_filter"] = ignored_paths + # Generate a machine-readable JSON report. - _generate_json_report(json_output_file_name, report_book, covered_cves) + _generate_json_report(json_output_file_name, report_book, covered_cves, ignored_cves) # Generate a human-readable HTML report. - _generate_html_report(html_output_file_name, report_book, covered_cves, stats) + _generate_html_report(html_output_file_name, report_book, covered_cves, ignored_cves, stats) # Generate a console output. scanned_files = stats.analyzed_files + stats.skipped_files diff --git a/vanir/detector_runner_test.py b/vanir/detector_runner_test.py index e808fc4..1b5d635 100644 --- a/vanir/detector_runner_test.py +++ b/vanir/detector_runner_test.py @@ -506,6 +506,13 @@ def test_main(self): + + Ignored CVEs + +
+        
+ +

Missing Patches in Target Files (in 1 vuln)

diff --git a/vanir/scanners/scanner_base.py b/vanir/scanners/scanner_base.py index 008628a..cc4cd21 100644 --- a/vanir/scanners/scanner_base.py +++ b/vanir/scanners/scanner_base.py @@ -136,12 +136,15 @@ def __init__(self, prefix: str): def filter(self, findings: Findings) -> Findings: filtered_findings = {} for sign, chunks in findings.items(): - filtered_findings[sign] = list( + filtered_chunks = list( filter( lambda chunk: not chunk.target_file.startswith(self._prefix), chunks, ) ) + # only add findings if chunks not empty + if filtered_chunks: + filtered_findings[sign] = filtered_chunks return filtered_findings diff --git a/vanir/vulnerability_manager.py b/vanir/vulnerability_manager.py index 024c4ed..46180ee 100644 --- a/vanir/vulnerability_manager.py +++ b/vanir/vulnerability_manager.py @@ -443,7 +443,7 @@ def add_vulnerability( vuln: vulnerability.Vulnerability, overwrite_older_duplicate: bool = False ): - """Adds a new vulnearbility to the manager. + """Adds a new vulnerability to the manager. Args: vuln: the Vulnerability object to be added. @@ -826,7 +826,8 @@ def generate_from_managers( vulnerabilities = [] vfilters = set() for manager in managers: - vulnerabilities.extend(manager.vulnerabilities) + # Get all vulnerabilites for tracking purposes. The same filters are applied later anyway. + vulnerabilities.extend(manager.get_vulnerabilities(ignore_filters=True)) vfilters.update(manager.vulnerability_filters) if vulnerability_filters: vfilters.update(vulnerability_filters)