#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" INPUT_FILE="${1:-$ROOT_DIR/public_ipv4_from_master.txt}" STAMP="$(date +%Y%m%d-%H%M%S)" OUTPUT_DIR="${2:-$ROOT_DIR/reports/inventory-$STAMP}" TARGETS_FILE="$OUTPUT_DIR/targets.txt" RAW_BASENAME="$OUTPUT_DIR/nmap_inventory" SUMMARY_CSV="$OUTPUT_DIR/summary.csv" SUMMARY_MD="$OUTPUT_DIR/summary.md" mkdir -p "$OUTPUT_DIR" if ! command -v nmap >/dev/null 2>&1; then echo "nmap is not installed. Install it first, then rerun this script." >&2 echo "Expected input: $INPUT_FILE" >&2 echo "Planned output directory: $OUTPUT_DIR" >&2 exit 1 fi if [[ ! -f "$INPUT_FILE" ]]; then echo "Input file not found: $INPUT_FILE" >&2 exit 1 fi python3 - "$INPUT_FILE" "$TARGETS_FILE" <<'PY' import ipaddress import sys src, dst = sys.argv[1], sys.argv[2] valid = [] seen = set() with open(src, "r", encoding="utf-8") as fh: for raw in fh: line = raw.strip() if not line or line.startswith("#"): continue try: ip = ipaddress.IPv4Address(line) except ipaddress.AddressValueError: print(f"Skipping invalid IPv4: {line}", file=sys.stderr) continue text = str(ip) if text not in seen: seen.add(text) valid.append(text) with open(dst, "w", encoding="utf-8") as fh: for ip in valid: fh.write(f"{ip}\n") print(len(valid)) PY TARGET_COUNT="$(wc -l < "$TARGETS_FILE" | tr -d ' ')" if [[ "$TARGET_COUNT" -eq 0 ]]; then echo "No valid IPv4 targets found in $INPUT_FILE" >&2 exit 1 fi echo "Validated $TARGET_COUNT targets" echo "Running a conservative external inventory scan" nmap \ -Pn \ -n \ -T3 \ --top-ports 20 \ -sV \ --version-light \ --open \ --max-retries 2 \ --host-timeout 2m \ -iL "$TARGETS_FILE" \ -oA "$RAW_BASENAME" python3 - "$RAW_BASENAME.gnmap" "$SUMMARY_CSV" "$SUMMARY_MD" "$TARGET_COUNT" <<'PY' import csv import sys from collections import defaultdict gnmap_path, csv_path, md_path, total_targets = sys.argv[1:5] rows = [] ports_by_ip = defaultdict(list) seen_hosts = set() with open(gnmap_path, "r", encoding="utf-8", errors="replace") as fh: for raw in fh: line = raw.strip() if not line.startswith("Host: "): continue if "Ports: " not in line: continue host = line.split()[1] seen_hosts.add(host) ports_blob = line.split("Ports: ", 1)[1] for item in ports_blob.split(", "): parts = item.split("/") if len(parts) < 7: continue port, state, proto, _, service, product, extra = parts[:7] if state != "open": continue product_info = " ".join(x for x in (product, extra) if x).strip() rows.append( { "ip": host, "port": port, "protocol": proto, "state": state, "service": service or "unknown", "product": product_info or "-", } ) ports_by_ip[host].append(f"{port}/{proto} {service or 'unknown'}".strip()) with open(csv_path, "w", newline="", encoding="utf-8") as fh: writer = csv.DictWriter( fh, fieldnames=["ip", "port", "protocol", "state", "service", "product"], ) writer.writeheader() writer.writerows(rows) sorted_hosts = sorted(ports_by_ip.items(), key=lambda item: (-len(item[1]), item[0])) with open(md_path, "w", encoding="utf-8") as fh: fh.write("# External Inventory Summary\n\n") fh.write(f"- Total validated targets: {total_targets}\n") fh.write(f"- Hosts with at least one open top port: {len(sorted_hosts)}\n") fh.write(f"- CSV details: `{csv_path}`\n") fh.write(f"- Raw Nmap files: `{gnmap_path[:-6]}` (`.nmap`, `.gnmap`, `.xml`)\n\n") fh.write("## Prioritized review queue\n\n") fh.write("| IP | Open ports found | Services |\n") fh.write("| --- | ---: | --- |\n") for ip, entries in sorted_hosts: fh.write(f"| {ip} | {len(entries)} | {', '.join(entries)} |\n") print(f"Wrote {len(rows)} rows to {csv_path}") print(f"Wrote Markdown summary to {md_path}") PY echo echo "Inventory complete" echo "Output directory: $OUTPUT_DIR" echo "Summary CSV: $SUMMARY_CSV" echo "Summary report: $SUMMARY_MD"