netscout.pyraw
#!/usr/bin/env python3
"""NetScout — Domain & IP reconnaissance CLI tool."""
import argparse
import json
import math
import socket
import ssl
import urllib.request
from datetime import datetime, timezone
class C:
GRAY = "\033[90m"
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
CYAN = "\033[96m"
WHITE = "\033[97m"
BOLD = "\033[1m"
DIM = "\033[2m"
ULINE = "\033[4m"
RESET = "\033[0m"
BOX_WIDTH = 60
BANNER = f"""
{C.CYAN}{C.BOLD} ███╗ ██╗███████╗████████╗███████╗ ██████╗ ██████╗ ██╗ ██╗████████╗
████╗ ██║██╔════╝╚══██╔══╝██╔════╝██╔════╝██╔═══██╗██║ ██║╚══██╔══╝
██╔██╗ ██║█████╗ ██║ ███████╗██║ ██║ ██║██║ ██║ ██║
██║╚██╗██║██╔══╝ ██║ ╚════██║██║ ██║ ██║██║ ██║ ██║
██║ ╚████║███████╗ ██║ ███████║╚██████╗╚██████╔╝╚██████╔╝ ██║
╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝{C.RESET}
{C.GRAY} {'─' * BOX_WIDTH}{C.RESET}
{C.DIM} Domain & IP Recon Tool v2.1 {C.GRAY}by mainstarkov{C.RESET}
{C.GRAY} {'─' * BOX_WIDTH}{C.RESET}
"""
def box_top(title: str, icon: str, color: str):
w = BOX_WIDTH - 2
print(f"\n{color} {icon} ╔{'═' * w}╗{C.RESET}")
pad = max(0, w - 3 - len(title))
print(f"{color} ║ {C.BOLD}{title}{C.RESET}{color}{' ' * pad}║{C.RESET}")
print(f"{color} ╠{'═' * w}╣{C.RESET}")
def box_row(label: str, value: str, color: str, val_color: str = ""):
vc = val_color or C.WHITE
padded = f"{label:<14}"
vlen = len(value)
pad = max(0, BOX_WIDTH - 3 - 14 - 3 - vlen)
print(f"{color} ║ {C.YELLOW}{padded}{C.RESET} {vc}{value}{C.RESET}{' ' * pad}{color}║{C.RESET}")
def box_sep(color: str):
print(f"{color} ╟{'─' * (BOX_WIDTH - 2)}╢{C.RESET}")
def box_end(color: str):
print(f"{color} ╚{'═' * (BOX_WIDTH - 2)}╝{C.RESET}")
def dot(ok: bool) -> str:
return f"{C.GREEN}●{C.RESET}" if ok else f"{C.RED}●{C.RESET}"
def flag(cc: str) -> str:
if not cc or len(cc) != 2:
return "🌐"
return chr(0x1F1E6 + ord(cc[0]) - ord("A")) + chr(0x1F1E6 + ord(cc[1]) - ord("A"))
def fetch_json(url: str) -> dict | None:
try:
req = urllib.request.Request(url, headers={"User-Agent": "NetScout/2.1"})
with urllib.request.urlopen(req, timeout=8) as r:
return json.loads(r.read().decode())
except Exception:
return None
def resolve_domain(domain: str) -> list[str]:
domain = domain.replace("https://", "").replace("http://", "").strip("/")
try:
return list(dict.fromkeys(r[4][0] for r in socket.getaddrinfo(domain, None)))
except socket.gaierror:
return []
def reverse_dns(ip: str) -> list[str]:
try:
result = socket.gethostbyaddr(ip)
names = [result[0]] + list(result[1])
return [n for n in dict.fromkeys(names) if not n.endswith(".in-addr.arpa")]
except socket.herror:
return []
def scan_ports(ip: str, ports: dict[int, str]) -> dict[int, bool]:
results = {}
for port in ports:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1.5)
try:
results[port] = sock.connect_ex((ip, port)) == 0
except OSError:
results[port] = False
finally:
sock.close()
return results
def ssl_info(domain: str) -> dict | None:
try:
ctx = ssl.create_default_context()
with ctx.wrap_socket(socket.socket(), server_hostname=domain) as s:
s.settimeout(5)
s.connect((domain, 443))
cert = s.getpeercert()
return {
"issuer": dict(x[0] for x in cert.get("issuer", [])).get("organizationName", "?"),
"subject": dict(x[0] for x in cert.get("subject", [])).get("commonName", "?"),
"expires": cert.get("notAfter", "?"),
"serial": cert.get("serialNumber", "")[:20],
}
except Exception:
return None
def http_info(domain: str) -> dict | None:
for proto in ("https", "http"):
try:
req = urllib.request.Request(
f"{proto}://{domain}", headers={"User-Agent": "NetScout/2.1"}, method="HEAD"
)
with urllib.request.urlopen(req, timeout=5) as r:
return {
"status": r.status,
"server": r.headers.get("Server", "—"),
"powered": r.headers.get("X-Powered-By", "—"),
"proto": proto.upper(),
}
except Exception:
continue
return None
GEO_FIELDS = ("source", "country", "cc", "region", "city", "district",
"zip", "lat", "lon", "tz", "isp", "org", "asn", "proxy", "hosting")
def _geo_entry(**kw) -> dict:
return {k: kw.get(k, "") for k in GEO_FIELDS}
def geo_ipapi(ip: str) -> dict | None:
d = fetch_json(
f"http://ip-api.com/json/{ip}?fields=status,country,countryCode,"
f"regionName,city,district,zip,lat,lon,timezone,isp,org,as,proxy,hosting"
)
if d and d.get("status") == "success":
return _geo_entry(
source="ip-api.com", country=d.get("country"), cc=d.get("countryCode"),
region=d.get("regionName"), city=d.get("city"), district=d.get("district"),
zip=d.get("zip"), lat=d.get("lat"), lon=d.get("lon"), tz=d.get("timezone"),
isp=d.get("isp"), org=d.get("org"), asn=d.get("as"),
proxy=d.get("proxy", False), hosting=d.get("hosting", False),
)
return None
def geo_ipwhois(ip: str) -> dict | None:
d = fetch_json(f"http://ipwho.is/{ip}")
if d and d.get("success"):
conn = d.get("connection", {})
tz = d.get("timezone", {})
return _geo_entry(
source="ipwho.is", country=d.get("country"), cc=d.get("country_code"),
region=d.get("region"), city=d.get("city"), zip=d.get("postal"),
lat=d.get("latitude"), lon=d.get("longitude"),
tz=tz.get("id") if isinstance(tz, dict) else "",
isp=conn.get("isp"), org=conn.get("org"),
asn=f"AS{conn.get('asn', '')} {conn.get('org', '')}",
proxy=d.get("security", {}).get("proxy", False),
hosting=d.get("type") == "hosting",
)
return None
def geo_ipinfo(ip: str) -> dict | None:
d = fetch_json(f"https://ipinfo.io/{ip}/json")
if d and "bogon" not in d:
parts = d.get("loc", ",").split(",")
lat = float(parts[0]) if len(parts) == 2 and parts[0] else None
lon = float(parts[1]) if len(parts) == 2 and parts[1] else None
return _geo_entry(
source="ipinfo.io", country=d.get("country"), cc=d.get("country"),
region=d.get("region"), city=d.get("city"), zip=d.get("postal"),
lat=lat, lon=lon, tz=d.get("timezone"),
isp=d.get("org"), org=d.get("org"), asn=d.get("org"),
)
return None
def geolocate(ip: str) -> list[dict]:
results = []
for fn in (geo_ipapi, geo_ipwhois, geo_ipinfo):
r = fn(ip)
if r:
results.append(r)
return results
def avg_coords(geos: list[dict]) -> tuple[float, float] | None:
lats = [g["lat"] for g in geos if g.get("lat") is not None]
lons = [g["lon"] for g in geos if g.get("lon") is not None]
if not lats:
return None
return round(sum(lats) / len(lats), 6), round(sum(lons) / len(lons), 6)
def spread_km(geos: list[dict]) -> float:
lats = [g["lat"] for g in geos if g.get("lat") is not None]
lons = [g["lon"] for g in geos if g.get("lon") is not None]
if len(lats) < 2:
return 0.0
dlat = (max(lats) - min(lats)) * 111.0
dlon = (max(lons) - min(lons)) * 111.0 * math.cos(math.radians(sum(lats) / len(lats)))
return round(math.sqrt(dlat**2 + dlon**2), 1)
def maps_url(lat: float, lon: float) -> str:
return f"https://www.google.com/maps?q={lat},{lon}"
PORTS = {
21: "FTP", 22: "SSH", 25: "SMTP", 53: "DNS",
80: "HTTP", 443: "HTTPS", 3306: "MySQL",
5432: "Postgres", 8080: "Proxy", 8443: "Alt-HTTPS",
}
def scan_target(target: str, skip_ports: bool = False):
is_ip = False
try:
socket.inet_aton(target)
is_ip = True
except OSError:
pass
domain = None
ip = target if is_ip else None
# DNS
if not is_ip:
domain = target.replace("https://", "").replace("http://", "").strip("/")
box_top(f"DNS RESOLUTION — {domain}", "🔍", C.CYAN)
ips = resolve_domain(domain)
if not ips:
box_row("Status", "✗ Could not resolve", C.CYAN, C.RED)
box_end(C.CYAN)
return
v4 = [i for i in ips if ":" not in i]
v6 = [i for i in ips if ":" in i]
for addr in v4:
box_row("IPv4", addr, C.CYAN, C.GREEN)
for addr in v6:
box_row("IPv6", addr, C.CYAN, C.BLUE)
box_row("Records", f"{len(ips)} total ({len(v4)} A / {len(v6)} AAAA)", C.CYAN)
box_end(C.CYAN)
ip = v4[0] if v4 else ips[0]
# Reverse DNS
box_top(f"REVERSE DNS — {ip}", "🔄", C.MAGENTA)
hostnames = reverse_dns(ip)
if hostnames:
for h in hostnames:
box_row("Hostname", h, C.MAGENTA, C.GREEN)
else:
box_row("Result", "No PTR records found", C.MAGENTA, C.GRAY)
box_end(C.MAGENTA)
# Geolocation
geos = geolocate(ip)
box_top(f"GEOLOCATION — {ip} [{len(geos)} sources]", "🌍", C.GREEN)
if geos:
g = geos[0]
box_row("Country", f"{flag(g['cc'])} {g['country']} ({g['cc']})", C.GREEN)
box_row("Region", g.get("region") or "—", C.GREEN)
box_row("City", g.get("city") or "—", C.GREEN)
if g.get("district"):
box_row("District", g["district"], C.GREEN)
if g.get("zip"):
box_row("ZIP / Postal", g["zip"], C.GREEN)
box_row("Timezone", g.get("tz") or "—", C.GREEN)
box_sep(C.GREEN)
box_row("ISP", g.get("isp") or "—", C.GREEN)
box_row("Organization", g.get("org") or "—", C.GREEN)
box_row("AS Number", g.get("asn") or "—", C.GREEN)
box_sep(C.GREEN)
is_proxy = any(x.get("proxy") for x in geos)
is_dc = any(x.get("hosting") for x in geos)
box_row("Proxy / VPN", f"{dot(not is_proxy)} {'Yes' if is_proxy else 'No'}", C.GREEN)
box_row("Hosting / DC", f"{dot(not is_dc)} {'Yes' if is_dc else 'No'}", C.GREEN)
box_sep(C.GREEN)
coords = avg_coords(geos)
if coords:
box_row("Avg Coords", f"{coords[0]}, {coords[1]}", C.GREEN)
sp = spread_km(geos)
if sp > 0:
sc = C.GREEN if sp < 50 else (C.YELLOW if sp < 200 else C.RED)
box_row("Spread", f"{sc}{sp} km{C.RESET}", C.GREEN)
box_row("📍 Map", f"{C.ULINE}{C.CYAN}{maps_url(*coords)}{C.RESET}", C.GREEN)
box_sep(C.GREEN)
for src in geos:
lat, lon = src.get("lat", "?"), src.get("lon", "?")
box_row(f"⊕ {src['source']}", f"{src['city']}, {src['region']} ({lat}, {lon})", C.GREEN, C.GRAY)
else:
box_row("Status", "✗ Geolocation unavailable", C.GREEN, C.RED)
box_end(C.GREEN)
# Port scan
ports = {} if skip_ports else PORTS
if ports:
box_top(f"PORT SCAN — {ip}", "🔌", C.YELLOW)
results = scan_ports(ip, ports)
opened = [p for p, ok in results.items() if ok]
closed = [p for p, ok in results.items() if not ok]
for p in opened:
box_row(f":{p}", f"{C.GREEN}● OPEN{C.RESET} {ports[p]}", C.YELLOW)
if closed:
s = ", ".join(str(p) for p in closed[:6])
if len(closed) > 6:
s += f" +{len(closed) - 6} more"
box_row("Closed", f"{C.GRAY}{s}{C.RESET}", C.YELLOW)
box_row("Summary", f"{len(opened)} open / {len(closed)} closed", C.YELLOW)
box_end(C.YELLOW)
# SSL
if domain:
box_top(f"SSL CERTIFICATE — {domain}", "🔒", C.BLUE)
si = ssl_info(domain)
if si:
box_row("Subject", si["subject"], C.BLUE, C.GREEN)
box_row("Issuer", si["issuer"], C.BLUE)
box_row("Expires", si["expires"], C.BLUE)
box_row("Serial", si["serial"] + "...", C.BLUE, C.GRAY)
else:
box_row("Status", "No SSL / connection failed", C.BLUE, C.RED)
box_end(C.BLUE)
# HTTP
if domain:
box_top(f"HTTP INFO — {domain}", "📡", C.CYAN)
hi = http_info(domain)
if hi:
sc = C.GREEN if hi["status"] < 400 else C.RED
box_row("Status", f"{sc}{hi['status']}{C.RESET}", C.CYAN)
box_row("Protocol", hi["proto"], C.CYAN)
box_row("Server", hi["server"], C.CYAN)
box_row("Powered By", hi["powered"], C.CYAN)
else:
box_row("Status", "Not reachable via HTTP(S)", C.CYAN, C.RED)
box_end(C.CYAN)
# Footer
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
print(f"\n{C.GRAY} {'─' * BOX_WIDTH}{C.RESET}")
print(f"{C.DIM} Scan completed at {ts}{C.RESET}")
print(f"{C.GRAY} {'─' * BOX_WIDTH}{C.RESET}\n")
def scan_json(targets: list[str], skip_ports: bool = False) -> list[dict]:
results = []
for target in targets:
entry = {"target": target, "timestamp": datetime.now(timezone.utc).isoformat()}
try:
socket.inet_aton(target)
is_ip = True
except OSError:
is_ip = False
if is_ip:
entry["ip"] = target
entry["domains"] = reverse_dns(target)
else:
ips = resolve_domain(target)
entry["ips"] = ips
if ips:
entry["domains"] = reverse_dns(ips[0])
entry["ip"] = ips[0]
ip = entry.get("ip")
if ip:
geos = geolocate(ip)
if geos:
entry["geo_sources"] = geos
coords = avg_coords(geos)
if coords:
entry["avg_coords"] = {"lat": coords[0], "lon": coords[1]}
entry["spread_km"] = spread_km(geos)
entry["maps_url"] = maps_url(*coords)
if not skip_ports:
port_results = scan_ports(ip, PORTS)
entry["ports"] = {
f"{p}/{PORTS[p]}": "open" if ok else "closed"
for p, ok in port_results.items()
}
if not is_ip:
si = ssl_info(target)
if si:
entry["ssl"] = si
results.append(entry)
return results
def main():
parser = argparse.ArgumentParser(prog="netscout", description="Domain & IP recon tool")
parser.add_argument("targets", nargs="+", help="domain(s) or IP(s) to scan")
parser.add_argument("--json", action="store_true", help="output as JSON")
parser.add_argument("--no-ports", action="store_true", help="skip port scanning")
args = parser.parse_args()
print(BANNER)
if args.json:
print(json.dumps(scan_json(args.targets, args.no_ports), indent=2, ensure_ascii=False))
else:
for i, t in enumerate(args.targets):
if i > 0:
print(f"\n{C.CYAN}{'━' * (BOX_WIDTH + 4)}{C.RESET}")
scan_target(t.strip(), args.no_ports)
if __name__ == "__main__":
main()