Monitoring prompt injection with Bastion Lens See every catch across your apps — tagged by exactly where it came from
You've blocked the attacks in-process — now see them. Every catch, across every app, tagged by exactly where it came from: the user's message, a document your RAG retrieved, or a tool result your agent trusted.
Once you run more than one app — a RAG assistant here, a support agent there — you want a single place to answer:
- What's being caught, right now and over time?
- Where is it coming from — a poisoned résumé in the vector store, a hijacked support ticket, or the user's own query?
- Is my false-positive rate creeping up as I tune thresholds?
That's Bastion Lens: a self-hosted collector + console. Wiring it in is purely additive — your application code doesn't change at all. You run one container and set two environment variables, and every detection your apps already make starts streaming into a live, filterable audit trail.
origin, so you can filter to exactly the attacks that arrived through your knowledge base and tools — the ones a user-input filter would never have seen.The core idea: provenance
Bastion Lens records more than "an attack happened." Every detection carries:
direction—input(screening what goes into the model) oroutput.vector—direct(typed by the user) orindirect(arrived via retrieved content or a tool result).origin—user_prompt,rag_document, ortool_result.
That provenance is the entire payoff of routing detections through Lens. The defence already blocked the attack; Lens tells you it arrived through the knowledge base, or through a tool the agent called — the distinction that turns a pile of alerts into something you can actually reason about.
app A (RAG) ─┐
app B (agent) ┤── detections (async, non-blocking) ──► Bastion Lens
app C (...) ─┘ ├─ Live feed
├─ Detections (filter by origin)
├─ Overview (fleet stats)
└─ Policies / retention
Nothing here changes how your apps decide — they already screen content in-process with the SDK. Lens is the lens you look through.
Prerequisites
- An app that already screens content with the Bastion SDK. Either of the two defence tutorials are perfect — we'll point them at Lens and watch the same attacks appear: Defending your LlamaIndex app from prompt injection and Defending your LangChain agent from prompt injection.
- Docker (the only new requirement; the defence itself needed none).
Bastion Lens is CPU-only, air-gapped, and makes no outbound calls — the detection model and the console are baked into the image, and your data stays in a local volume.
Step 1 — Run the collector
No clone, no build — pull the published image:
docker run -d --name bastion -p 8080:8080 -v bastion-data:/data \
-e BASTION_ADMIN_KEY=admin-secret-change-me \
bastionsoft/bastion-lens:cpu
That's the whole install. It gives you:
- Console → http://localhost:8080/console
- Ingest + admin API →
/v1/*(OpenAPI at/docs) - Health →
/health
Events persist to a local SQLite store in the bastion-data volume. BASTION_ADMIN_KEY is your bootstrap admin credential — set a real secret in anything but a local demo.
ghcr.io/bastion-soft/bastion-lens:cpu (identical image).
Step 2 — Create an ingest key
Bastion Lens has two key roles, and you want the least-privileged one for apps:
admin— full console, config, and events access (what you log in with).ingest— can only report detections. This is what your apps use, so a leaked app key can't read or change anything.
In the console (recommended): open http://localhost:8080/console, sign in with admin-secret-change-me, then go to Settings → API keys → Create key, choose role ingest, and copy the token — it's shown exactly once and never stored in clear.
Or via the API:
curl -s -X POST http://localhost:8080/v1/keys \
-H "X-API-Key: admin-secret-change-me" \
-H "Content-Type: application/json" \
-d '{"name":"tutorial-sdk","role":"ingest"}'
# → {"key_id":"...","role":"ingest","token":"<KEY_ID>.<SECRET>"} ← copy the token
The token is already the full KEY_ID.SECRET value — use it verbatim, don't re-assemble it.
Step 3 — Point your app at Lens (two env vars, no code change)
In the same shell that runs your app, set:
export BASTION_TELEMETRY_ENDPOINT=http://localhost:8080
export BASTION_TELEMETRY_KEY='<KEY_ID>.<SECRET>' # the ingest token from Step 2
Now re-run either app from the defence tutorial:
python llamaindex/app.py # the poisoned-résumé RAG attack
# or
python langchain/app.py # the hijacked support-ticket agent
That's all. The SDK's reporter batches detections in the background and flushes them to POST /v1/events:batch about once a second — it's non-blocking and off your app's hot path, so it never adds latency to a screen. Without these two env vars set, the SDK reports nothing and runs exactly as before: monitoring is pure opt-in.
Guard threshold — see the LlamaIndex or LangChain defence tutorial. The console's Policies page, below, governs a different deployment: traffic you screen through the gateway's /v1/protect API.)Step 4 — Watch it land live
Open Live feed. With the apps running, detections appear in real time (the page holds an open stream — you'll see the green Live pill, and rows animate in as they arrive; no refresh needed). Each row shows the time, a prompt snippet, the reporting app, the detection stage, the risk score, and the action taken.
Step 5 — Isolate the indirect attacks (the payoff)
Go to Detections — the filterable table over your whole event history. Set the filter to vector = indirect, and you're now looking at only the attacks that arrived through retrieved content and tool results: the poisoned résumé from the LlamaIndex app (origin: rag_document) and the hijacked ticket from the LangChain agent (origin: tool_result). These are precisely the attacks a user-input filter would never have surfaced.
Click any row to open the detail drawer:
- a risk gauge and the verdict (
attack/safe,blocked/flagged/allowed), - the stored prompt snippet (or a "logging disabled" notice — see Step 7),
- the per-scanner breakdown (which scanner fired, its score, matched rules),
- and full provenance: direction, vector, origin, the reporting SDK, model version.
Need it elsewhere? Export the filtered set as CSV or JSON for a report or a SIEM.
Step 6 — The fleet view
Overview is the at-a-glance health of everything reporting in: total screened, blocked count, block rate, the score histogram, and volume over time. It updates live as detections stream, and it respects the time-range selector (last 24h / 7d / 30d / all) in the header. Run several apps and each reports its own source, so the picture is your whole fleet, not one service.
Step 7 — Privacy, retention, and policy
A few operator controls worth knowing, all in Settings and Policies:
- Prompt-logging policy (Settings) — controls what content Lens stores alongside the always-present SHA-256 hash: off (hash only, no content), snippet (a truncated excerpt — the default), or full (the entire prompt). Prompts can carry PII or secrets, so this is a real privacy lever; a stricter setting affects only new events.
- Data retention — events auto-purge after a configurable window; hashes can be retained for audit correlation.
- API keys (Settings) — create, revoke, and permanently delete keys; keep apps on
ingest, humans onadmin. - Policies / scanners — the injection threshold, the secrets / PII / output scanners, and their on/off toggles. Note: this governs traffic screened through the gateway's own
/v1/protectendpoint. Detections your apps report via the SDK arrive pre-decided and aren't re-judged here — the console says as much at the top of the page.
Taking it to production
- Tag your apps. Set
BASTION_SOURCE(and optionallyBASTION_CLIENT_ID,BASTION_ENVIRONMENT) per app so Detections and Overview can be filtered by service and environment. - High volume?
BASTION_TELEMETRY_SAMPLE_RATEsamples reporting (attacks are always kept); the reporter is async and drops rather than blocks under back-pressure, so it never stalls your app. - Air-gap. Lens makes no outbound calls — the model and console are baked into the image, data lives in your local volume. The only network traffic is your apps → the telemetry endpoint you configured.
- Scaling the store. The default is local SQLite, ideal for a single node. Postgres, SIEM/S3 export, multi-tenant, and RBAC are enterprise features.
- Licensing. Bastion Lens (this image) is source-available under the Business Source License 1.1 — free to self-host for your own use; you just can't resell it as a hosted service. (The in-process SDK is AGPL). A commercial license unlocks the enterprise tier — contact sales@bastionsoft.com.
What you've got
You started with a deterministic, infrastructure-free defence and added the operational half: a live, filterable, provenance-tagged audit trail across every app — with the attacks that matter most, the indirect ones, one filter away. You can see not just that an injection was caught, but that it arrived through your knowledge base or a tool your agent trusted — the visibility a user-input filter can't give you. It runs on infrastructure you own, fully offline, and your application code never changed.
Start here if you haven't yet: Defending your LlamaIndex app from prompt injection and Defending your LangChain agent from prompt injection.