Safe Penetration Testing Labs · intermediate · ~15 min

Parse a saved nmap XML output (defensive)

Walk a small fixture of nmap -oX output and count open ports.

Overview

Walk an nmap-XML dump byte by byte, count <port> elements whose <state> is "open". Pure string parsing — no network, no XML library.

Why it matters

Reading saved scan output is the bread-and-butter of defensive triage. When someone asks "which hosts have an unexpected port open?", a 20-line parser is the answer.

Core concepts

Token-walk parsing: find the next <port substring, look ahead for state="open" within the same element. Never trust the input — always bound the look-ahead with the next <port or a fixed window.

Defensive parsing: count what you can, skip the rest, never crash. Malformed XML is normal in the wild.

Lesson

Why this matters

Defenders read scan output far more often than they run scans themselves. When the SOC team asks "which of our hosts have port 23 open?", the answer is one tiny parser away.

This lesson teaches the parsing side: take the bytes that nmap saved to disk and pull out the answer your team actually needs. The file is bundled in the harness; the function never opens a socket.

What the file looks like

A trimmed nmap -oX dump:

<host><address addr="127.0.0.1"/><ports>
  <port portid="22"><state state="open"/></port>
  <port portid="80"><state state="filtered"/></port>
  <port portid="443"><state state="open"/></port>
</ports></host>

You don't need a real XML parser. strstr walking from one <port to the next is enough — the format is regular and the buffer is small.

Your job

Implement a function that returns the count of <port> elements whose nested <state> has state="open". Skip filtered and closed.

Code example

int count_open_ports(const char *xml) {
    int count = 0;
    const char *p = xml;
    while ((p = strstr(p, "<port")) != NULL) {
        const char *end = strchr(p, '>');
        const char *next = strstr(p + 1, "<port");
        const char *limit = next ? next : p + 256;
        const char *state = strstr(p, "state=\"open\"");
        if (state && state < limit) count++;
        if (!end) break;
        p = end + 1;
    }
    return count;
}

Common mistakes

  • Counting state="open|filtered" as open. nmap uses that exact string — match it whole.
  • Walking past the buffer end. The buffer is NUL-terminated; check it.
  • Treating malformed XML as a fatal error. Return what you've counted so far; never crash on bad input.

What this is NOT

A live nmap runner. The bytes you read came from a previous, authorised scan. The function never opens a socket and never makes a DNS query.

Practice tasks

  1. Modify the parser to ALSO return the count of <port> elements that are 'filtered'. 2. Extend it to ignore comments (<!-- ... -->). 3. Add bounds-checking so the function never reads past the NUL.

Summary

20 lines of strstr + strchr gives you a working nmap-XML parser. No XML library required. The fixture is static; no network touched.

Practice with these exercises