I made a mistake early on that cost me real time: I ran a full port sweep across a decent-sized subnet, watched the results scroll by in my terminal, closed the window, and then realized I needed that data again an hour later. It was gone. Since then, I never run a scan of any real consequence without saving structured output — and understanding which output format to use for which purpose has saved me from re-scanning things more times than I can count.
This article covers every output format Nmap supports, how to actually use each one, and how to convert between them.
Why Output Format Matters
The terminal output you see by default is meant for humans reading in real time. It’s not meant for parsing, storing long-term, or feeding into other tools. Nmap gives you four distinct output options, each suited to a different purpose:
flowchart TD
A[Nmap Scan] --> B[-oN Normal]
A --> C[-oX XML]
A --> D[-oG Grepable]
A --> E[-oA All formats]
B --> F[Human reading later]
C --> G[Programmatic parsing, reports]
D --> H[Quick command-line filtering]
E --> I[Everything, always]
Normal Output (-oN)
nmap -oN scan_results.txt 192.168.1.10
This saves exactly what you’d see in the terminal to a text file — no more, no less.
Starting Nmap 7.94 ( https://nmap.org ) at 2026-08-16 10:23 PKT
Nmap scan report for 192.168.1.10
Host is up (0.00034s latency).
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
Nmap done: 1 IP address (1 host up) scanned in 2.14 seconds
When I use it: Quick reference I might glance at later, or when I’m just documenting a single scan for a report appendix. It’s readable but genuinely painful to parse programmatically — I’d never write a script that greps normal output if XML is available.
XML Output (-oX)
nmap -oX scan_results.xml 192.168.1.10
This is the format I actually build tooling around. It’s structured, well-documented, and every other Nmap-adjacent tool expects it.
<?xml version="1.0"?>
<nmaprun scanner="nmap" version="7.94">
<host>
<status state="up"/>
<address addr="192.168.1.10" addrtype="ipv4"/>
<ports>
<port protocol="tcp" portid="22">
<state state="open"/>
<service name="ssh" product="OpenSSH" version="8.2p1"/>
</port>
<port protocol="tcp" portid="80">
<state state="open"/>
<service name="http" product="Apache httpd" version="2.4.41"/>
</port>
</ports>
</host>
</nmaprun>
Why it matters: XML preserves everything — service versions, script output, OS detection details, timing data — in a machine-parseable tree structure. Every reporting tool I’ve used (including Nmap’s own xsltproc-based HTML converter) expects XML as input.
Converting XML to HTML
Nmap ships with an XSL stylesheet that turns XML output into a readable HTML report:
xsltproc scan_results.xml -o scan_report.html
This is genuinely one of my favorite quick wins — one command turns a raw scan into something presentable enough to hand to a non-technical stakeholder.
Grepable Output (-oG)
nmap -oG scan_results.gnmap 192.168.1.10
Host: 192.168.1.10 () Status: Up
Host: 192.168.1.10 () Ports: 22/open/tcp//ssh///, 80/open/tcp//http///, 443/open/tcp//https///
Everything on one line per host, designed specifically to be filtered with classic Unix tools:
# Extract all hosts with port 80 open
grep "80/open" scan_results.gnmap
# Extract just the IP addresses of live hosts
grep "Status: Up" scan_results.gnmap | awk '{print $2}'
# Count open ports across all scanned hosts
grep -o "[0-9]*/open" scan_results.gnmap | wc -l
Note on deprecation: Grepable output is technically deprecated by the Nmap project in favor of XML, and newer features sometimes don’t get grepable-format support at all. I still use it constantly for quick one-off filtering in a terminal, but I never build serious tooling around it — that’s what XML is for.
Saving All Formats at Once (-oA)
nmap -oA full_scan 192.168.1.10
This produces three files in one command:
full_scan.nmap (normal format)
full_scan.xml (XML format)
full_scan.gnmap (grepable format)
This is what I actually use on every real engagement. There’s essentially no cost to saving all three, and having the XML available means I can generate reports or feed data into other tools later without ever re-running the scan.
JSON Output
Nmap doesn’t natively output JSON, which surprises people the first time they look for a -oJ flag that doesn’t exist. The standard path is converting XML to JSON:
Using a Python conversion
import xmltodict
import json
with open('scan_results.xml') as f:
xml_content = f.read()
data_dict = xmltodict.parse(xml_content)
json_output = json.dumps(data_dict, indent=2)
with open('scan_results.json', 'w') as f:
f.write(json_output)
pip install xmltodict
python3 xml_to_json.py
Using python-nmap directly for JSON-friendly output
Since python-nmap parses XML internally, its results are already Python dictionaries that serialize cleanly:
import nmap
import json
scanner = nmap.PortScanner()
scanner.scan('192.168.1.10', arguments='-sV')
results = {}
for host in scanner.all_hosts():
results[host] = {
'state': scanner[host].state(),
'ports': {}
}
for proto in scanner[host].all_protocols():
for port in scanner[host][proto]:
port_info = scanner[host][proto][port]
results[host]['ports'][port] = {
'state': port_info['state'],
'service': port_info['name'],
'version': port_info.get('version', '')
}
print(json.dumps(results, indent=2))
This is genuinely the cleanest path I’ve found to get real JSON out of a scan without shelling out to a separate conversion tool.
Third-party JSON output libraries
Tools like nmap-formatter (a standalone Go binary) can convert Nmap XML directly to JSON, CSV, or Markdown from the command line:
nmap -oX - 192.168.1.10 | nmap-formatter json > results.json
I reach for this when I want a quick JSON conversion without writing a Python script for a one-off task.
Script Kiddie Output
nmap -oS scan_results.txt 192.168.1.10
A joke format that renders output in leetspeak. I mention it purely for completeness — it has zero practical use beyond novelty, but it’s a genuinely fun piece of Nmap trivia.
Practical Example: A Reporting Pipeline
Here’s a full workflow I use to go from raw scan to shareable report:
# Step 1: run the scan, saving all formats
sudo nmap -sV -sC -p- 192.168.1.10 -oA client_scan
# Step 2: generate a readable HTML report from the XML
xsltproc client_scan.xml -o client_report.html
# Step 3: extract a quick summary for a Slack update using grepable
grep "open" client_scan.gnmap | wc -l
# Step 4: convert XML to JSON for a custom dashboard
python3 -c "
import xmltodict, json
with open('client_scan.xml') as f:
print(json.dumps(xmltodict.parse(f.read()), indent=2))
" > client_scan.json
Four different output needs — archival, presentation, quick command-line check, and structured data for a dashboard — all from a single scan run.
Comparing Formats at a Glance
| Format | Flag | Human-Readable | Machine-Parseable | Best For |
|---|---|---|---|---|
| Normal | -oN | Yes | Poor | Quick reference |
| XML | -oX | Somewhat | Excellent | Automation, reports, HTML conversion |
| Grepable | -oG | Somewhat | Good (via grep/awk) | Quick CLI filtering |
| All | -oA | Yes | Excellent | Every real engagement |
Troubleshooting
XML output file is empty or malformed — the scan was likely interrupted (Ctrl+C) before completion. Nmap only finalizes the XML root element on a clean exit; use --stats-every to monitor long scans instead of killing them mid-run.
Grepable format missing newer scan data (like NSE script output) — this is a known limitation; grepable format doesn’t fully support script output formatting. Switch to XML for anything involving NSE results.
xsltproc command not found — install it: sudo apt install xsltproc on Debian/Ubuntu.
xmltodict conversion produces deeply nested, awkward JSON — this is inherent to how XML-to-JSON conversion works structurally; for cleaner JSON, use python-nmap‘s parsed dictionary output directly instead of a generic XML-to-JSON converter.
Limitations
Nmap has no native JSON output — every JSON workflow is a conversion step, which adds a dependency (either a Python library or a third-party binary) to your pipeline. Grepable format is officially deprecated and doesn’t reliably capture newer scan features like extensive NSE output. Normal format, while the most readable, is the worst choice for anything you plan to parse or reuse programmatically.
Security Best Practices
- Always use
-oAon any engagement scan — storage is cheap, and re-scanning a target because you didn’t save output the first time wastes both your time and the target’s tolerance for repeated probing. - Store scan output files securely; XML output contains detailed service and version information about a target’s infrastructure, which is sensitive data in its own right.
- When sharing scan reports externally, generate a clean HTML or PDF version rather than handing over raw XML, which can expose more detail than intended for a given audience.
- Timestamp and version-control your saved scans if you’re tracking a target’s security posture over multiple engagements — this makes drift and remediation genuinely trackable.
Frequently Asked Questions
Why doesn’t Nmap support JSON output natively? Nmap predates JSON’s widespread adoption as a standard interchange format and has stuck with XML as its structured format since, relying on the broader ecosystem to provide conversion tooling.
Which format should I default to for every scan? -oA, always. It costs almost nothing extra and gives you every format’s benefits without having to decide up front which one you’ll need later.
Can I parse XML output without Python? Yes — xsltproc for HTML conversion, standard XML libraries in virtually any language (Ruby, Go, PHP, Java all have mature XML parsers), or command-line XML tools like xmllint and xmlstarlet for quick queries.
Is grepable output actually going away? It’s officially deprecated but still functional and shipped with current Nmap releases. I wouldn’t build new tooling around it, but existing scripts that rely on it aren’t at immediate risk of breaking.
Resuming Interrupted Scans
One output-format-adjacent feature I rely on more than I’d like to admit: Nmap can resume a scan that was interrupted, but only if it was saved with -oN or -oG originally.
nmap --resume scan_results.gnmap
This has genuinely saved me during a few long overnight scans that got interrupted by a laptop sleeping or a VPN dropping — instead of starting over, Nmap picks up from roughly where it left off, using the partial output file as its reference point. It’s one more reason I default to saving output for anything longer than a quick single-host check.
Comparing Scans Over Time
Because XML output is structured, comparing two scans of the same target taken weeks apart is a genuinely useful exercise for tracking configuration drift — new ports opening unexpectedly, a service version changing, a port that used to be filtered suddenly showing as open. I do this with a small script rather than manually eyeballing two XML files:
import xml.etree.ElementTree as ET
def get_open_ports(xml_file):
tree = ET.parse(xml_file)
ports = set()
for host in tree.findall('host'):
for port in host.findall('.//port'):
state = port.find('state').get('state')
if state == 'open':
ports.add(port.get('portid'))
return ports
old_scan = get_open_ports('scan_january.xml')
new_scan = get_open_ports('scan_august.xml')
print("Newly opened ports:", new_scan - old_scan)
print("Newly closed ports:", old_scan - new_scan)
For a client I’m doing recurring assessments for, this kind of diff is often more valuable than any single point-in-time scan, since it directly answers “what changed since we last looked,” which is usually the question that actually matters for ongoing security posture tracking.
CSV Output for Spreadsheet-Friendly Reporting
Sometimes a stakeholder just wants a spreadsheet, not a structured document. Converting XML to CSV is a quick script away:
import xml.etree.ElementTree as ET
import csv
tree = ET.parse('scan_results.xml')
rows = []
for host in tree.findall('host'):
addr = host.find('address').get('addr')
for port in host.findall('.//port'):
portid = port.get('portid')
state = port.find('state').get('state')
service = port.find('service')
service_name = service.get('name') if service is not None else ''
rows.append([addr, portid, state, service_name])
with open('scan_summary.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Host', 'Port', 'State', 'Service'])
writer.writerows(rows)
This is a small enough script that I keep a copy of it in my personal toolkit permanently — it’s saved me from manually reformatting scan data for a client deliverable more times than I can count.
Wrapping Up
The format you choose to save Nmap output in isn’t a trivial detail — it determines whether your scan data is a disposable terminal scroll-back or a genuine, reusable asset. My rule of thumb after enough repeated scans: always -oA, always. The extra two output files cost nothing, and you’ll be glad you have the XML the first time you need to generate a report, build a dashboard, or answer “wait, was that port open last month?” without touching the target again.
