Skip to main content
Black Friday: 40% off your first 3 months on any monthly plan with code BF40. Ends Dec 2.See the offer

Reading scan results

How to compare two vulnerability scans from the command line

Find what was fixed, what is new and what is still open between two Nessus CSV exports with grep, cut, sort and comm, then check for the two things that make a finding look fixed when it isn't.

By Brandon Sooknanan, Founder, Varaxon Technologies · · 6 min read

Last month's scan had 9 findings and this month's has 5. Does that mean 4 were fixed? Not necessarily. This guide shows how to compare two Nessus CSV exports line by line, so you can see exactly which findings went away, which are new and which are still open, and then how to check whether the ones that went away were really fixed.

You need grep, cut, sort, comm and awk, which come with Linux and macOS, plus Python 3 for two optional steps. We ran every command on 9 November 2026 against two sample Nessus exports we built for this guide, one from October and one from November. The example output below comes from those files.

1. Turn each export into one line per finding

A finding is the same finding in both scans when the host, the Nessus plugin ID and the port match. The command below pulls those out of a default Nessus CSV, where Plugin ID is column 1, Risk is column 4, Host is column 5, Protocol is column 6 and Port is column 7. It skips plugins with a Risk of None, which are informational.

One sorted line per finding: host, plugin ID, protocol/port
What it doesCommand
Make the list for the older scangrep -E '^"[0-9]+",' october.csv | cut -d, -f1,4,5,6,7 | tr -d '"' | awk -F, '$2 != "None" {print $3","$1","$4"/"$5}' | LC_ALL=C sort -u > october.txt
Make the list for the newer scangrep -E '^"[0-9]+",' november.csv | cut -d, -f1,4,5,6,7 | tr -d '"' | awk -F, '$2 != "None" {print $3","$1","$4"/"$5}' | LC_ALL=C sort -u > november.txt
Count the findings in eachwc -l october.txt november.txt

Nessus puts long descriptions and plugin output on several lines inside quotes. The grep keeps only lines that start a new row with a quoted plugin ID, so those extra lines are dropped. cut is safe here because the first seven columns never contain commas. A line looks like 10.0.0.5,104743,tcp/443. Our October file had 9 findings and our November file had 5.

2. Split them into fixed, new and still open

comm compares two sorted lists and prints three columns: lines only in the first, lines only in the second, and lines in both. The flags hide the columns you don't want.

The three lists
What it doesCommand
Gone from the newer scan (looks fixed)LC_ALL=C comm -23 october.txt november.txt > fixed.txt
New in the newer scanLC_ALL=C comm -13 october.txt november.txt > new.txt
In both scans (still open)LC_ALL=C comm -12 october.txt november.txt > open.txt
Count each listwc -l fixed.txt new.txt open.txt

Our sample gave 5 findings gone, 1 new and 4 still open. The new one was the SSH Terrapin weakness (CVE-2023-48795) on 10.0.0.12. LC_ALL=C makes sort and comm use the same order. Without it, comm can complain that a file is not sorted.

3. Check for hosts that weren't scanned

If a host was switched off, moved or left out of the scan range, every finding on it disappears from the newer scan. That isn't a fix. List the hosts in each export and look for any that dropped out.

Hosts in the older scan that are missing from the newer one
What it doesCommand
Hosts in the older scangrep -E '^"[0-9]+",' october.csv | cut -d, -f5 | tr -d '"' | LC_ALL=C sort -u > october-hosts.txt
Hosts in the newer scangrep -E '^"[0-9]+",' november.csv | cut -d, -f5 | tr -d '"' | LC_ALL=C sort -u > november-hosts.txt
Hosts that dropped outLC_ALL=C comm -23 october-hosts.txt november-hosts.txt > gone-hosts.txt
Remove their findings from the fixed listawk -F, 'NR==FNR {gone[$1]; next} !($1 in gone)' gone-hosts.txt fixed.txt > fixed-scanned.txt

