Tool · Python · Penetration Testing

Automated Nmap
Network Scanner

📅 2024 💻 Python · python-nmap · argparse 🛠 CLI Tool · Open Source

Python CLI tool that automates network reconnaissance using Nmap as the underlying scan engine. Accepts single IPs, CIDR ranges, or hostnames; supports multiple scan profiles (quick, full, stealth); and exports structured JSON or HTML reports with timestamps — replicating the initial enumeration phase of a professional penetration test workflow in a controlled lab environment.

Python Nmap CLI Automation Network Recon JSON Export Penetration Testing
View Source on GitHub Code Snippets ↓

Tool Capabilities

🌐
Flexible Target Input
Single IP, CIDR notation (e.g. 10.0.0.0/24), hostname, or newline-delimited target file
🔎
Scan Profiles
--quick (top 100 ports, fast), --full (all 65535 ports, -sV), --stealth (SYN scan, slower timing T2)
📄
Structured Export
JSON output for programmatic use, HTML report for human review, plain terminal table for quick reads
🕐
Timestamped Reports
Each report filename includes ISO-8601 timestamp; scan metadata (duration, Nmap version) embedded in output
🔧
Service Version Detection
Wraps -sV flag to probe open ports for service banners, product names, and version strings
🔒
OS Detection (Optional)
--os flag enables -O (requires root/sudo). Adds OS guess with confidence percentage to report

Tool Architecture

The tool is structured as a single-entry Python CLI application. The python-nmap library wraps the system Nmap binary, parsing its XML output into Python data structures. argparse handles the CLI interface, and a report generator module converts the parsed scan data to the requested output format.

cli_entry.py (main)
│
├── argparse CLI layer
│   ├── --target / -t       (single IP, CIDR, or file)
│   ├── --profile           (quick | full | stealth)
│   ├── --output / -o       (json | html | terminal)
│   ├── --os                (enable OS detection, requires sudo)
│   └── --outdir            (output directory, default ./reports/)
│
├── scanner.py
│   ├── NmapScanner class
│   │   ├── build_args()    (maps profile → nmap flags)
│   │   ├── run_scan()      (calls python-nmap PortScanner)
│   │   └── parse_results() (extracts hosts / ports / services)
│   └── ScanResult dataclass
│       ├── metadata        (timestamp, duration, nmap_version)
│       └── hosts[]         (host_ip, state, ports[], os_guess)
│
└── report.py
    ├── JSONReporter.write()
    ├── HTMLReporter.write()   (Jinja2 template)
    └── TerminalReporter.print() (rich table)

Key Code Snippets

cli_entry.py — Main entry point & argument parser

#!/usr/bin/env python3
"""Automated Nmap Network Scanner — CLI entry point."""

import argparse
import sys
from pathlib import Path
from scanner import NmapScanner
from report import JSONReporter, HTMLReporter, TerminalReporter


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        prog="nmap-auto",
        description="Automated Nmap network scanner with structured output",
    )
    p.add_argument("-t", "--target", required=True,
                   help="Target: IP, CIDR range, hostname, or path to target file")
    p.add_argument("--profile",
                   choices=["quick", "full", "stealth"],
                   default="quick",
                   help="Scan profile (default: quick)")
    p.add_argument("-o", "--output",
                   choices=["json", "html", "terminal"],
                   default="terminal",
                   help="Output format (default: terminal)")
    p.add_argument("--outdir", default="./reports",
                   help="Directory for saved reports (default: ./reports)")
    p.add_argument("--os", action="store_true",
                   help="Enable OS detection (requires sudo)")
    return p


def main():
    args = build_parser().parse_args()

    scanner = NmapScanner(
        target=args.target,
        profile=args.profile,
        os_detect=args.os,
    )

    print(f"[*] Starting {args.profile} scan → {args.target}")
    result = scanner.run_scan()
    print(f"[+] Scan complete in {result.metadata['duration']:.1f}s")
    print(f"[+] Hosts up: {result.hosts_up} / {result.hosts_total}")

    Path(args.outdir).mkdir(parents=True, exist_ok=True)

    reporters = {
        "json":     JSONReporter(args.outdir),
        "html":     HTMLReporter(args.outdir),
        "terminal": TerminalReporter(),
    }
    reporters[args.output].write(result)


if __name__ == "__main__":
    main()

scanner.py — NmapScanner class & scan logic

import nmap
import time
from datetime import datetime, timezone
from dataclasses import dataclass, field
from typing import List, Dict, Any


# --- Scan profiles mapped to nmap argument strings ---
PROFILES: Dict[str, str] = {
    "quick":   "--top-ports 100 -T4",
    "full":    "-p- -sV -T4",
    "stealth": "-sS -T2 -p 1-10000",
}


@dataclass
class PortInfo:
    port:     int
    protocol: str
    state:    str
    service:  str
    product:  str = ""
    version:  str = ""


@dataclass
class HostResult:
    ip:       str
    state:    str
    hostname: str = ""
    os_guess: str = ""
    ports:    List[PortInfo] = field(default_factory=list)


@dataclass
class ScanResult:
    metadata:    Dict[str, Any]
    hosts:       List[HostResult] = field(default_factory=list)
    hosts_up:    int = 0
    hosts_total: int = 0


class NmapScanner:
    def __init__(self, target: str, profile: str = "quick", os_detect: bool = False):
        self.target     = target
        self.nmap_args  = PROFILES.get(profile, PROFILES["quick"])
        if os_detect:
            self.nmap_args += " -O"
        self._scanner   = nmap.PortScanner()

    def run_scan(self) -> ScanResult:
        t_start = time.monotonic()
        self._scanner.scan(hosts=self.target, arguments=self.nmap_args)
        duration = time.monotonic() - t_start

        metadata = {
            "timestamp":    datetime.now(timezone.utc).isoformat(),
            "duration":     duration,
            "nmap_version": self._scanner.nmap_version(),
            "command":      self._scanner.command_line(),
            "profile":      self.nmap_args,
        }
        result = ScanResult(metadata=metadata)
        result.hosts_total = len(self._scanner.all_hosts())

        for ip in self._scanner.all_hosts():
            host_data = self._scanner[ip]
            host = HostResult(
                ip=ip,
                state=host_data.state(),
                hostname=host_data.hostname() or "",
            )
            if host.state == "up":
                result.hosts_up += 1

            # Extract OS detection if present
            if "osmatch" in host_data and host_data["osmatch"]:
                top = host_data["osmatch"][0]
                host.os_guess = f"{top['name']} ({top['accuracy']}% confidence)"

            # Extract open ports
            for proto in host_data.all_protocols():
                for port in sorted(host_data[proto].keys()):
                    p = host_data[proto][port]
                    host.ports.append(PortInfo(
                        port=port, protocol=proto,
                        state=p["state"],   service=p.get("name", ""),
                        product=p.get("product", ""),
                        version=p.get("version", ""),
                    ))
            result.hosts.append(host)
        return result

report.py — JSON export (excerpt)

import json
from pathlib import Path
from dataclasses import asdict


class JSONReporter:
    def __init__(self, outdir: str):
        self.outdir = Path(outdir)

    def write(self, result) -> Path:
        ts = result.metadata["timestamp"].replace(":", "-")[:19]
        filename = self.outdir / f"scan_{ts}.json"
        payload = {
            "metadata": result.metadata,
            "summary": {
                "hosts_up":    result.hosts_up,
                "hosts_total": result.hosts_total,
            },
            "hosts": [asdict(h) for h in result.hosts],
        }
        filename.write_text(json.dumps(payload, indent=2))
        print(f"[+] JSON report saved → {filename}")
        return filename

Terminal Scan Result

$ python nmap-auto.py -t 192.168.100.0/24 --profile quick -o terminal

[*] Starting quick scan → 192.168.100.0/24
[*] Running: nmap --top-ports 100 -T4 192.168.100.0/24
[+] Scan complete in 14.3s
[+] Hosts up: 3 / 256

╔══════════════════╦═══════╦═══════════╦══════════════════════════════════╗
║ IP Address       ║ State ║ Hostname  ║ Open Ports (Service / Version)   ║
╠══════════════════╬═══════╬═══════════╬══════════════════════════════════╣
║ 192.168.100.10   ║  up   ║ wazuh-mgr ║ 22/tcp  ssh  OpenSSH 8.9         ║
║                  ║       ║           ║ 443/tcp https nginx/1.22          ║
╠══════════════════╬═══════╬═══════════╬══════════════════════════════════╣
║ 192.168.100.20   ║  up   ║ ubuntu-ep ║ 22/tcp  ssh  OpenSSH 8.9         ║
║                  ║       ║           ║ 80/tcp  http Apache httpd 2.4.52  ║
╠══════════════════╬═══════╬═══════════╬══════════════════════════════════╣
║ 192.168.100.50   ║  up   ║ WIN10-EP  ║ 135/tcp msrpc Microsoft RPC       ║
║                  ║       ║           ║ 139/tcp netbios-ssn               ║
║                  ║       ║           ║ 445/tcp microsoft-ds              ║
║                  ║       ║           ║ 3389/tcp ms-wbt-server (RDP)      ║
╚══════════════════╩═══════╩═══════════╩══════════════════════════════════╝

[+] Done. 3 hosts up, 9 open ports found.

Fit in a Pentest Workflow

Network reconnaissance is the foundation of every penetration test. This tool was built to practice automating the initial enumeration phase — reducing manual repetition and producing consistent, timestamped outputs that feed directly into the next engagement phases.

1 · Recon This tool. Discover live hosts, open ports, and running services. Export structured JSON for downstream tools or import into reporting templates.
2 · Enum Deep-dive each service: banner grabbing, web directory enumeration (Gobuster/Feroxbuster), SMB share enumeration (smbclient), version-specific vulnerability checks.
3 · Exploit Target confirmed vulnerabilities using Metasploit, manual PoC scripts, or custom exploits. Results feed back into scanner for post-exploitation host mapping.
4 · Post-Exploit Privilege escalation, lateral movement, persistence. The scanner's CIDR mode can be re-run on internal subnets discovered post-compromise.
5 · Report The tool's timestamped JSON and HTML reports provide ready-made scan evidence to attach directly to the penetration test report appendix.

Key Learning Outcomes