ezhik.

NetScout

Open source
GitHub ↗ Скачать ZIP
netscout.py15.9 KB · pythonraw
#!/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()