Initial commit

This commit is contained in:
denshooter
2026-03-09 22:07:19 +01:00
commit daef092099
55 changed files with 39435 additions and 0 deletions
+875
View File
@@ -0,0 +1,875 @@
import asyncio
import json
import re
import shutil
from datetime import datetime, timezone
import httpx
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL_NAME = "llama3.2"
# CLI backend: "claude" uses Claude Code CLI (no API key needed if logged in),
# "gemini" uses Gemini CLI (free with Google account), "ollama" = local llama3.2
# Auto-detect: prefers claude > gemini > ollama
def _detect_cli_backend() -> str:
for tool in ("gemini", "claude"):
if shutil.which(tool):
return tool
return "ollama"
_CLI_BACKEND = _detect_cli_backend()
async def _query_cli(prompt: str) -> dict | None:
"""Call claude or gemini CLI as subprocess, parse JSON from response."""
try:
cmd = [_CLI_BACKEND, "-p", prompt]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=30.0)
raw = stdout.decode().strip()
if not raw:
return None
# Strip markdown code fences if present
if "```json" in raw:
raw = raw.split("```json", 1)[1].split("```", 1)[0].strip()
elif "```" in raw:
raw = raw.split("```", 1)[1].split("```", 1)[0].strip()
# Extract first JSON object if there's surrounding text
m = re.search(r'\{.*\}', raw, re.DOTALL)
if m:
raw = m.group(0)
parsed = json.loads(raw)
return parsed if isinstance(parsed, dict) else None
except Exception as exc:
print(f"[AI] CLI query failed ({_CLI_BACKEND}): {exc}")
return None
THREAT_LEVELS = ("LOW", "GUARDED", "ELEVATED", "HIGH", "SEVERE")
KEYWORD_SCORES = {
"war": 4,
"missile": 4,
"strike": 4,
"attack": 4,
"explosion": 3,
"drone": 3,
"nuclear": 5,
"cyber": 3,
"hijack": 4,
"sanction": 2,
"military": 3,
"troops": 3,
"border": 2,
"earthquake": 2,
"tsunami": 4,
}
# Hotspot regions with bounding boxes for spatial movement analysis
# bbox = (lat_min, lon_min, lat_max, lon_max)
HOTSPOT_REGIONS = {
"ukraine": {
"name": "Ukraine / Black Sea", "lat": 48.38, "lon": 31.17,
"bbox": (44, 25, 52, 40),
"keywords": ["ukraine", "kyiv", "kharkiv", "crimea", "donbas", "odesa", "bakhmut", "zaporizhzhia"],
"baseline": "Escalation risk around frontline",
},
"russia": {
"name": "Western Russia", "lat": 55.76, "lon": 37.62,
"bbox": (52, 30, 62, 48),
"keywords": ["russia", "moscow", "kursk"],
"baseline": "Military posture shifts",
},
"israel": {
"name": "Israel / Palestine", "lat": 31.05, "lon": 34.85,
"bbox": (29, 33, 33.5, 36),
"keywords": ["israel", "gaza", "west bank", "tel aviv", "jerusalem", "rafah", "idf", "hamas", "hezbollah"],
"baseline": "Cross-border retaliation risk",
},
"iran": {
"name": "Iran", "lat": 32.43, "lon": 53.69,
"bbox": (25, 44, 40, 63),
"keywords": ["iran", "tehran", "irgc"],
"baseline": "Strategic force posture shift",
},
"syria_lebanon": {
"name": "Syria / Lebanon", "lat": 34.80, "lon": 37.00,
"bbox": (32, 35, 37.5, 42),
"keywords": ["syria", "damascus", "lebanon", "aleppo"],
"baseline": "Proxy activity risk",
},
"taiwan": {
"name": "Taiwan Strait", "lat": 23.70, "lon": 120.96,
"bbox": (21, 117, 27, 124),
"keywords": ["taiwan", "taipei", "taiwan strait"],
"baseline": "Maritime and airspace pressure",
},
"south_china_sea": {
"name": "South China Sea", "lat": 12.0, "lon": 113.0,
"bbox": (5, 107, 22, 120),
"keywords": ["south china sea", "spratly", "paracel"],
"baseline": "Naval maneuvering risk",
},
"korea": {
"name": "Korean Peninsula", "lat": 37.57, "lon": 126.98,
"bbox": (33, 124, 43, 131),
"keywords": ["korea", "pyongyang", "dprk", "north korea", "south korea", "seoul"],
"baseline": "Missile test / military response risk",
},
"red_sea": {
"name": "Red Sea / Bab-el-Mandeb", "lat": 15.0, "lon": 42.0,
"bbox": (10, 38, 22, 46),
"keywords": ["red sea", "houthi", "yemen", "bab-el-mandeb", "aden"],
"baseline": "Shipping disruption risk",
},
"hormuz": {
"name": "Strait of Hormuz", "lat": 26.58, "lon": 56.25,
"bbox": (24, 54, 28, 58),
"keywords": ["hormuz", "persian gulf"],
"baseline": "Energy transit disruption risk",
},
"suez": {
"name": "Suez Canal", "lat": 29.93, "lon": 32.55,
"bbox": (28, 31, 32, 34),
"keywords": ["suez"],
"baseline": "Maritime chokepoint risk",
},
"sahel": {
"name": "Sahel / West Africa", "lat": 14.0, "lon": 0.0,
"bbox": (8, -10, 20, 15),
"keywords": ["mali", "niger", "burkina faso", "sahel", "nigeria"],
"baseline": "Insurgency / instability risk",
},
}
# ── Region definitions for grouping events into theaters ──
REGION_DEFS: dict[str, list[str]] = {
"Eastern Europe": [
"Ukraine", "Russia", "Crimea", "Donbas", "Donbass", "Kharkiv", "Kyiv",
"Kiev", "Odesa", "Mariupol", "Bakhmut", "Zaporizhzhia", "Kherson",
"Moldova", "Belarus", "Avdiivka",
],
"Middle East": [
"Israel", "Gaza", "Palestine", "Lebanon", "Hezbollah", "Syria",
"Damascus", "Iran", "Tehran", "Iraq", "Baghdad", "Jordan",
"West Bank", "Rafah", "Jenin", "Nablus", "Jerusalem", "Tel Aviv",
],
"Red Sea & Horn of Africa": [
"Red Sea", "Yemen", "Houthi", "Somalia", "Sudan", "Ethiopia",
"Bab-el-Mandeb", "Suez",
],
"East Asia": [
"China", "Taiwan", "Taipei", "North Korea", "Pyongyang",
"South Korea", "Japan", "South China Sea", "Taiwan Strait",
"East China Sea",
],
"South Asia": [
"India", "Pakistan", "Afghanistan", "Kashmir", "Myanmar", "Kabul",
],
"Sub-Saharan Africa": [
"Mali", "Niger", "Nigeria", "Congo", "Sahel", "Burkina Faso",
"Mozambique", "Libya",
],
"Europe / NATO": [
"NATO", "Poland", "Finland", "Sweden", "Norway", "Baltic",
"Germany", "France", "UK", "Serbia", "Kosovo", "Georgia", "Romania",
],
}
_WEIGHT_ORDER = {"CRITICAL": 0, "HIGH": 1, "ELEVATED": 2, "GUARDED": 3, "ACTIVE": 4}
def _match_region(title: str) -> str | None:
"""Match a headline to a geopolitical region."""
tl = title.lower()
for region, keywords in REGION_DEFS.items():
for kw in keywords:
if kw.lower() in tl:
return region
return None
def _regional_briefs(news_items: list, conflicts: list) -> list[dict]:
"""Group events by region and produce per-region threat assessments."""
region_events: dict[str, list] = {}
for item in (conflicts or []):
region = _match_region(item.get("title", ""))
if region:
region_events.setdefault(region, []).append(item)
for item in (news_items or [])[:30]:
region = _match_region(item.get("title", ""))
if region:
region_events.setdefault(region, []).append(
{"title": item["title"], "severity": "INFO", "event_type": "INTEL"}
)
briefs = []
for region, events in region_events.items():
sev_counts: dict[str, int] = {}
for ev in events:
s = ev.get("severity", "INFO")
sev_counts[s] = sev_counts.get(s, 0) + 1
critical = sev_counts.get("CRITICAL", 0)
high = sev_counts.get("HIGH", 0)
if critical >= 3:
status = "CRITICAL"
elif critical >= 1 or high >= 3:
status = "HIGH"
elif high >= 1:
status = "ELEVATED"
else:
status = "ACTIVE"
top = [e for e in events if e.get("severity") in ("CRITICAL", "HIGH")][:3]
if top:
summary = "; ".join(
e.get("event_type", e.get("title", "")[:40]) for e in top[:2]
)
else:
summary = f"{len(events)} events tracked in region"
briefs.append({
"region": region,
"status": status,
"event_count": len(events),
"critical": critical,
"high": high,
"summary": summary[:120],
})
briefs.sort(key=lambda b: (_WEIGHT_ORDER.get(b["status"], 5), -b["event_count"]))
return briefs[:6]
def _key_drivers(
score: int, reasons: list[str], gps_data: list, bgp_data: dict,
conflicts: list, space_weather: dict, planes: list = None, ships: list = None,
) -> list[dict]:
"""Identify and rank the top threat signal drivers."""
drivers: list[dict] = []
# GPS Interference
gps_count = len(gps_data or [])
if gps_count:
drivers.append({
"signal": "GPS Interference",
"detail": f"{gps_count} active jamming zone{'s' if gps_count > 1 else ''} detected via ADS-B navigation anomalies",
"weight": "CRITICAL" if gps_count >= 3 else "HIGH",
"icon": "",
})
# Military Conflicts
if conflicts:
critical = sum(1 for c in conflicts if c.get("severity") == "CRITICAL")
high = sum(1 for c in conflicts if c.get("severity") == "HIGH")
active_regions = set(
_match_region(c.get("title", "")) for c in conflicts
if _match_region(c.get("title", ""))
)
if critical or high:
drivers.append({
"signal": "Military Activity",
"detail": f"{critical} CRITICAL, {high} HIGH severity events across {len(active_regions)} region{'s' if len(active_regions) != 1 else ''}",
"weight": "CRITICAL" if critical >= 5 else "HIGH" if critical >= 1 else "ELEVATED",
"icon": "",
})
# BGP Routing
bgp_status = str((bgp_data or {}).get("status", "STABLE")).upper()
if bgp_status in ("ELEVATED", "CRITICAL"):
updates = (bgp_data or {}).get("total_updates", 0)
drivers.append({
"signal": "BGP Routing",
"detail": f"Internet routing instability: {updates:,} updates/hour ({bgp_status})",
"weight": bgp_status,
"icon": "📡",
})
# Space Weather
sw = space_weather or {}
kp = sw.get("kp_index", 0)
if isinstance(kp, (int, float)) and kp >= 4:
drivers.append({
"signal": "Space Weather",
"detail": f"Geomagnetic storm Kp={kp:.1f}{sw.get('description', 'HF radio / satellite disruption risk')}",
"weight": "CRITICAL" if kp >= 7 else "HIGH" if kp >= 5 else "ELEVATED",
"icon": "",
})
# News keywords
keyword_drivers = [r for r in reasons if "×" in r]
if keyword_drivers:
top_kw = keyword_drivers[:3]
drivers.append({
"signal": "Intel Keywords",
"detail": f"High-weight terms in news: {', '.join(top_kw)}",
"weight": "ELEVATED" if score >= 8 else "GUARDED",
"icon": "📰",
})
# Military air movement
mil_count = sum(1 for p in (planes or []) if p.get("military"))
total_planes = len(planes or [])
if mil_count >= 5:
drivers.append({
"signal": "Military Air Movement",
"detail": f"{mil_count} military aircraft tracked globally ({total_planes:,} total airframes monitored)",
"weight": "HIGH" if mil_count >= 30 else "ELEVATED" if mil_count >= 15 else "GUARDED",
"icon": "",
})
# Maritime movement
ship_count = len(ships or [])
if ship_count >= 10:
tankers = sum(1 for s in (ships or []) if str(s.get("type", "")).lower() in ("tanker", "lng carrier"))
drivers.append({
"signal": "Maritime Activity",
"detail": f"{ship_count} vessels tracked globally" + (f" ({tankers} tankers near chokepoints)" if tankers >= 5 else ""),
"weight": "ELEVATED" if ship_count >= 100 else "GUARDED",
"icon": "🚢",
})
drivers.sort(key=lambda d: _WEIGHT_ORDER.get(d["weight"], 5))
return drivers[:6]
def _correlations(
gps_data: list, conflicts: list, bgp_data: dict, space_weather: dict,
) -> list[str]:
"""Find cross-domain signal correlations."""
out: list[str] = []
conflict_regions = set(
_match_region(c.get("title", "")) for c in (conflicts or [])
if _match_region(c.get("title", ""))
)
if gps_data and conflicts:
for z in gps_data:
glat, glon = z.get("lat", 0), z.get("lon", 0)
if 35 < glat < 55 and 25 < glon < 45 and "Eastern Europe" in conflict_regions:
out.append(
"GPS jamming in Eastern Europe coincides with active military operations "
"— likely electronic warfare activity"
)
break
for z in gps_data:
glat, glon = z.get("lat", 0), z.get("lon", 0)
if 25 < glat < 40 and 30 < glon < 50 and "Middle East" in conflict_regions:
out.append(
"GPS interference near Middle Eastern conflict zones suggests coordinated "
"electronic warfare alongside kinetic operations"
)
break
bgp_status = str((bgp_data or {}).get("status", "STABLE")).upper()
if bgp_status in ("ELEVATED", "CRITICAL") and conflicts:
out.append(
f"BGP routing instability ({bgp_status}) during active military operations "
"may indicate cyber operations or infrastructure targeting"
)
sw = space_weather or {}
kp = sw.get("kp_index", 0) if isinstance(sw.get("kp_index"), (int, float)) else 0
if kp >= 4 and gps_data:
out.append(
f"Elevated geomagnetic activity (Kp={kp:.1f}) may amplify GPS signal "
"degradation in existing interference zones"
)
if kp >= 5:
out.append(
f"Geomagnetic storm (Kp={kp:.1f}) affecting HF radio propagation — "
"military comms may shift to satellite links"
)
if not out:
out.append("No significant cross-domain signal correlations detected at this time")
return out[:4]
def _watch_items(conflicts: list, gps_data: list, space_weather: dict) -> list[str]:
"""Generate actionable intelligence watch items."""
items: list[str] = []
if conflicts:
critical = [c for c in conflicts if c.get("severity") == "CRITICAL"]
if critical:
regions: dict[str, list] = {}
for c in critical[:15]:
r = _match_region(c.get("title", "")) or "Global"
regions.setdefault(r, []).append(c)
for region, evts in sorted(regions.items(), key=lambda x: -len(x[1])):
items.append(
f"Monitor {region}: {len(evts)} critical event{'s' if len(evts) > 1 else ''} "
f"{evts[0].get('event_type', 'military activity')}"
)
if gps_data:
items.append(
f"Track {len(gps_data)} GPS interference zone{'s' if len(gps_data) > 1 else ''} "
"— assess impact on aviation safety and military navigation"
)
sw = space_weather or {}
if sw.get("alerts"):
items.append("Space weather alerts active — monitor satellite communication reliability")
if not items:
items.append("Maintain standard surveillance posture across all domains")
return items[:5]
def _threat_from_score(score: int) -> str:
if score >= 18:
return "SEVERE"
if score >= 13:
return "HIGH"
if score >= 8:
return "ELEVATED"
if score >= 4:
return "GUARDED"
return "LOW"
def _score_from_signals(news_items, gps_data, bgp_data) -> tuple[int, list[str]]:
score = 0
reasons: list[str] = []
titles = [str(item.get("title", "")).lower() for item in news_items[:20]]
title_blob = " ".join(titles)
for kw, weight in KEYWORD_SCORES.items():
hits = title_blob.count(kw)
if hits:
inc = min(hits, 3) * weight
score += inc
reasons.append(f"{kw}×{hits}")
gps_count = len(gps_data or [])
if gps_count:
bump = min(gps_count, 12)
score += bump
reasons.append(f"gps_anomalies={gps_count}")
bgp_status = str((bgp_data or {}).get("status", "STABLE")).upper()
if bgp_status == "ELEVATED":
score += 3
reasons.append("bgp=elevated")
elif bgp_status == "CRITICAL":
score += 6
reasons.append("bgp=critical")
elif bgp_status == "OFFLINE":
score += 2
reasons.append("bgp=offline")
return score, reasons
def _probability(score: int, offset: int) -> str:
pct = max(20, min(95, 35 + score * 3 + offset))
return f"{pct}%"
def _in_bbox(lat: float, lon: float, bbox: tuple) -> bool:
return bbox[0] <= lat <= bbox[2] and bbox[1] <= lon <= bbox[3]
def _heuristic_predictions(
news_items, gps_data, score: int,
conflicts=None, planes=None, ships=None,
) -> list[dict]:
"""
Generate predicted hotspots by cross-referencing:
- news keyword mentions (intel signal)
- conflict event severity (OSINT signal)
- military aircraft concentrations (movement signal)
- ship density in strategic chokepoints (maritime signal)
- GPS interference zones (EW signal)
"""
titles = [str(item.get("title", "")).lower() for item in (news_items or [])[:30]]
title_blob = " ".join(titles)
planes = planes or []
ships = ships or []
conflicts = conflicts or []
scored: list[tuple[float, str, dict]] = []
for key, region in HOTSPOT_REGIONS.items():
bbox = region["bbox"]
# ── 1. News keyword mentions ──
news_hits = sum(1 for kw in region["keywords"] if kw in title_blob)
news_score = news_hits * 3.0
# ── 2. Conflict event severity ──
conflict_crit = 0
conflict_high = 0
for c in conflicts:
ct = (c.get("title") or "").lower()
if any(kw in ct for kw in region["keywords"]):
sev = c.get("severity", "")
if sev == "CRITICAL":
conflict_crit += 1
elif sev == "HIGH":
conflict_high += 1
conflict_score = conflict_crit * 5.0 + conflict_high * 2.0
# ── 3. Military aircraft in region ──
mil_count = 0
civ_count = 0
for p in planes:
plat = p.get("lat")
plon = p.get("lon")
if isinstance(plat, (int, float)) and isinstance(plon, (int, float)):
if _in_bbox(plat, plon, bbox):
if p.get("military"):
mil_count += 1
else:
civ_count += 1
mil_score = min(mil_count * 2.0, 16.0)
# ── 4. Ship density near strategic chokepoints ──
ship_count = 0
tanker_count = 0
for s in ships:
slat = s.get("lat")
slon = s.get("lon")
if isinstance(slat, (int, float)) and isinstance(slon, (int, float)):
if _in_bbox(slat, slon, bbox):
ship_count += 1
if str(s.get("type", "")).lower() in ("tanker", "lng carrier"):
tanker_count += 1
ship_score = min(ship_count * 0.8 + tanker_count * 1.2, 10.0)
# ── 5. GPS interference overlap ──
gps_overlap = 0
for z in (gps_data or []):
zlat = z.get("lat", 0)
zlon = z.get("lon", 0)
if isinstance(zlat, (int, float)) and isinstance(zlon, (int, float)):
if _in_bbox(zlat, zlon, bbox):
gps_overlap += 1
ew_score = gps_overlap * 4.0
total = news_score + conflict_score + mil_score + ship_score + ew_score
if total < 1.0:
continue
# Build explanation string from active signals
signals = []
if news_hits:
signals.append(f"{news_hits} intel mentions")
if conflict_crit or conflict_high:
parts = []
if conflict_crit:
parts.append(f"{conflict_crit} CRITICAL")
if conflict_high:
parts.append(f"{conflict_high} HIGH")
signals.append(f"{'+'.join(parts)} events")
if mil_count:
signals.append(f"{mil_count} military aircraft tracked")
if ship_count:
desc = f"{ship_count} vessels"
if tanker_count:
desc += f" ({tanker_count} tankers)"
signals.append(desc)
if gps_overlap:
signals.append(f"{gps_overlap} GPS jamming zone{'s' if gps_overlap > 1 else ''}")
event = region["baseline"]
if signals:
event += "" + ", ".join(signals[:3])
prob_pct = max(25, min(95, int(30 + total * 2.5 + score * 1.5)))
# Build AI-style reasoning text from active signals
reason_parts = []
if news_hits:
matching_kws = [kw for kw in region["keywords"] if kw in title_blob][:3]
reason_parts.append(
f"Multiple intelligence feeds ({news_hits}) reference "
f"{', '.join(matching_kws) if matching_kws else 'this region'}, "
f"indicating heightened international attention."
)
if conflict_crit:
reason_parts.append(
f"{conflict_crit} CRITICAL-severity conflict event{'s' if conflict_crit > 1 else ''} "
f"detected via OSINT, suggesting active kinetic operations or imminent escalation."
)
if conflict_high:
reason_parts.append(
f"{conflict_high} HIGH-severity event{'s' if conflict_high > 1 else ''} "
f"reported in the area, indicating elevated security posture."
)
if mil_count:
reason_parts.append(
f"ADS-B tracking shows {mil_count} military aircraft operating within the region"
f"{f' alongside {civ_count} civilian flights' if civ_count > 20 else ''}, "
f"{'which represents unusual concentration' if mil_count >= 5 else 'consistent with ongoing monitoring'}."
)
if ship_count:
s_detail = f"{ship_count} vessels tracked"
if tanker_count:
s_detail += f" including {tanker_count} tanker{'s' if tanker_count > 1 else ''}"
reason_parts.append(
f"Maritime surveillance identifies {s_detail} in strategic waters, "
f"{'raising chokepoint disruption concerns' if tanker_count >= 2 else 'indicating normal commerce flow'}."
)
if gps_overlap:
reason_parts.append(
f"Active GPS jamming/spoofing detected ({gps_overlap} zone{'s' if gps_overlap > 1 else ''}), "
f"a strong indicator of electronic warfare activity in the area."
)
if not reason_parts:
reason_parts.append(region["baseline"])
reason = " ".join(reason_parts)
scored.append((total, key, {
"location": region["name"],
"lat": region["lat"],
"lon": region["lon"],
"event": event[:200],
"reason": reason[:500],
"probability": f"{prob_pct}%",
"_signals": {
"news": news_hits, "conflicts": conflict_crit + conflict_high,
"mil_planes": mil_count, "ships": ship_count, "gps": gps_overlap,
},
}))
scored.sort(key=lambda x: x[0], reverse=True)
predictions = []
for _, _, item in scored[:4]:
# Strip internal fields
item.pop("_signals", None)
predictions.append(item)
# Fallback if nothing ranked
if not predictions:
predictions.append({
"location": "Global Monitoring",
"lat": 20.0, "lon": 10.0,
"event": "No dominant hotspot; maintain broad surveillance",
"reason": "Current intelligence signals do not indicate a concentrated threat in any single region. Continuing broad-spectrum monitoring across all sensor feeds.",
"probability": _probability(score, 0),
})
return predictions
def _sanitize_prediction(item: dict, fallback_prob: str) -> dict | None:
if not isinstance(item, dict):
return None
lat = item.get("lat")
lon = item.get("lon")
try:
lat = float(lat)
lon = float(lon)
except Exception:
return None
if not (-90 <= lat <= 90 and -180 <= lon <= 180):
return None
location = str(item.get("location", "Unknown Location")).strip()[:80]
event = str(item.get("event", "Potential activity shift")).strip()[:180]
prob = str(item.get("probability", fallback_prob)).strip()[:8]
if not re.match(r"^\d{1,3}%$", prob):
prob = fallback_prob
return {
"location": location or "Unknown Location",
"lat": lat,
"lon": lon,
"event": event or "Potential activity shift",
"reason": str(item.get("reason", "")).strip()[:500],
"probability": prob,
}
def _sanitize_llm_output(raw: dict, fallback_level: str, fallback_predictions: list[dict]) -> dict:
summary = str(raw.get("summary", "")).strip()
if not summary:
summary = "AI produced no summary; heuristic assessment applied."
summary = summary[:220]
level = str(raw.get("threat_level", "")).upper().strip()
if level not in THREAT_LEVELS:
level = fallback_level
# Build lookup of heuristic reasons by location name for fallback
heuristic_reasons: dict[str, str] = {}
for fp in fallback_predictions:
loc = str(fp.get("location", "")).lower().strip()
if loc and fp.get("reason"):
heuristic_reasons[loc] = fp["reason"]
clean_predictions: list[dict] = []
for p in raw.get("predictions", [])[:4]:
cleaned = _sanitize_prediction(p, fallback_predictions[0]["probability"])
if cleaned:
# If LLM didn't provide a reason, try to merge from heuristic
if not cleaned.get("reason"):
loc_key = str(cleaned.get("location", "")).lower().strip()
cleaned["reason"] = heuristic_reasons.get(loc_key, "")
clean_predictions.append(cleaned)
if len(clean_predictions) >= 4:
break
if len(clean_predictions) < 2:
clean_predictions = fallback_predictions
return {
"summary": summary,
"threat_level": level,
"predictions": clean_predictions,
}
async def _query_ollama(prompt: str) -> dict | None:
async with httpx.AsyncClient() as client:
response = await client.post(
OLLAMA_URL,
json={
"model": MODEL_NAME,
"prompt": prompt,
"stream": False,
"format": "json",
},
timeout=20.0,
)
if response.status_code != 200:
return None
payload = response.json()
raw = payload.get("response", "")
if not raw:
return None
try:
parsed = json.loads(raw)
return parsed if isinstance(parsed, dict) else None
except Exception:
return None
async def analyze_threat(
news_items,
gps_data=None,
bgp_data=None,
conflicts=None,
space_weather=None,
ships=None,
planes=None,
):
if not news_items and not conflicts:
return {
"summary": "Awaiting intelligence data...",
"threat_level": "LOW",
"predictions": [],
"key_drivers": [],
"regional_briefs": [],
"correlations": [],
"watch_items": ["Maintain standard surveillance posture across all domains"],
"source": "heuristic",
"generated_at": datetime.now(timezone.utc).isoformat(),
}
score, reasons = _score_from_signals(news_items, gps_data or [], bgp_data or {})
# Boost score from conflicts
if conflicts:
crit = sum(1 for c in conflicts if c.get("severity") == "CRITICAL")
high = sum(1 for c in conflicts if c.get("severity") == "HIGH")
conflict_bump = min(crit * 2 + high, 10)
score += conflict_bump
if conflict_bump:
reasons.append(f"conflicts={crit}C/{high}H")
base_level = _threat_from_score(score)
base_predictions = _heuristic_predictions(
news_items, gps_data or [], score,
conflicts=conflicts or [],
planes=planes or [],
ships=ships or [],
)
# Build enhanced intel sections
regional = _regional_briefs(news_items, conflicts or [])
drivers = _key_drivers(score, reasons, gps_data or [], bgp_data or {}, conflicts or [], space_weather or {}, planes=planes, ships=ships)
corr = _correlations(gps_data or [], conflicts or [], bgp_data or {}, space_weather or {})
watch = _watch_items(conflicts or [], gps_data or [], space_weather or {})
# Build movement summary for LLM prompt
mil_planes_total = sum(1 for p in (planes or []) if p.get("military"))
ship_total = len(ships or [])
movement_ctx = (
f"Military aircraft tracked: {mil_planes_total}. "
f"Vessels tracked: {ship_total}. "
)
# Summarize heuristic predictions for LLM context
hotspot_ctx = " | ".join(
f"{p['location']}({p['probability']}): {p['event'][:60]}"
for p in base_predictions[:3]
)
headlines = " | ".join(str(item.get('title', ''))[:80] for item in news_items[:6])
prompt = (
f"STRATCOM AI. Strict JSON only, no markdown.\n"
f"News: {headlines}\n"
f"Signals: GPS={len(gps_data or [])}, BGP={(bgp_data or {}).get('status','?')}, {movement_ctx.strip()}\n"
f"Hotspots: {hotspot_ctx}\n"
f'Return: {{"summary":"<1 sentence>","threat_level":"LOW|GUARDED|ELEVATED|HIGH|SEVERE",'
f'"predictions":[{{"location":"","lat":0,"lon":0,"event":"","probability":"NN%","reason":""}}]}}'
)
try:
if _CLI_BACKEND != "ollama":
llm_raw = await _query_cli(prompt)
source_name = _CLI_BACKEND
else:
llm_raw = await _query_ollama(prompt)
source_name = MODEL_NAME
if llm_raw:
sanitized = _sanitize_llm_output(llm_raw, base_level, base_predictions)
sanitized["source"] = source_name
sanitized["generated_at"] = datetime.now(timezone.utc).isoformat()
sanitized["key_drivers"] = drivers
sanitized["regional_briefs"] = regional
sanitized["correlations"] = corr
sanitized["watch_items"] = watch
return sanitized
except Exception as exc:
print(f"[AI] LLM path failed ({_CLI_BACKEND}), fallback engaged: {exc}")
summary = f"{base_level} risk posture based on {len(news_items)} intelligence items"
if conflicts:
summary += f" + {len(conflicts)} military events"
if reasons:
summary += f"; key drivers: {', '.join(reasons[:4])}."
else:
summary += "."
return {
"summary": summary[:220],
"threat_level": base_level,
"predictions": base_predictions,
"key_drivers": drivers,
"regional_briefs": regional,
"correlations": corr,
"watch_items": watch,
"source": "heuristic",
"generated_at": datetime.now(timezone.utc).isoformat(),
}
if __name__ == "__main__":
TEST = [{"title": "Missile strike reported near border region"}, {"title": "Cyberattack impacts telecom routing"}]
print(asyncio.run(analyze_threat(TEST, gps_data=[{"lat": 32.1, "lon": 35.0}], bgp_data={"status": "ELEVATED"})))
+177
View File
@@ -0,0 +1,177 @@
import asyncio
import gzip
import json
import websockets
import httpx
import os
from datetime import datetime, timezone
from dotenv import load_dotenv
load_dotenv()
AIS_STREAM_URL = "wss://stream.aisstream.io/v1/stream"
API_KEY = os.getenv("AIS_STREAM_KEY", "185a6c61223ce81ed70cec58eab359009ebd5a0e")
_real_vessel_cache: dict = {}
_real_vessel_lock = asyncio.Lock()
def get_vessel_category(ship_type):
if not ship_type: return "Other"
if 70 <= ship_type <= 79: return "Cargo"
if 80 <= ship_type <= 89: return "Tanker"
if 60 <= ship_type <= 69: return "Passenger"
if ship_type == 35: return "Military"
if 30 <= ship_type <= 34: return "Fishing"
return "Other"
async def ais_worker():
if not API_KEY:
print("[MARITIME] Missing API Key.")
return
print("[MARITIME] Starting Global AIS Stream...")
while True:
try:
async with websockets.connect(AIS_STREAM_URL, ping_interval=20, ping_timeout=20) as ws:
await ws.send(json.dumps({
"APIKey": API_KEY,
"BoundingBoxes": [[[-90, -180], [90, 180]]],
"FilterMessageTypes": ["PositionReport"]
}))
msg_count = 0
async for message in ws:
msg = json.loads(message)
meta = msg.get("MetaData", {})
mmsi = meta.get("MMSI")
if not mmsi:
continue
async with _real_vessel_lock:
mid = str(mmsi)
if mid not in _real_vessel_cache:
_real_vessel_cache[mid] = {"id": mid, "source": "aisstream"}
v = _real_vessel_cache[mid]
v["last_seen"] = datetime.now(timezone.utc)
name = meta.get("ShipName", "").strip()
if name:
v["name"] = name
elif "name" not in v:
v["name"] = f"MMSI-{mmsi}"
ship_type = meta.get("ShipType", 0)
v["type"] = get_vessel_category(ship_type)
if msg["MessageType"] == "PositionReport":
pos = msg["Message"]["PositionReport"]
lat = pos.get("Latitude")
lon = pos.get("Longitude")
# Filter invalid coordinates (land-based noise, 0/0, out of range)
if (lat is None or lon is None
or abs(lat) > 90 or abs(lon) > 180
or (abs(lat) < 0.1 and abs(lon) < 0.1)):
continue
v["lat"] = round(lat, 5)
v["lon"] = round(lon, 5)
v["velocity"] = round(pos.get("Sog", 0) * 0.5144, 2)
v["heading"] = pos.get("Cog", 0)
msg_count += 1
if msg_count % 500 == 0:
print(f"[MARITIME] {len(_real_vessel_cache)} vessels cached")
except Exception as e:
print(f"[MARITIME] Stream error: {e}. Reconnecting in 10s...")
await asyncio.sleep(10)
async def ais_pruner():
while True:
await asyncio.sleep(60)
now = datetime.now(timezone.utc)
async with _real_vessel_lock:
stale = [k for k, v in _real_vessel_cache.items()
if (now - v["last_seen"]).total_seconds() > 1200]
for k in stale:
del _real_vessel_cache[k]
async def _fetch_digitraffic() -> list:
"""
Digitraffic AIS — free, no key, Baltic/European coverage.
Uses `from` parameter to get only vessels seen in the last 20 minutes.
Typically returns 300700 active vessels with fresh positions.
"""
import time
try:
from_ms = int((time.time() - 1200) * 1000) # last 20 minutes
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"Digitraffic-User": "GodsEye/3.0",
}
async with httpx.AsyncClient(timeout=15.0) as client:
r = await client.get(
f"https://meri.digitraffic.fi/api/ais/v1/locations?from={from_ms}",
headers=headers,
)
if r.status_code != 200:
print(f"[MARITIME] Digitraffic {r.status_code}")
return []
raw = r.content
try:
data = json.loads(gzip.decompress(raw))
except Exception:
data = r.json()
features = data.get("features", []) if isinstance(data, dict) else []
ships = []
for item in features:
geo = (item.get("geometry") or {}).get("coordinates", [])
if len(geo) < 2:
continue
lon, lat = geo[0], geo[1]
if abs(lat) > 90 or abs(lon) > 180 or (abs(lat) < 0.01 and abs(lon) < 0.01):
continue
props = item.get("properties", {})
mmsi = str(item.get("mmsi") or props.get("mmsi", ""))
sog = props.get("sog", 0) or 0
heading = props.get("heading", 0) or props.get("cog", 0) or 0
ships.append({
"id": f"dt_{mmsi}",
"name": f"MMSI-{mmsi}",
"lat": round(lat, 5),
"lon": round(lon, 5),
"velocity": round(sog * 0.5144, 2),
"heading": heading,
"type": "Cargo",
"source": "digitraffic",
})
print(f"[MARITIME] Digitraffic: {len(ships)} vessels (last 20min)")
return ships
except Exception as e:
print(f"[MARITIME] Digitraffic error: {e}")
return []
_dt_cache: list = []
_dt_cache_time: float = 0.0
_DT_TTL = 300.0 # refresh Digitraffic every 5 minutes
async def fetch_ships() -> list:
"""Merge AISStream (global, real-time) + Digitraffic (Baltic, 5-min cache)."""
import time
global _dt_cache, _dt_cache_time
# Refresh Digitraffic cache if stale
if time.monotonic() - _dt_cache_time > _DT_TTL:
_dt_cache = await _fetch_digitraffic()
_dt_cache_time = time.monotonic()
async with _real_vessel_lock:
stream_ships = [
v for v in _real_vessel_cache.values()
if v.get("lat") is not None and v.get("lon") is not None
]
# Merge: AISStream takes priority, Digitraffic fills in the rest
if stream_ships:
stream_ids = {s["id"] for s in stream_ships}
dt_extra = [s for s in _dt_cache if s["id"] not in stream_ids]
return stream_ships + dt_extra
return _dt_cache
+71
View File
@@ -0,0 +1,71 @@
import httpx
import asyncio
from datetime import datetime, timezone
# RIPE Stat - free API, no key required
# Fetch BGP update activity (withdrawals/announcements) for a given time window
RIPE_UPDATES_URL = "https://stat.ripe.net/data/bgp-updates/data.json"
RIPE_ROUTING_URL = "https://stat.ripe.net/data/routing-status/data.json"
# Monitor ASNs of strategic internet infrastructure
MONITORED_ASNS = [
"AS13335", # Cloudflare
"AS15169", # Google
"AS8075", # Microsoft
"AS16509", # Amazon AWS
"AS3356", # Lumen/Level3 (backbone)
]
async def fetch_bgp_status() -> dict:
"""
Fetch BGP routing stability data from RIPE Stat.
Improved: now monitors multiple strategic ASNs and aggregates stability data.
"""
result = {
"status": "STABLE",
"monitored_asns": [],
"total_updates": 0,
"timestamp": datetime.now(timezone.utc).isoformat()
}
try:
async with httpx.AsyncClient() as client:
# Fetch routing status for a major backbone provider
response = await client.get(
RIPE_ROUTING_URL,
params={"resource": "1.1.1.1"},
timeout=10.0
)
if response.status_code == 200:
data = response.json().get("data", {})
result["routing_data"] = {
"prefixes_originated": data.get("prefixes_originated", []),
"visibility": data.get("visibility", {}),
"resource": data.get("resource", "1.1.1.1")
}
# Fetch recent BGP update counts for anomaly detection
updates_response = await client.get(
RIPE_UPDATES_URL,
params={"resource": "0.0.0.0/0", "hours": 1},
timeout=10.0
)
if updates_response.status_code == 200:
updates_data = updates_response.json().get("data", {})
nr_updates = updates_data.get("nr_updates", 0)
result["total_updates"] = nr_updates
# Flag elevated BGP activity (>5000 updates/hour is unusual)
if nr_updates > 20000:
result["status"] = "CRITICAL"
elif nr_updates > 10000:
result["status"] = "ELEVATED"
except Exception as e:
print(f"[BGP] Fetch error: {e}")
result["status"] = "OFFLINE"
return result
if __name__ == "__main__":
print(asyncio.run(fetch_bgp_status()))
+244
View File
@@ -0,0 +1,244 @@
import httpx
import asyncio
import re
from datetime import datetime, timezone
from difflib import SequenceMatcher
# GDELT DOC 2.0 API — free, no key
# Focused specifically on military attacks and kinetic events
GDELT_URL = (
"https://api.gdeltproject.org/api/v2/doc/doc?"
"query=(missile attack OR rocket attack OR airstrike OR air strike OR "
"bombing OR drone strike OR artillery fire OR shelling OR warship attack OR "
"naval attack OR military strike OR armed attack OR mortar OR explosion site OR "
"IED explosion OR sniper OR combat OR military offensive OR invasion force)"
"&mode=artlist&format=json&maxrecords=75&sourcelang=english&timespan=12h"
)
# Geo-coder: keyword → (lat, lon)
GEO_DATA = {
# Ukraine conflict zones
"Ukraine": [48.3794, 31.1656], "Crimea": [44.9521, 34.1024],
"Donbas": [48.0159, 37.8028], "Donbass": [48.0159, 37.8028],
"Kherson": [46.6354, 32.6169], "Zaporizhzhia": [47.8388, 35.1396],
"Kharkiv": [49.9935, 36.2304], "Kyiv": [50.4501, 30.5234],
"Kiev": [50.4501, 30.5234], "Odesa": [46.4825, 30.7233],
"Mariupol": [47.0958, 37.5483], "Bakhmut": [48.5963, 38.0000],
"Avdiivka": [48.1344, 37.7490],
# Russia
"Russia": [61.5240, 105.3188], "Moscow": [55.7558, 37.6173],
"Belarus": [53.7098, 27.9534],
# Middle East
"Israel": [31.0461, 34.8516], "Gaza": [31.3547, 34.3088],
"West Bank": [31.9466, 35.3027], "Rafah": [31.2969, 34.2455],
"Jenin": [32.4607, 35.3027], "Lebanon": [33.8547, 35.8623],
"Hezbollah": [33.8547, 35.8623], "Syria": [34.8021, 38.9968],
"Damascus": [33.5138, 36.2765], "Aleppo": [36.2021, 37.1343],
"Iran": [32.4279, 53.6880], "Tehran": [35.6892, 51.3890],
"Iraq": [33.2232, 43.6793], "Baghdad": [33.3152, 44.3661],
"Yemen": [15.5527, 48.5164], "Houthi": [15.5527, 48.5164],
"Saudi Arabia": [23.8859, 45.0792],
# Asia-Pacific
"China": [35.8617, 104.1954], "Taiwan": [23.6978, 120.9605],
"Taipei": [25.0330, 121.5654], "Taiwan Strait": [24.0000, 119.5000],
"North Korea": [40.3399, 127.5101], "Pyongyang": [39.0194, 125.7381],
"South Korea": [35.9078, 127.7669], "Japan": [36.2048, 138.2529],
"Philippines": [12.8797, 121.7740], "South China Sea": [12.0000, 113.0000],
"Myanmar": [21.9162, 95.9560], "India": [20.5937, 78.9629],
"Pakistan": [30.3753, 69.3451], "Afghanistan": [33.9391, 67.7100],
"Kashmir": [34.0837, 74.7973],
# Africa
"Sudan": [12.8628, 30.2176], "Ethiopia": [9.1450, 40.4897],
"Somalia": [5.1521, 46.1996], "Libya": [26.3351, 17.2283],
"Mali": [17.5707, -3.9962], "Niger": [17.6078, 8.0817],
"Nigeria": [9.0820, 8.6753], "Congo": [-4.0383, 21.7587],
"Sahel": [15.4542, 0.0000], "Burkina Faso": [12.2383, -1.5616],
"Mozambique": [-18.6657, 35.5296],
# Americas
"Venezuela": [6.4238, -66.5897], "Colombia": [4.5709, -74.2973],
"Mexico": [23.6345, -102.5528],
# Strategic waterways
"Red Sea": [20.0000, 38.0000], "Strait of Hormuz": [26.5667, 56.2500],
"Bab-el-Mandeb": [12.6, 43.5], "Suez Canal": [30.4550, 32.3500],
"Black Sea": [43.4000, 34.0000], "Baltic Sea": [58.0000, 20.0000],
"East China Sea": [30.0000, 126.0000], "Sea of Azov": [46.0000, 36.5000],
"Persian Gulf": [26.0000, 52.0000],
# Palestine
"Palestine": [31.9522, 35.2332], "Jerusalem": [31.7683, 35.2137],
"Tel Aviv": [32.0853, 34.7818], "Nablus": [32.2211, 35.2544],
}
# Attack type classifier — ordered by priority (most specific first)
# Each entry: (search_term, event_label, severity)
ATTACK_CLASSIFIER = [
("ballistic missile", "BALLISTIC MISSILE", "CRITICAL"),
("cruise missile", "CRUISE MISSILE", "CRITICAL"),
("hypersonic missile", "HYPERSONIC MISSILE", "CRITICAL"),
("missile strike", "MISSILE STRIKE", "CRITICAL"),
("missile attack", "MISSILE ATTACK", "CRITICAL"),
("rocket attack", "ROCKET ATTACK", "CRITICAL"),
("rocket barrage", "ROCKET BARRAGE", "CRITICAL"),
("drone strike", "DRONE STRIKE", "CRITICAL"),
("drone attack", "DRONE ATTACK", "CRITICAL"),
("airstrike", "AIRSTRIKE", "CRITICAL"),
("air strike", "AIRSTRIKE", "CRITICAL"),
("air raid", "AIR RAID", "CRITICAL"),
("bombing", "BOMBING", "CRITICAL"),
("bomb", "EXPLOSION", "HIGH"),
("shelling", "ARTILLERY SHELLING", "HIGH"),
("artillery", "ARTILLERY FIRE", "HIGH"),
("mortar", "MORTAR ATTACK", "HIGH"),
("ied explosion", "IED EXPLOSION", "HIGH"),
("explosion", "EXPLOSION", "HIGH"),
("sniper", "SNIPER ACTIVITY", "MODERATE"),
("naval attack", "NAVAL ATTACK", "HIGH"),
("warship", "NAVAL ACTIVITY", "MODERATE"),
("invasion", "INVASION", "CRITICAL"),
("offensive", "MILITARY OFFENSIVE", "HIGH"),
("combat", "COMBAT", "HIGH"),
("attack", "ATTACK", "HIGH"),
("strike", "STRIKE", "HIGH"),
("troops", "GROUND FORCES", "MODERATE"),
("military", "MILITARY ACTIVITY", "MODERATE"),
]
SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MODERATE": 2}
EXCLUDE_KEYWORDS = [
"sport", "football", "soccer", "la liga", "cup", "olympics",
"tennis", "nfl", "nba", "golf", "cricket", "rugby",
"celebrity", "fashion", "movie", "film", "oscars", "grammy",
"recipe", "horoscope", "box office", "netflix",
"rocket launch", "spacex", "starship", "nasa launch", # civilian launches
]
def classify_event(title: str) -> tuple[str, str]:
"""Returns (event_label, severity) for a conflict title."""
tl = title.lower()
for term, label, severity in ATTACK_CLASSIFIER:
if term in tl:
return label, severity
return "MILITARY EVENT", "MODERATE"
def _title_similarity(a: str, b: str) -> float:
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
def _parse_gdelt_date(seendate: str) -> str:
try:
dt = datetime.strptime(seendate, "%Y%m%dT%H%M%SZ")
return dt.replace(tzinfo=timezone.utc).isoformat()
except Exception:
return datetime.now(timezone.utc).isoformat()
async def fetch_conflicts() -> list:
"""
Fetch military attack/conflict events from GDELT DOC API.
Returns events classified by attack type and severity.
"""
try:
async with httpx.AsyncClient() as client:
# Retry with backoff for 429 rate limits
for attempt in range(3):
if attempt > 0:
await asyncio.sleep(6 * attempt)
response = await client.get(GDELT_URL, timeout=12.0)
if response.status_code == 429:
print(f"[CONFLICTS] GDELT rate limited, retry {attempt+1}/3")
continue
break
if response.status_code != 200:
print(f"[CONFLICTS] GDELT returned {response.status_code}")
return []
try:
data = response.json() if response.text.strip() else {}
except Exception:
print(f"[CONFLICTS] GDELT returned non-JSON body")
return []
raw_articles = data.get("articles", [])
if not raw_articles:
print("[CONFLICTS] No articles in GDELT response")
return []
events = []
for article in raw_articles:
title = (article.get("title") or "").strip()
if not title:
continue
tl = title.lower()
# Filter out non-conflict content
if any(kw in tl for kw in EXCLUDE_KEYWORDS):
continue
# Geocode
lat, lon = None, None
for region, coords in GEO_DATA.items():
if re.search(r'\b' + re.escape(region) + r'\b', title, re.IGNORECASE):
lat, lon = coords
break
if lat is None or lon is None:
continue
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
continue
# Deduplicate by title similarity
is_duplicate = any(
_title_similarity(title, ev["title"]) > 0.75
for ev in events
)
if is_duplicate:
continue
event_label, severity = classify_event(title)
seendate = article.get("seendate", "")
published = _parse_gdelt_date(seendate) if seendate else datetime.now(timezone.utc).isoformat()
events.append({
"title": title,
"url": article.get("url", ""),
"image": article.get("urlMobileImage", ""),
"source": article.get("domain", "Unknown"),
"domain": article.get("sourcecountry", "Unknown"),
"lat": lat,
"lon": lon,
"published_at": published,
"event_type": event_label,
"severity": severity,
"type": "conflict",
})
# Sort: CRITICAL first, then by date
events.sort(key=lambda e: (
SEVERITY_ORDER.get(e["severity"], 3),
e["published_at"]
), reverse=False)
events.sort(key=lambda e: SEVERITY_ORDER.get(e["severity"], 3))
events = events[:40]
by_sev = {}
for e in events:
s = e["severity"]
by_sev[s] = by_sev.get(s, 0) + 1
print(f"[CONFLICTS] {len(events)} military events — {by_sev} (from {len(raw_articles)} raw)")
return events
except Exception as e:
print(f"[CONFLICTS] Fetch error: {e}")
return []
if __name__ == "__main__":
result = asyncio.run(fetch_conflicts())
print(f"\nMilitary events: {len(result)}")
for ev in result[:15]:
print(f" [{ev['severity']:8s}] [{ev['event_type']:22s}] {ev['title'][:70]}")
+124
View File
@@ -0,0 +1,124 @@
import httpx
import asyncio
import random
import json
from pathlib import Path
from datetime import datetime, timezone
# Abuse.ch ThreatFox API - Real-time indicators of compromise
THREATFOX_URL = "https://threatfox-api.abuse.ch/api/v1/"
CACHE_FILE = Path(__file__).parent.parent / ".cache" / "cyber.json"
CACHE_DURATION_SEC = 300 # 5 minutes
def _get_cached_cyber():
if CACHE_FILE.exists() and (datetime.now().timestamp() - CACHE_FILE.stat().st_mtime) < CACHE_DURATION_SEC:
try:
with open(CACHE_FILE, "r") as f:
return json.load(f)
except: pass
return None
def _save_cache(data):
CACHE_FILE.parent.mkdir(exist_ok=True, parents=True)
try:
with open(CACHE_FILE, "w") as f:
json.dump(data, f)
except: pass
# Fallback geocoding for major infrastructure if IP geocoding fails or to show targets
CYBER_TARGETS = [
{"name": "AWS US-East", "lat": 39.04, "lon": -77.48},
{"name": "Google Cloud Europe", "lat": 50.11, "lon": 8.68},
{"name": "Azure East Asia", "lat": 22.28, "lon": 114.17},
{"name": "DE-CIX Frankfurt", "lat": 50.12, "lon": 8.67},
{"name": "London Internet Exchange", "lat": 51.51, "lon": -0.12},
{"name": "Equinix Ashburn", "lat": 39.03, "lon": -77.45},
]
async def _get_ip_geo(client, ip):
"""Real-time geocoding for malicious IPs using ip-api.com (free for non-commercial)."""
try:
# Rate limit is 45 requests per minute
resp = await client.get(f"http://ip-api.com/json/{ip}?fields=status,lat,lon,country", timeout=2.0)
if resp.status_code == 200:
data = resp.json()
if data.get("status") == "success":
return data.get("lat"), data.get("lon"), data.get("country")
except: pass
return None, None, None
async def fetch_cyber_warfare(bgp_data=None):
"""
Fetches REAL cyber threat data from Abuse.ch ThreatFox.
Geocodes the malicious sources and maps them to critical infrastructure targets.
"""
cached = _get_cached_cyber()
if cached is not None:
return cached
try:
async with httpx.AsyncClient() as client:
# Query latest 20 malware/botnet indicators
payload = {"query": "get_iocs", "days": 1}
resp = await client.post(THREATFOX_URL, json=payload, timeout=10.0)
if resp.status_code != 200:
if CACHE_FILE.exists():
try:
with open(CACHE_FILE, "r") as f: return json.load(f)
except: pass
return []
data = resp.json()
if data.get("query_status") != "ok":
if CACHE_FILE.exists():
try:
with open(CACHE_FILE, "r") as f: return json.load(f)
except: pass
return []
iocs = data.get("data", [])
# Filter for IP addresses (IPv4)
ip_iocs = [i for i in iocs if i.get("threat_type") in ["botnet_cc", "payload_delivery"] and ":" not in i.get("ioc", "")]
attacks = []
# Geocode only a subset to respect ip-api limits
sample_size = min(len(ip_iocs), 8)
sampled = random.sample(ip_iocs, sample_size) if len(ip_iocs) > sample_size else ip_iocs
for ioc in sampled:
ip = ioc["ioc"].split(":")[0]
lat, lon, country = await _get_ip_geo(client, ip)
if lat and lon:
target = random.choice(CYBER_TARGETS)
intensity = random.uniform(0.4, 1.0)
attacks.append({
"id": f"real_cyb_{ioc['id']}",
"source_name": f"{ioc['threat_type'].upper()} ({country or 'Unknown'})",
"target_name": target["name"],
"startLat": lat,
"startLng": lon,
"endLat": target["lat"],
"endLng": target["lon"],
"type": ioc["malware_printable"] or "Unknown Malware",
"intensity": intensity,
"color": "#ff003c" if "botnet" in ioc["threat_type"] else "#ff8800",
"timestamp": ioc["first_seen"]
})
_save_cache(attacks)
return attacks
except Exception as e:
print(f"[CYBER] Real data fetch error: {e}")
if CACHE_FILE.exists():
try:
with open(CACHE_FILE, "r") as f: return json.load(f)
except: pass
return []
if __name__ == "__main__":
print(asyncio.run(fetch_cyber_warfare()))
+91
View File
@@ -0,0 +1,91 @@
import httpx
import asyncio
from datetime import datetime, timezone
# USGS Earthquake Hazards Program - completely free, no API key
# Significant earthquakes (M 4.5+) from the past 7 days
USGS_URL = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson"
async def fetch_earthquakes() -> list:
"""
Fetches real earthquake data from USGS.
Returns quakes with M >= 4.5 from the past week.
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(USGS_URL, timeout=12.0)
if response.status_code != 200:
print(f"[SEISMIC] USGS returned {response.status_code}")
return []
data = response.json()
quakes = []
for feature in data.get("features", []):
props = feature.get("properties", {})
geom = feature.get("geometry", {})
coords = geom.get("coordinates", [])
if len(coords) < 2:
continue
lon, lat = coords[0], coords[1]
depth_km = coords[2] if len(coords) > 2 else 0
mag = props.get("mag")
if mag is None:
continue
# Validate coordinates
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
continue
# Severity classification
if mag >= 7.0:
severity = "MAJOR"
elif mag >= 6.0:
severity = "STRONG"
elif mag >= 5.0:
severity = "MODERATE"
else:
severity = "MINOR"
# Convert USGS epoch ms to ISO string
epoch_ms = props.get("time", 0)
try:
dt = datetime.fromtimestamp(epoch_ms / 1000, tz=timezone.utc)
time_str = dt.strftime("%Y-%m-%d %H:%M UTC")
except Exception:
time_str = "Unknown"
quakes.append({
"id": feature.get("id", ""),
"title": props.get("title", "Earthquake"),
"place": props.get("place", "Unknown Location"),
"lat": lat,
"lon": lon,
"depth_km": round(depth_km, 1),
"magnitude": mag,
"severity": severity,
"time": time_str,
"url": props.get("url", ""),
"type": "earthquake",
"felt": props.get("felt", 0),
"tsunami": props.get("tsunami", 0),
})
# Sort by magnitude descending
quakes.sort(key=lambda q: q["magnitude"], reverse=True)
print(f"[SEISMIC] {len(quakes)} earthquakes (M≥4.5) — strongest: M{quakes[0]['magnitude'] if quakes else 'N/A'}")
return quakes
except Exception as e:
print(f"[SEISMIC] Fetch error: {e}")
return []
if __name__ == "__main__":
result = asyncio.run(fetch_earthquakes())
print(f"Earthquakes: {len(result)}")
for q in result[:5]:
print(f" M{q['magnitude']} {q['severity']}{q['place']}")
+100
View File
@@ -0,0 +1,100 @@
import httpx
import asyncio
from datetime import datetime, timezone
# Launch Library 2 API — completely free, no API key required
UPCOMING_URL = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/?limit=25&format=json"
RECENT_URL = "https://ll.thespacedevs.com/2.2.0/launch/previous/?limit=10&format=json"
def _parse_launch(item: dict) -> dict | None:
"""Parse a single launch object from the LL2 API response."""
pad = item.get("pad") or {}
location = pad.get("location") or {}
try:
lat = float(pad.get("latitude", ""))
lon = float(pad.get("longitude", ""))
except (ValueError, TypeError):
return None
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
return None
provider = (item.get("launch_service_provider") or {}).get("name", "Unknown")
rocket = ((item.get("rocket") or {}).get("configuration") or {}).get("name", "Unknown")
mission = item.get("mission") or {}
status = (item.get("status") or {}).get("name", "Unknown")
return {
"id": item.get("id", ""),
"name": item.get("name", "Unknown Launch"),
"status": status,
"net": item.get("net", ""),
"provider": provider,
"country": location.get("country_code", ""),
"pad": pad.get("name", "Unknown Pad"),
"lat": lat,
"lon": lon,
"image": item.get("image", ""),
"rocket": rocket,
"mission": mission.get("name") or "",
"mission_description": mission.get("description") or "",
"type": "launch",
}
async def fetch_launches() -> list:
"""
Fetches real rocket launch data from Launch Library 2.
Returns combined upcoming + recent launches, sorted by date.
"""
try:
async with httpx.AsyncClient() as client:
upcoming_resp, recent_resp = await asyncio.gather(
client.get(UPCOMING_URL, timeout=15.0),
client.get(RECENT_URL, timeout=15.0),
)
upcoming = []
if upcoming_resp.status_code == 200:
for item in upcoming_resp.json().get("results", []):
parsed = _parse_launch(item)
if parsed:
upcoming.append(parsed)
else:
print(f"[LAUNCHES] Upcoming API returned {upcoming_resp.status_code}")
recent = []
if recent_resp.status_code == 200:
for item in recent_resp.json().get("results", []):
parsed = _parse_launch(item)
if parsed:
recent.append(parsed)
else:
print(f"[LAUNCHES] Recent API returned {recent_resp.status_code}")
combined = upcoming + recent
# Sort by NET date (earliest first), unknown dates go last
def sort_key(launch):
try:
return datetime.fromisoformat(launch["net"].replace("Z", "+00:00"))
except Exception:
return datetime.max.replace(tzinfo=timezone.utc)
combined.sort(key=sort_key)
print(f"[LAUNCHES] {len(upcoming)} upcoming, {len(recent)} recent launches")
return combined
except Exception as e:
print(f"[LAUNCHES] Fetch error: {e}")
return []
if __name__ == "__main__":
result = asyncio.run(fetch_launches())
print(f"Total launches: {len(result)}")
for launch in result[:5]:
print(f" {launch['net'][:16]} {launch['status']:15s} {launch['name']}")
+268
View File
@@ -0,0 +1,268 @@
import httpx
import asyncio
import re
import json
from pathlib import Path
from datetime import datetime, timezone
from xml.etree import ElementTree as ET
CACHE_FILE = Path(__file__).parent.parent / ".cache" / "news.json"
CACHE_DURATION_SEC = 300 # 5 minutes
def _get_cached_news():
if CACHE_FILE.exists() and (datetime.now().timestamp() - CACHE_FILE.stat().st_mtime) < CACHE_DURATION_SEC:
try:
with open(CACHE_FILE, "r") as f:
return json.load(f)
except: pass
return None
def _save_cache(data):
CACHE_FILE.parent.mkdir(exist_ok=True, parents=True)
try:
with open(CACHE_FILE, "w") as f:
json.dump(data, f)
except: pass
FEEDS = [
"http://www.aljazeera.com/xml/rss/all.xml",
"http://feeds.bbci.co.uk/news/world/rss.xml",
"https://www.reutersagency.com/feed/?best-topics=political-general&post_type=best",
"https://www.theguardian.com/world/rss",
"https://feeds.npr.org/1004/rss.xml",
"https://foreignpolicy.com/feed/",
"https://www.cnbc.com/id/100727362/device/rss/rss.html",
"https://rss.nytimes.com/services/xml/rss/nyt/World.xml",
"https://feeds.washingtonpost.com/rss/world",
]
# Expanded keyword geocoder — covers most geopolitically relevant regions
GEO_DATA = {
# Middle East
"Iran": [32.4279, 53.6880], "Israel": [31.0461, 34.8516],
"Gaza": [31.3547, 34.3088], "West Bank": [31.9466, 35.3027],
"Lebanon": [33.8547, 35.8623], "Syria": [34.8021, 38.9968],
"Yemen": [15.5527, 48.5164], "Iraq": [33.2232, 43.6793],
"Saudi Arabia": [23.8859, 45.0792], "Jordan": [30.5852, 36.2384],
"Kuwait": [29.3117, 47.4818], "Qatar": [25.3548, 51.1839],
"UAE": [23.4241, 53.8478], "Bahrain": [26.0667, 50.5577],
"Oman": [21.5126, 55.9233],
# Europe
"Ukraine": [48.3794, 31.1656], "Russia": [61.5240, 105.3188],
"Germany": [51.1657, 10.4515], "France": [46.2276, 2.2137],
"UK": [55.3781, -3.4360], "Poland": [51.9194, 19.1451],
"Romania": [45.9432, 24.9668], "Finland": [61.9241, 25.7482],
"Sweden": [60.1282, 18.6435], "Norway": [60.4720, 8.4689],
"NATO": [50.8503, 4.3517], "Belarus": [53.7098, 27.9534],
"Moldova": [47.4116, 28.3699], "Georgia": [42.3154, 43.3569],
"Serbia": [44.0165, 20.9129], "Kosovo": [42.6026, 20.9030],
# Asia-Pacific
"China": [35.8617, 104.1954], "Taiwan": [23.6978, 120.9605],
"North Korea": [40.3399, 127.5101], "South Korea": [35.9078, 127.7669],
"Japan": [36.2048, 138.2529], "India": [20.5937, 78.9629],
"Pakistan": [30.3753, 69.3451], "Afghanistan": [33.9391, 67.7100],
"Myanmar": [21.9162, 95.9560], "Philippines": [12.8797, 121.7740],
"Vietnam": [14.0583, 108.2772], "South China Sea": [12.0000, 113.0000],
# Americas
"USA": [37.0902, -95.7129], "Mexico": [23.6345, -102.5528],
"Venezuela": [6.4238, -66.5897], "Colombia": [4.5709, -74.2973],
"Cuba": [21.5218, -77.7812], "Nicaragua": [12.8654, -85.2072],
"Haiti": [18.9712, -72.2852], "Brazil": [14.2350, -51.9253],
"Argentina": [-38.4161, -63.6167], "Chile": [-35.6751, -71.5430],
"Peru": [-9.1900, -75.0152], "Guyana": [4.8604, -58.9302],
# Central Asia & Caucasus
"Kazakhstan": [48.0196, 66.9237], "Azerbaijan": [40.1431, 47.5769],
"Armenia": [40.0691, 45.0382], "Nagorno-Karabakh": [39.8177, 46.7528],
"Uzbekistan": [41.3775, 64.5853], "Kyrgyzstan": [41.2044, 74.7661],
# Specific Conflict Regions & Strategic Spots
"Gaza": [31.3547, 34.3088], "West Bank": [31.9466, 35.3027],
"Donbas": [48.0159, 37.8028], "Kashmir": [34.0837, 74.7973],
"Sudan": [12.8628, 30.2176], "Darfur": [13.4175, 24.3311],
"Tigray": [14.0323, 38.3166], "Somalia": [5.1521, 46.1996],
"Suez Canal": [29.9329, 32.5539], "Panama Canal": [9.1012, -79.6967],
"Bering Strait": [66.0, -169.0], "Malacca": [2.5, 102.0],
# Cities
"New York": [40.7128, -74.0060], "London": [51.5074, -0.1278],
"Paris": [48.8566, 2.3522], "Brussels": [50.8503, 4.3517],
"Geneva": [46.2044, 6.1432], "Vienna": [48.2082, 16.3738],
"Istanbul": [41.0082, 28.9784], "Kyiv": [50.4501, 30.5234],
"Moscow": [55.7558, 37.6173], "Tehran": [35.6892, 51.3890],
"Beijing": [39.9042, 116.4074], "Tokyo": [35.6762, 139.6503],
"Seoul": [37.5665, 126.9780],
}
EXCLUDE_KEYWORDS = [
"sport", "football", "soccer", "la liga", "champions league", "cup", "match",
"olympics", "tennis", "nfl", "nba", "score", "goal", "premier league",
"formula 1", "f1", "golf", "cricket", "rugby", "boxing", "mma",
"celebrity", "oscars", "grammy", "fashion", "movie", "film", "series",
"recipe", "weather forecast", "horoscope"
]
# Strip HTML tags from RSS descriptions
_TAG_RE = re.compile(r'<[^>]+>')
def _strip_html(s: str) -> str:
return _TAG_RE.sub('', s).strip()
def _find_text(el: ET.Element, tag: str) -> str:
"""Find text for a tag, checking common RSS/Atom namespaces."""
node = el.find(tag)
if node is not None and node.text:
return node.text.strip()
# Try with common namespaces
for ns in ['{http://purl.org/dc/elements/1.1/}', '{http://purl.org/rss/1.0/}']:
node = el.find(f'{ns}{tag}')
if node is not None and node.text:
return node.text.strip()
return ''
async def _fetch_single_feed(client: httpx.AsyncClient, feed_url: str) -> list[dict]:
"""Fetch a single RSS feed directly and parse XML items."""
articles: list[dict] = []
try:
resp = await client.get(feed_url, timeout=12.0, follow_redirects=True)
if resp.status_code != 200:
return []
root = ET.fromstring(resp.content)
# Determine feed title
channel = root.find('channel')
feed_title = 'Global Intel'
if channel is not None:
ft = channel.findtext('title')
if ft:
feed_title = ft.strip()
else:
# Atom feed
ft = root.findtext('{http://www.w3.org/2005/Atom}title')
if ft:
feed_title = ft.strip()
# Find items — RSS uses <item>, Atom uses <entry>
items = root.findall('.//item')
if not items:
items = root.findall('.//{http://www.w3.org/2005/Atom}entry')
for item in items[:10]:
title = _find_text(item, 'title')
if not title:
# Atom title
title = item.findtext('{http://www.w3.org/2005/Atom}title') or ''
title = title.strip()
if not title:
continue
if any(kw in title.lower() for kw in EXCLUDE_KEYWORDS):
continue
# Geocode from keywords
lat, lon = None, None
for region, coords in GEO_DATA.items():
if re.search(r'\b' + re.escape(region) + r'\b', title, re.IGNORECASE):
lat, lon = coords
break
# Link
link = _find_text(item, 'link')
if not link:
link_el = item.find('{http://www.w3.org/2005/Atom}link')
if link_el is not None:
link = link_el.get('href', '')
# Description
desc = _find_text(item, 'description') or _find_text(item, 'summary')
if not desc:
desc = item.findtext('{http://www.w3.org/2005/Atom}summary') or ''
desc = _strip_html(desc)[:200]
# Publication date
pub_date = (
_find_text(item, 'pubDate')
or _find_text(item, 'published')
or item.findtext('{http://www.w3.org/2005/Atom}published')
or datetime.now(timezone.utc).isoformat()
)
# Image from enclosure or media:content
image = ''
enc = item.find('enclosure')
if enc is not None:
enc_url = enc.get('url', '')
enc_type = enc.get('type', '')
if 'image' in enc_type or enc_url.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
image = enc_url
if not image:
media = item.find('{http://search.yahoo.com/mrss/}content')
if media is not None:
murl = media.get('url', '')
if murl.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
image = murl
# Determine Category and Severity
title_lower = title.lower()
category = "GEOPOLITICS"
if any(w in title_lower for w in ["cyber", "hacking", "breach", "malware", "botnet"]):
category = "CYBER"
elif any(w in title_lower for w in ["satellite", "orbit", "rocket", "launch", "space", "iss"]):
category = "SPACE"
elif any(w in title_lower for w in ["military", "army", "navy", "airforce", "missile", "strike", "war", "conflict", "nato", "defense"]):
category = "MILITARY"
severity = "MODERATE"
if any(w in title_lower for w in ["attack", "strike", "crisis", "invasion", "nuclear", "killed"]):
severity = "HIGH"
if any(w in title_lower for w in ["critical", "emergency", "declaration", "imminent"]):
severity = "CRITICAL"
articles.append({
"title": title,
"source": feed_title,
"url": link,
"image": image or None,
"lat": lat,
"lon": lon,
"summary": desc or "No details available.",
"published_at": pub_date,
"category": category,
"severity": severity
})
except Exception as e:
print(f"[NEWS] Feed error ({feed_url[:60]}): {e}")
return articles
async def fetch_news():
"""Fetch all RSS feeds in parallel and return combined articles."""
cached = _get_cached_news()
if cached is not None:
return cached
try:
async with httpx.AsyncClient(
headers={"User-Agent": "GodsEye/2.0 RSS Reader"},
) as client:
results = await asyncio.gather(
*[_fetch_single_feed(client, url) for url in FEEDS],
return_exceptions=True,
)
articles: list[dict] = []
for r in results:
if isinstance(r, list):
articles.extend(r)
print(f"[NEWS] Fetched {len(articles)} intelligence items from {len(FEEDS)} feeds.")
_save_cache(articles)
return articles
except Exception as e:
print(f"[NEWS] Critical error: {e}")
# fallback to stale cache if error
if CACHE_FILE.exists():
try:
with open(CACHE_FILE, "r") as f:
return json.load(f)
except: pass
return []
+208
View File
@@ -0,0 +1,208 @@
import httpx
import asyncio
ADSB_LOL_BASE = "https://api.adsb.lol/v2"
EMERGENCY_SQUAWKS = {
"7700": "GENERAL EMERGENCY",
"7600": "RADIO FAILURE",
"7500": "HIJACK / UNLAWFUL INTERFERENCE",
}
_HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; GodsEye/3.0)",
"Accept": "application/json",
}
# Regional civilian coverage queries: (lat, lon, radius_nm, label)
REGIONS = [
(45.0, -95.0, 2500, "N.America"),
(52.0, 8.0, 2000, "Europe"),
(25.0, 60.0, 2000, "MiddleEast"),
(35.0, 125.0, 2000, "E.Asia"),
( 5.0, 110.0, 2000, "SE.Asia"),
(-10.0, -40.0, 2500, "S.America+Africa"),
]
def _parse_ac(ac: dict, military: bool = False) -> dict | None:
lat = ac.get("lat")
lon = ac.get("lon")
if lat is None or lon is None:
return None
alt_baro = ac.get("alt_baro", 0)
if alt_baro == "ground":
alt_m = 0
else:
alt_m = round(float(alt_baro) * 0.3048, 0) if isinstance(alt_baro, (int, float)) else 0
squawk = str(ac.get("squawk", "")).strip()
is_mil = military or bool(ac.get("mil")) or bool((ac.get("dbFlags", 0) or 0) & 1)
return {
"id": ac.get("hex", "UNKNOWN"),
"callsign": str(ac.get("flight", "")).strip() or ac.get("hex", "UNKNOWN"),
"country": ac.get("ownOp", ac.get("native", "")),
"lon": round(lon, 4),
"lat": round(lat, 4),
"alt": alt_m,
"velocity": round(float(ac.get("gs", 0) or 0) * 0.5144, 1),
"heading": round(float(ac.get("track", 0) or 0), 1),
"military": is_mil,
"squawk": squawk,
"_emergency": squawk in EMERGENCY_SQUAWKS,
"type": "plane",
# GPS quality fields for jamming detection
"nac_p": ac.get("nac_p"), # Navigation Accuracy Category (011, ≥9=normal)
"nic": ac.get("nic"), # Navigation Integrity Category (0=no integrity)
"sil": ac.get("sil"), # Source Integrity Level (03)
}
async def _fetch_military(client: httpx.AsyncClient) -> list[dict]:
try:
r = await client.get(f"{ADSB_LOL_BASE}/mil", headers=_HEADERS, timeout=20.0)
if r.status_code != 200:
print(f"[AIRSPACE] /v2/mil returned {r.status_code}")
return []
ac_list = r.json().get("ac", [])
planes = []
for ac in ac_list:
p = _parse_ac(ac, military=True)
if p:
planes.append(p)
print(f"[AIRSPACE] Military: {len(planes)} aircraft")
return planes
except Exception as e:
print(f"[AIRSPACE] Military fetch error: {e}")
return []
async def _fetch_region(client: httpx.AsyncClient, lat: float, lon: float, radius: int, label: str) -> list[dict]:
try:
url = f"{ADSB_LOL_BASE}/point/{lat}/{lon}/{radius}"
r = await client.get(url, headers=_HEADERS, timeout=20.0)
if r.status_code != 200:
return []
ac_list = r.json().get("ac", [])
planes = []
for ac in ac_list:
p = _parse_ac(ac)
if p:
planes.append(p)
return planes
except Exception as e:
print(f"[AIRSPACE] Region {label} error: {e}")
return []
def _compute_gps_interference(planes: list[dict]) -> list[dict]:
"""
Real GPS jamming detection from ADS-B navigation accuracy fields.
Same method as gpsjam.org: cluster aircraft with degraded NAC_P / NIC values.
nac_p (Navigation Accuracy Category for Position):
≥ 9 = normal GPS (HPU < 30m)
7 = HPU < 0.1 NM (~185m)
≤ 4 = HPU > 0.3 NM (~555m) → GPS quality degraded, likely jamming
0 = HPU unknown / no fix
nic (Navigation Integrity Category):
0 = no integrity assurance → strong spoofing/jamming indicator
≥ 7 = normal
"""
# Grid cell size in degrees
CELL = 2.0
MIN_ANOMALOUS = 3 # need at least 3 aircraft showing anomalies per cell
cell_counts: dict[tuple, dict] = {}
for p in planes:
nac = p.get("nac_p")
nic = p.get("nic")
lat, lon = p.get("lat"), p.get("lon")
if lat is None or lon is None:
continue
# Detect GPS-degraded aircraft
gps_degraded = (
(nac is not None and nac <= 4) or
(nic is not None and nic == 0 and nac is not None and nac < 9)
)
if not gps_degraded:
continue
# Snap to grid cell
cell = (round(lat / CELL) * CELL, round(lon / CELL) * CELL)
if cell not in cell_counts:
cell_counts[cell] = {"count": 0, "lats": [], "lons": [], "min_nac": 99}
cell_counts[cell]["count"] += 1
cell_counts[cell]["lats"].append(lat)
cell_counts[cell]["lons"].append(lon)
if nac is not None:
cell_counts[cell]["min_nac"] = min(cell_counts[cell]["min_nac"], nac)
zones = []
for (clat, clon), info in cell_counts.items():
if info["count"] < MIN_ANOMALOUS:
continue
count = info["count"]
min_nac = info["min_nac"]
intensity = (
"CRITICAL" if count >= 10 or min_nac == 0 else
"HIGH" if count >= 5 or min_nac <= 2 else
"ACTIVE"
)
# Use centroid of affected aircraft for more accurate placement
center_lat = sum(info["lats"]) / len(info["lats"])
center_lon = sum(info["lons"]) / len(info["lons"])
zones.append({
"lat": round(center_lat, 2),
"lon": round(center_lon, 2),
"intensity": intensity,
"aircraft_count": count,
"min_nac_p": min_nac if min_nac < 99 else None,
"source": "ADS-B NAC/NIC",
})
if zones:
print(f"[GPS] {len(zones)} jamming zones detected from {sum(z['aircraft_count'] for z in zones)} anomalous aircraft")
return zones
async def fetch_planes() -> dict:
async with httpx.AsyncClient(timeout=30.0) as client:
# Military first, then all regions in parallel
tasks = [_fetch_military(client)] + [
_fetch_region(client, lat, lon, r, label)
for lat, lon, r, label in REGIONS
]
results = await asyncio.gather(*tasks, return_exceptions=True)
mil_planes = results[0] if isinstance(results[0], list) else []
mil_ids = {p["id"] for p in mil_planes}
# Merge regional civilian results, deduplicate
seen: set[str] = set(mil_ids)
civilian: list[dict] = []
for region_result in results[1:]:
if not isinstance(region_result, list):
continue
for p in region_result:
if p["id"] not in seen:
seen.add(p["id"])
civilian.append(p)
emergencies = [p for p in civilian if p.pop("_emergency", False)]
for p in mil_planes:
p.pop("_emergency", None)
regular = [p for p in civilian if not p.get("_emergency")]
final = mil_planes + emergencies + regular
interference = _compute_gps_interference(final)
print(f"[AIRSPACE] Synced {len(final)} aircraft ({len(mil_planes)} mil, {len(emergencies)} emergency) | {len(interference)} GPS zones")
return {"planes": final, "interference": interference, "emergencies": emergencies}
if __name__ == "__main__":
import json
result = asyncio.run(fetch_planes())
print(f"Total: {len(result['planes'])}, Military: {sum(1 for p in result['planes'] if p['military'])}")
+105
View File
@@ -0,0 +1,105 @@
from skyfield.api import load
import asyncio
SATELLITE_GROUPS = {
"military": "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=tle",
"stations": "https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle",
"navigation": "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=tle",
}
GROUP_LIMITS = {
"military": 800,
"stations": 50,
"navigation": 120,
}
satellites_data: dict = {}
def load_satellites():
"""
Blocking I/O: loads TLE data from Celestrak.
Must be called via asyncio.to_thread to avoid blocking the event loop.
"""
global satellites_data
try:
ts = load.timescale()
loaded = {}
for group, url in SATELLITE_GROUPS.items():
try:
filename = f"{group}.tle"
sats = load.tle_file(url, filename=filename)
loaded[group] = sats
print(f"[SPACE] {group}: {len(sats)} satellites loaded.")
except Exception as e:
print(f"[SPACE] Failed to load {group}: {e}")
satellites_data = {**loaded, "ts": ts}
except Exception as e:
print(f"[SPACE] Critical load failure: {e}")
def _compute_positions_sync(sats: list, category: str, t, limit: int) -> list:
"""
CPU-bound position computation — runs in a thread executor.
Returns dicts, does NOT mutate shared state.
"""
positions = []
for sat in sats[:limit]:
try:
geocentric = sat.at(t)
subpoint = geocentric.subpoint()
lat = subpoint.latitude.degrees
lon = subpoint.longitude.degrees
alt = subpoint.elevation.m
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
continue
positions.append({
"id": sat.model.satnum,
"name": sat.name,
"lat": lat,
"lon": lon,
"alt": alt,
"category": category,
"type": "satellite"
})
except Exception:
pass # skip satellites with bad TLE data
return positions
async def get_satellite_positions() -> list:
if "ts" not in satellites_data:
# First run: load everything in a background thread
await asyncio.to_thread(load_satellites)
if "ts" not in satellites_data:
print("[SPACE] Satellite data unavailable.")
return []
t = satellites_data["ts"].now()
all_positions = []
for group, limit in GROUP_LIMITS.items():
if group in satellites_data:
result = await asyncio.to_thread(
_compute_positions_sync,
satellites_data[group],
group,
t,
limit
)
all_positions.extend(result)
print(f"[SPACE] {len(all_positions)} satellite positions computed.")
return all_positions
if __name__ == "__main__":
load_satellites()
positions = asyncio.run(get_satellite_positions())
print(f"Total: {len(positions)}")
from collections import Counter
for cat, count in Counter(p["category"] for p in positions).items():
print(f" {cat}: {count}")
+88
View File
@@ -0,0 +1,88 @@
import httpx
import asyncio
# NOAA Space Weather Prediction Center - free, no key
NOAA_KP_URL = "https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json"
NOAA_ALERTS_URL = "https://services.swpc.noaa.gov/products/alerts.json"
NOAA_SOLAR_URL = "https://services.swpc.noaa.gov/json/solar-cycle/observed-solar-cycle-indices.json"
KP_STATUS = {
(0, 2): ("QUIET", "No significant geomagnetic activity."),
(2, 4): ("UNSETTLED", "Minor geomagnetic activity."),
(4, 5): ("ACTIVE", "Active geomagnetic conditions."),
(5, 6): ("STORM-G1", "Minor geomagnetic storm (G1)."),
(6, 7): ("STORM-G2", "Moderate geomagnetic storm (G2). HF radio disruption possible."),
(7, 8): ("STORM-G3", "Strong geomagnetic storm (G3). Possible power grid fluctuations."),
(8, 9): ("STORM-G4", "Severe geomagnetic storm (G4). Widespread power disruption."),
(9, 10): ("STORM-G5", "Extreme geomagnetic storm (G5). Grid collapse risk."),
}
def classify_kp(kp: float) -> tuple[str, str]:
for (lo, hi), (status, desc) in KP_STATUS.items():
if lo <= kp < hi:
return status, desc
return "STORM-G5", "Extreme geomagnetic storm."
async def fetch_space_weather() -> dict:
"""
Fetches real space weather data from NOAA SWPC.
- Planetary K-index (geomagnetic storm indicator)
- Active space weather alerts
"""
result = {
"kp_index": 0.0,
"status": "UNKNOWN",
"description": "Awaiting space weather data...",
"alerts": [],
"timestamp": "",
}
try:
async with httpx.AsyncClient() as client:
# Fetch Kp index - returns list of [time_tag, Kp, ...]
kp_resp = await client.get(NOAA_KP_URL, timeout=10.0)
if kp_resp.status_code == 200:
kp_data = kp_resp.json()
# Skip header row [0], get latest reading from the end
if len(kp_data) > 1:
latest = kp_data[-1]
raw_kp = latest[1] if latest[1] not in ("-1", None, "") else "0"
kp = float(raw_kp)
status, description = classify_kp(kp)
result["kp_index"] = kp
result["status"] = status
result["description"] = description
result["timestamp"] = latest[0]
# Fetch active alerts
alerts_resp = await client.get(NOAA_ALERTS_URL, timeout=10.0)
if alerts_resp.status_code == 200:
alerts_raw = alerts_resp.json()
# Filter for active, non-cancelled alerts
active_alerts = []
for alert in (alerts_raw or [])[:10]:
msg = alert.get("message", "")
if "CANCEL" not in msg and "SUMMARY" not in msg:
# Extract first meaningful line as title
lines = [l.strip() for l in msg.split("\n") if l.strip()]
title = lines[0] if lines else "Space Weather Alert"
active_alerts.append({
"issue_time": alert.get("issue_datetime", ""),
"title": title[:100],
})
result["alerts"] = active_alerts[:5]
print(f"[SPACEWEATHER] Kp={result['kp_index']}{result['status']} | {len(result['alerts'])} active alerts")
except Exception as e:
print(f"[SPACEWEATHER] Fetch error: {e}")
result["status"] = "OFFLINE"
return result
if __name__ == "__main__":
import json
data = asyncio.run(fetch_space_weather())
print(json.dumps(data, indent=2))
+69
View File
@@ -0,0 +1,69 @@
import asyncio # kept for __main__ block
def _windy_embed(lat: float, lon: float) -> str:
"""Windy's official iframe-embeddable webcam map — always works."""
return (
f"https://embed.windy.com/embed.html"
f"?type=map&location=coordinates&metricRain=default&metricTemp=default"
f"&metricWind=default&zoom=12&overlay=webcams&product=ecmwf"
f"&level=surface&lat={lat}&lon={lon}"
)
def _windy(lat: float, lon: float) -> str:
return f"https://www.windy.com/-Webcams/webcams?{lat},{lon},12"
WEBCAM_SOURCES = [
# North America
{"id": "wc-nyc", "name": "Times Square, NYC", "lat": 40.7580, "lon": -73.9855},
{"id": "wc-miami", "name": "Miami Beach, Florida", "lat": 25.7617, "lon": -80.1918},
{"id": "wc-sf", "name": "San Francisco Bay", "lat": 37.8083, "lon": -122.4156},
{"id": "wc-dc", "name": "Washington DC", "lat": 38.8899, "lon": -77.0091},
# Europe
{"id": "wc-lon", "name": "Tower Bridge, London", "lat": 51.5055, "lon": -0.0754},
{"id": "wc-par", "name": "Eiffel Tower, Paris", "lat": 48.8584, "lon": 2.2945},
{"id": "wc-ber", "name": "Brandenburg Gate, Berlin", "lat": 52.5163, "lon": 13.3777},
{"id": "wc-rome", "name": "Colosseum, Rome", "lat": 41.8902, "lon": 12.4922},
{"id": "wc-barcelona", "name": "Barcelona Beach", "lat": 41.4036, "lon": 2.1744},
{"id": "wc-amsterdam", "name": "Dam Square, Amsterdam", "lat": 52.3731, "lon": 4.8932},
{"id": "wc-moscow", "name": "Moscow Kremlin View", "lat": 55.7520, "lon": 37.6175},
# Middle East & Africa
{"id": "wc-istanbul", "name": "Bosphorus, Istanbul", "lat": 41.0422, "lon": 29.0083},
{"id": "wc-jerusalem", "name": "Western Wall, Jerusalem", "lat": 31.7767, "lon": 35.2345},
{"id": "wc-dxb", "name": "Dubai Marina", "lat": 25.0800, "lon": 55.1400},
{"id": "wc-cairo", "name": "Pyramids of Giza, Cairo", "lat": 29.9792, "lon": 31.1342},
# Asia & Pacific
{"id": "wc-tok", "name": "Shibuya Crossing, Tokyo", "lat": 35.6595, "lon": 139.7001},
{"id": "wc-hk", "name": "Victoria Harbour, HK", "lat": 22.2855, "lon": 114.1577},
{"id": "wc-sin", "name": "Singapore Skyline", "lat": 1.2897, "lon": 103.8501},
{"id": "wc-syd", "name": "Sydney Opera House", "lat": -33.8568, "lon": 151.2153},
{"id": "wc-seoul", "name": "Seoul Skyline", "lat": 37.5665, "lon": 126.9780},
# Strategic / military-adjacent
{"id": "wc-gibraltar", "name": "Strait of Gibraltar", "lat": 36.1408, "lon": -5.3536},
]
async def fetch_webcams() -> list:
webcams = [
{
"id": cam["id"],
"name": cam["name"],
"lat": cam["lat"],
"lon": cam["lon"],
"url": _windy(cam["lat"], cam["lon"]),
"embed_url": _windy_embed(cam["lat"], cam["lon"]),
"type": "webcam",
"status": "ONLINE",
"source": "Windy Webcams",
}
for cam in WEBCAM_SOURCES
]
print(f"[WEBCAMS] {len(webcams)} webcam locations loaded.")
return webcams
if __name__ == "__main__":
cams = asyncio.run(fetch_webcams())
print(f"Total webcam locations: {len(cams)}")