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.

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 against it, and verify the evidence chain offline.

$ attestral scan examples/vulnerable-agent
attestral · examples/vulnerable-agent 6 components · 14 findings · 4 critical · 7 high · 3 medium Attack paths (1) internal chain: entry: untrusted input ingested by a tool [jira, web] pivot: code execution [deploy, shell] impact: exfiltration [web] CRITICAL (4) ATL-103 Shell-capable MCP server configured (mcp_server.shell) fix: Replace generic shell access with narrowly scoped, allowlisted tools; gate with human approval. run: attestral explain ATL-103 ATL-103 Shell-capable MCP server configured (mcp_server.deploy) fix: Replace generic shell access with narrowly scoped, allowlisted tools; gate with human approval. run: attestral explain ATL-103 ATL-108 Agent tool calls auto-approved without a human checkpoint (mcp_server.shell) fix: Remove blanket auto-approval; allowlist only low-risk read-only tools and require confirmation for... run: attestral explain ATL-108 ATL-202 Tool fleet forms an exfiltration chain (lethal trifecta) (model) fix: Split the workflow so no single agent session combines private-data access with unrestricted egress... run: attestral explain ATL-202 HIGH (7) ATL-101 MCP server uses non-TLS transport (mcp_server.jira) fix: Serve the MCP endpoint over HTTPS/WSS only. run: attestral explain ATL-101 ATL-102 Filesystem MCP server rooted at a broad path (mcp_server.filesystem) fix: Scope the server to the narrowest project directory that supports the workflow. run: attestral explain ATL-102 ATL-105 MCP server auto-installs packages at launch (mcp_server.filesystem) fix: Pin the package to an exact version and integrity hash; vendor or mirror it; drop the auto-confirm... run: attestral explain ATL-105 ATL-105 MCP server auto-installs packages at launch (mcp_server.web) fix: Pin the package to an exact version and integrity hash; vendor or mirror it; drop the auto-confirm... run: attestral explain ATL-105 ATL-115 Remote MCP server holds a downstream credential (confused-deputy risk) (mcp_server.jira) fix: Do not co-locate delegated credentials with a remote endpoint. Exchange the caller's own identity f... run: attestral explain ATL-115 ATL-203 Tool fleet combines shell execution with outbound network reach (model) fix: Remove one side of the pair per session, or force shell use through an allowlisted, non-networked s... run: attestral explain ATL-203 ATL-207 Unsafe data flow - untrusted input can reach a sensitive action (model:taint_flow) fix: Break the path - keep untrusted-input tools and execution tools in separate agent sessions, or inte... run: attestral explain ATL-207 MEDIUM (3) ATL-104 Secrets passed to MCP server via environment (mcp_server.jira) fix: Use a secret manager or OS keychain; never place raw credentials where tool output can echo them. run: attestral explain ATL-104 ATL-106 MCP server pinned to a mutable tag (mcp_server.deploy) fix: Pin to an immutable version or digest so the reviewed tool is the tool that runs. run: attestral explain ATL-106 ATL-107 MCP server grants outbound network or browser access (mcp_server.web) fix: Constrain the tool to an allowlist of destinations; deny access to internal metadata endpoints and... run: attestral explain ATL-107 (no files written - add -o to save a report)

The scan is examples/vulnerable-agent: two small config files, fourteen design findings, three of them fleet-level. Compile and drift run against examples/demo-project. 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). --ml always works even with no extra installed: it degrades to a zero-dependency heuristic 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:

attestral scan

attestral scan PATH [--local] [-o STEM] [--format md|json|both|sarif|aibom]
               [--ml] [--llm] [--judge] [--waivers FILE] [--fail-on SEVERITY] [-q]
FlagBehavior
--localScan the MCP configs already installed on this machine (Claude Desktop, Cursor, VS Code, Windsurf) instead of a PATH.
-o, --outputWrite report files to this stem. Without -o or --format, results only print; nothing is written to disk.
--formatmd for the human report, json for the chained evidence, both, sarif for GitHub Code Scanning, or aibom for a CycloneDX 1.6 AI-BOM of the agent stack.
--mlScore agentic text surfaces (tool descriptions, system prompts) for prompt injection. Runs with zero extra installs; --ml-engine picks the tier. Findings are tagged origin: ml. See the ML layer page.
--llmAdds 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.
--judgeCross-examine findings with an LLM judge; see LLM-as-judge.
--waiversYAML of documented waivers; attestral-waivers.yaml at the scan root is picked up automatically.
--fail-onExit non-zero if findings at or above this severity exist. Works as a fail-closed CI gate.
-q, --quietSuppress 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:

Do not hand-edit a compiled policy. Change the design, re-run the review, re-compile. If policy and review can diverge silently, the evidence is worthless. That is the point of the binding.

attestral drift

attestral drift POLICY_FILE EVENTS_FILE [--fail-on-drift]
RuleSeverityFires when
DRF-001criticalA server appears in telemetry that is not in the attested design.
DRF-002criticalA server the review denied is invoked at runtime.
DRF-003highA filesystem path outside the attested roots is accessed.
DRF-004highA TLS-constrained server is observed over plaintext.
DRF-005criticalA server's tool manifest changed since attestation (a rug-pull).
DRF-006highA runaway tool-call loop suggests resource drain.
DRF-007mediumA 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.

Code scanning (SARIF)

Scan with --format sarif and the review becomes SARIF 2.1.0, the format GitHub Code Scanning speaks. 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.

A waiver suppresses a finding from the --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 of judges (--judge-panel N) votes, and the majority verdict wins, which blunts any single model's error.

The judge layer needs an API key and the anthropic package (pip install "attestral[llm]"). 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.

    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"]
    MatcherSemantics
    attr_equalsAttribute equals value exactly.
    attr_inAttribute is one of the listed values.
    attr_missingAttribute is absent from the ingested design.
    attr_starts_with / attr_containsString prefix / substring on the attribute.
    attr_list_contains / attr_list_any_ofMembership tests over list attributes.
    attr_any_containsAny of several needles across one or more attributes.
    model_has_bothModel-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.

    chain VALID · 4 entries recomputed in this browser

    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"]}
    FieldRequiredMeaning
    serveryesMCP server name as configured.
    toolyesTool invoked.
    argsnoArguments; paths are checked against attested roots.
    urlnoTransport endpoint; checked against TLS constraints.
    tsnoISO-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 optional ML layer (three tiers of 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.