mirror of
https://git.churchofmalware.org/leviathan/OVERWATCH
synced 2026-09-24 08:35:03 +00:00
Adds a sixth source: police and surveillance aircraft overhead, via the free
community ADS-B networks (opendata.adsb.fi, api.adsb.lol fallback, no API
key). Contacts are matched by ICAO 24-bit address against a bundled registry
of 1,971 US law-enforcement airframes generated by scripts/gen-le-aircraft.py
from ADSBexchange's basic-ac-db and committed as res/raw/le_aircraft.csv.
Community feeds are used deliberately: FlightAware and Flightradar24 filter
law-enforcement flights at government request, which is the traffic this
source exists to see. Two parse traps handled — the envelope is {"ac":[..]}
on radius queries but {"aircraft":[..]} on wider ones, and alt_baro is the
string "ground" when parked.
Registry coverage is imperfect, so the scanner also detects loiter/orbit
behaviour (3+ samples over 4+ min within 5 km of their centroid, below
12,000 ft and under 200 kt). Surveillance aircraft circle; airliners and
medevac flights going somewhere do not. Behaviour-only hits are capped at 69
and must be within 8 km, so ordinary traffic never alerts.
Rejected with measurements: plane-alert-db matched 0 of 394 live aircraft
over a 250 nm sweep of DC while 15 helicopters were aloft (only 255 of its
entries are US-registered); registry.faa.gov is Akamai-403 and N-number
keyed; OpenSky's CSV is 94 MB for the same data; EFF's Atlas of Surveillance
has no aircraft identities at all, only a 2022 FAA drone lookup.
Overpass query widened from ALPR-only to man_made=surveillance plus
highway=speed_camera, classified into ALPR / speed camera / generic camera
with separate falloff curves so a shop's CCTV cannot alarm like a Flock
install. Cache key bumped to deflock2_ so v1 ALPR-only entries are not served
for another 24 h.
DeFlock, Waze and aircraft are now scored by continuous distance falloff
rather than flat values. The anchors are absolute metres and deliberately
independent of the detection-radius setting: moving a slider must not move
the threat level.
New SOURCES.md documents every endpoint, identifier and scoring table,
including the sources that were tried and rejected and the measurements that
killed them.
Verified against live traffic: detected San Diego PD's AS50 at 1580 m /
1000 ft scoring 83 (ORANGE) and a San Diego County Sheriff B407 at 11742 m
scoring 44, with the score tracking the helicopter from 81 to 83 as it
closed. Both match the falloff formula exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
90 lines
3.8 KiB
Python
Executable File
90 lines
3.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Regenerate app/src/main/res/raw/le_aircraft.csv — the bundled list of
|
|
law-enforcement aircraft, keyed by the ICAO hex that ADS-B actually broadcasts.
|
|
|
|
python3 scripts/gen-le-aircraft.py
|
|
|
|
Source: ADSBexchange's `basic-ac-db.json.gz` (~14.5 MB, ~618k aircraft), which
|
|
is derived from the FAA registry and already keyed by ICAO hex. That matters:
|
|
registry.faa.gov itself is behind Akamai bot mitigation and answers 403, and
|
|
the raw FAA registry is keyed by N-number, which would need a separate
|
|
N-number -> hex conversion step.
|
|
|
|
Why not the community plane-alert-db lists: measured 2026-09-17, their
|
|
plane-alert-pol.csv holds 996 aircraft but only 255 US-registered, and matching
|
|
all 2,760 of their police+government hexes against 394 live aircraft over a
|
|
250 nm sweep of Washington DC produced zero hits. Filtering this database by
|
|
owner name yields ~1,950 US law-enforcement aircraft — roughly 7.7x the US
|
|
coverage.
|
|
|
|
This runs at development time and commits its output; the app never downloads
|
|
it. Re-run occasionally to pick up registry changes.
|
|
"""
|
|
import gzip, io, json, re, sys, urllib.request
|
|
from pathlib import Path
|
|
|
|
SRC = "https://downloads.adsbexchange.com/downloads/basic-ac-db.json.gz"
|
|
OUT = Path(__file__).resolve().parent.parent / "app/src/main/res/raw/le_aircraft.csv"
|
|
|
|
# Owner-name patterns. Word boundaries matter: a bare "MARSHAL" matches
|
|
# "MARSHALL SPACE FLIGHT CENTER", and a bare "POLICE" would be fine but
|
|
# "SHERIFF" needs to catch both "SHERIFF" and "SHERIFFS".
|
|
LE = re.compile(r"(\bSHERIFF|\bPOLICE\b|\bSTATE PATROL\b|\bHIGHWAY PATROL\b|"
|
|
r"\bSTATE TROOPER|\bPUBLIC SAFETY\b|\bUS MARSHAL\b|"
|
|
r"\bMARSHALS SERVICE\b|\bCONSTABLE\b|\bDEPT OF CORRECTION|"
|
|
r"\bDEPARTMENT OF CORRECTION|\bBORDER PATROL\b|"
|
|
r"\bCUSTOMS AND BORDER|\bCUSTOMS & BOR|\bHOMELAND SECURITY\b|"
|
|
r"\bDRUG ENFORCEMENT\b|\bFEDERAL BUREAU OF INVESTIGATION\b)", re.I)
|
|
|
|
# Air-ambulance and firefighting outfits share a lot of vocabulary with police
|
|
# aviation ("PUBLIC SAFETY"), and a medevac helicopter overhead is not the
|
|
# thing this app exists to warn about.
|
|
EXCLUDE = re.compile(r"MARSHALL SPACE|\bFIRE\b|AMBULANCE|MEDICAL|\bEMS\b|"
|
|
r"AIR METHODS|LIFE FLIGHT|MEDEVAC|HOSPITAL", re.I)
|
|
|
|
|
|
def main() -> int:
|
|
print(f"fetching {SRC} …")
|
|
req = urllib.request.Request(SRC, headers={"User-Agent": "OVERWATCH-build/1.0"})
|
|
with urllib.request.urlopen(req, timeout=180) as r:
|
|
raw = gzip.decompress(r.read())
|
|
|
|
rows, ladd, seen = [], 0, 0
|
|
for line in io.BytesIO(raw):
|
|
line = line.decode("utf-8", "replace").strip().rstrip(",")
|
|
if not line or line in "[]":
|
|
continue
|
|
try:
|
|
rec = json.loads(line)
|
|
except ValueError:
|
|
continue
|
|
seen += 1
|
|
own = (rec.get("ownop") or "").strip()
|
|
if not own or not LE.search(own) or EXCLUDE.search(own):
|
|
continue
|
|
hexid = (rec.get("icao") or "").strip().lower()
|
|
if len(hexid) != 6:
|
|
continue
|
|
# Commas would break the one-line-per-record format; the owner is
|
|
# display text only, so squashing punctuation is harmless.
|
|
owner = re.sub(r"[,\r\n]+", " ", own)[:48].strip()
|
|
if rec.get("faa_ladd"):
|
|
ladd += 1
|
|
rows.append((hexid, owner, "1" if rec.get("faa_ladd") else "0"))
|
|
|
|
rows.sort()
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
with OUT.open("w", encoding="utf-8") as f:
|
|
f.write("# hex,owner,ladd — generated by scripts/gen-le-aircraft.py, do not edit\n")
|
|
for hexid, owner, l in rows:
|
|
f.write(f"{hexid},{owner},{l}\n")
|
|
|
|
print(f"scanned {seen} aircraft -> {len(rows)} law-enforcement ({ladd} LADD-flagged)")
|
|
print(f"wrote {OUT} ({OUT.stat().st_size // 1024} KB)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|