In our sample, 10.0.0.20 was in October's scan but not November's, so its two findings (TLS 1.0 and a self-signed certificate) only looked fixed. That left 3 findings in fixed-scanned.txt. Find out why the host is missing, and scan it again before you report on it.

4. Check that both scans logged in

A credentialed scan logs in to each host and checks installed packages and patches. If the login fails next time, those local checks don't run and their findings disappear, even though nothing was patched. Nessus records the result for each host in plugin 19506 (Nessus Scan Information), in a line that reads Credentialed checks : yes or no.

Login result per host, from plugin 19506
What it doesCommand
Print each host and whether credentialed checks ranpython3 -c 'import csv,sys; [print(r["Host"], l.split(":", 1)[1].strip()) for r in csv.DictReader(open(sys.argv[1], newline="")) if r["Plugin ID"] == "19506" for l in r["Plugin Output"].splitlines() if l.startswith("Credentialed checks")]' november.csv

Run it on both files. In our sample, every host logged in during October, but 10.0.0.12 printed no in November. Its OpenSSH package finding needs a local check, so it went missing because the login failed, not because OpenSSH was updated. Fix the scan credentials and rescan before you count it. Credentialed vs non-credentialed scans covers why this matters.

5. Put names on the results

Plugin IDs are hard to read. This prints the risk, host and finding name for each line in a list. Use the older export for fixed findings and the newer export for new ones.

Risk, host and name for every finding in a list
What it doesCommand
Name the findings that were really fixedpython3 -c 'import csv,sys; keys = {tuple(l.strip().split(",")) for l in open(sys.argv[2])}; rows = {(r["Host"], r["Plugin ID"], r["Protocol"] + "/" + r["Port"]): r for r in csv.DictReader(open(sys.argv[1], newline=""))}; [print(rows[k]["Risk"], k[0], rows[k]["Name"], sep="\t") for k in sorted(keys) if k in rows]' october.csv fixed-scanned.txt
Name the new findingsRun the same command with november.csv new.txt at the end.

In our sample this listed three findings. Setting aside the OpenSSH finding on 10.0.0.12 from step 4, two were really fixed: MS17-010 (EternalBlue) on 10.0.0.10, rated Critical, and TLS 1.0 on 10.0.0.5. Five findings disappeared, but only two of them were fixed.

Putting it to use

  • Compare scans from the same scanner with the same policy. A Nessus scan and a Qualys scan name and check issues differently, so their lists won't line up.
  • Keep the dated lists with both exports. A list of what was fixed, with the scan it was confirmed in, is useful evidence for audits and client reviews.
  • Run steps 3 and 4 every time. They catch the two most common reasons a finding looks fixed when it isn't.
  • For other scanners, the same method works once you know which columns hold the host, the check ID and the port.

To see which of the new and still-open findings are being exploited, run them through how to check your scan results against CISA KEV. We built proof of fix into Varaxon Scan Hub to do this comparison on every report. It marks each finding as fixed, new or still open, and it doesn't count a finding as fixed when the host wasn't rescanned or the login failed. Every new account gets a 14-day free trial of Core, with no card needed. See sample reports.

Common questions

Why not just compare the finding counts?

Counts hide what changed. A month with 3 fixes and 3 new findings has the same count as a month where nothing happened. Counts also fall when a host is missed or a login fails.

Why does comm say a file is not in sorted order?

The lists were sorted with different locale settings. Make both with LC_ALL=C sort -u and run comm with LC_ALL=C too.

Does this work if I changed the Nessus CSV columns?

Yes, if you change the column numbers to match. Check the header row with head -1 october.csv. The two Python steps read columns by name, so they work whatever the order.

Turn your next scan export into a client-ready report.

Run a free pre-flight check

Cookie preferences

Essential site functions remain available whichever option you choose. Your advertising choice is saved on this site. The Scan Hub has a separate choice.

Google and Reddit may receive cookies, identifiers, browser and device information, and your network/IP information. Reject or withdraw at any time. Read the Privacy Policy.