Email breach exposure checker built on the XposedOrNot free API.
Checks whether an email address appears in known data breaches and renders the result as a dashboard: risk score, exposure timeline, password-storage breakdown, leaked data types, and a filterable record of every matching breach.
A page that collects an email and a password is architecturally identical to a phishing page. This tool takes an email address only. Password checking is possible without transmitting the password — XposedOrNot exposes a k-anonymity endpoint where you hash locally and send a 10-character prefix — but that is a separate feature, not part of this form.
Nothing entered is written to disk or logged.
pip install -r requirements.txt
python -m streamlit run streamlit_app.py
Opens on http://localhost:8501
Same backend, from the command line:
python xon_check.py you@example.com
python xon_check.py you@example.com --json
python xon_check.py --quota
Exit codes: 0 clean, 1 exposed, 2 usage error, 3 API error.
The free tier allows 2 requests/second, 25/hour and 100/day per IP. Usage is metered locally in ~/.xon_check_usage.json and a request that would exceed the cap is refused before it is sent, so you fail locally instead of being throttled upstream. The file stores timestamps only.
Lookups are cached for an hour. Streamlit re-runs the whole script on every widget interaction, so without the cache each filter change would spend a request. Filtering, sorting and re-rendering cost nothing.
This ceiling is why there is no bulk-upload mode. Checking a list of addresses at 100/day is not viable; that is what the domain-breaches endpoint is for, and it requires a verified-domain API key.
The differentiator. Most breach tools report exposure: "you appear in 8 breaches." That is the wrong unit. A 2012 dump where passwords were bcrypted and you have since rotated is archaeology. A 2024 infostealer log is an active incident. Both count as one breach.
liveness.py scores each breach 0-100 on whether the leaked credential is plausibly STILL VALID, from:
- whether a password was actually leaked, or only an email
- how it was stored (plaintext through bcrypt)
- exponential recency decay, 4-year half-life
- whether the user rotated after the breach date
- aggregated combo lists discounted, their date overstates freshness
- infostealer logs floored, device compromise does not decay
Liveness is kept separate from impact. MFA does not make a leaked password less valid, it makes takeover harder. A password manager does not make a credential stale, it limits blast radius. Folding those into one "risk" number is why most breach scores are unactionable.
Every score carries the factors that produced it. A number without its reasoning cannot be argued with, which makes it useless in triage.
Exposure data is only useful once something consumes it. The Export tab generates four artifacts from the liveness assessment:
Sentinel watchlist CSV reference data, carries the liveness score so a
single query can tier its response
KQL analytics rule the correlation - alerts when an exposed identity
authenticates from an ASN unseen in 90 days,
severity driven by liveness not breach count
Sigma rule vendor-neutral signature for a detection repo
MISP event JSON structured sharing, org-only by default
Sigma is a signature format, not a correlation language. It cannot express a watchlist join or a rolling baseline, so that logic lives in the KQL and the Sigma rule stays within what converts cleanly. It is an enrichment signal, not a standalone alert, and says so in its own description.
The MISP event defaults to distribution 0 (organisation only) with to_ids false. A breach-exposed address is personal data about an identifiable person, and these are context rather than blockable indicators.
The detection content has NOT been run against a production tenant and will not be - I do not have authorisation to deploy detection content to my employer's environment. That is a real limit on what this project claims.
What is validated: the correlation logic, against synthetic Entra ID sign-in data with known-correct answers. detection_harness.py is a Python reimplementation of the generated KQL, step for step, and the suite asserts that exposure gates the alert, failed authentication does not fire, archival exposure is filtered by threshold, and the baseline window does not contaminate itself.
That last one is the regression worth naming. If the baseline ran to now instead of to ago(DetectionWindow), the first sign-in from an attacker's ASN would enter its own baseline and suppress every later one. The test fails if the window is widened - verified by mutating the harness and watching it break.
What is NOT validated: field behaviour in a real tenant. Whether AutonomousSystemNumber is stable enough for mobile-carrier traffic to avoid constant false positives is an empirical question about a specific user population, and no amount of synthetic data answers it. In a K-12 environment with heavy mobile traffic I would expect this rule to need a carrier-ASN allowlist before it is deployable.
The harness is also a translation of the KQL, not the KQL executing. A translation can drift from its source. Each stage names the KQL construct it mirrors so the two can be diffed by eye, but that is mitigation, not proof.
The weights are hand-set from reasoning about how credentials age, not fitted to data. The 4-year recency half-life is a judgement call.
There is no ground truth available. Establishing which leaked credentials still authenticate would require testing them against live services, which is not something to do. So the model cannot be calibrated, only specified and then tested for whether it behaves as specified.
Treat the scores as a structured argument about priority, not a measurement.
streamlit_app.py Dashboard and all presentation
liveness.py Credential liveness scoring and triage
exporters.py Sentinel, Sigma and MISP artifact generation
detection_harness.py Python reference implementation of the KQL
synthetic_logs.py SigninLogs-shaped test data with known answers
tests/ Behavioural tests - pytest -v
insights.py Aggregation — kept separate so it can be
tested without booting the UI
xon_check.py API client, response parsing, rate-limit
accounting, CLI entry point
.streamlit/config.toml Theme. Streamlit reads this at startup from
the directory you launch from.