Documentation
Reference for the open-source Attestral CLI, the security scanner for AI agents and MCP servers. Everything here is licensed Apache 2.0 and complete: nothing below requires an account, a key, or a server.
# install and run the whole loop, offline, in under a minute
pip install attestral
attestral scan ./my-project # review the design (16 findings on the demo)
attestral validate ./my-project # show the attack path is reachable
attestral verify review.json # chain VALID, offline
attestral compile ./my-project -o policy.yaml # turn the review into a default-deny policy
attestral drift policy.yaml events.jsonl # catch when runtime diverges from the design
Prefer clicking to installing? Run the whole loop live in the Playground, real CLI output streamed in your browser. Or jump to the loop, adversarial validation, or the rule pack.
See it run
Every panel below is real output from the CLI against the fixtures that ship in the repo, not a mockup. Switch tabs to walk the whole loop: review a deliberately vulnerable agent, inspect a rule, compile the attested design into a policy, catch runtime drift (the running system diverging from the reviewed design) against it, show a whole attack path is reachable, and verify the evidence chain offline.
The scan is examples/vulnerable-agent: two small config files, sixteen design findings, three of them fleet-level and two caught by the default prompt-injection classifier. Compile and drift run against examples/demo-project. In that output, (model:taint_flow) on ATL-207 means untrusted input reaching a sensitive action along a tracked path. To see how a scan flows through the package itself, the architecture page draws the pipeline from Attestral's own code graph.
Pipeline
A scan flows through three stages: ingest your agent (MCP configs, system prompts, tool descriptions) and the cloud it runs on into one model, review in layers (each finding tagged by origin), and commit to a tamper-evident evidence chain. The deterministic core is never silently mixed with model reasoning.
flowchart TB
MCP["MCP configs (mcp.json)"] --> M["SystemModel
components · edges · trust boundaries"]
SP["System prompts + tool descriptions"] --> M
TF["Terraform (.tf)"] --> M
K8S["Kubernetes manifests (.yaml)"] --> M
M --> L1["L1 Deterministic rules
typed matchers · fail-closed"]
L1 --> L2["L2 ML classifier (optional)
prompt-injection scoring, three tiers"]
L2 --> L3["L3 LLM: elicitation + judge
(optional, tagged origin: llm)"]
L3 --> W["Waivers
documented, expiring"]
W --> EV["Evidence: SHA-256 chain
verify offline"]
EV --> OUT["Markdown · JSON · SARIF · AI-BOM"]
And the product loop: attest the design, prove the record, enforce it as a runtime policy, and detect divergence.
flowchart LR
A["scan (attest)"] --> B["verify (prove)"]
A --> C["compile (enforce)"]
C --> D["drift (detect)"]
D -->|"design changed? re-attest"| A
Install
pip install attestral
attestral --version
Python 3.10+. The core has two dependencies (click, pyyaml). Optional extras: [llm] adds the anthropic package for elicitation and the judge; [onnx] adds a light, model-grade prompt-injection classifier without torch; [ml] adds the full local DeBERTa tier (transformers, torch). the heuristic prompt-injection tier runs by default with no extra installed; --ml / --ml-engine upgrades to a model-grade tier.
Try the full loop on the bundled demo:
git clone https://github.com/attestral-labs/attestral && cd attestral
attestral scan examples/demo-project
attestral compile examples/demo-project -o policy.yaml
attestral drift policy.yaml examples/demo-project/runtime-events.jsonl
The loop
Attestral treats the design review, the runtime policy, and the audit evidence as one artifact in three forms:
- Attest.
scanbuilds a system model from Terraform, Kubernetes manifests, and MCP configs and reviews it against typed rules. Findings are committed to a hash chain. - Enforce.
compileturns the reviewed model into a default-deny mcp-guard (Attestral's runtime enforcement layer) policy, stamped with the model hash and the review chain head. Your threat model stops being a document and becomes the configuration itself. - Prove.
driftdiffs runtime telemetry against the compiled policy. Anything the review never saw (a new server, a denied capability, an out-of-scope path) is drift, andverifydetects any later edit to the record (against the head you hold).
attestral scan
attestral scan PATH [--local] [-o STEM] [--format md|json|both|sarif|aibom]
[--ml] [--llm] [--judge] [--waivers FILE] [--fail-on SEVERITY] [-q]
| Flag | Behavior |
|---|---|
--local | Scan the MCP configs already installed on this machine (Claude Desktop, Cursor, VS Code, Windsurf) instead of a PATH. |
-o, --output | Write report files to this stem. Without -o or --format, results only print; nothing is written to disk. |
--format | md for the human report, json for the chained evidence, both, sarif for GitHub Code Scanning, or aibom for a CycloneDX 1.6 AI-BOM (AI Bill of Materials) of the agent stack. |
--ml / --no-ml | The zero-dependency heuristic prompt-injection tier runs by default. --no-ml turns it off; --ml / --ml-engine upgrades to the ONNX or DeBERTa tier. Findings tagged origin: ml. See the ML layer page. |
--aivss | Rank agentic findings by an OWASP AIVSS (AI Vulnerability Scoring System) Agentic AI Risk Score (AARS), a 0–10 score, each mapped to an OWASP Agentic (ASI, the OWASP Agentic Security Initiative) / LLM Top-10 category. AARS measures agentic amplification, a different axis from CVSS severity. |
--llm | Adds LLM threat elicitation on top of the deterministic layer. Requires ANTHROPIC_API_KEY. Findings are tagged origin: llm and never mixed silently with rule findings. |
--judge | Cross-examine findings with an LLM judge; see LLM-as-judge. |
--waivers | YAML of documented waivers; attestral-waivers.yaml at the scan root is picked up automatically. |
--fail-on | Exit non-zero if findings at or above this severity exist. Works as a fail-closed CI gate. |
-q, --quiet | Suppress per-finding detail; print only the summary and gate. |
Ingestion is automatic: *.tf feeds the cloud model; Kubernetes manifests feed the cluster model; mcp.json, *.mcp.json, and claude_desktop_config.json feed the agent runtime, along with system prompts, skill and instruction files, subagent definitions, hooks, and A2A agent cards.
attestral verify
attestral verify report.json
Recomputes the hash chain offline and exits 0 (VALID) or 1 (INVALID). No network, no server, no account. An auditor can verify a two-year-old report on an air-gapped laptop. The evidence chain section below runs the same check live on a real chain.
attestral compile
attestral compile PATH [-o mcp-guard-policy.yaml]
Compilation is fail-closed by construction:
default: deny, so a server absent from the reviewed design is never allowed.- A critical finding against a server compiles to
allow: falsewith the rule id as the recorded reason. - Filesystem servers are narrowed to their attested roots; broad roots (
/,~,/home) deny until the design is re-scoped. - TLS-constrained servers observed with
http://URLs deny. - The policy header carries the model hash and review chain head, which binds the policy cryptographically to the review that produced it.
attestral drift
attestral drift POLICY_FILE EVENTS_FILE [--fail-on-drift]
| Rule | Severity | Fires when |
|---|---|---|
DRF-001 | critical | A server appears in telemetry that is not in the attested design. |
DRF-002 | critical | A server the review denied is invoked at runtime. |
DRF-003 | high | A filesystem path outside the attested roots is accessed. |
DRF-004 | high | A TLS-constrained server is observed over plaintext. |
DRF-005 | critical | A server's tool manifest changed since attestation (a rug-pull, a tool silently changed after you approved it). |
DRF-006 | high | A runaway tool-call loop suggests resource drain. |
DRF-007 | medium | A server's call volume exceeds its attested budget. |
--fail-on-drift exits non-zero on any finding, so drift checks run as a CI step or a cron job against yesterday's telemetry.
attestral validate
The other commands stop at findings. attestral validate takes the attack path the model already assembled and walks it over the model's own edges to show it is reachable, entry to pivot to impact, then commits the walked path to the evidence chain. A finding is a claim about one config block; a walked, chained record of the exact path is not.
attestral validate PATH [-o STEM] [--fail-on-reachable] [--remediate] [--action-space] [--generate] [--execute]
Each reachable path becomes a redteam-origin finding: ATL-RT-EXTERNAL when an external caller can reach a sink, or ATL-RT-INTERNAL when a prompt injection can. A design with no complete path yields no reachable path, which is itself an attestable result. Pass -o to write the reachability report plus an evidence chain attestral verify confirms offline; --fail-on-reachable exits non-zero when any path is reachable, so it gates CI.
| Tier | What it does | Status |
|---|---|---|
| Symbolic | Walks each path over the model's edges. Deterministic, zero-dependency, no execution, no network. | shipping now |
| Generative | With --generate, an LLM drafts the predicted payload and transcript for a path. No live target is touched, labeled predicted. | shipping (opt-in) |
| Executed | With --execute, replays the path through Attestral's sandbox harness with a planted canary, capturing the transcript. No real system, secret, or network is touched; execution against your own fingerprinted live target stays gated. | shipping (harness) |
Verify the fix, not just the flaw. --remediate gives the minimal fix for each reachable path and verifies it by re-synthesis: it strips the rung, rebuilds the whole model, and re-runs the walk to confirm the path is gone from the graph. It also reports the OWASP AIVSS agentic risk score (AARS) before vs after each fix, and ranks fixes by how far they lower it, so the highest-leverage change comes first. --action-space enumerates the tool-call sequences the fleet can be induced into, not just the one collapsed chain.
Scope, by design. The symbolic tier never touches a live system, and the executed tier will run only against a target whose fingerprint matches the attested model, with explicit authorization, in a sandbox, moving planted canaries rather than real secrets. Attestral surfaces the reachable paths in a design you own; it is not a point-it-at-any-endpoint attack tool, and it never will be.
Watch it render live: the interactive proof walker lets you toggle a fleet, animate the attack path across the trust boundary, and break a chain in your browser.
Code scanning (SARIF)
Scan with --format sarif and the review becomes SARIF 2.1.0 (the static-analysis result format GitHub's Security tab reads). Findings show up in the Security tab and inline on the pull request, each mapped to a severity GitHub understands and tagged with its framework references.
attestral scan . --format sarif -o attestral
Wire it into a workflow and every push publishes findings straight to the Security tab:
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: attestral.sarif
A complete workflow ships in examples/github-actions/code-scanning.yml. The job needs security-events: write permission to upload.
Baseline & waivers
Real repositories start with findings. A waiver lets you accept a known risk and keep the gate green, without hiding anything: the finding stays in the evidence chain with its justification, so an auditor sees exactly what was accepted and why. Drop an attestral-waivers.yaml at your scan root and it is picked up automatically.
waivers:
- rule: ATL-005
component: aws_db_instance.app # or "*" for every component
reason: Encryption enforced at the storage layer; tracked in SEC-1234.
expires: 2026-12-31 # optional; waiver lapses after this date
Two rules keep waivers honest. A waiver with no reason is ignored, and an expired waiver stops suppressing, so a finding can only be silenced by a current, justified exception. In SARIF, a waived finding becomes a suppression, which GitHub Code Scanning shows as dismissed rather than an open alert.
--fail-on gate. It does not remove it. The whole point of the evidence chain is that accepted risk stays on the record.LLM-as-judge
Deterministic rules fire on patterns; some are false positives in context. The judge is an optional layer that cross-examines each finding. It sees the finding plus its component and returns a structured verdict, confirmed, false_positive, or needs_review, with a confidence and reasoning. Verdicts are recorded on the finding and carried into the evidence chain, so the judgment itself is auditable.
export ATTESTRAL_JUDGE_API_KEY=... # or reuse ANTHROPIC_API_KEY
attestral scan . --judge --judge-panel 3 # 3 judges vote per finding
attestral scan . --judge --judge-suppress
The judge never deletes a finding. By default it only annotates. With --judge-suppress, a high-confidence false_positive becomes a machine-generated waiver whose reason is the judge's reasoning: suppressed from the gate, but kept on the record like any human waiver.
A panel (--judge-panel N) is a real cross-examination, not the same prompt polled N times: each panelist reviews through a different adversarial lens, one for exploitability, one for a compensating-control false positive, one for blast radius, and the majority verdict wins, which blunts any single angle's error. The verdict itself is schema-constrained, so a well-formed result is guaranteed rather than parsed hopefully, and if a call fails the scan says so instead of quietly returning nothing. Tune rigor per run with --judge-effort (low through max).
anthropic package (pip install "attestral[llm]"). It defaults to claude-opus-4-8; override with --judge-model. Like the elicitation layer, its output is clearly separated from the deterministic core.The rule pack
Every deterministic check ships in the open, in plain YAML, and each cites the control it enforces. This is the complete built-in pack, searchable. Click a rule for the same detail attestral explain prints. For the holistic picture, what each of the five packs covers, how they compose, and what the ID bands mean, see the rule packs guide.
The index is generated from the packs in attestral/rules/ at build time, so it matches the release exactly. Agentic and cross-boundary rules cite OWASP Agentic Security and MITRE ATLAS; cloud rules cite CIS and NIST.
Writing rules
Rules are YAML with structured matchers. There is no eval anywhere, and an unknown matcher fails closed: the rule simply never matches.
rules:
- id: ORG-001
title: Internal ALB missing auth attribute
severity: high
target: aws_lb # component type prefix, or "model"
match: { attr_missing: auth }
description: ...
recommendation: ...
frameworks: ["NIST AC-3", "SOC2 CC6.1"]
| Matcher | Semantics |
|---|---|
attr_equals | Attribute equals value exactly. |
attr_in | Attribute is one of the listed values. |
attr_missing | Attribute is absent from the ingested design. |
attr_starts_with / attr_contains | String prefix / substring on the attribute. |
attr_list_contains / attr_list_any_of | Membership tests over list attributes. |
attr_any_contains | Any of several needles across one or more attributes. |
model_has_both | Model-level: both component type prefixes exist in the design. |
Load custom packs alongside the core pack via RuleEngine(["org_rules.yaml"]).
Evidence chain
Every run commits its findings to a SHA-256 hash chain: entry N hashes the canonical JSON of finding N together with the hash of entry N-1, starting from a zero genesis. The final hash (the chain head) appears in the report header and in every compiled policy.
The consequence: altering, inserting, or deleting any past entry changes every subsequent hash. A reviewer who records the chain head (in a PR comment, a ticket, an email) has permanently committed the whole review.
Try to tamper with one. Below are the first four entries of a real chain from attestral scan examples/demo-project. Downgrade an entry's severity and your browser recomputes the chain with the exact algorithm attestral verify uses. Watch the break propagate.
Each card shows the entry's recorded hash and the hash of the entry before it. Tampering rewrites one finding's canonical JSON, so its recomputed hash no longer matches the recorded one, and every later entry points at a hash that no longer exists. The only way to hide an edit is to rewrite every subsequent entry, and the chain head an auditor recorded elsewhere still gives it away.
Telemetry format
drift reads JSONL, one event per line. mcp-guard emits this format natively, and anything else can adapt to it in a few lines:
{"ts": "2026-07-10T14:01:02Z", "server": "docs", "tool": "read_file", "args": ["/srv/docs/design.md"]}
| Field | Required | Meaning |
|---|---|---|
server | yes | MCP server name as configured. |
tool | yes | Tool invoked. |
args | no | Arguments; paths are checked against attested roots. |
url | no | Transport endpoint; checked against TLS constraints. |
ts | no | ISO-8601 timestamp, carried into drift findings. |
GitHub Action
Run drift checks on a schedule so conformance is continuous, not annual:
- uses: attestral-labs/attestral@v1
with:
policy: policy.yaml
events: events.jsonl
A complete nightly workflow (pull telemetry, re-compile the policy from the reviewed main branch, drift-check) ships in examples/github-actions/drift-schedule.yml. The mcp-guard telemetry emitter lives in integrations/mcp-guard/.
Terraform extra
The core ships a dependency-free Terraform scanner that resolves variables, locals, and modules. For full-fidelity HCL parsing (nested blocks, correct types), install the extra. Attestral picks it up automatically and falls back gracefully on malformed files:
pip install "attestral[terraform]"
Roadmap
Everything documented above ships today: the deterministic rule pack you can search on this page, SARIF, waivers, the ML layer (a zero-dependency heuristic tier that runs by default, plus optional ONNX and DeBERTa tiers; prompt-injection detection on agentic surfaces, tagged origin: ml, with a fine-tuning recipe in training/), and the LLM-as-judge verifier. Next up, roughly in order: more cloud and Kubernetes rules, PR design-diffing, and richer drift telemetry adapters. Issues and contributions are welcome on GitHub.