Looks at a stream of login events and asks: does this user's path make sense?
I wrote this because failed-login counters alone miss the interesting cases — same password accepted in Lagos and London 45 minutes apart. That isn't a fat-finger. That's either account sharing, a VPN story worth checking, or session theft.
Python does the analysis. TypeScript wraps it for an HTTP POST /analyze and a small CLI.
Two successful logins, different cities, short gap.
Haversine distance → km → km/h. If speed is past ~800 km/h (demo jet ceiling), raise impossible_travel.
Cities are a fixed table in engine/geo.py so the project runs offline. Swap for real GeoIP later if you wire this into production logs.
Enough failed attempts on one account (default 5+) → fail_burst.
Could be the user locked out. Could be stuffing. Either way it should show up.
Many distinct device_ids in the window → device_churn. Shared accounts and attackers rotating devices both look like this.
Findings are sorted with high severity first.
flowchart TD
L[login events JSON] --> T[TypeScript api]
T -->|stdin JSON| P[engine/cli.py]
P --> A[analyzer.py]
A --> G[geo.py haversine]
A --> F1[impossible travel]
A --> F2[fail bursts]
A --> F3[device churn]
F1 --> M[merge findings]
F2 --> M
F3 --> M
M --> O[JSON findings]
O --> T
T --> R[API response / terminal]
session-watch/
engine/
geo.py
analyzer.py
cli.py
api/
bridge.ts
server.ts
analyze.ts
sample_data/logins.json
tests/
git clone https://github.com/Nabil201-ctrl/session-watch.git
cd session-watchpython3 engine/cli.py -i sample_data/logins.jsoncd api
npm install
npx ts-node analyze.ts ../sample_data/logins.jsonnpx ts-node server.ts
# listens on 3855
curl -s -X POST http://127.0.0.1:3855/analyze \
-H 'content-type: application/json' \
-d @../sample_data/logins.jsonpython3 -c "
import sys
from pathlib import Path
sys.path.insert(0, str(Path('engine').resolve()))
sys.path.insert(0, str(Path('tests').resolve()))
from test_analyzer import test_lagos_to_london_is_flagged
test_lagos_to_london_is_flagged()
print('ok')
"The included logins.json is hand-written to trigger each path:
u_nabil— Lagos then London under an houru_sara— five fails from mixed IPsu_ken— four devices, same city
When I change thresholds I re-run this file first before touching real data.
I already had the math in Python and didn't want to port haversine + event grouping into TS just for an endpoint. Spawning the engine keeps risk logic in one place. Tradeoff: process startup cost. Fine for batch/demo traffic; for hot path you'd keep a long-lived worker.
- Real GeoIP lookup
- Single combined risk score
- Hook “high” findings into a payment risk check
- MFA step-up callback