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
|
||||||
+87
-16
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
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"
|
||||||
|
|||||||
@@ -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
@@ -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 []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user