HackBlue

Tools · Nmap and network discovery · 19 / 25

How to compare two Nmap scans with ndiff to spot new services

Run the same scan on a schedule, diff the XML with ndiff, and alert on hosts or ports that appeared since the last baseline.

9 min read · practice only with authorization

Change is the signal

A single scan says what is open. Two scans a week apart say what changed, and change is what defenders care about: a new host on the server VLAN, a database port that appeared on a web server, or a service version that went backward. ndiff, which ships with Nmap, compares two XML files and prints only the differences.

For the comparison to mean anything, the scans must use the same targets, options, and vantage point. Keep the command in a script so it never drifts.

Reading ndiff output

Lines starting with a minus are from the old scan only, and lines with a plus are new. A host that appears under a plus with its ports is new. A port line changing from closed to open on an existing host is a new service. Version changes appear as a minus and plus pair on the same port.

ndiff --xml produces machine-readable differences, which is the format to feed into alerting.

Snippet
ndiff baseline.xml today.xml
ndiff --xml baseline.xml today.xml > changes.xml

Lab: a weekly diff job

Write a small shell script that scans the lab with fixed options, saves a dated XML file, and diffs it against the previous one. Run it, then start a new listener on a lab VM and run it again. The diff should show only that port.

Schedule it with cron or a systemd timer on a lab machine. Mail or post the output only when it is non-empty, so the job is quiet when nothing changes.

Snippet
#!/usr/bin/env bash
set -euo pipefail
cd /srv/scans
today=$(date +%F)
sudo nmap -sV --top-ports 1000 -T4 -oX "lab-$today.xml" 192.168.56.0/24 >/dev/null
prev=$(ls lab-*.xml | sort | tail -2 | head -1)
if [ "$prev" != "lab-$today.xml" ]; then
  ndiff "$prev" "lab-$today.xml" > "diff-$today.txt" || true
  [ -s "diff-$today.txt" ] && cat "diff-$today.txt"
fi

Triage a change

For every new host or port, ask three questions: does it match a change ticket, who owns it, and should it be reachable from where the scan ran. If any answer is unknown, it becomes a ticket.

Treat disappearances seriously too. A monitoring agent port that vanished might mean a host was decommissioned, or that someone disabled the agent.

Diagram

What ndiff prints for one change

A new port on an existing host between two weekly scans.

baseline.xmltoday.xmlndiff

ndiff baseline.xml today.xml

-Nmap 7.95 scan initiated Mon Oct 26 as: nmap -sV --top-ports 1000 ...
+Nmap 7.95 scan initiated Mon Nov 02 as: nmap -sV --top-ports 1000 ...
 192.168.56.101:
 PORT     STATE  SERVICE VERSION
-5432/tcp closed postgresql
+5432/tcp open   postgresql PostgreSQL DB 16.4
  • The host existed in both scans.
  • Port 5432 changed from closed to open.
  • Check for a change ticket and confirm it should be reachable.

Check yourself

1. In ndiff output, what does a line beginning with + mean?

2. Why must both scans use identical options?

3. A monitoring agent port disappeared from a server. What is the right reaction?