Skip to content

rksharma-owg/vuln-scan-diff

Repository files navigation

Vuln Scan Diff

A command-line utility that compares two vulnerability scan exports and identifies exactly what changed between them. Designed for security engineers, vulnerability managers, and DevSecOps teams who need to track remediation progress across successive scans.

Why This Tool Exists

Most vulnerability scanners provide web-based dashboards for trending, but those require an active scanner connection and often lack the precision needed for audit-grade reporting. Common workarounds -- dumping both exports to CSV and running diff, or manually cross-referencing spreadsheets -- are error-prone and do not distinguish between a vulnerability that was actually fixed and one that simply disappeared because the host was not rescanned.

Vuln Scan Diff fills that gap by providing offline, local comparison of scan exports with a strict distinction between FIXED and UNVERIFIED findings, credential-failure detection, severity change tracking, and host-change analysis -- all without sending data to any external service.

Supported Formats

Scanner Format Extension
Tenable Nessus .nessus XML export .nessus
Qualys VMDR CSV vulnerability export .csv

Scanner type is auto-detected from file extension and content, or can be specified explicitly with --scanner.

Installation

pipx (recommended):

pipx install vuln-scan-diff

venv + pip:

python -m venv .venv
source .venv/bin/activate
pip install vuln-scan-diff

Requires Python 3.11 or later.

CLI Usage

vuln-diff compare -- Compare two scans

vuln-diff compare OLD_SCAN NEW_SCAN [options]
Option Description
OLD_SCAN Path to the previous (older) scan export
NEW_SCAN Path to the latest (newer) scan export
--scanner, -s Scanner type: auto, nessus, or qualys (default: auto)
--output, -o Generate an Excel report at this path
--json, -j Generate a JSON report at this path
--html Generate an HTML report at this path
--host-key Host identification strategy: auto, hostname, ip, asset-id (default: auto)
--severity Comma-separated severity filter for output (e.g. critical,high)
--include-informational Include informational findings in results
--fail-on-new-critical Exit with code 3 if new critical findings are detected
--no-color Disable colored terminal output
--verbose Show additional details (all new findings, severity decreases)

vuln-diff summary -- Summarize a single scan

vuln-diff summary SCAN_FILE [options]
Option Description
SCAN_FILE Path to a vulnerability scan export
--scanner, -s Scanner type (default: auto)
--no-color Disable colored terminal output

Displays host counts, findings by severity and category, most vulnerable hosts, most common findings, OS distribution, and credentialed scan status.

vuln-diff credentials -- Check credential status

vuln-diff credentials SCAN_FILE [options]
Option Description
SCAN_FILE Path to a vulnerability scan export
--scanner, -s Scanner type (default: auto)
--no-color Disable colored terminal output

Analyzes each host for signs of failed or missing credentialed scanning and reports which hosts have credential problems, with supporting evidence.

Examples

Basic comparison:

$ vuln-diff compare old.nessus new.nessus

Vulnerability Scan Comparison
Previous scan: old.nessus
Latest scan:   new.nessus
Scanner:       nessus

Hosts
  Previous:              4
  Latest:                5
  New:                   1
  Missing:               0
  Credential issues:     5
Findings
  New:                   12
  Fixed:                 7
  Still open:            43
  Unverified:            3
  Severity changed:      2
New findings by severity
  Critical:              1
  High:                  3
  Medium:                5
  Low:                   3

                                   New Critical & High Findings
Host                    Plugin ID  Plugin Name                        Severity  Port  CVEs
10.0.1.50 (db01)        12345      SSL Certificate Expired           Critical  443   CVE-2024-1234
10.0.1.12 (web03)       98765      Apache Struts2 RCE               High      8080  CVE-2023-50164, CVE-2023-50148
10.0.1.30 (app02)       54321      OpenSSH Key Exchange              High      22    CVE-2023-48795

Generate all report formats:

$ vuln-diff compare old.nessus new.nessus -o report.xlsx -j report.json --html report.html

Qualys CSV comparison:

$ vuln-diff compare --scanner qualys old_scan.csv new_scan.csv --output qualys_diff.xlsx

CI/CD gate with JSON output:

$ vuln-diff compare prev.nessus latest.nessus --json diff.json --fail-on-new-critical
$ echo $?
3

Summarize a single scan:

$ vuln-diff summary scan.nessus

Check credential status:

$ vuln-diff credentials scan.nessus

Comparison Logic

Every finding in the previous scan is matched against the latest scan using a stable identity key (see Finding Identity Logic below). Based on whether a match is found, findings are classified into these categories:

