An offensive toolkit for the identification and risk profiling of unmanaged local AI infrastructure.
Self-hosted AI assistants, copilots, and bot-style integrations are landing inside organisations faster than their security teams can inventory them β let alone audit what they will actually do when prompted. Tezcat is a security tool built for that gap. Point it at the AI agents you (or someone in your org) is running, run a battery of probes against them, and get back a typed risk profile: what each endpoint can be made to do, mapped onto industry risk frameworks, with drift comparison between probe runs so you can tell when behaviour changes underneath you.
Tezcat drives targets over eight channels (HTTP/REST + OpenAPI, OpenAI-compatible, Anthropic-compatible, Open WebUI, MCP, Telegram, Mattermost, and Email/IMAP), classifies every response with rule-based parsers (no LLM in the analysis loop), and stores the result as queryable, diffable state.
- Session-oriented, C2-style plugin contract. One
AccessPluginABC supports both request/response channels (REST, OpenAI/Anthropic-compatible, Open WebUI) and async session channels (MCP, Telegram, Mattermost, Email). See ADR 0001. - Multi-framework risk model. Capabilities are cross-linked to MITRE ATLAS, OWASP Top 10 for LLM Apps (2025), OWASP Top 10 for Agentic Apps (2026), NIST AI RMF, and AVID simultaneously, with no schema migration to add more. See ADR 0002.
- Drift, not just findings. The first probe run is the baseline; every subsequent run produces a typed diff across seven classes (new / lost / confirmed / evidence-changed / regressed-to-inconclusive / still-absent / severity-changed). See ADR 0008.
- No LLM in the parser. Response classification is rule-based (regex-with-negation, slot-fill, structured-JSON, voting + confidence). Tezcat never feeds prompts back to an LLM to interpret responses, which keeps results reproducible, auditable, and free of an injection surface. See ADR 0003.
- Security-first defaults. Encrypted credential store (Fernet + HKDF + env-var key, gated decryption, never returned by the API), append-only audit log enforced at the database grant level, single-tenant deployment with three roles (Admin / Operator / Auditor). See ADRs 0005 and 0006.
- Internal red teams / AppSec engineers auditing the LLM-powered tools their developers have spun up.
- Blue teams / SOC who need a continuous-monitoring signal on AI agents that don't show up in any conventional asset inventory.
- Security researchers wanting a reproducible, scriptable harness for behavioural probing of AI endpoints, with results stored in a queryable, diffable form.
Tezcat is not a public-cloud SaaS, a guardrail / runtime defence, or an LLM evaluation harness for model quality. It is an offensive auditing tool for assets you are authorised to test.
Warning
Only run Tezcat against AI endpoints you own or have explicit written authorisation to probe. Probing third-party services may violate their terms of service or applicable computer-misuse law. You are responsible for your use of this tool.
| Subsystem | Status |
|---|---|
| Endpoint inventory + tagging | Yes |
| Encrypted credential store (Fernet, HKDF, env-var key) | Yes |
AccessPlugin contract + plugin loader |
Yes |
| REST / OpenAPI channel | Yes |
| OpenAI-compatible channel | Yes |
| Anthropic-compatible channel | Yes |
| Open WebUI channel (tool-enabled chat) | Yes |
| MCP (Streamable HTTP) channel | Yes |
| Telegram Bot channel | Yes |
| Mattermost bot channel | Yes |
| Email / IMAP+SMTP channel | Yes |
Curated prompt library + ProbeProfiles (explicit_list + filter_spec resolvers) |
Yes |
| Probe orchestration (Celery, live concurrency + rate limiting, per-prompt retries) | Yes |
| Rule-based response parsing (regex / slot-fill / structured-JSON / voting) | Yes |
Typed CapabilityType catalog + per-(endpoint, capability_type) findings |
Yes |
| Risk frameworks: MITRE ATLAS, OWASP LLM, OWASP Agentic, NIST AI RMF, AVID | Yes |
| Drift comparison between probe runs (7 drift classes) | Yes |
| Scheduled probes (cron, Celery beat) | Yes |
| In-app + external notifications (Slack / webhook / email) | Yes |
| Append-only audit log (DB-grant enforced) | Yes |
| RBAC (Admin / Operator / Auditor), TOTP MFA, account lockout | Yes |
| React + TanStack frontend (setup β endpoint β probe β findings β drift) | Yes |
| Nginx TLS termination + security headers | Yes |
| Docker Compose dev + prod overlays | Yes |
| Playwright golden-path E2E test | Yes |
See the ADRs in docs/adr/ for the load-bearing decisions behind each subsystem, and Limitations for what Tezcat does not do.
ββββββββββββββββ HTTPS ββββββββββββββββ
β Browser β ββββββββββββββββΊ β nginx β TLS, /api proxy, SPA fallback
β (React SPA) β ββββββββ¬ββββββββ
ββββββββββββββββ β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β Django + DRF (web) β
β β’ Auth / RBAC / MFA β
β β’ Service layer (no view-layer ORM) β
β β’ OpenAPI 3.1 (drf-spectacular) β
ββββββββ¬ββββββββββββββββββββββ¬ββββββββββββββ
β β
βΌ βΌ
ββββββββββββββ ββββββββββββββ
β Postgres β β Redis β
β (state + β β (broker + β
β audit) β β cache) β
ββββββββββββββ βββββββ¬βββββββ
β
βββββββββββββββββββββ΄βββββββββββββββββ
βΌ βΌ
ββββββββββββββββββββββββββ ββββββββββββββββββββββββββ
β Celery worker(s) β β Celery beat (singleton)β
β β’ Probe orchestrator β β β’ Scheduled probes β
β β’ AccessPlugin loader β β β’ Retention pruning β
β β’ Rule-based parsers β ββββββββββββββββββββββββββ
β β’ Drift engine β
βββββββββββββ¬βββββββββββββ
βΌ
Target AI endpoints
(REST / OpenAI / Anthropic / Open WebUI / MCP / Telegram / Mattermost / Email)
The decisions behind every box on this diagram are recorded as ADRs in docs/adr/.
- Docker 24+ and the Docker Compose v2 plugin (
docker compose, notdocker-compose). - Git (with submodule support β see below).
- GNU Make (optional, for the convenience targets).
- ~4 GB free RAM and a few GB of disk for images and the Postgres volume.
Everything runs in containers; you do not need a local Python or Node toolchain to deploy. (You do for backend/frontend development β see CONTRIBUTING.md.)
The upstream risk-framework data (MITRE ATLAS, OWASP LLM Top 10, AVID) is vendored as git submodules under assets/. Clone recursively so the framework seeder can load the full data sets. The backend Docker image bakes the MITRE ATLAS data in at build time (assets/atlas-data), so docker build fails fast if that submodule is not checked out. Running the backend directly from a checkout without the submodules seeds a built-in baseline stub instead.
git clone --recurse-submodules https://github.com/<your-fork>/Tezcat.git
cd Tezcat
# Already cloned without --recurse-submodules? Pull them now:
git submodule update --init --recursivecp deploy/env.example .envThen edit .env. At minimum, rotate every secret away from its dev default. See the configuration reference for the full list.
# Generate a Django secret key (50+ chars):
python3 -c 'import secrets; print(secrets.token_urlsafe(50))'
# Generate the credential-store data key:
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'Set, at a minimum:
DJANGO_SECRET_KEYβ a fresh random value.POSTGRES_PASSWORDandREDIS_PASSWORDβ rotate the dev defaults; keepDATABASE_URL/REDIS_URLin sync with them.TEZCAT_CREDENTIAL_KEY_V1β the key that encrypts every stored credential. If you lose or change it, all stored credentials become undecryptable. Keep it out of version control and back it up alongside (never inside) the database.DJANGO_ALLOWED_HOSTSandDJANGO_CSRF_TRUSTED_ORIGINSβ your real hostname(s) for anything beyondlocalhost.
The dev overlay runs Django via runserver, mounts the source for live reload, and binds ports to 127.0.0.1 only.
make dev # build + start: postgres, redis, web, worker, nginx (frontend built into the nginx image)
make logs # follow all containersOn first boot the web container runs migrations, seeds the framework + prompt catalogs, and collects static files automatically (TEZCAT_AUTO_MIGRATE=1, the default). This can take a couple of minutes; the container is marked healthy only once it finishes. To run those steps by hand instead, set TEZCAT_AUTO_MIGRATE=0 and use:
make migrate # python manage.py migrate
make seed # seed_frameworks + seed_promptsWithout make, the equivalent is:
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml up -d --buildOpen the app and complete the first-run setup wizard, which creates the initial Admin account:
open https://localhost # accept the self-signed certificate warningThe wizard only appears while no user exists (GET /api/v1/auth/setup-status β needs_setup). Alternatively, create a superuser from the CLI:
docker compose -f deploy/docker-compose.yml exec web python manage.py createsuperuserYou are now ready to register an endpoint, add a credential, build or pick a probe profile, run a probe, and review findings and drift.
The stack terminates TLS at nginx using nginx/tls/tezcat.crt and nginx/tls/tezcat.key (this directory is gitignored β no key material is committed). Generate a self-signed certificate for dev/internal use in either of two ways:
# From the CLI:
cd nginx/tls && ./generate-cert.sh testing # or: ./generate-cert.sh production <hostname>β¦or from the UI as an Admin: Settings β Deployment networking β Use self-signed SSL certificate, set the hostname, save, then docker compose restart nginx.
For internet-facing production, replace the self-signed cert with one issued by your corporate / internal CA. Full generation, verification, and rotation steps are in docs/runbook/tls.md and nginx/tls/README.md.
The prod overlay (deploy/docker-compose.prod.yml) runs Django under gunicorn (5 workers), scales Celery workers to two replicas, sets DJANGO_DEBUG=False, and publishes ports 80/443 on all interfaces. Bring it up with:
docker compose \
-f deploy/docker-compose.yml \
-f deploy/docker-compose.prod.yml \
up -d --buildBefore exposing it:
- Set
DJANGO_DEBUG=False, a realDJANGO_ALLOWED_HOSTS, and matchingDJANGO_CSRF_TRUSTED_ORIGINS. - Replace the self-signed certificate with a CA-issued one.
- Rotate all secrets and store
TEZCAT_CREDENTIAL_KEY_V1in your secrets manager, not in a checked-in.env. - Put the host behind your own network controls; Tezcat is single-tenant and assumes a trusted operator network. See ADR 0006.
Install, upgrade, and backup steps are covered in the sections above; TLS certificate management and deployment-networking changes have dedicated runbooks under docs/runbook/.
Scheduled probes, retention pruning, and deleted-drift purging are driven by a periodic tick from a Celery beat process. The compose stack ships a dedicated beat service, so these fire automatically once the stack is up β no extra steps. The schedule itself (probe dispatch every 60s, daily pruning) is defined in backend/tezcat/celery.py.
Important
beat is a singleton. Never scale it past one replica or every periodic task fires multiple times. The worker service is the one you scale for throughput. On-demand probes do not depend on beat.
All runtime configuration is via the .env file consumed by the web and worker containers. Deployment-time options that an Admin can change at runtime (probe limits, lockout thresholds, retention windows, MFA policy, networking/TLS) live in the in-app Settings area instead.
| Variable | Required | Default (dev) | Purpose |
|---|---|---|---|
DJANGO_SECRET_KEY |
Yes | β | Django cryptographic signing key. Use 50+ random chars. |
DJANGO_DEBUG |
β | True |
Set False in production. |
DJANGO_ALLOWED_HOSTS |
Yes (prod) | localhost,127.0.0.1,tezcat.local |
Comma-separated hostnames Django will serve. |
DJANGO_CSRF_TRUSTED_ORIGINS |
Yes (prod) | localhost origins | Comma-separated origins trusted for CSRF (scheme + host). |
TEZCAT_AUTO_MIGRATE |
β | 1 |
Run migrate + seed + collectstatic on web boot. Set 0 to manage manually. |
DATABASE_URL |
Yes | postgres://tezcat:devpw@postgres:5432/tezcat |
Postgres DSN. Keep credentials in sync with the POSTGRES_* vars. |
POSTGRES_DB |
Yes | tezcat |
Database name. |
POSTGRES_USER |
Yes | tezcat |
Database user. |
POSTGRES_PASSWORD |
Yes | devpw |
Database password β rotate this. |
REDIS_URL |
Yes | redis://:devredispw@redis:6379/0 |
Celery broker + cache DSN. |
REDIS_PASSWORD |
Yes | devredispw |
Redis password β rotate this. |
TEZCAT_CREDENTIAL_KEY_V1 |
Yes | β | Data key for the encrypted credential store. Never commit it; losing it makes stored credentials undecryptable. |
CORS_ALLOWED_ORIGINS |
β | empty (same-origin) | Comma-separated origins for cross-origin API access. Leave empty for the bundled single-origin deployment. |
Note
Postgres and Redis are docker-internal only (no published ports). In the dev overlay, web is published on 127.0.0.1:8000 and nginx on 127.0.0.1:80/443. Never bind these to 0.0.0.0 on an untrusted network.
- Health checks.
GET /api/v1/readyzreports readiness (used by the container healthcheck);GET /api/v1/auth/meconfirms an authenticated session. - Upgrades. Pull the new revision (
git pull --recurse-submodules), rebuild, and restart:docker compose ... up -d --build. WithTEZCAT_AUTO_MIGRATE=1, migrations run onwebboot. - Backups. Back up the
tezcat_pgdataPostgres volume and yourTEZCAT_CREDENTIAL_KEY_V1β both are required to restore. The encrypted credential ciphertext is useless without the key, and the key is useless without the database. - Retention. Raw-response and probe-run retention windows are configured in Settings β Data retention and enforced by the daily prune task (requires Celery beat β see above).
0means keep forever. - Audit. Every privileged action is recorded in an append-only audit log;
UPDATE/DELETEare revoked on the audit table at the Postgres grant level and never re-granted.
.
βββ assets/ Vendored risk-framework data as git submodules (ATLAS, OWASP LLM, AVID)
βββ backend/ Django + DRF + Celery + AccessPlugin host
β βββ apps/ One bounded context per app (auth, audit, inventory, credentials,
β β access, prompts, frameworks, probes, findings, drift, notifications,
β β settings, core)
β βββ plugins/ First-party AccessPlugins (rest_openapi, openai_compat, anthropic_compat,
β β open_webui, mcp, telegram, mattermost, email_imap)
β βββ api/ Generated OpenAPI 3.1 schema (committed; diffable in PRs)
β βββ tezcat/ Django project (settings, urls, asgi, wsgi, celery)
βββ frontend/ React 18 + Vite + TS + TanStack + shadcn/ui SPA
βββ deploy/ docker-compose.{yml,dev.yml,prod.yml} + env.example
βββ nginx/ nginx.conf + conf.d + Dockerfile + TLS material (gitignored)
βββ scripts/ set_version.py and other maintenance helpers
βββ tests-e2e/ Playwright golden-path test (login β probe β findings)
βββ docs/
β βββ adr/ Architecture Decision Records (load-bearing decisions)
β βββ api/ API contract notes (the schema itself lives in backend/api/)
β βββ data-model/ ER diagrams + per-app model walkthroughs
β βββ plugins/ How to write an AccessPlugin
β βββ runbook/ Operator runbooks (TLS, deployment networking)
β βββ threat-model/ Security posture + threat-model notes
βββ Makefile dev / test / lint / migrate / seed targets
βββ VERSION.txt Single source of truth for the app version
βββ LICENSE MIT
CONTRIBUTING.mdβ development setup, code style, and the test/lint gates.docs/adr/β Architecture Decision Records. Read these to understand why Tezcat is shaped the way it is. Start with 0001 (plugin contract), 0004 (capability/drift model), 0008 (scope and drift).docs/plugins/β how to write a newAccessPlugin, grounded in the contract inbackend/apps/access/contract.pyand the worked example inplugin-template/.docs/runbook/β operator runbooks for TLS certificate management and deployment-networking changes.- In-app docs β once deployed, the Docs section in the app provides concept guides (endpoints, credentials, prompts, profiles, probe runs, findings, drift, frameworks, RBAC, audit, settings).
A short summary; the deployed security-header posture and threat-model notes live in docs/threat-model/.
- Single-tenant, single-team (ADR 0006). No multi-workspace isolation. Three roles: Admin, Operator, Auditor. TOTP MFA and account lockout are configurable in Settings.
- Session-cookie + CSRF auth. No JWTs in
localStorage. The frontend fetch wrapper reads thecsrftokencookie automatically for non-GET requests. - Credentials are encrypted at rest with Fernet + HKDF; the data key is held in
TEZCAT_CREDENTIAL_KEY_V1(env var only, never in the database). Decryption is gated to plugin probe execution; the API surface never returns plaintext. See ADR 0005. - Audit log is append-only at the Postgres grant level: a migration revokes
UPDATEandDELETEon the audit table. Tests inapps/audit/tests/enforce this; the right is never granted back. - Plugins are pure. They never touch the DB, never parse responses, never classify capabilities. The orchestrator does all of that.
- No LLM in the analysis loop. Responses are classified by deterministic rule-based parsers, so a malicious target response cannot prompt-inject the classifier.
- Service-layer pattern. Views and Celery tasks call
apps/<app>/services.py; views never write to the ORM directly. Services re-check authorisation against theactorargument.
To report a vulnerability, follow the private disclosure process in SECURITY.md rather than opening a public issue.
Tezcat is deliberately scoped. Known boundaries of the current implementation:
- Single-tenant. No multi-workspace or multi-team isolation; the deployment assumes one trusted operator team behind your own network controls. See ADR 0006.
- First-party plugins only. The bundled channels run in-process; there is no sandboxed runtime for third-party plugins.
- Rule-based scoring. Capability scoring is threshold-based rule parsing. The adaptive-testing and item-response-theory measurement layer is documented as a research direction in ADR 0012, not as a shipped feature.
Tezcat began as a thesis project and is now developed in the open. Issues and pull requests are welcome:
- File issues for bugs and feature ideas. For security findings, follow the private disclosure process in
SECURITY.mdrather than opening a public issue. - To add an
AccessPluginfor a channel Tezcat doesn't yet support, start withdocs/plugins/. - Development setup, code style, and the test/lint gates are documented in
CONTRIBUTING.md.
MIT Β© 2026 TezcatAI.

