HackBlue

Tools · Nmap and network discovery · 18 / 25

Turn Nmap XML into a CSV asset inventory with Python

Parse Nmap XML with the Python standard library to build a host, port, and service spreadsheet that owners can review.

11 min read · practice only with authorization

Owners read spreadsheets, not scan logs

A scan finding is only useful when the person who owns the host can see it. Most service owners will not read a terminal dump, but they will review a sorted table of address, hostname, port, service, and version. Building that table from XML means it is reproducible and does not depend on copy-paste.

Python’s xml.etree.ElementTree is enough. No third-party packages are needed, which keeps the script easy to run on a locked-down analyst machine.

Lab: the script

Save the script below as nmap2csv.py and run it against an XML file from your lab scan. It walks every host element, skips hosts that were not up, and writes one row per open port. Product and version come from the service element that -sV fills in.

Open the CSV in a spreadsheet and sort by service. Clusters jump out immediately: every host running an old web server version, or every machine with RDP open.

Snippet
import csv, sys
import xml.etree.ElementTree as ET

tree = ET.parse(sys.argv[1])
writer = csv.writer(sys.stdout)
writer.writerow(["ip", "hostname", "port", "proto", "service", "product", "version"])
for host in tree.getroot().iter("host"):
    if host.find("status").get("state") != "up":
        continue
    ip = host.find("address[@addrtype='ipv4']")
    ip = ip.get("addr") if ip is not None else ""
    name = host.find("hostnames/hostname")
    name = name.get("name") if name is not None else ""
    for port in host.iter("port"):
        if port.find("state").get("state") != "open":
            continue
        svc = port.find("service")
        get = (lambda k: svc.get(k, "")) if svc is not None else (lambda k: "")
        writer.writerow([ip, name, port.get("portid"), port.get("protocol"),
                         get("name"), get("product"), get("version")])

# python3 nmap2csv.py lab.xml > inventory.csv

Add the columns that drive action

Add an owner column by joining on hostname or address with an export from your CMDB or DHCP server. Add a “first seen” column by keeping last week’s CSV and marking rows that are new. Those two fields turn a list into a work queue.

Keep the scan date and the Nmap command in the file’s first rows or filename. A reader should never have to guess how stale the table is.

Handle it as sensitive data

An inventory of open services is a map an attacker would love. Share it through access-controlled storage, not email attachments, and trim it to the hosts each owner is responsible for.

If you have an existing vulnerability platform, import the XML there instead. The script is for small teams and labs where no platform exists yet.

Diagram

From scan to owner-ready table

  1. Scan

    Run with -sV -oA so XML holds service details.

  2. Parse

    Walk host and port elements with ElementTree.

    Command
    python3 nmap2csv.py lab.xml > inventory.csv
  3. Enrich

    Join owners and mark rows new since last week.

  4. Share

    Give each owner their slice through controlled storage.

Check yourself

1. Which XML element holds product and version from -sV?

2. Why does the script check host status before reading ports?

3. Which added column most directly turns the table into a work queue?