Classification Meaning
New The finding identity exists in the latest scan but not in the previous scan.
Fixed The finding identity exists in the previous scan but not in the latest, and the host was successfully rescanned with credentials.
Still Open The same finding identity is present in both scans with no severity change.
Unverified The finding is absent from the latest scan, but the tool cannot confirm remediation (see FIXED vs UNVERIFIED below).
Severity Changed The finding identity exists in both scans but the severity level differs. Tracked as increased or decreased with CVSS details.
Reopened A subset of New -- the finding carries historical metadata (last_fixed date) suggesting it was previously remediated and has reappeared. Flagged with low confidence.

Hosts are also tracked: New, Missing, Possibly Renamed (matched by MAC or IP), and Possible Duplicates (shared IP or MAC in the same scan).

Finding Identity Logic

A finding's identity is computed as:

host_identity | protocol | port | plugin_id

Where host_identity is derived from the host identification strategy (--host-key):

  • auto (default): asset ID > MAC address > FQDN > hostname > IP (picks the most stable available identifier)
  • hostname: normalized hostname or FQDN
  • ip: normalized IP address
  • asset-id: scanner-assigned asset ID

Why not CVE-only? A single CVE can appear on multiple hosts, ports, and services. A single plugin can report multiple CVEs. Matching by CVE alone would produce false positives when the same CVE affects different services or hosts. The host+port+protocol+pluginID composite key ensures that a finding is tracked at the level of a specific vulnerability on a specific service of a specific host -- which is what matters for remediation tracking.

FIXED vs UNVERIFIED

This is the core distinction that sets Vuln Scan Diff apart from simple diff tools.

FIXED is assigned only when all of the following are true:

  • The host is present in the latest scan.
  • The host shows no signs of credential failures.
  • The latest scan was authenticated (not uncredentialed).
  • The scan does not appear incomplete (e.g., the host had findings before but zero now, or the scan duration was extremely short).

UNVERIFIED is assigned when any of these conditions are not met. Specific reasons include:

  • The host is missing entirely from the latest scan.
  • Credentialed scanning failed or was disabled for the host.
  • The latest scan was unauthenticated and the finding was medium severity or higher.
  • The host had findings in the previous scan but zero findings in the latest scan (suggesting an incomplete scan rather than full remediation).
  • The scan duration for the host was extremely short.

This tool does not determine that a vulnerability is fixed merely because it is absent from the latest export.

UNVERIFIED findings require manual review before they can be considered remediated. They are surfaced separately in the terminal report, Excel report, HTML report, and JSON output so they are never accidentally counted as fixed.

Excel Report

Generated with --output report.xlsx. The workbook contains 10 worksheets:

Sheet Contents
Executive Summary Host counts, finding deltas, severity breakdown, credentialed scan stats
New Findings All new findings with host, plugin, severity, port, CVEs, CVSS, synopsis, solution
Fixed Findings All confirmed-fixed findings with the same columns
Still Open All findings present in both scans with unchanged severity
Unverified All findings that cannot be confirmed fixed, with unverified reasons
Severity Changes Findings whose severity increased or decreased, with previous/current CVSS
New Hosts Hosts appearing in the latest scan but not the previous
Missing Hosts Hosts present in the previous scan but absent from the latest
Credential Issues Hosts with detected credential scanning failures and evidence
All Current Findings Complete inventory of all findings in the latest scan

All cells are formatted with professional styling (severity-colored headers, wrapped text, auto-sized columns) and every value is sanitized against formula injection.

JSON Schema

Generated with --json report.json. Structure:

