Organising large nmap scans into reports
How I use nmap to scan large IP blocks and turn those results into reports for readability.
On an engagement, you may have thousands of hosts in-scope that need to be scanned. This quickly gets out-of-hand because once those scans are finished, it becomes difficult to view and filter them out for plausible targets that are worth exploring.
For that, nmap XML outputs can be easily converted into HTML reports using xsltproc, and if you’re doing your scans in one-shot then this option is perfectly valid for you.
1
xsltproc nmap.xml -o report.html
To suit my workflow for how I perform my scans, this needed a few tweaks to get it working.
How I scan large IP blocks
If you had a long list of hosts to scan, you’d maybe use the -iL flag in nmap to do everything in one-go.
1
nmap -iL ips.txt -sS -vv -oA scans/
However, there are a couple of nuisances when doing scans this way in my opinion:
- If you need to cancel your scans mid-way, you lose all your output results. I’m not sure why results don’t get appended as each host is being scanned, but this is the main drawback for me.
- Your scan output files don’t get written until the full scan is finished, and if you’re scanning thousands of hosts, this could mean hours before you get any meaningful results.
- If you messed up while interrupting your scan, you’re cooked.
For me, I like to split my list of IPs into smaller chunks, and then do my scans in batches. This lets me work on batches that finish earlier, while others are still running in the background.
1
2
3
4
5
6
# split into 30 lines chunks
split -l 30 ../ip-targets.txt
# then do the scans
mkdir -p nmapscans
for file in x*; do sudo nmap -sS -vv -oA nmapscans/$file -iL $file; done
Converting nmap XMLs into a report
Once the scans are done, you’ll end up with a bunch of x*.xml outputs. To combine them into a single XML, you can use xml-cat from xml-coreutils to concatenate them.
The easiest way to install
xml-coreutilsthat I found was through homebrew.
1
xml-cat *.xml > combined.xml
To use the combined XML with xsltproc, we will need to replace the <root> nodes with <nmaprun>, and insert the XML stylesheet for nmap.
Here’s the script for automating those steps.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/env python3
import shutil
import argparse
import sys
import subprocess
import glob
def is_tool_available(name):
"""Check whether `name` is on PATH and marked as executable."""
return shutil.which(name) is not None
def check_tools() -> bool:
for tool in ("xml-cat", "xsltproc"):
if is_tool_available(tool) is None:
print(f"{tool} is not installed. Go install it.")
return False
return True
def run() -> None:
parser = argparse.ArgumentParser(
description="Parse nmap XML results into a HTML report"
)
parser.add_argument(
"-d", "--directory", type=str, help="Directory containing nmap XML results"
)
parser.add_argument(
"-o",
"--output",
type=str,
default="report.html",
help="Path to the output file (default: report.html)",
)
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help(sys.stderr)
sys.exit(1)
if not check_tools():
sys.exit(1)
files = glob.glob(f"{args.directory}/*.xml")
xml_results = subprocess.run(
["xml-cat", *files], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
).stdout
xml_results = xml_results.replace(
"<root>",
"""<!DOCTYPE nmaprun>
<?xml-stylesheet href="file:///usr/share/nmap/nmap.xsl" type="text/xsl"?>
<nmaprun>""",
)
xml_results = xml_results.replace("</root>", "</nmaprun>")
PARSED_XML = "nmap.xml"
with open(PARSED_XML, "w") as f:
f.write(xml_results)
r = subprocess.run(
["xsltproc", PARSED_XML, "-o", args.output], capture_output=True, text=True
)
print(r.stdout)
if __name__ == "__main__":
run()
You now have a nice HTML report to look through all your scan results :)
