Compare commits
10 Commits
c20f189d49
...
2c22ba388f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c22ba388f | |||
| c33cb9ddab | |||
| 2e7e7f0cfb | |||
| 62906d9676 | |||
| aec636f195 | |||
| f21e5a6ee0 | |||
| 615937b20b | |||
| feb34ada8b | |||
| 61a6285c36 | |||
| 3cbecd16b1 |
+11
@@ -0,0 +1,11 @@
|
||||
lib/__pycache__/
|
||||
data/
|
||||
results/
|
||||
|
||||
# Per-project workflow gate dotfiles (do not commit)
|
||||
.active-item.json
|
||||
.closeout-required.json
|
||||
.workflow-approved
|
||||
.workflow-bypass
|
||||
.workflow-state.json
|
||||
paused.conf
|
||||
+135
-64
@@ -4,6 +4,7 @@ geo-scout — geo-targeted network scanner
|
||||
masscan for port discovery, nmap for fingerprinting, probe engine for matching
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
@@ -43,12 +44,25 @@ def ok(msg): print(f"{G}[+]{RST} {msg}")
|
||||
def info(msg): print(f"{C}[*]{RST} {msg}")
|
||||
def warn(msg): print(f"{Y}[-]{RST} {msg}")
|
||||
def banner(): print(f"""{B}
|
||||
__ _ ___ ___ ___ ___ ___ _ _ _
|
||||
/ _` / _ \\/ _ \\ ___ / __|/ __/ _ \\| | | | |_
|
||||
| (_| | __/ (_) |___\\__ \\ (_| (_) | |_| | _|
|
||||
\\__, |\\___|\\___/ |___/\\___\\___/ \\__,_|\\__|
|
||||
|___/
|
||||
{RST}{W} geo-targeted network scanner v1.0{RST}
|
||||
· * · * · * · * ·
|
||||
* ╔══════════════════════════════════╗ *
|
||||
· ║ · · · · · · · · ·║ ·
|
||||
* ║ · ─────────────────────────── · ║ *
|
||||
· ║ ───── · ╔═══╗ · ───── ║ ·
|
||||
* ║ · ──── · ────║{R} ◉ {B}║──── · ───· ║ *
|
||||
· ║ ───── · ╚═══╝ · ───── ║ ·
|
||||
* ║ · ─────────────────────────── · ║ *
|
||||
· ║ · · · · · · · · ·║ ·
|
||||
* ╚══════════════════════════════════╝ *
|
||||
· * · * · * · * ·
|
||||
{RST}{C}
|
||||
██████ ███████ ██████ ███████ ██████ ██████ ██ ██ ████████
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
██ ███ █████ ██ ██ ████ ███████ ██ ██ ██ ██ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
██████ ███████ ██████ ███████ ██████ ██████ ██████ ██
|
||||
{RST}{B} ───────────────────────────────────────────────────────────────────────
|
||||
{RST}{W} geo-targeted threat surface scanner {Y}v1.0{RST} {R}[ authorized use only ]{RST}
|
||||
""")
|
||||
|
||||
|
||||
@@ -77,6 +91,33 @@ def collect_all_ports(targets: list[dict]) -> list[int]:
|
||||
return sorted(ports)
|
||||
|
||||
|
||||
def count_ips(cidrs: list[str]) -> int:
|
||||
total = 0
|
||||
for c in cidrs:
|
||||
try:
|
||||
total += ipaddress.ip_network(c, strict=False).num_addresses
|
||||
except ValueError:
|
||||
pass
|
||||
return total
|
||||
|
||||
|
||||
def fmt_ip_count(n: int) -> str:
|
||||
if n >= 1_000_000:
|
||||
return f"{n/1_000_000:.1f}M"
|
||||
if n >= 1_000:
|
||||
return f"{n/1_000:.0f}K"
|
||||
return str(n)
|
||||
|
||||
|
||||
def fmt_eta(total_ips: int, n_ports: int, rate: int) -> str:
|
||||
if rate <= 0:
|
||||
return "?"
|
||||
secs = int(total_ips * n_ports / rate)
|
||||
m, s = divmod(secs, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return f"{h}h {m:02d}m" if h else f"{m}m {s:02d}s"
|
||||
|
||||
|
||||
# ── Probe worker ──────────────────────────────────────────────────────────────
|
||||
def probe_host(ip: str, port: int, targets: list[dict]) -> list[dict]:
|
||||
matches = []
|
||||
@@ -147,77 +188,107 @@ def cmd_run(args):
|
||||
total_hosts = 0
|
||||
total_matches = 0
|
||||
workers = args.workers
|
||||
n_countries = len(countries)
|
||||
_interrupted = False
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmp = Path(tmpdir)
|
||||
def progress(msg: str, end="\n"):
|
||||
print(f" {W}{msg}{RST}", end=end, flush=True)
|
||||
|
||||
for country in countries:
|
||||
cc = country["code"].upper()
|
||||
exclude = country.get("exclude_cidrs", [])
|
||||
cidrs = get_cidrs(cc, cidr_index)
|
||||
cidrs = merge_cidrs(cidrs, exclude)
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmp = Path(tmpdir)
|
||||
|
||||
if not cidrs:
|
||||
warn(f"{cc}: no CIDR ranges found, skipping")
|
||||
continue
|
||||
for idx, country in enumerate(countries, 1):
|
||||
cc = country["code"].upper()
|
||||
exclude = country.get("exclude_cidrs", [])
|
||||
cidrs = get_cidrs(cc, cidr_index)
|
||||
cidrs = merge_cidrs(cidrs, exclude)
|
||||
|
||||
if max_hosts:
|
||||
cidrs = cidrs[:max_hosts // 256 + 1]
|
||||
if not cidrs:
|
||||
warn(f"{cc}: no CIDR ranges found, skipping")
|
||||
continue
|
||||
|
||||
info(f"{BOLD}{cc}{RST}: {len(cidrs)} CIDR blocks → masscan...")
|
||||
if max_hosts:
|
||||
cidrs = cidrs[:max_hosts // 256 + 1]
|
||||
|
||||
open_ports = masscan_sweep(cidrs, all_ports, rate, tmp)
|
||||
hosts_found = len(set(e["ip"] for e in open_ports))
|
||||
total_hosts += hosts_found
|
||||
ok(f"{cc}: {hosts_found} live hosts ({len(open_ports)} open ports)")
|
||||
total_ips = count_ips(cidrs)
|
||||
eta = fmt_eta(total_ips, len(all_ports), rate)
|
||||
print(f"\n{C}[{idx}/{n_countries}]{RST} {BOLD}{cc}{RST} — {len(cidrs)} CIDR blocks {W}({fmt_ip_count(total_ips)} IPs ~{eta} at {rate} pps){RST}")
|
||||
info(f"{cc}: running masscan (Ctrl-C once = skip country, twice = abort all)...")
|
||||
|
||||
if not open_ports:
|
||||
continue
|
||||
try:
|
||||
open_ports = masscan_sweep(cidrs, all_ports, rate, tmp)
|
||||
_interrupted = False
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
if _interrupted:
|
||||
raise
|
||||
_interrupted = True
|
||||
warn(f"{cc}: skipping — press Ctrl-C again to abort entire scan")
|
||||
continue
|
||||
|
||||
# Group by IP for nmap
|
||||
ip_ports: dict[str, list[int]] = {}
|
||||
for entry in open_ports:
|
||||
ip_ports.setdefault(entry["ip"], []).append(entry["port"])
|
||||
hosts_found = len(set(e["ip"] for e in open_ports))
|
||||
total_hosts += hosts_found
|
||||
ok(f"{cc}: {hosts_found} live hosts, {len(open_ports)} open ports found")
|
||||
|
||||
info(f"{cc}: fingerprinting {hosts_found} hosts with nmap...")
|
||||
nmap_results: dict[str, dict] = {}
|
||||
with ThreadPoolExecutor(max_workers=min(workers, 10)) as ex:
|
||||
futs = {ex.submit(nmap_fingerprint, ip, ports, tmp): ip
|
||||
for ip, ports in ip_ports.items()}
|
||||
for fut in as_completed(futs):
|
||||
ip = futs[fut]
|
||||
try:
|
||||
nmap_results[ip] = fut.result()
|
||||
except Exception:
|
||||
nmap_results[ip] = {}
|
||||
if not open_ports:
|
||||
continue
|
||||
|
||||
info(f"{cc}: probing and matching targets...")
|
||||
with ThreadPoolExecutor(max_workers=workers) as ex:
|
||||
futs = []
|
||||
# Group by IP for nmap
|
||||
ip_ports: dict[str, list[int]] = {}
|
||||
for entry in open_ports:
|
||||
futs.append(ex.submit(probe_host, entry["ip"], entry["port"], targets))
|
||||
ip_ports.setdefault(entry["ip"], []).append(entry["port"])
|
||||
|
||||
for fut in as_completed(futs):
|
||||
try:
|
||||
matches = fut.result()
|
||||
except Exception:
|
||||
continue
|
||||
for match in matches:
|
||||
ip = match["ip"]
|
||||
port = match["port"]
|
||||
nmap_info = nmap_results.get(ip, {}).get(port, {})
|
||||
hit = {
|
||||
**match,
|
||||
"country_code": cc,
|
||||
"nmap_service": nmap_info.get("service", ""),
|
||||
"nmap_banner": nmap_info.get("banner", ""),
|
||||
}
|
||||
db.insert_hit(conn, run_id, hit)
|
||||
total_matches += 1
|
||||
ver = f" [{match['version']}]" if match.get("version") else ""
|
||||
print(f" {G}HIT{RST} {BOLD}{ip}:{port}{RST} {Y}{match['target_name']}{RST}{ver} [{match['confidence']}] {cc}")
|
||||
total_nmap = len(ip_ports)
|
||||
info(f"{cc}: fingerprinting {total_nmap} hosts with nmap...")
|
||||
nmap_results: dict[str, dict] = {}
|
||||
nmap_done = 0
|
||||
with ThreadPoolExecutor(max_workers=min(workers, 10)) as ex:
|
||||
futs = {ex.submit(nmap_fingerprint, ip, ports, tmp): ip
|
||||
for ip, ports in ip_ports.items()}
|
||||
for fut in as_completed(futs):
|
||||
ip = futs[fut]
|
||||
nmap_done += 1
|
||||
progress(f"nmap: {nmap_done}/{total_nmap}", end="\r")
|
||||
try:
|
||||
nmap_results[ip] = fut.result()
|
||||
except Exception:
|
||||
nmap_results[ip] = {}
|
||||
print()
|
||||
|
||||
print()
|
||||
total_probes = len(open_ports)
|
||||
info(f"{cc}: probing {total_probes} open ports for target matches...")
|
||||
probe_done = 0
|
||||
with ThreadPoolExecutor(max_workers=workers) as ex:
|
||||
futs = [ex.submit(probe_host, entry["ip"], entry["port"], targets)
|
||||
for entry in open_ports]
|
||||
|
||||
for fut in as_completed(futs):
|
||||
probe_done += 1
|
||||
progress(f"probe: {probe_done}/{total_probes}", end="\r")
|
||||
try:
|
||||
matches = fut.result()
|
||||
except Exception:
|
||||
continue
|
||||
for match in matches:
|
||||
ip = match["ip"]
|
||||
port = match["port"]
|
||||
nmap_info = nmap_results.get(ip, {}).get(port, {})
|
||||
hit = {
|
||||
**match,
|
||||
"country_code": cc,
|
||||
"nmap_service": nmap_info.get("service", ""),
|
||||
"nmap_banner": nmap_info.get("banner", ""),
|
||||
}
|
||||
db.insert_hit(conn, run_id, hit)
|
||||
total_matches += 1
|
||||
ver = f" [{match['version']}]" if match.get("version") else ""
|
||||
print(f"\r {G}HIT{RST} {BOLD}{ip}:{port}{RST} {Y}{match['target_name']}{RST}{ver} [{match['confidence']}] {cc}")
|
||||
print()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
warn("Scan aborted by user")
|
||||
|
||||
db.finish_run(conn, run_id, total_hosts, total_matches)
|
||||
conn.close()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
scan_profile:
|
||||
name: "panos-ve"
|
||||
rate: 3000
|
||||
max_hosts_per_country: null
|
||||
|
||||
countries:
|
||||
- code: VE
|
||||
priority: high
|
||||
exclude_cidrs: []
|
||||
notes: "Venezuela"
|
||||
+40
-9
@@ -1,20 +1,51 @@
|
||||
scan_profile:
|
||||
name: "example-sweep"
|
||||
rate: 1000
|
||||
name: "adversarial-nations-panos"
|
||||
rate: 3000
|
||||
max_hosts_per_country: null
|
||||
|
||||
countries:
|
||||
- code: VE
|
||||
# Largest internet footprint first — most likely to surface exposed devices
|
||||
- code: RU
|
||||
priority: high
|
||||
exclude_cidrs: []
|
||||
notes: "Russia"
|
||||
|
||||
- code: US
|
||||
- code: IR
|
||||
priority: high
|
||||
exclude_cidrs: []
|
||||
notes: "Iran"
|
||||
|
||||
- code: BY
|
||||
priority: medium
|
||||
exclude_cidrs:
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
- "192.168.0.0/16"
|
||||
exclude_cidrs: []
|
||||
notes: "Belarus"
|
||||
|
||||
- code: NL
|
||||
- code: VE
|
||||
priority: medium
|
||||
exclude_cidrs: []
|
||||
notes: "Venezuela"
|
||||
|
||||
- code: SY
|
||||
priority: medium
|
||||
exclude_cidrs: []
|
||||
notes: "Syria"
|
||||
|
||||
- code: NI
|
||||
priority: low
|
||||
exclude_cidrs: []
|
||||
notes: "Nicaragua"
|
||||
|
||||
- code: CU
|
||||
priority: low
|
||||
exclude_cidrs: []
|
||||
notes: "Cuba"
|
||||
|
||||
- code: MM
|
||||
priority: low
|
||||
exclude_cidrs: []
|
||||
notes: "Myanmar"
|
||||
|
||||
- code: KP
|
||||
priority: low
|
||||
exclude_cidrs: []
|
||||
notes: "North Korea — minimal public internet, low yield expected"
|
||||
|
||||
@@ -1,4 +1,35 @@
|
||||
targets:
|
||||
- name: "Palo Alto PAN-OS < 10.2.14"
|
||||
tags: [panos, firewall, palo-alto, cve-2024-3400, network-device]
|
||||
ports: [443, 4443, 8443]
|
||||
probes:
|
||||
- type: https
|
||||
path: "/api/?type=version"
|
||||
method: GET
|
||||
match_in: body
|
||||
patterns: ["sw-version"]
|
||||
version_extract: "<sw-version>([0-9]+\\.[0-9]+\\.[0-9]+)"
|
||||
version_compare:
|
||||
operator: "<"
|
||||
value: "10.2.14"
|
||||
confidence: high
|
||||
|
||||
- type: https
|
||||
path: "/php/login.php"
|
||||
method: GET
|
||||
match_in: [body, headers]
|
||||
patterns:
|
||||
- "Palo Alto Networks"
|
||||
- "PAN-OS"
|
||||
confidence: low
|
||||
|
||||
- type: https
|
||||
path: "/global-protect/portal/gp-portal-esp.esp"
|
||||
method: GET
|
||||
match_in: body
|
||||
patterns: ["globalprotect", "PAN-OS"]
|
||||
confidence: low
|
||||
|
||||
- name: "Ubuntu 24.04 SSH"
|
||||
tags: [linux, ubuntu, ssh]
|
||||
ports: [22]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+44
-4
@@ -1,7 +1,8 @@
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
@@ -14,6 +15,12 @@ def check_deps() -> list[str]:
|
||||
return missing
|
||||
|
||||
|
||||
def _fmt_elapsed(seconds: int) -> str:
|
||||
m, s = divmod(seconds, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return f"{h}h{m:02d}m{s:02d}s" if h else f"{m}m{s:02d}s"
|
||||
|
||||
|
||||
def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Path) -> list[dict]:
|
||||
if not cidrs or not ports:
|
||||
return []
|
||||
@@ -29,17 +36,50 @@ def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Pat
|
||||
f"--ports={port_str}",
|
||||
"-iL", str(cidr_file),
|
||||
"-oJ", str(out_file),
|
||||
"--output-format", "json",
|
||||
"--wait", "3",
|
||||
]
|
||||
|
||||
stop = threading.Event()
|
||||
start = time.time()
|
||||
|
||||
def _progress():
|
||||
while not stop.is_set():
|
||||
found = 0
|
||||
if out_file.exists():
|
||||
try:
|
||||
found = out_file.read_text(errors="replace").count('"ip"')
|
||||
except Exception:
|
||||
pass
|
||||
elapsed = _fmt_elapsed(int(time.time() - start))
|
||||
print(f"\r [{elapsed}] masscan running — {found} hosts found so far ", end="", flush=True)
|
||||
stop.wait(timeout=4)
|
||||
|
||||
t = threading.Thread(target=_progress, daemon=True)
|
||||
t.start()
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=3600)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
proc.wait(timeout=3600)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
stop.set()
|
||||
t.join(timeout=2)
|
||||
print()
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
stop.set()
|
||||
t.join(timeout=2)
|
||||
print()
|
||||
raise RuntimeError("masscan not found — install it first")
|
||||
|
||||
stop.set()
|
||||
t.join(timeout=2)
|
||||
print()
|
||||
|
||||
if not out_file.exists():
|
||||
return []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user