Compare commits

...

10 Commits

12 changed files with 271 additions and 77 deletions
+11
View File
@@ -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
+87 -16
View File
@@ -4,6 +4,7 @@ geo-scout — geo-targeted network scanner
masscan for port discovery, nmap for fingerprinting, probe engine for matching masscan for port discovery, nmap for fingerprinting, probe engine for matching
""" """
import ipaddress
import json import json
import os import os
import sys import sys
@@ -43,12 +44,25 @@ def ok(msg): print(f"{G}[+]{RST} {msg}")
def info(msg): print(f"{C}[*]{RST} {msg}") def info(msg): print(f"{C}[*]{RST} {msg}")
def warn(msg): print(f"{Y}[-]{RST} {msg}") def warn(msg): print(f"{Y}[-]{RST} {msg}")
def banner(): print(f"""{B} 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) 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 ────────────────────────────────────────────────────────────── # ── Probe worker ──────────────────────────────────────────────────────────────
def probe_host(ip: str, port: int, targets: list[dict]) -> list[dict]: def probe_host(ip: str, port: int, targets: list[dict]) -> list[dict]:
matches = [] matches = []
@@ -147,11 +188,17 @@ def cmd_run(args):
total_hosts = 0 total_hosts = 0
total_matches = 0 total_matches = 0
workers = args.workers workers = args.workers
n_countries = len(countries)
_interrupted = False
def progress(msg: str, end="\n"):
print(f" {W}{msg}{RST}", end=end, flush=True)
try:
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir) tmp = Path(tmpdir)
for country in countries: for idx, country in enumerate(countries, 1):
cc = country["code"].upper() cc = country["code"].upper()
exclude = country.get("exclude_cidrs", []) exclude = country.get("exclude_cidrs", [])
cidrs = get_cidrs(cc, cidr_index) cidrs = get_cidrs(cc, cidr_index)
@@ -164,12 +211,25 @@ def cmd_run(args):
if max_hosts: if max_hosts:
cidrs = cidrs[:max_hosts // 256 + 1] cidrs = cidrs[:max_hosts // 256 + 1]
info(f"{BOLD}{cc}{RST}: {len(cidrs)} CIDR blocks → masscan...") 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)...")
try:
open_ports = masscan_sweep(cidrs, all_ports, rate, tmp) 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
hosts_found = len(set(e["ip"] for e in open_ports)) hosts_found = len(set(e["ip"] for e in open_ports))
total_hosts += hosts_found total_hosts += hosts_found
ok(f"{cc}: {hosts_found} live hosts ({len(open_ports)} open ports)") ok(f"{cc}: {hosts_found} live hosts, {len(open_ports)} open ports found")
if not open_ports: if not open_ports:
continue continue
@@ -179,25 +239,33 @@ def cmd_run(args):
for entry in open_ports: for entry in open_ports:
ip_ports.setdefault(entry["ip"], []).append(entry["port"]) ip_ports.setdefault(entry["ip"], []).append(entry["port"])
info(f"{cc}: fingerprinting {hosts_found} hosts with nmap...") total_nmap = len(ip_ports)
info(f"{cc}: fingerprinting {total_nmap} hosts with nmap...")
nmap_results: dict[str, dict] = {} nmap_results: dict[str, dict] = {}
nmap_done = 0
with ThreadPoolExecutor(max_workers=min(workers, 10)) as ex: with ThreadPoolExecutor(max_workers=min(workers, 10)) as ex:
futs = {ex.submit(nmap_fingerprint, ip, ports, tmp): ip futs = {ex.submit(nmap_fingerprint, ip, ports, tmp): ip
for ip, ports in ip_ports.items()} for ip, ports in ip_ports.items()}
for fut in as_completed(futs): for fut in as_completed(futs):
ip = futs[fut] ip = futs[fut]
nmap_done += 1
progress(f"nmap: {nmap_done}/{total_nmap}", end="\r")
try: try:
nmap_results[ip] = fut.result() nmap_results[ip] = fut.result()
except Exception: except Exception:
nmap_results[ip] = {} nmap_results[ip] = {}
print()
info(f"{cc}: probing and matching targets...") 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: with ThreadPoolExecutor(max_workers=workers) as ex:
futs = [] futs = [ex.submit(probe_host, entry["ip"], entry["port"], targets)
for entry in open_ports: for entry in open_ports]
futs.append(ex.submit(probe_host, entry["ip"], entry["port"], targets))
for fut in as_completed(futs): for fut in as_completed(futs):
probe_done += 1
progress(f"probe: {probe_done}/{total_probes}", end="\r")
try: try:
matches = fut.result() matches = fut.result()
except Exception: except Exception:
@@ -215,10 +283,13 @@ def cmd_run(args):
db.insert_hit(conn, run_id, hit) db.insert_hit(conn, run_id, hit)
total_matches += 1 total_matches += 1
ver = f" [{match['version']}]" if match.get("version") else "" 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}") print(f"\r {G}HIT{RST} {BOLD}{ip}:{port}{RST} {Y}{match['target_name']}{RST}{ver} [{match['confidence']}] {cc}")
print() print()
except KeyboardInterrupt:
print()
warn("Scan aborted by user")
db.finish_run(conn, run_id, total_hosts, total_matches) db.finish_run(conn, run_id, total_hosts, total_matches)
conn.close() conn.close()
+10
View File
@@ -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
View File
@@ -1,20 +1,51 @@
scan_profile: scan_profile:
name: "example-sweep" name: "adversarial-nations-panos"
rate: 1000 rate: 3000
max_hosts_per_country: null max_hosts_per_country: null
countries: countries:
- code: VE # Largest internet footprint first — most likely to surface exposed devices
- code: RU
priority: high priority: high
exclude_cidrs: [] exclude_cidrs: []
notes: "Russia"
- code: US - code: IR
priority: high
exclude_cidrs: []
notes: "Iran"
- code: BY
priority: medium priority: medium
exclude_cidrs: exclude_cidrs: []
- "10.0.0.0/8" notes: "Belarus"
- "172.16.0.0/12"
- "192.168.0.0/16"
- code: NL - code: VE
priority: medium
exclude_cidrs: []
notes: "Venezuela"
- code: SY
priority: medium
exclude_cidrs: []
notes: "Syria"
- code: NI
priority: low priority: low
exclude_cidrs: [] 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"
+31
View File
@@ -1,4 +1,35 @@
targets: 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" - name: "Ubuntu 24.04 SSH"
tags: [linux, ubuntu, ssh] tags: [linux, ubuntu, ssh]
ports: [22] 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
View File
@@ -1,7 +1,8 @@
import json import json
import shutil import shutil
import subprocess import subprocess
import tempfile import threading
import time
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from pathlib import Path from pathlib import Path
@@ -14,6 +15,12 @@ def check_deps() -> list[str]:
return missing 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]: def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Path) -> list[dict]:
if not cidrs or not ports: if not cidrs or not ports:
return [] return []
@@ -29,17 +36,50 @@ def masscan_sweep(cidrs: list[str], ports: list[int], rate: int, output_dir: Pat
f"--ports={port_str}", f"--ports={port_str}",
"-iL", str(cidr_file), "-iL", str(cidr_file),
"-oJ", str(out_file), "-oJ", str(out_file),
"--output-format", "json",
"--wait", "3", "--wait", "3",
] ]
stop = threading.Event()
start = time.time()
def _progress():
while not stop.is_set():
found = 0
if out_file.exists():
try: try:
subprocess.run(cmd, capture_output=True, timeout=3600) found = out_file.read_text(errors="replace").count('"ip"')
except subprocess.TimeoutExpired: except Exception:
pass 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:
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
proc.wait(timeout=3600)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
except KeyboardInterrupt:
proc.kill()
proc.wait()
stop.set()
t.join(timeout=2)
print()
raise
except FileNotFoundError: except FileNotFoundError:
stop.set()
t.join(timeout=2)
print()
raise RuntimeError("masscan not found — install it first") raise RuntimeError("masscan not found — install it first")
stop.set()
t.join(timeout=2)
print()
if not out_file.exists(): if not out_file.exists():
return [] return []