{
  "metadata": {
    "tool_version": "1.0.0",
    "scanner": "nessus",
    "previous_scan": "old.nessus",
    "latest_scan": "new.nessus",
    "generated_at": "2025-01-15T10:30:00+00:00"
  },
  "summary": {
    "previous_hosts": 4,
    "latest_hosts": 5,
    "new_hosts": 1,
    "missing_hosts": 0,
    "new_findings": 12,
    "fixed_findings": 7,
    "still_open": 43,
    "unverified": 3,
    "severity_changed": 2,
    "credential_issues": 5,
    "new_critical_findings": 1
  },
  "findings": {
    "new": [
      {
        "host_ip": "10.0.1.50",
        "host_hostname": "db01",
        "plugin_id": "12345",
        "plugin_name": "SSL Certificate Expired",
        "severity": "critical",
        "port": "443",
        "protocol": "tcp",
        "cves": ["CVE-2024-1234"],
        "cvss_v2": null,
        "cvss_v3": 9.8
      }
    ],
    "fixed": [...],
    "still_open": [...],
    "unverified": [...],
    "severity_changed": [
      {
        "host_ip": "10.0.1.12",
        "host_hostname": "web03",
        "plugin_id": "54321",
        "plugin_name": "Apache HTTP Server Version",
        "severity": "high",
        "port": "443",
        "protocol": "tcp",
        "cves": [],
        "cvss_v2": null,
        "cvss_v3": 7.5,
        "previous_severity": "medium",
        "current_severity": "high",
        "direction": "increased"
      }
    ]
  },
  "hosts": {
    "new": [...],
    "missing": [...],
    "possible_renames": [...],
    "credential_issues": [
      {
        "host_ip": "10.0.1.30",
        "host_hostname": "app02",
        "status": "likely",
        "evidence": ["Authentication Failure - Local Checks Not Run"]
      }
    ]
  }
}

The JSON output is designed for CI/CD pipeline consumption. The --fail-on-new-critical flag provides a simple exit-code mechanism for build gates.

HTML Report

Generated with --html report.html. Features:

  • Self-contained: Single file with embedded CSS and inline JavaScript. No external dependencies, no CDN, no analytics, no network requests.
  • Offline: Opens directly in any browser without an internet connection.
  • Searchable: Each table has a search box for filtering by host, plugin, CVE, etc.
  • Filterable: Severity filter buttons (All, Critical, High, Medium, Low, Informational) on each findings table.
  • Expandable details: Click to reveal synopsis, solution, and description for each finding.
  • Printable: Print stylesheet hides interactive controls and shows all rows.
  • Security: All scanner-controlled content is HTML-escaped to prevent injection.

Security and Privacy Model

Vuln Scan Diff is designed to handle sensitive vulnerability data safely:

  • No network access: The tool never makes network requests. All processing is local.
  • No telemetry: No usage data, scan contents, or metadata are sent anywhere.
  • XXE protection: Nessus XML is parsed with defusedxml, which blocks entity expansion, DTD processing, and other XML attack vectors.
  • Formula injection prevention: All values written to Excel cells are sanitized -- strings starting with =, +, -, or @ are prefixed with a single quote.
  • HTML escaping: All scanner-controlled content in HTML reports is escaped with html.escape(quote=True).
  • Secret redaction: Plugin output displayed in verbose terminal mode is scanned for common secret patterns (passwords, API keys, bearer tokens, private keys, AWS keys) and replaced with [REDACTED].

Performance

  • Streaming XML parsing: Nessus .nessus files are parsed with iterparse -- elements are processed and cleared incrementally, so only one host's worth of data is in memory at a time. A 2 GB .nessus file does not require 2 GB of RAM.
  • Handles large scans: Tested with 100,000+ findings and 10,000+ hosts.
  • Qualys CSV: Processed line by line using Python's csv module.

Known Limitations

  • Scanner type must be the same for both files. Comparing a Nessus export against a Qualys export is not supported.
  • Host rename detection relies on MAC and IP matching. If both changed between scans, the host will appear as both missing and new.
  • Credential analysis is rule-based and uses Nessus-specific heuristics (plugin IDs, plugin families, output patterns). It may produce false positives or miss subtle credential failures.
  • The last_fixed date used for reopened-finding detection is only available if the scanner includes it in the export.
  • Port-agnostic findings (findings not associated with a specific port) use an empty port component in the identity key.
  • Severity normalization maps scanner-specific severity labels (e.g., Nessus severity 0-4, Qualys 1-5) to a common scale but cannot account for custom severity thresholds configured in the scanner.
  • The tool compares two point-in-time exports. It does not access scanner databases or trend data.

Development Setup

git clone https://github.com/your-org/vuln-scan-diff.git
cd vuln-scan-diff
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

Testing

pytest
pytest --cov=src/vuln_scan_diff
pytest -m "not slow"  # skip slow tests

The test suite contains 249 tests across 10 modules, using synthetic fixture files (small .nessus XML and Qualys CSV files) -- no real scan data is needed to run the tests.

Contributing

  1. Fork the repository.
  2. Create a feature branch.
  3. Write tests for new functionality.
  4. Ensure all tests pass with pytest.
  5. Submit a pull request.

Please keep changes focused and ensure backward compatibility with existing CLI flags and report formats.

License

MIT License. See LICENSE for the full text.

About

Compare Nessus and Qualys vulnerability scans to track new, fixed, and unverified findings.

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages