Tool Capabilities
10.0.0.0/24), hostname, or newline-delimited target file--quick (top 100 ports, fast), --full (all 65535 ports, -sV), --stealth (SYN scan, slower timing T2)-sV flag to probe open ports for service banners, product names, and version strings--os flag enables -O (requires root/sudo). Adds OS guess with confidence percentage to reportTool 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.
Key Learning Outcomes
-
✓
Practised wrapping a system binary (
nmap) through a Python library (python-nmap) and parsing XML output into clean Python data structures using dataclasses. -
✓
Built a multi-format report generator — a pattern directly applicable to security tooling that must produce both machine-readable (JSON, CSV) and human-readable (HTML) outputs.
-
✓
Deepened understanding of Nmap scan types (SYN scan vs connect scan vs service probe) and the trade-off between speed, stealth, and detection accuracy at each profile.
-
✓
Understood how network enumeration data feeds downstream exploitation tooling — reinforcing the recon phase's role in a structured penetration testing methodology.