152 lines
4.3 KiB
Bash
Executable File
152 lines
4.3 KiB
Bash
Executable File
#!/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"
|