API reference¶
All services expose HTTP. The ext-proc service additionally exposes a gRPC endpoint.
kysira-proxy (default: :8080)¶
The proxy handles all traffic. Every inbound request is scored before being forwarded to the upstream application.
GET /_kysira/health¶
Returns the proxy health and current configuration.
Response 200 application/json
{
"service": "kysira-proxy",
"status": "ok",
"mode": "shadow",
"threshold": 0.95,
"target": "http://localhost:3000",
"inference": "http://localhost:8081"
}
GET /metrics¶
Prometheus metrics in text exposition format. Scraped by Prometheus or any compatible collector.
Response 200 text/plain
# HELP kysira_requests_total Total requests processed by action.
# TYPE kysira_requests_total counter
kysira_requests_total{action="passed"} 1024
kysira_requests_total{action="shadow_kill"} 12
kysira_requests_total{action="active_kill"} 3
kysira_requests_total{action="error"} 1
...
GET /api/mode¶
Returns the current operating mode.
Response 200 application/json
POST /api/mode¶
Switches the operating mode at runtime without a restart.
Request application/json
mode must be "shadow" or "active". Any other value returns 400.
Response 200 application/json
GET /api/events¶
Server-Sent Events stream of scored requests. On connect, the proxy replays the 200 most recent events before streaming live ones. A keepalive comment (: keepalive) is sent every 15 s.
Response text/event-stream
data: {"timestamp":"2025-01-01T00:00:00Z","method":"POST","path":"/rest/user/login","source_ip":"127.0.0.1","score":0.98,"reason":"sqli:union_select","mode":"shadow","action":"shadow_kill","latency_ms":41.2}
data: {"timestamp":"2025-01-01T00:00:01Z","method":"GET","path":"/","source_ip":"127.0.0.1","score":0.01,"reason":"","mode":"shadow","action":"passed","latency_ms":12.1}
: keepalive
Event fields
| Field | Type | Description |
|---|---|---|
timestamp | string | RFC 3339 UTC |
method | string | HTTP method |
path | string | Request path |
source_ip | string | Client IP (respects CF-Connecting-IP, X-Real-IP, X-Forwarded-For) |
score | float | Classifier score [0.0–1.0] |
reason | string | Human-readable reason from the winning classifier |
mode | string | Operating mode at time of request: shadow or active |
action | string | passed, shadow_kill, or active_kill |
latency_ms | float | Inference call latency in milliseconds |
GET /api/events/recent¶
Returns the last 500 events as a JSON array. Useful for initial page load without subscribing to the stream.
Response 200 application/json
[
{
"timestamp": "2025-01-01T00:00:00Z",
"method": "POST",
"path": "/rest/user/login",
"source_ip": "127.0.0.1",
"score": 0.98,
"reason": "sqli:union_select",
"mode": "shadow",
"action": "shadow_kill",
"latency_ms": 41.2
}
]
Events are ordered oldest-first (same order as the SSE stream).
ALL /* (catch-all)¶
Every other path is reverse-proxied to TARGET_URL. The proxy adds the following headers to forwarded requests:
Headers added to forwarded requests (shadow mode):
| Header | Value |
|---|---|
X-Kysira-Score | Classifier score, e.g. 0.9823 |
X-Kysira-Reason | Reason string, e.g. sqli:union_select |
X-Kysira-Mode | Current mode: shadow or active |
X-Kysira-Would-Have-Killed | true — only present when score exceeds threshold in shadow mode |
In active mode, requests above the score threshold receive a TCP RST and are never forwarded. The response includes X-Kysira-Killed: true and X-Kysira-Score.
kysira-inference (default: :8081)¶
The inference sidecar is an internal service. It is not exposed to the internet — only the proxy and ext-proc call it.
GET /health¶
Returns model load status.
Response 200 application/json
{
"service": "kysira-inference",
"status": "ok",
"sqli_model": "/path/to/models/sqli-classifier",
"pi_model": "/path/to/models/prompt-injection",
"detectors": {"xss": "regex", "nosqli": "regex", "sqli": "model", "prompt-injection": "model", "...": "..."}
}
status is "loading" while models are initialising, "ok" once both are ready. detectors maps every enabled detector slug to its backing ("model" or "regex").
POST /score¶
Scores request text for SQL injection (primary classifier). The proxy calls this endpoint for every request.
Request application/json
request_text is a free-form string. The proxy composes it as:
Response 200 application/json
| Field | Type | Description |
|---|---|---|
score | float | [0.0–1.0] — higher is more suspicious |
reason | string | Human-readable classifier reason |
POST /score/xss¶
Scores request text for cross-site scripting using a regex-based detector.
Request same as /score
Response same schema. score is 0.95 when any XSS pattern matches, 0.0 otherwise.
POST /score/prompt-injection¶
Scores request text for prompt injection using the ML classifier.
Request same as /score
Response same schema.
POST /score/{detector}¶
Generic per-detector endpoint. {detector} is any registered detector slug (sqli, xss, prompt-injection, nosqli, command-injection, ssti, path-traversal, ldap-injection, xpath-injection, ssrf, xxe, deserialization, open-redirect, crlf). Returns 404 for an unknown slug. The legacy /score/xss and /score/prompt-injection routes above are just specific cases of this.
Request same as /score. Response same {score, reason} schema.
POST /score/all¶
Runs every enabled detector over the request (after a single normalization pass) and returns the highest score plus a per-detector breakdown. This is the endpoint the proxy and ext-proc now call — one round-trip replaces the old per-detector fan-out. Cheap regex detectors run first and short-circuit the expensive ML models when one already crosses the kill threshold.
Request same as /score.
Response 200 application/json
{
"score": 0.95,
"reason": "Detected directory traversal sequence: '../' (decoded)",
"detector": "path-traversal",
"breakdown": {"xss": 0.0, "path-traversal": 0.95, "sqli": 0.02, "...": 0.0}
}
| Field | Type | Description |
|---|---|---|
score | float | Max across all detectors |
reason | string | Human reason from the winning detector |
detector | string | Slug of the detector that produced the max |
breakdown | object | {detector: score} for observability |
See detection-architecture.md for the full detector catalog and the request-normalization (encoding) model.
kysira-ext-proc¶
The ext-proc service has two ports: HTTP (default :9090) for management, and gRPC (default :50051) for the Envoy ext_proc protocol.
HTTP port (:9090)¶
GET /_kysira/health¶
Response 200 application/json
GET /metrics¶
Prometheus metrics, same format as the proxy. See deployment.md for the full metric list.
GET /api/mode¶
Response 200 application/json
POST /api/mode¶
Same contract as the proxy's /api/mode. Allows the dashboard to toggle active/shadow mode when deployed with ext_proc instead of the standalone proxy.
Request application/json
Response 200 application/json
gRPC port (:50051)¶
Service: envoy.service.ext_proc.v3.ExternalProcessor
Method: Process(stream ProcessingRequest) returns (stream ProcessingResponse)
This is the Envoy External Processing protocol. The ext_proc service is not called directly — Envoy calls it automatically for every request matching the EnvoyFilter workload selector.
Request header phase (ProcessingRequest.request_headers)
The service reads method, path, query string, and the end_of_stream flag.
- If
end_of_streamis true (GET, HEAD, DELETE — no body): scores immediately and either passes or blocks. - If
end_of_streamis false (POST, PUT — body follows): sends a pass response and waits for the body.
Request body phase (ProcessingRequest.request_body)
Scores method + path + body together, then passes or blocks.
Pass response — ProcessingResponse.request_headers with X-Kysira-* headers added.
Block response — ProcessingResponse.immediate_response with HTTP status 403 Forbidden:
The failure_mode_allow: true setting in the EnvoyFilter means Envoy passes the request through if the ext_proc service is unreachable or times out.