Passive SMTP / IMAP / POP3 mail-security forensics from packet captures.
KryxAI reads a capture that already exists and reports what the traffic shows: whether a mail server advertised STARTTLS and then quietly stopped offering it, what TLS version and cipher each session actually negotiated, whether the certificate chain validates, and which statutory control each observation maps to. It never probes, scans, probes or modifies anything. A clean report is not proof of a compliant control — it reflects only the traffic in the capture you supplied.
Indian mail servers are widely reported to strip the STARTTLS capability from
EHLO replies — a pattern associated with Vodafone-style intercepting
middleboxes. The capability is replaced with a token such as XXXXXXXA so the
client proceeds in plaintext believing no upgrade was on offer. This is
K1 capability suppression, and it is the signature KryxAI was built to detect
and evidence.
One command sets up everything and starts the demo:
./run_kryxai.sh # POSIX
run_kryxai.bat # WindowsIt creates a virtualenv, installs .[all], generates the synthetic corpus,
waits for the API to actually answer, then starts the dashboard. Useful flags:
--recreate (rebuild the venv), --no-frontend (engine only, no Node
needed), --api-port=N, --web-port=N.
Install the all extra, not the base package. The base install gives you a
working kryxai CLI but no uvicorn or FastAPI, so the API fails to start —
the launcher verifies both imports and tells you if either is missing.
python -m pip install -e ".[all]" # everything
python -m pip install -e . # core engine onlyThe core engine needs only cryptography, pydantic and pydantic-settings,
and is enough for kryxai scan to produce JSON and HTML reports. Extras:
| Extra | Adds | Needed for |
|---|---|---|
api |
FastAPI, uvicorn | the HTTP API |
reports |
ReportLab, Jinja2 | PDF output |
model |
onnxruntime, numpy | optional ONNX fusion |
dev |
pytest, httpx2, api + reports | running the test suite |
PDF is deliberately optional: a base install still runs a full scan and writes JSON and HTML, and the CLI says explicitly that the PDF was skipped rather than leaving you to infer it from a missing file.
Writable state goes to a per-user data directory, never into the installed package:
| Platform | Location |
|---|---|
| Windows | %LOCALAPPDATA%\KryxAI |
| Linux / macOS | $XDG_DATA_HOME/kryxai, else ~/.local/share/kryxai |
| Source checkout | the repository root |
| Anywhere | KRYXAI_HOME overrides all of the above |
That holds the SQLite evidence database, the reports/ directory, and the
RSA-3072 report-signing key. Override the individual paths with
KRYXAI_DATABASE_PATH, KRYXAI_REPORTS_DIR and KRYXAI_REPORT_KEYS_DIR.
# generate the synthetic demo corpus (17 captures, all clearly labelled fake)
kryxai corpus corpus
# analyse a capture
kryxai scan corpus/02_star_ttlssuppressed_vodafone_style.pcap
# check the evidence chain
kryxai chain
# what this build can and cannot see
kryxai capabilitiesLibrary:
from pathlib import Path
from kryxai.config import Settings
from kryxai.engine import run_scan
from kryxai.store import Store
from kryxai.reports import builder
store = Store("evidence.db")
result = run_scan(Path("office.pcap"), Settings(), store=store)
print(result.report["posture"]["grade"], result.report["counts"]["findings"])
builder.write(result.report, Path("reports"), Settings(), langs=["en", "hi"])uvicorn kryxai.api:app --port 8000| Method | Path | Purpose |
|---|---|---|
GET |
/health |
liveness, version, chain id, anchor posture |
GET |
/api/v1/capabilities |
what is and is not observable |
POST |
/api/v1/scan |
analyse a capture already on the server |
POST |
/api/v1/upload |
upload and analyse a capture |
GET |
/api/v1/report/{scan_id} |
full report JSON (or Hindi HTML) |
GET |
/api/v1/report/{scan_id}/html?lang=en|hi |
rendered report |
GET |
/api/v1/chain |
evidence chain verification and blocks |
Report bodies are held in process memory, so report retrieval does not survive a restart. The evidence chain is durable in SQLite.
Frontend:
cd frontend
npm install
npm run dev # proxies to 127.0.0.1:8000render.yaml and Dockerfile deploy the whole thing to Render's free tier for
$0 — the API as a Docker web service and the SPA as a static site, no credit
card required.
# API image
docker build -t kryxai-api:local -f Dockerfile .See docs/DEPLOY.md for the deploy steps, the free-tier limits, what is lost on every restart, and the optional Fabric anchor.
STARTTLS suppression (the India focus)
K1_capability_suppression— the capability was advertised, then removed or replaced with a placeholder token.K2_cross_flow_inconsistency— the same server advertises STARTTLS on some sessions and not others.K3_refused_upgrade— the client asked, the server refused.K4_plaintext_after_upgrade— a negotiated upgrade followed by cleartext protocol, which no honest server does.
Transport — TLS 1.0–1.3 version, cipher suite, key-exchange group, signature algorithm, forward secrecy.
Certificates — SAN/CN matching, validity judged at capture time (not analysis time), key strength, signature algorithm, chain-signature verification.
Posture — a 0–100 score across transport encryption, certificate hygiene,
forward secrecy, tamper resistance and evidence integrity, with a letter grade.
Dimensions that could not be measured are reported as not_assessed rather
than scored as zero.
Compliance — DPDP Act 2023 and CERT-In Directions 20(3)/2022 mapping in
kryxai/compliance/sources.json.
- TLS 1.3 certificates are not visible. The server certificate and handshake signature are inside encrypted handshake records. KryxAI reports this as a visibility limit; it cannot assess a TLS 1.3 chain passively, and a TLS 1.3 result is never a pass.
- A middlebox that rewrites identically in both directions is indistinguishable from a server. Only out-of-band comparison resolves that.
- Encrypted payload content is never decrypted or retained.
- Passive only. KryxAI cannot tell a hostile middlebox from a misconfigured one, and cannot attribute intent.
- Not legal advice and not a compliance determination.
- No authoritative CERT-In IoC feed. There is no verified machine-readable CERT-In API. The bundled feed is an empty, clearly-unverified example; operator-supplied files are the supported path and every feed records its own provenance.
- Alerting is opt-in and is the only outbound call KryxAI makes. Everything else is observation of a capture that already exists.
Optional. When a scan produces a finding at or above
KRYXAI_ALERT_MIN_SEVERITY (default high), the configured channels are
notified. With no channel configured, KryxAI sends nothing — which is the
default install, and the reason a stock deployment makes no network call at all.
KRYXAI_ALERTS_ENABLED=true
KRYXAI_ALERT_MIN_SEVERITY=high # critical|high|medium|low|info
KRYXAI_MAX_ALERT_RETRIES=3
KRYXAI_ALERT_TIMEOUT_S=5
KRYXAI_TELEGRAM_BOT_TOKEN=... KRYXAI_TELEGRAM_CHAT_ID=...
KRYXAI_SMTP_HOST=... KRYXAI_SMTP_USER=... KRYXAI_SMTP_PASSWORD=...
KRYXAI_ALERT_EMAIL_TO=...
KRYXAI_NTFY_TOPIC=...
KRYXAI_WEBHOOK_URL=.../health and kryxai capabilities report which channels are live. Three
guarantees, each tested:
- Alerting can never fail a scan. A scan is evidence; losing it because a webhook timed out would be the wrong trade.
- A failed delivery is recorded, not swallowed. The outcome lands in
report.alerts, and the dashboard's Notification panel renders it directly below the finding strips, so a scan whose notification failed says so rather than implying a page went out that never did. That panel makes the failure state the loud one, and states a 2xx as a hand-off rather than a read receipt. Delivery status is deliberately kept off the evidence chain, because it varies run to run for identical evidence. - No credential is ever logged or echoed. Delivery records carry the channel and a redacted target; secrets are stripped from exception text, since a urllib error echoes the URL that failed and a Telegram URL carries the token.
Unconfigured channels are skipped, never fatal. Retries are bounded and
backoff is capped, so a dead channel costs seconds rather than minutes —
worst case channels × (retries + 1) × KRYXAI_ALERT_TIMEOUT_S added to a scan.
Every scan appends a proof-of-work block to a local SQLite ledger, and signed reports are written as JSON, English HTML, Hindi HTML and PDF.
- Chain id
KryxAIV1, report AADb"kryxai-report-v1". - Difficulty is stored per block and verified against the stored value, so weakening the default cannot silently rewrite history.
- A local block is tamper-evident but not externally attested, so it is
labelled
pending. It is never labelledanchored. - Optional external anchoring (Hyperledger Fabric + IPFS) lives in
deploy/nbf-fabric. SetKRYXAI_BLOCKCHAIN_EXTERNAL_ANCHOR=trueandKRYXAI_BLOCKCHAIN_ANCHOR_REQUIRED=trueto make anchoring mandatory; in that posture a report that cannot be anchored is rejected with 503 and no report file is written.
JSON and HTML reports are fully bilingual (English / Devanagari). The PDF is English-only unless a Devanagari-capable font is configured, because a default Windows install ships none and a PDF cannot render Devanagari without one:
KRYXAI_DEVANAGARI_FONT_PATH=/path/to/NotoSansDevanagari-Regular.ttfWhen no font is available the PDF states that Hindi text was omitted rather than emitting blank glyphs.
KryxAI scores every finding with a validated rule engine. That path is the default and needs no model file.
A 46-feature ONNX model is also supported. When one is present the report records both scores alongside the fused total:
| Field | Meaning |
|---|---|
risk.rule_score |
The rule engine's score alone. Always present. |
risk.model_score |
The model's score, or null if no model is loaded. |
risk.total |
The fusion. Identical to rule_score when rules-only. |
report.model.used |
false whenever no model file is configured. |
report.model.sessions_scored |
Sessions the model scored, of sessions_total. |
So a disagreement between the two is visible rather than resolved silently.
The model scores one row per session, built from that session's most serious
finding by the same fusion.session_feature_row() that builds its training set.
Every finding in a session carries its session's score. A clean session is
therefore still model-scored even though it produces no findings — which matters,
because those clean sessions are the only examples teaching the model what
"acceptable" looks like. Session-level means in the report use the same unit, so
sessions_scored < sessions_total is a real signal rather than an artefact of
counting findings.
KRYXAI_ONNX_MODEL_PATH= # force the rules-only path
KRYXAI_ONNX_MODEL_PATH=/path/m.onnx # use a specific modelA model is auto-discovered at kryxai/scoring/risk_model.onnx, and is only
accepted if it declares both the feature schema version and the exact ordered
feature names matching this build. Either one missing is a refusal, not a
warning: the version is a manual bump that can be forgotten, whereas the names
fail loudly the moment a feature is renamed, reordered or dropped. A model
trained against a different vector is refused, not applied.
The model is optional because it does not outperform the rules. It is trained entirely on synthetic captures and reaches parity with the rule engine on held-out data. Parity is a statement about that corpus: the training labels are the same weakness categories the rules detect, so the rules are already close to a perfect classifier on this data. There is no claim that a learned model is better here, and no validation against real mail traffic exists.
To retrain, or to see why, see training/README.md and
open training/kryxai_train.ipynb in Colab.
python -m pip install -e ".[dev]"
python -m pytest tests/ -q # 338 tests
cd frontend && npm run build && npx oxlint
cd deploy/nbf-fabric/go && go vet ./...
python training/run_notebook_locally.py # executes the training notebook's cellstests/test_frontend_contract.py parses the TypeScript interfaces in
frontend/src/lib/api.ts and asserts the API responses match them, so a
renamed field fails the Python suite rather than silently rendering blank in the
browser. In a source checkout those tests are required, not skipped, so the
guard cannot quietly disappear in CI.
training/run_notebook_locally.py runs the training notebook's code cells
against this checkout, replacing the clone and pip install steps. Run it after
touching features_for, the model loader or the notebook: it is what catches a
training/serving skew before it ships.
kryxai/
pcap/ capture decoding, TCP reassembly, STARTTLS, TLS, X.509, corpus
policy/ unified knowledge base, cipher policy, finding emission
scoring/ risk scoring, posture, anomalies, optional ONNX fusion
compliance/ DPDP / CERT-In mapping and statutory sources
feeds/ offline IoC adapter with provenance
alerts.py opt-in outbound notification (Telegram, email, ntfy, webhook)
reports/ signatures and JSON / HTML / PDF rendering
store.py SQLite evidence chain, proof of work, anchors
engine.py orchestration
api.py FastAPI surface
cli.py command line interface
deploy/nbf-fabric/ Fabric + IPFS external anchoring
frontend/ React dashboard
training/ synthetic corpus, Colab notebook, model installer
tests/ 338 tests
Dockerfile API image (Render free tier, Cloud Run)
render.yaml Render blueprint: API web service + static site
docs/DEPLOY.md free-tier deployment guide
MIT.