Files
Flock_SCAN-fix/modules/prometheus_scraper.py
T

292 lines
9.4 KiB
Python

#!/usr/bin/env python3
"""
prometheus_scraper.py — Passive Prometheus discovery from camera traffic.
When cameras are on a network that exposes Prometheus endpoints, we can
scrape metrics without authentication. This module:
1. Checks for Prometheus on discovered gateway/subnet IPs
2. Scrapes /api/v1/targets to find ALL monitored endpoints
3. Extracts device names, job names, health status
4. Queries key metrics (up, flock_device_info, etc.)
This is 100% passive recon — we're reading what Prometheus already exposes.
"""
import json
import re
import socket
from datetime import datetime
try:
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
HAVE_REQUESTS = True
except ImportError:
HAVE_REQUESTS = False
# ── Common Prometheus ports ──
PROMETHEUS_PORTS = [9090, 9091, 9100, 9101]
# ── Flock-specific metrics patterns ──
FLOCK_METRIC_PATTERNS = [
"flock", "camera", "device", "alpr", "lpr",
"temperature", "uptime", "disk_", "memory_",
"cpu_", "network_", "wifi_", "signal",
]
# ── Interesting metric names ──
INTERESTING_METRICS = [
"up", "flock_device_info", "flock_camera_status",
"node_uname_info", "node_load1", "node_memory_MemTotal_bytes",
"node_filesystem_size_bytes", "node_network_receive_bytes_total",
"node_time_seconds", "process_start_time_seconds",
]
def probe_prometheus(host, port=9090, timeout=5):
"""
Check if a host is running Prometheus on the given port.
Probes /api/v1/targets (common open endpoint).
Returns dict with targets + status, or None if not Prometheus.
"""
if not HAVE_REQUESTS:
return None
try:
r = requests.get(
f"http://{host}:{port}/api/v1/targets",
timeout=timeout,
headers={"User-Agent": "FLOCK_scan/3.0"},
)
if r.status_code == 200:
data = r.json()
if data.get("status") == "success":
return _parse_targets(data, host, port)
except requests.exceptions.ConnectionError:
pass
except requests.exceptions.Timeout:
pass
except Exception:
pass
# Fallback: try /metrics (older Prometheus)
try:
r = requests.get(
f"http://{host}:{port}/metrics",
timeout=timeout,
headers={"User-Agent": "FLOCK_scan/3.0"},
)
if r.status_code == 200 and b"prometheus" in r.content[:500].lower():
return {
"host": host,
"port": port,
"type": "prometheus",
"metrics_accessible": True,
"targets_api": False,
"target_count": None,
"targets": [],
"flock_targets": 0,
}
except Exception:
pass
# Grafana fallback check
try:
r = requests.get(
f"http://{host}:{port}/api/health",
timeout=timeout,
headers={"User-Agent": "FLOCK_scan/3.0"},
)
if r.status_code == 200 and "grafana" in r.text.lower():
return {
"host": host,
"port": port,
"type": "grafana",
"metrics_accessible": True,
"targets_api": False,
"target_count": None,
"targets": [],
"flock_targets": 0,
}
except Exception:
pass
return None
def _parse_targets(data, host, port):
"""Parse Prometheus /api/v1/targets response."""
result = {
"host": host,
"port": port,
"type": "prometheus",
"metrics_accessible": True,
"targets_api": True,
"target_count": 0,
"targets": [],
"flock_targets": 0,
}
active = data.get("data", {}).get("activeTargets", [])
dropped = data.get("data", {}).get("droppedTargets", [])
result["dropped_count"] = len(dropped)
for target in active:
labels = target.get("labels", {})
discovered = target.get("discoveredLabels", {})
scrape_url = target.get("scrapeUrl", "")
health = target.get("health", "unknown")
entry = {
"job": labels.get("job", ""),
"instance": labels.get("instance", ""),
"module": labels.get("module", ""),
"device": labels.get("device", ""),
"model": labels.get("model", ""),
"scrape_url": scrape_url,
"health": health,
"last_scrape": target.get("lastScrape", ""),
"scrape_duration": target.get("lastScrapeDuration", ""),
}
# Extract Flock-specific info
name_parts = " ".join(str(v) for v in entry.values()).lower()
is_flock = any(p in name_parts for p in FLOCK_METRIC_PATTERNS)
if is_flock:
result["flock_targets"] += 1
entry["flock_device"] = True
result["targets"].append(entry)
result["target_count"] = len(active)
# Try /api/v1/query for key metrics
result["metrics"] = _scrape_metrics(host, port)
return result
def _scrape_metrics(host, port, timeout=5):
"""Query specific Prometheus metrics."""
metrics = {}
for metric in INTERESTING_METRICS:
try:
r = requests.get(
f"http://{host}:{port}/api/v1/query",
params={"query": metric},
timeout=timeout,
headers={"User-Agent": "FLOCK_scan/3.0"},
)
if r.status_code == 200:
data = r.json()
if data.get("status") == "success":
results = data.get("data", {}).get("result", [])
if results:
metrics[metric] = []
for res in results:
val = res.get("value", [None, ""])
labels = res.get("metric", {})
metrics[metric].append({
"labels": labels,
"value": val[1] if len(val) > 1 else None,
})
except Exception:
pass
return metrics if metrics else None
def scan_subnet_for_prometheus(gateway=None, local_ips=None, timeout=3):
"""
Scan likely addresses for Prometheus instances.
Scans: gateway, .1, .2, .254, and any known local IPs.
Returns list of discovered Prometheus instances.
"""
targets = set()
if gateway:
targets.add(gateway)
# Scan nearby IPs
try:
parts = gateway.split(".")
for last in [1, 2, 254]:
targets.add(f"{parts[0]}.{parts[1]}.{parts[2]}.{last}")
except Exception:
pass
if local_ips:
for ip in local_ips:
targets.add(ip)
try:
parts = ip.split(".")
for last in [1, 254]:
targets.add(f"{parts[0]}.{parts[1]}.{parts[2]}.{last}")
except Exception:
pass
discovered = []
for host in targets:
for port in PROMETHEUS_PORTS:
result = probe_prometheus(host, port=port, timeout=timeout)
if result:
discovered.append(result)
break # Don't re-check same host on other ports
return discovered
def format_prometheus_findings(findings):
"""Format Prometheus findings for terminal display."""
lines = []
if not findings:
return " No Prometheus/Grafana instances found."
for f in findings:
lines.append(f"\n \033[96m{f['type'].upper()}\033[0m at {f['host']}:{f['port']}")
if f.get("target_count") is not None:
lines.append(f" Targets: {f['target_count']} ({f.get('flock_targets', 0)} Flock-related)")
if f.get("targets"):
# Show Flock-related targets
flock_targets = [t for t in f["targets"] if t.get("flock_device")]
if flock_targets:
lines.append(f" \033[91mFlock Devices in Prometheus:\033[0m")
for t in flock_targets[:10]:
lines.append(f" └─ {t['instance']:25} job={t['job']} \033[92m{t['health']}\033[0m")
if t.get("model"):
lines.append(f" model={t['model']}")
# Show key metrics
if f.get("metrics") and len(flock_targets) > 0:
lines.append(f" Metrics:")
for metric, values in f["metrics"].items():
lines.append(f" {metric}: {len(values)} results")
if f.get("dropped_count"):
lines.append(f" Dropped targets: {f['dropped_count']}")
return "\n".join(lines)
# ═══════════════════════════════════════════════════════════════════
# CLI TEST
# ═══════════════════════════════════════════════════════════════════
if __name__ == "__main__":
import sys
target = sys.argv[1] if len(sys.argv) > 1 else "192.168.1.1"
print(f"Probing {target} for Prometheus...")
result = probe_prometheus(target)
if result:
print(json.dumps(result, indent=2, default=str))
else:
print("No Prometheus found.")