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
+135 -64
View File
@@ -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()
+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:
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"
+31
View File
@@ -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
View File
@@ -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 []