Attestral
The security scanner for AI agents and MCP servers.
Your agent has a shell, a browser, your database, and a Slack token. Each tool is fine on its own. Together they are one injected sentence away from walking your secrets out the door. Attestral is the scanner that reads the whole picture.
It parses your MCP configs, agent instructions, system prompts, tool descriptions, and agents defined in code (LangGraph, CrewAI, the OpenAI Agents SDK, raw Anthropic/MCP tool definitions), builds a single system model of the fleet, and reviews the agentic surfaces every other scanner walks right past: prompt injection, tool poisoning, excessive agency, memory poisoning, and the toxic flows that only exist across servers. A shell tool and an egress tool are one injected sentence apart whether they were declared in .mcp.json or three @tool functions, and Attestral sees the flow either way. It models your cloud (Terraform) and Kubernetes in the same graph, so it sees the trust boundary between the agent and the infrastructure it can reach, not each in isolation - and the graph carries real node-to-node edges (Terraform references, Kubernetes Service and ServiceAccount links, and credential reach from a cred-holding server to the exact same-provider resources it can touch), not just co-presence.
Three layers, and every finding is labeled by which one found it: deterministic rules (always on, no eval, fails closed), an optional local ML classifier for injection text, and an optional LLM-as-judge to cut false positives. Every finding lands in a tamper-evident SHA-256 evidence chain you can hand an auditor and verify offline. No account, no server, no telemetry.
pip install attestral
attestral scan ./my-project
Scan the MCP servers you've already installed
No repo needed. Audit the MCP servers your agent tools are already wired to:
attestral scan --local # full report: inventory + every finding
attestral scan --local --card # a compact, screenshot-ready self-audit card
Discovers and scans configs from Claude Code (user scope, project .mcp.json, and the current project's local scope inside ~/.claude.json), Claude Desktop, Cursor, VS Code, and Windsurf. It reports which sources were found vs absent and how many servers each contributed, prints an inventory of the agent tool surface it reviewed, and runs everything through the same rule pipeline as a repo scan.
--card answers one question in one screen: does your installed fleet assemble the lethal trifecta, yes or no. It is honest by construction, a single-server machine is told the cross-surface checks could not fire (a thin result, not a clean bill of health), and a genuinely clean machine gets a clean result rather than a manufactured scare.
Get started in one command
attestral init # scaffold CI, pre-commit, a waivers file, and a Claude Code skill
attestral scan . # review the current project - prints straight to your terminal
attestral init writes four onboarding files, and never overwrites anything that already exists (existing files are skipped and reported):
| File | What it does |
|---|---|
.github/workflows/attestral.yml |
Reviews every PR by installing the attestral CLI from PyPI in an inline job (works whether the repo is public or private): a sticky review comment, findings in the Security tab, and a net-new gate. |
.pre-commit-config.yaml |
Runs attestral on every commit (see pre-commit). |
attestral-waivers.yaml |
Starter for documented, expiring exceptions. |
.claude/skills/attestral-review/SKILL.md |
Makes Attestral a review reflex in Claude Code: it knows to scan when you add or edit an MCP server, agent prompt, or tool. |
Review every pull request
Already have a repo on GitHub? Drop this workflow in and every PR gets a sticky review comment, inline annotations in the Security tab, and a build gate that fails only on net-new high or critical findings. It installs the attestral CLI from PyPI (which is public) and runs it inline, so it works whether your repo is public or private.
# .github/workflows/attestral.yml
name: attestral
on: [pull_request]
permissions:
contents: read
security-events: write # SARIF into the Security tab
pull-requests: write # the sticky review comment
jobs:
design-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- run: pip install --quiet "attestral[terraform]"
- run: attestral scan . --format sarif -o attestral
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: attestral.sarif
# A sticky PR comment + a job summary, then a net-new gate. See the file
# `attestral init` scaffolds for the github-script comment step in full.
- run: |
BL=""; [ -f attestral-baseline.json ] && BL="--baseline attestral-baseline.json"
attestral scan . $BL --format md-summary -o attestral
cat attestral.summary.md >> "$GITHUB_STEP_SUMMARY"
- run: |
BL=""; [ -f attestral-baseline.json ] && BL="--baseline attestral-baseline.json"
attestral scan . $BL --min-confidence high --fail-on high --quiet
Commit an attestral-baseline.json (from attestral scan . --baseline attestral-baseline.json --update-baseline) so day-one debt does not block the first PR and only newly introduced findings fail the gate. attestral init scaffolds this exact workflow for you, including the sticky-comment step.
Use it inside Claude Code
Attestral also ships as a Claude Code plugin, so the review reflex travels with you across projects:
/plugin marketplace add attestral-labs/attestral
/plugin install attestral@attestral-labs
The plugin's attestral-review skill runs attestral scan when your agent's attack surface changes (a new MCP server, tool, or system prompt), then explains the findings and how to gate them. attestral init scaffolds the same skill into a single repo; the plugin makes it available everywhere.
Zero config: point it at a repo
No model file, no setup. attestral scan . autodiscovers the Terraform, the Kubernetes manifests, the .mcp.json, the agent instructions and A2A cards in a repo and reviews them in one graph. Every scan opens with what it found and, honestly, what it did not read, so a clean result reads as "clean", never "it didn't look":
Reviewed 6 components across 2 source files: 6 agent / MCP surface
Design review, not SAST: reads declared config and agent wiring, not arbitrary application logic.
Terminal-first output
attestral scan prints a colour-coded, severity-grouped review straight to your terminal and writes nothing to disk by default - no more attestral-report.* files littering your repo. Ask for report files explicitly, with -o (a file stem) or --format:
attestral scan . # print only - nothing is written
attestral scan . -o review # write review.md + review.json
attestral scan . --format sarif -o out # write out.sarif for GitHub Code Scanning
attestral scan . --format aibom -o inv # write inv.cdx.json - a CycloneDX 1.6 AI-BOM
attestral scan . --format html -o topo # write topo.html - blast-radius topography: trust-boundary zones + walked attack paths
attestral scan . --quiet --fail-on high # CI: just the summary + gate line, exit 1 on high+
attestral scan . --baseline attestral-baseline.json # first run records; later runs show only net-new
The AI-BOM is the inventory counterpart to the findings: every MCP server, subagent, A2A endpoint, and instruction surface in the scan as a CycloneDX 1.6 component or service - with pinned-package purls, capability classes, canonical manifest hashes, and the authenticated flag on remote endpoints - ready for the compliance and procurement workflows that consume SBOMs today.
--quiet drops the per-finding detail and prints only the summary and gate (nothing at all on a clean scan). Colour is emitted only to an interactive terminal and is suppressed under NO_COLOR or when the output is piped, so CI logs and pipes stay plain.
Explain any rule
attestral explain ATL-103 # title, severity, description, fix, and framework refs
Every finding in the terminal output carries a run: attestral explain <RULE_ID> pointer, so the reasoning and the fix are one command away. Rule ids are matched case-insensitively.
Grade the whole design: attestral grade
A scan hands you a list of findings; a grade answers what that list does not - given everything reviewed, how is this design doing overall - and, unlike a finding list, it credits good architecture (a retained human checkpoint, scoped tools, constrained egress, no lethal trifecta), not only the bad. The letter always ships with its reasons: what lowered it, what held or raised it, and the specific findings whose removal would raise it.
attestral grade ./my-project # one defensible letter A-F, with the full reasoning
attestral grade ./my-project --fail-under B # CI gate: exit 1 if the posture grade is below B
attestral grade ./my-project -o grade.json # write the itemized grade as JSON
The rubric is transparent and deterministic: start from A, deduct for findings (a high costs a step, a medium less, lows are noted but never cap), let a critical or a present lethal trifecta cap the grade at D, and credit up to six detectable good patterns (about one high finding's worth) - so strong architecture recovers from a minor issue instead of being scored like a fragile design with the same finding count. A grade is a heuristic summary of the declared design, not a safety guarantee, and the output says so.
Latest: five ways the system model got deeper
The newest work pushes the moat, a whole-system model, further than a per-file scanner can follow. Each of these reasons across components, edges, and trust boundaries, and each ships with a fixture and a real control citation.
flowchart LR
subgraph NEW["what a system model now reasons about"]
G["Posture grade A-F<br/>attestral grade<br/>credits good architecture,<br/>not only findings"]
T["Multi-hop taint / IFC<br/>ATL-224<br/>source -> hops -> sink path,<br/>clears on declassification"]
S["Agent Skills supply chain<br/>ATL-177 / ATL-178<br/>SKILL.md claim-vs-capability,<br/>external source"]
MA["Memory + A2A<br/>ATL-225 / ATL-226<br/>cross-agent memory poisoning,<br/>unauthenticated agent card"]
D["Cross-session drift<br/>DRF-013<br/>a task split across sessions,<br/>recombined at runtime"]
end
| Capability | What only a system model catches | Try it |
|---|---|---|
| Posture grade A-F | Given everything reviewed, one defensible letter that credits a retained human checkpoint, scoped tools, and constrained egress, not only the bad. vulnerable-agent grades F; a hardened design grades A. |
attestral grade ./my-project |
Multi-hop taint / IFC (ATL-224) |
Propagates confidentiality/integrity labels along the graph and reports the ordered source to hops to sink path. A declassifier (egress allowlist / approval gate) on the path clears it while the co-presence alarm still rings. | attestral scan ./my-project |
Agent Skills supply chain (ATL-177/178, OWASP Agentic-Skills Top 10) |
A SKILL.md that claims low risk while granting broad tools, or that sources its content from an external location, is a shareable, auto-loaded supply-chain surface. |
attestral scan ./my-project |
Memory + inter-agent (ATL-225/226, OWASP ASI06/ASI07) |
A shared writable memory store an untrusted-input agent writes and a different agent reads (cross-agent poisoning), and an unauthenticated A2A agent card exposing privileged skills. | attestral scan ./my-project |
Cross-session drift (DRF-013) |
A harmful task split into individually-permissible requests across fresh sessions, then recombined, defeats per-session enforcement. Drift correlates the sessions on a strong link and reports the joint capability. | attestral drift policy.yaml events.jsonl |
What it catches (294-rule pack)
| Area | Examples |
|---|---|
| Agentic / MCP (OWASP LLM Top 10, MCP research, 2026 CVEs) | shell-capable servers, broad filesystem roots, non-TLS transport, secrets in env, auto-installed packages (supply chain), mutable @latest tags (rug-pull), outbound-fetch/browser tools, auto-approved actions, unauthenticated remote servers, confused-deputy credential holders, known-CVE package versions (e.g. mcp-remote CVE-2025-6514), known-CVE versions in the agent's own dependency manifest (requirements.txt / pyproject / package.json, e.g. langchain-core "LangGrinch" CVE-2025-68664), hook config-injection in .claude/settings.json (CVE-2025-59536) |
| Memory / context poisoning (OWASP ASI06, agent-security SoK) | world-writable agent-instruction files (CLAUDE.md, .cursorrules, AGENTS.md) that anyone can rewrite to steer every future run; persistent memory / vector stores as memory-poisoning targets; a shared writable store an untrusted-input agent writes and a different agent reads (the cross-agent GraphWake channel) modeled as a store component with read/write edges |
| Inter-agent / A2A (OWASP ASI07) | agent cards with no authentication, plaintext-HTTP or auth-defined-but-not-required endpoints, removed OAuth flows, unsigned public cards, and unauthenticated cards exposing skills that read private data or execute actions (auth-vs-capability mismatch); external-agent reachability into sensitive tools and cloud |
| Agent skills (SKILL.md) | packaged, auto-loaded skills that grant shell or wildcard tool access (excessive agency in a shareable artifact); skill text scored for injection like any instruction file |
ML layer (attestral[ml]) |
prompt-injection / jailbreak text in MCP tool & server descriptions, system prompts, and agent-instruction files |
| AWS (CIS-grounded) | public S3/RDS/Redshift, 0.0.0.0/0 security groups, wildcard IAM, unencrypted RDS/EBS/EFS/Neptune, disabled backups, KMS rotation off, public EC2/EKS, CloudTrail gaps, mutable ECR tags, plaintext ELB listeners |
| Azure (CIS-grounded) | public blob access, non-HTTPS storage, storage TLS < 1.2 and no infrastructure encryption, public SQL, wildcard NSG rules, Key Vault purge protection off / public network access, Postgres/MySQL SSL not enforced, Postgres flexible server public access, SQL database TDE off, App Service not HTTPS-only, VM password auth, AKS local accounts enabled |
| GCP (CIS-grounded) | 0.0.0.0/0 firewall rules, public Cloud SQL, SQL without SSL, public bucket IAM (allUsers), bucket uniform-access off, KMS keys without rotation, Compute cloud-platform scope / IP forwarding / non-Shielded VMs, GKE legacy ABAC, non-private nodes, non-Shielded nodes, client-cert auth |
| Kubernetes (CIS K8s) | privileged containers, privilege escalation, dangerous capabilities, run-as-root, host network/PID, hostPath mounts, missing resource limits, mutable image tags |
| Cross-cutting / toxic flows (fleet-level, only visible in a system model) | lethal-trifecta capability combos (private data + egress), the same flow as a formal information-flow lattice property (confidentiality/integrity labels, ATL-217) that clears when you apply the recommended mitigation (an egress allowlist or a human-approval gate) while the heuristic still fires, unsafe data flow (untrusted input → code execution, with named source/sink servers and taint edges), shell + network reach, cross-server tool shadowing (tool-name collisions, steering descriptions, server-identity conflicts), cross-server split-injection (an injection payload cut across two servers' descriptions so each half reads benign, detected by scoring the reassembled pair a reference marker names), agent runtime and cloud sharing no declared boundary controls |
Every finding maps to NIST 800-53, ASVS, SOC 2, CIS (AWS/Azure/GCP/K8s), OWASP LLM/Agentic, and MITRE ATLAS references. The agentic checks are additionally mapped to the attack/risk taxonomy of the agent-security SoK (Kim et al. 2026) in docs/agentic-threat-model.md.
Recall you cannot self-grade. The in-repo benchmark scores 189/189, but its labels come from our own fixtures. So we also measure against eleven published 2025-2026 CVE advisories labelled from the advisory, not our output (evaluation/external-recall.md): that number is allowed to fall below 100% and does, with every miss itemised and a concrete path to close it.
How a scan works (the pipeline)
flowchart TB
subgraph ING["1 · Ingest"]
TF["Terraform (.tf)<br/>vars · locals · local modules resolved"] --> M
K8S["Kubernetes<br/>manifests (.yaml)"] --> M
MCP["MCP configs<br/>(mcp.json, JSONC-tolerant)"] --> M
SP["System prompts, agent instructions<br/>(CLAUDE.md/.cursorrules), skills (SKILL.md)<br/>+ tool descriptions"] --> M
AC["Agent settings + hooks, subagents,<br/>A2A agent cards (.claude/**, .well-known/)"] --> M
CODE["Agent code (.py)<br/>@tool functions, Anthropic/MCP tool defs,<br/>LangGraph · CrewAI · OpenAI Agents SDK"] --> M
DEP["Dependency manifests<br/>(requirements.txt · pyproject · package.json)<br/>known-CVE version match"] --> M
GHA["GitHub Actions workflows<br/>(.github/workflows/*.yml)<br/>untrusted-trigger · AI agent · secrets · shell"] --> M
GW["Credential broker config<br/>(agentgateway · CB4A)<br/>fail-open · inlined secret"] --> M
DENV["Deployment env<br/>(docker-compose · .env)<br/>credential concentration"] --> M
LC["Installed agent configs<br/>(scan --local)"] --> M
M["SystemModel<br/>components · edges · trust boundaries"]
end
M --> L1
subgraph REV["2 · Review (layered, each finding tagged by origin)"]
L1["<b>L1 Deterministic rules</b><br/>294 typed matchers · fail-closed<br/>+ cross-server attack path synthesis<br/>+ cross-boundary reach into named cloud/IaC sinks<br/>+ cross-repo fleet toxic-flow detection<br/>+ information-flow lattice (IFC labels)<br/>+ multi-hop information flow (labelled edge paths)<br/>+ OWASP AIVSS agentic risk score<br/>origin: deterministic"]
L2["<b>L2 ML classifier</b> (optional)<br/>DeBERTa prompt-injection on agentic surfaces<br/>origin: ml"]
L3["<b>L3 LLM</b> (optional)<br/>elicitation + LLM-as-judge verifier<br/>origin: llm"]
L1 --> L2 --> L3
end
REV --> RS["Reachability-based severity<br/>finding on a walked attack chain:<br/>chain attached · raised one band"]
REV --> BR["Blast-radius scoring<br/>rank every surface by if-compromised reach<br/>feeds the OWASP AIVSS score"]
REV --> IR["Injection-reachability fusion<br/>escalate an injectable surface only when it<br/>can reach a secret, egress, or code execution"]
REV --> TA["Trust-asymmetry<br/>raise a tool-name collision when a lower-trust<br/>server can shadow a trusted tool"]
RS --> W["Waivers + inline suppression<br/>documented exceptions · one-line // attestral:ignore"]
BR --> W
IR --> W
TA --> W
W --> BL["Baseline<br/>diff-aware: report only net-new findings"]
BL --> EV["3 · Evidence<br/>SHA-256 hash chain · verify offline<br/>+ optional Ed25519/DSSE signed head"]
EV --> OUT["Output: Terminal (default, writes nothing) · Markdown · JSON · <b>SARIF</b> (Code Scanning) · <b>AI-BOM</b> (CycloneDX 1.6) · <b>Threat Topography</b> (interactive HTML)"]
EV --> GRADE["Posture grade (A-F)<br/>one defensible letter · credits good patterns<br/>attestral grade"]
style L1 fill:#0a7d3611,stroke:#0a7d36
style L3 fill:#96222E11,stroke:#96222E
| Layer | What it does | Reproducible? | Cost |
|---|---|---|---|
| L1 Deterministic | 294 typed matchers over the model, fail-closed (unknown matcher never matches), plus cross-server attack-path synthesis | Yes, fully | Free, offline |
| L2 ML (optional) | Scores agentic text surfaces (MCP tool/server descriptions, system prompts, embedded MCP Apps HTML bodies) for prompt injection / jailbreaks. Three tiers: zero-dep heuristic (default), ONNX (attestral[onnx], model-grade, no torch), or DeBERTa (attestral[ml]). The model tiers layer over the heuristic (never replace it): they score the decoded text too, and a model-only hit the pattern bank cannot corroborate is reported one band lower |
Pinned model + revision | Free, offline after first cache |
| L3 LLM (optional) | Elicits novel design threats, and a judge cross-examines findings to cut false positives | Verdicts recorded in the chain | Your API key |
Every finding carries its origin, so the deterministic core is never silently mixed with model reasoning. That separation is what makes the review audit-grade.
Severity you can defend. When a finding's component sits on an attack chain the symbolic walk shows reachable (a way in, a way to run code, a way out), the finding carries that chain and is raised one severity band - never above the chain's own severity:
HIGH (3)
ATL-107 MCP server grants outbound network or browser access (mcp_server.web)
path: internal chain: web -> ops -> web · this component: entry+impact · raised from medium
A raised HIGH ships with the entry → pivot → impact path that justifies it, so it is trusted rather than argued with. The inverse move is deliberately never made: a finding off every chain is not downgraded, because the absence of a modeled path is not evidence of safety.
The sophistication layers (optional)
# ML prompt-injection scan of agentic text surfaces (MCP tool/server descriptions,
# system-prompt files, embedded MCP Apps HTML bodies). Hits are tagged origin: ml
# and flow into the same evidence chain.
# The zero-dep heuristic tier runs on EVERY scan by default (no flag, no install);
# --ml opts into the model-grade tiers. Three tiers:
# heuristic zero-dependency, instant, ships in core <- the default, always on
# onnx model-grade DeBERTa via onnxruntime, no torch, ~276 MB <- recommended upgrade
# deberta heaviest, fine-tunable, pulls torch (~700 MB+)
# `--ml` selects engine `auto` (precedence onnx -> deberta -> heuristic); a missing
# extra is never an error, it just falls back. `--no-ml` turns the layer off entirely.
attestral scan ./my-project # heuristic tier already included
pip install "attestral[onnx]" # add the light, accurate ONNX tier
attestral scan ./my-project --ml --ml-engine onnx # weights auto-download once, offline after
# custom or air-gapped model? run scripts/export_onnx.py, then set ATTESTRAL_ML_MODEL=/path
attestral scan ./my-project --ml --ml-threshold 0.7 # tune sensitivity
# LLM threat elicitation on top of the deterministic layer
export ANTHROPIC_API_KEY=...
attestral scan ./my-project --llm
attestral scan ./my-project --llm-deep # fan across 4 adversarial lenses, dedup (deeper, ~4x cost)
# LLM-as-judge: cross-examine findings to cut false positives.
# Verdicts (confirmed / false_positive / needs_review) are recorded in the chain.
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 # auto-waive confident false positives, on the record
The judge never deletes a finding. A confident false_positive becomes a machine-generated waiver carrying the judge's reasoning: suppressed from the gate, but kept on the record.
Every finding also carries a static confidence (high / medium / low). Deterministic rules are high by contract - structural facts with zero false positives on the benign corpus - while the ML tier's confidence tracks its probability. attestral scan --min-confidence high keeps only that CI-safe set and reports what it dropped, so you can fail a build on the findings that cannot be wrong and leave the probabilistic ones for a human. For the individual false positive, a one-line // attestral:ignore ATL-xxx reason: ... marker in the config waives it in place (kept in the evidence chain, not hidden). The benign zero-FP promise is a gated test, not a claim; see docs/false-positive-budget.md.
Tuning / training the ML layer
The ML layer ships pointed at a DeBERTa classifier already fine-tuned for prompt injection, so start zero-shot (--ml, no training). If you need to adapt it to your own surfaces, climb three tiers - use as-is, calibrate the --ml-threshold on your labeled data, then fine-tune only if a gap remains. A runnable recipe (fine-tune + threshold-calibration scripts, data format, and where to source training data) lives in training/.
Baseline and waivers
Real repos start with findings. A waiver accepts a known risk and keeps the gate green without hiding anything: the waived finding stays in the evidence chain with its justification, and becomes a SARIF suppression (GitHub shows it dismissed, not open).
# attestral-waivers.yaml (auto-discovered at the scan root)
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
Fail-safe: a waiver with no reason is ignored, and an expired waiver stops suppressing. A finding can only be silenced by a current, justified exception.
Accepting a risk is itself an audit record
Prefer attestral accept over hand-editing the YAML - it writes the waiver with provenance and a content pin:
# one line, copied from the finding in the scan output
attestral accept . ATL-104 mcp_server.jira -r "Secrets rotated nightly; tracked in SEC-1234." --expires 2026-12-31
The recorded entry says who accepted the risk (your git identity), when, why, and what was accepted - a finding_sha256 pin over the rule, component, severity, and reachable chain as they were at acceptance. The suppressed finding carries that provenance into the evidence chain, so an auditor reads "this engineer accepted this risk on this date with this justification" straight from the record.
The pin is what keeps the acceptance honest: if the risk itself changes - a rule wave re-rates the finding, or a new tool completes an attack chain through the component and reachability raises its severity - the pin stops matching, the scan reports the acceptance as stale, and the finding comes back until someone re-accepts the current risk. You accepted a medium; you did not accept the high it became.
The flow that spans repos: attestral fleet
Agentic risk lives in the integration. A shell tool in one repo and an untrusted-input tool in another are each fine on their own; together they are an attack chain. No per-repo scanner can see that, because each repo is clean in isolation. attestral fleet models several repos as one system:
attestral fleet ./data-agent ./ops-agent ./notify-agent
Fleet: 3 repos
data-agent 2 components · reach: network, saas_data
ops-agent 1 components · reach: shell
notify-agent 1 components · reach: messaging
cross-repo chain: entry [data-agent] -> pivot [ops-agent] -> impact [notify-agent]
It merges every repo into one graph (tagging each component with its repo), then runs the full review over the union. When the fleet's combined capabilities complete an attack chain that no single repo completes alone, it fires ATL-213 and names which repo supplies the entry, the pivot, and the exfiltration. Reachability escalation follows: a medium finding in one repo is raised to high because another repo is what completes its chain. This is the one thing a single-file scanner structurally cannot answer, and it is the point of building a system model in the first place.
Beyond findings: prove it, enforce it, verify it
A scanner stops at a list of findings. Attestral turns the reviewed design into a tamper-evident record and a runtime policy: the depth that makes the review audit-grade, and the reason it can't be trivially cloned. Attest the design, prove the record has not been altered, compile it into a default-deny runtime policy, and detect when what runs diverges from what was reviewed. The whole loop runs offline, on a laptop, free.
Integrity, and now authenticity
The SHA-256 chain is tamper-evident: edit any past finding and every later hash, and the head, stop matching. On its own that proves the chain is internally consistent, not that it is the chain you sealed, an attacker could edit a finding, recompute the whole chain and a new head, and verify would still say VALID. attestral sign closes that with an Ed25519 signature over the head, wrapped in a DSSE envelope (the same envelope Sigstore and in-toto use). Now attestral verify --public-key checks both: integrity (no entry altered) and authenticity (this is the chain the key holder sealed, not a recomputed forgery). Signing needs the attestral[sign] extra; the integrity check still runs with zero dependencies.
Sovereign mode: run fully local, and prove it
In data-residency markets (GCC, defense, regulated finance), "no client code, config, or data leaves the environment" is frequently the only compliant posture, not a preference. Attestral is local by default; --sovereign makes that enforced and provable. It refuses the online layers fail-closed (--llm, --judge, and the networked ML tiers), arms an egress guard that blocks any outbound connection at the socket for the whole review (loopback and UNIX sockets still work; a blocked attempt is recorded, never hidden), and seals a self-verifying no-egress attestation bound to the evidence-chain head. attestral verify recomputes the attestation and its chain binding, so a tampered receipt is caught.
attestral scan . --sovereign # runs offline; prints the no-egress attestation
attestral scan . --sovereign -o review --format json # embeds the attestation in review.json
attestral verify review.json # chain VALID + no-egress attestation VALID
Verifiable conformance attestation
attestral attest is the capstone: it binds, into one DSSE-signed in-toto Statement, the reviewed design (model hash), the review chain head, a digest and severity summary of the findings, the hash of both compiled policies (mcp-guard and Cedar), and, with --runtime, a digest of the runtime events plus the drift verdict (CONFORM, or the list of DRF ids). attestral attest --verify recomputes every one of those digests offline from the supplied design, re-runs drift on the supplied events, and checks the signature, so any tamper - a changed design, a swapped policy, a doctored event stream - makes verification FAIL, and names the failing step. The structure and every hash recompute run with zero dependencies; only the signature step needs the attestral[sign] extra, so an unsigned attestation (all digests still bound) is produced with no install.
One attestation proves one moment. attestral attest --log conformance.log makes conformance monitorable over time: each attestation is appended to an append-only transparency log (an RFC 6962 Merkle tree, one JSONL file, zero dependencies), every append records the tree root it produced, and attest --verify --log proves both that the bundle is in the log (a portable inclusion proof, verifiable without the log file) and that the recorded history is internally consistent - editing any past entry breaks every later checkpoint. A deployment gains an auditable answer to "did this system stay conformant, and when did it drift?" The honest boundary is stated in the output: a self-hosted file proves append-only history, not distributed witness, so publish the head root somewhere you do not control and a rewrite has an external copy to contradict. attestral attest --rekor does exactly that: it anchors a signed attestation in Sigstore Rekor, a public transparency log outside your control, and writes the receipt (log index, integrated time, inclusion proof) to <output>.rekor.json. This is deliberately not a blockchain - Rekor is the Certificate-Transparency-style log the supply-chain world already trusts, and it slots into the SLSA / in-toto / Sigstore stack. Opt-in and online; attest --verify --rekor checks the receipt binds this attestation, and the entry stays independently verifiable with rekor-cli verify --uuid.
attestral attest . --log conformance.log # attest and append to the log
attestral attest . --verify --log conformance.log # verify bundle + history + inclusion
This is a tamper-evident, signature-based conformance attestation, not a formal or mathematical proof of security - the same class of artifact as SLSA's Verification Summary Attestation. It proves exactly one thing: the runtime observed matches the design that was reviewed and the policies compiled from it. It does not prove the design is safe, the rule pack is complete, or that no vulnerability exists; a clean attestation over a weak design is still a weak design. The novel contribution is that a third party - an auditor, a platform, another agent - can verify offline, without trusting the runtime or the scanner, that the running system is the one reviewed and that drift (including DRF-008) either did not occur or is recorded honestly in the signed verdict. See docs/attestation.md for the full shape and the copyable flagship sequence.
The loop in one picture
flowchart LR
A["attestral scan<br/><b>attest</b>"] --> B["attestral verify<br/><b>prove</b>"]
A --> R["attestral remediate<br/><b>concrete source edit</b>"]
A --> F["attestral fix<br/><b>compile-the-fix</b>"]
A --> BR["attestral broker<br/><b>strip a standing credential, generate a per-call broker</b>"]
A --> C["attestral compile<br/><b>enforce</b><br/>+ --verify: prove policy properties"]
C --> D["attestral drift<br/><b>detect</b><br/>+ --remediate: propose the tightening<br/>+ --lockdown: instant narrowing-verified containment<br/>+ --enforce: push it to the live enforcement point"]
D --> AT["attestral attest<br/><b>signed conformance attestation</b><br/>+ --log: append-only transparency log<br/>+ --rekor: public Sigstore Rekor witness"]
AT -->|"verify offline"| B
D --> INC["attestral incident<br/><b>signed incident attestation</b><br/>replay + containment journal bound;<br/>an auditor re-derives the whole reconstruction"]
INC -->|"verify offline"| B
D -->|"design changed?<br/>re-attest"| A
A --> V["attestral validate<br/><b>show the path is reachable</b><br/>+ proof-of-exploit per path (gated)"]
A --> DF["attestral diff<br/><b>PR security-impact delta</b>"]
A --> WI["attestral whatif<br/><b>counterfactual: what-if I change this</b>"]
A --> AD["attestral admit<br/><b>may this agent load this tool? prove why</b>"]
A --> DD["attestral design-diff<br/><b>did this revision widen the agent's reach</b>"]
A --> CH["attestral chaos<br/><b>simulate poisoning attacks, report what the review catches</b>"]
V --> PU["attestral pursue<br/><b>adaptive red-team: confirm or refute a modeled path</b><br/>drives an attacker at a mock target; a canary that reaches the sink CONFIRMS,<br/>a denied decisive hop REFUTES (the one evidence-backed downgrade)"]
style A fill:#96222E,color:#fff
style B fill:#1F6A4A,color:#fff
style F fill:#96222E11,stroke:#96222E
style V fill:#96222E11,stroke:#96222E
style AT fill:#96222E,color:#fff
Everything in the offensive stack until now PREDICTS exploitability; attestral pursue is the one that CONFIRMS it. It takes each modeled attack path as a hypothesis and drives an attacker agent at a target to realize it: read a planted canary through the pivot, carry it out through the impact tool, adapting when the target refuses. A deterministic oracle grades the real transcript, so a path becomes EXPLOITED (a canary reached the sink), empirically REFUTED (the target denied the decisive hop, the only evidence-backed downgrade in the pipeline), or inconclusive, and those verdicts re-grade the findings on the chain. v1 runs against a mock target: no real server, secret, or socket. attestral pursue ./my-project.
Two commands answer "so what do I do about this finding" from both ends. attestral remediate reads the rule's own matcher and the component's real value and prints the concrete source edit to make: the boolean flag to flip (set publicly_accessible = false), the bad value to replace (http://… -> https://…), the control to add, tied to the file it lives in. attestral fix compiles the exact enforceable control that closes the finding, bound to the review's chain head, with a verification verdict: a fleet finding is proven closed by re-synthesizing the model without the isolated capability (re-synthesized), and a per-server finding gets the mcp-guard constraint that governs it at the proxy (enforced-at-proxy). A remediation that is also an enforceable runtime control is the payoff of the attest-compile-drift loop, and the thing a linter structurally cannot offer.
Standing-credential findings (ATL-104/112/115/149/164) go one step further: they compile to a broker-backed fix. fix names the exact env keys to strip and generates the CB4A broker route that replaces them (--broker-output writes the routes as one agentgateway config), remediate prints the same keys as the source edit, and the verification is real - the model copy has the keys removed and its credential attributes re-derived through the ingester's own classifiers, then the rule engine confirms the finding no longer fires. Detection to remediation in one step, not a recommendation string. At runtime, DRF-012 closes the loop's tail: a server whose policy requires a credential-free environment (broker_required or forbid_env_secrets) that positively reports a standing key still present in its env is drift - the broker carries the traffic, but the raw key never left.
attestral drift --remediate closes the loop the other way, the self-healing half: detect -> propose the tightening -> a human approves -> re-compile. A drift finding means the runtime diverged from the reviewed design, so remediation synthesizes the minimal policy delta that would have prevented each finding - quarantine the offending server (allow: false, carrying the DRF id as the reason) - and re-emits it to both compiled targets (mcp-guard and Cedar). The safety principle is load-bearing and non-negotiable: it only ever narrows the policy toward denial, and it never widens the design to match the drift, because widening would rubber-stamp the very attack the drift caught. A compromised runtime cannot drive its own policy. Every proposed delta is verified a narrowing (narrowing.classify must return NARROWING or UNCHANGED, never EXPANSION) before it is emitted, and it is a proposal only - nothing is applied until a human re-compiles. Terminal-first: the proposed ops and the narrowing verdict print to the terminal; the re-emitted policies are written only with -o.
attestral drift --watch --lockdown --enforce <path> is the same loop run live, with the human moved to review-after: detect the moment it happens -> prove the narrowing -> push the containment. The streaming monitor keeps evaluating every event against the original attested policy (drift is always measured against the reviewed design, never against its own containment), and when drift crosses it rebuilds the quarantine lockdown, re-verifies the narrowing proof, and atomically pushes the tightened policy to the file the enforcement point - a running mcp-guard - actually reads; --reload-cmd runs a hook after each push (say, a container HUP). A push happens only when the quarantine set grows, so repeat drift is idempotent and new drift escalates. A lockdown that fails the narrowing proof is refused, never written, and the monitor keeps running. Every push and every refusal is appended to a hash-chained, append-only <enforce>.journal.jsonl (before/after policy digests, triggers, narrowing verdict); verify_journal detects any edited, removed, or reordered entry - the evidence-chain contract applied to containment actions themselves. The narrowing proof is what makes auto-apply defensible: the only drift response safe to automate is one that provably removes capability. And because the journal is the containment record, drift --replay --journal later reconstructs the whole incident on one timeline: when the runtime first diverged, what contained it, how long the gap was, and whether the journal chain is still intact - a tampered journal earns no containment credit.
One drift check reasons across sessions, not within one. DRF-013 catches the Decomposition-attack class (arXiv 2608.17445): a harmful task split into individually-permissible requests spread across fresh sessions or identities, then recombined - a structural blind spot of per-session enforcement, since every guard and every other DRF check judges one call, in one session, at a time. DRF-013 correlates sessions and fires when a set of them collectively exercises a lethal-trifecta-style flow (a private-data read in one session, an egress in another) that no single session held - the runtime, cross-session recombination of exactly what ATL-202 flags at design time. Because correlating unrelated traffic would be mass-noise, correlation is deliberately conservative and fail-closed: sessions link only on a strong signal - a shared correlation/trace id (honored on its own), or the same actor/target within a bounded time window - and an event with no session id, a session with no correlation key, a lone session, and absent metadata never fire. It is a sound over-approximation: it proves both halves were exercised across linked sessions, necessary but not sufficient for a real attack, since telemetry cannot prove the read's data itself reached the egress - and the finding says so.
attestral guard is the enforcement point that makes the whole loop stand on its own. compile writes a default-deny policy and drift reads events back against it, but between them the review needed something a user already runs to actually consult the verdict. guard is that something: a standard-library stdio Model Context Protocol proxy you drop in front of a server by wrapping its launch command (attestral guard policy.yaml --server files -- npx ... /srv, or let --print-config write the .mcp.json stanza). A server the review denied - or one absent from the attested design - never starts, so "may this agent load this tool" is answered by the tool not running. Every tools/call and resources/read is judged before it reaches the server, by the same drift.evaluate_event the detector uses, so what the guard blocks is exactly what drift would flag: a call (or a resource read, file:///etc/passwd) that escapes the attested filesystem roots (DRF-003) or downgrades a TLS-only transport (DRF-004) comes back as a JSON-RPC error and is never forwarded. And because the guard writes every decision - plus the live tool surface it sees on tools/list - as telemetry in the exact schema drift, lockdown, and incident consume, running it is what finally gives the runtime half of the loop a real event source instead of a hand-written fixture. It is an enforcement point, not the only one: the same compiled policy still drives a running mcp-guard or a Cedar evaluator, and compile --target claude-managed renders it as Claude Code's own enterprise-managed config (managed-mcp.json, which loads only the attested servers and refuses claude mcp add, plus a managed-settings.json allowlist pinned to each server's launch identity) so a whole fleet enforces the review through MDM, and --target copilot-registry renders the allowed set as a GitHub Copilot internal MCP registry v0.1 catalog for "Registry only" mode. With guard the loop also closes with zero external dependencies on a single machine.
The runtime loop end-to-end (what we are building right now)
The thread we keep pulling: the review is not a report, it is the source of truth the runtime is held to - and every stage of holding it there produces the same kind of tamper-evident record the review itself does. This is the full lifecycle, containment and forensics included:
flowchart TB
DES["Design<br/>(Terraform · K8s · MCP · prompts · skills)"] --> SCAN["scan -> findings + evidence chain<br/>(SHA-256, Ed25519-signable)"]
SCAN --> ATT["attest: signed conformance statement<br/>design + policies + runtime verdict"]
SCAN --> POS["posture: signed capability posture<br/>envelope + lethal trifecta + cross-boundary reach<br/>an in-toto predicate a cosign/Kyverno gate verifies"]
SCAN --> CMP["compile: default-deny policy<br/>mcp-guard · Cedar · agentgateway (CB4A) · claude-managed · copilot-registry"]
CMP --> ENF["attestral guard<br/>enforcement point: a stdio MCP proxy that<br/>refuses a denied server + gates every call<br/>(or any policy consumer: mcp-guard, Cedar)"]
ENF -->|telemetry| DRIFT["drift: every event judged against the<br/>ORIGINAL attested policy - detection never moves"]
DRIFT -->|"drift crosses"| LOCK["lockdown: narrowing-verified quarantine<br/>pushed atomically; a widening is REFUSED"]
LOCK --> JRNL["hash-chained containment journal<br/>every push + refusal, tamper-evident"]
DRIFT --> RPL["replay: incident forensics<br/>first drift · containment gap ·<br/>CONFORM / DRIFTED / CONTAINED"]
JRNL --> RPL
RPL --> INC2["incident: signed incident attestation<br/>the reconstruction itself becomes<br/>audit-grade, offline-verifiable evidence"]
INC2 --> AUD["auditor: re-derives everything<br/>from policy + events + journal<br/>(+ Rekor public witness, opt-in)"]
style DES fill:#96222E11,stroke:#96222E
style ENF fill:#0a7d3611,stroke:#0a7d36
style INC2 fill:#96222E,color:#fff
The invariants that hold at every stage: detection is always measured against
the reviewed design (a compromised runtime cannot drive its own policy),
enforcement only ever narrows (the one drift response safe to automate is
one that provably removes capability), and every action lands in a
hash-chained record (chain, journal, attestation) an auditor can re-derive
offline. attestral incident is the newest stage: it closes the loop by
giving the incident record the same signature-grade standing as the design
review it descends from.
The four commands
flowchart LR
subgraph scan["attestral scan"]
s1["Terraform + MCP"] --> s2["findings + evidence chain<br/>md / json / sarif"]
end
subgraph verify["attestral verify"]
v1["report.json"] --> v2["VALID / INVALID<br/>(offline)"]
end
subgraph compile["attestral compile"]
c1["attested model"] --> c2["default-deny policy<br/>tool manifest hashes pinned,<br/>bound to chain head"]
c2 --> c3["narrowing check (--against)<br/>a re-attestation must not widen<br/>the reviewed capability envelope"]
end
subgraph drift["attestral drift"]
d1["policy + telemetry"] --> d2["drift findings<br/>rug-pulls (DRF-005),<br/>loop / volume budgets (DRF-006/007),<br/>cross-session recombination (DRF-013)"]
end
subgraph memory["attestral memory"]
m1["memory store + keyring"] --> m2["signed-provenance audit<br/>relabelled / tampered / unsigned<br/>trust claims (MEM-001/002/003)"]
end
# SCAN: review a project (Terraform + MCP configs discovered automatically)
attestral scan ./my-project --format both # md + json
attestral scan . --fail-on high # CI gate: exit 1 on high/critical
attestral scan . --min-confidence high --fail-on high # CI-safe set only: structural, 0-FP-on-benign
attestral scan . --format sarif -o attestral # SARIF -> GitHub Security tab + PR annotations
# VERIFY: prove a report has not been altered (no network, no server)
attestral verify review.json
# SIGN: make the chain authentic, not just tamper-evident (Ed25519 / DSSE)
attestral sign --gen-key reviewer # one-time: a keypair
attestral sign review.json --key reviewer.key --signer "Ada L"
attestral verify review.json --public-key reviewer.pub # checks integrity AND authenticity
# COMPILE: turn the attested design into a default-deny mcp-guard policy
attestral compile ./my-project -o policy.yaml
# other targets: Cedar (AWS Verified Permissions) and a credential-broker config
attestral compile ./my-project --target cedar -o attested.cedar
attestral compile ./my-project --target agentgateway -o gw-policy.yaml # CB4A default-deny broker
attestral compile ./my-project --target claude-managed -o managed/ # Claude Code enterprise allowlist: managed-mcp.json (only these load) + managed-settings.json, deploy via MDM
attestral compile ./my-project --target copilot-registry -o registry.json # GitHub Copilot internal MCP registry v0.1 catalog for "Registry only" mode
# and verify a later design still NARROWS the reviewed one (fails on an expansion)
attestral compile ./my-project --against policy.yaml
# GUARD: enforce the compiled policy live - a stdio MCP proxy in front of a server.
# A denied (or unattested) server never loads; every tools/call is gated by the SAME
# function drift uses; every decision is written as the telemetry drift then reads.
attestral guard policy.yaml --server files -- npx @modelcontextprotocol/server-filesystem /srv/docs
attestral guard policy.yaml --server files --observe -- npx ... /srv/docs # dry-run: record, don't block
attestral guard policy.yaml --server files --print-config -- npx ... /srv/docs # wrapped .mcp.json stanza
# DRIFT: diff runtime telemetry against the attested design
attestral drift policy.yaml events.jsonl --fail-on-drift
# and close the loop: PROPOSE the minimal tightening that would have prevented each
# drift finding, re-emitted to both targets (proposed only, always a narrowing)
attestral drift policy.yaml events.jsonl --remediate
attestral drift policy.yaml events.jsonl --remediate -o mcp-guard-policy.yaml # + sibling .cedar
attestral drift policy.yaml events.jsonl --lockdown -o lock # act: emit lock.lockdown.yaml + .json, exit 2
attestral drift policy.yaml events.jsonl --watch --lockdown --enforce live.yaml --reload-cmd "docker kill -s HUP mcp-guard" # live containment loop + hash-chained journal
attestral drift policy.yaml events.jsonl --replay --journal live.yaml.journal.jsonl # incident forensics: when did it drift, what contained it
# ATTEST: bind the reviewed design, both compiled policies, and the runtime drift
# verdict into ONE signed conformance attestation a third party can verify offline
attestral attest ./my-project --runtime events.jsonl --gen-key demo --signer "Ada L" -o attestation.json
attestral attest --verify ./my-project --runtime events.jsonl --public-key demo.pub -o attestation.json
# POSTURE: sign WHAT the agent can do - its capability envelope, whether the fleet
# forms a lethal trifecta, and the named cloud it can reach - as an in-toto predicate
# a cosign/Kyverno gate verifies offline (agent-capability-posture/v1)
attestral posture ./my-project --gen-key demo --signer "Ada L"
attestral posture ./my-project --key demo.key -o agent-posture.json
attestral posture --verify ./my-project --public-key demo.pub -o agent-posture.json
attestral posture ./my-project --against agent-posture.json --fail-on-widen # CI gate: fail if the agent gained a capability/trifecta/reach vs the signed baseline
attestral posture --verify ./my-project --runtime events.jsonl -o agent-posture.json # runtime check: fail if the agent exercised a capability outside its attested envelope
# INCIDENT: after a drift incident, bind the replay reconstruction - policy digest,
# event stream, containment-journal chain head, final verdict - into ONE signed
# attestation; --verify re-derives the whole reconstruction from the inputs
attestral incident policy.yaml events.jsonl --journal live.yaml.journal.jsonl --key reviewer.key -o incident.json
attestral incident policy.yaml events.jsonl --journal live.yaml.journal.jsonl -o incident.json --verify --public-key reviewer.pub
# MEMORY: bind an agent-memory entry's trust label to its content with a signature,
# so a relabelled or tampered entry is caught cryptographically (not by hoping)
attestral memory sign --content "..." --label trusted --writer alice --key alice.key -o mem.jsonl
attestral memory verify mem.jsonl --keyring writers.yaml --fail-on-untrusted
# VALIDATE: prove whether the assembled attack paths actually hold
# (tier 0: symbolic walk over the model's edges, no execution, no network)
attestral validate ./my-project
attestral validate ./my-project -o proof --fail-on-reachable # write proof.md + chain, gate CI
# PENTEST: the executed red-team tier - for each reachable path, spawn an isolated
# sandbox agent, plant a canary, and MEASURE whether it exfiltrates (parent-side
# sink oracle) while the sandbox's egress guard blocks the real outbound (contained).
# Attestral's own stubs only: no live agent, no real secret or network.
attestral pentest ./my-project
attestral pentest ./my-project --fail-on-exfil -o pentest.json # gate CI on a working exploit
attestral pentest ./my-project --isolation container # strongest jail: docker --network none
attestral pentest ./my-project --techniques # + the attack-technique playbook (17 techniques, 7 classes)
attestral pentest ./my-project --techniques --probe # + does a real model follow each? (needs an API key)
# CLOSE THE LOOP: compile the runtime policy, but first run the executed pentest and
# DENY any server whose egress carried a proven canary out. static review ->
# sandbox-proven exploit -> runtime-enforced deny.
attestral compile ./my-project --close-loop -o mcp-guard-policy.yaml
# BLAST-RADIUS: rank every agent surface by its if-compromised reach, so
# hardening prioritises itself (the lethal-trifecta host rises to the top)
attestral blast-radius ./my-project
# REACH: name the cloud/Kubernetes resources each agent surface can touch - the
# agent->cloud crossing a per-server scanner cannot see (evidence behind ATL-222)
attestral reach ./my-project # e.g. "web-fetch [injectable] reaches aws_s3_bucket.customer_data"
# ADMIT: decide whether an agent may load a proposed tool, and prove why - the
# security delta of adding it (new findings, new reach, blast shift). PR-time gate.
attestral admit ./my-project --add ./new-server.json --fail-on-deny
# DIFF: post the security-impact delta between two design revisions - the
# engine behind the PR-review bot (examples/github-actions/security-delta.yml)
attestral diff ./base ./head
attestral diff ./base ./head --fail-on high # gate CI on newly-introduced risk
# WHAT-IF: the security delta of a change BEFORE you make it (design assistant)
attestral whatif ./my-project --deny web:network # scope a capability away, see what it fixes
attestral whatif ./my-project --remove shell # remove a server, see the findings/paths resolved
attestral design-diff ./main-checkout ./pr-checkout --fail-on-widen # CI gate: exit 3 if the change widens the capability envelope
# FLEET: model several repos as ONE agent fleet and find flows that span them
attestral fleet ./repo-a ./repo-b ./repo-c # ATL-213: cross-repo toxic flow
Install and run the whole loop (60 seconds)
pip install attestral
attestral scan examples/first-run # START HERE: a normal-looking .mcp.json that
# hides a lethal trifecta (see examples/first-run)
attestral scan --local # then audit the MCP configs on THIS machine
attestral scan examples/demo-project -o review # attest -> review.md + review.json
attestral verify review.json # prove -> chain VALID
attestral remediate examples/demo-project # remediate -> concrete source edit per finding
attestral fix examples/demo-project # fix -> enforceable control per finding
attestral fix examples/vulnerable-agent --broker-output broker.yaml # + the CB4A routes replacing the stripped keys
attestral broker examples/vulnerable-agent # broker -> strip standing creds, generate a per-call broker
attestral chaos examples/demo-project # chaos -> simulate poisoning attacks, report caught vs missed
attestral chaos examples/demo-project --generate # + an LLM tier authoring novel payloads (opt-in, ANTHROPIC_API_KEY)
attestral compile examples/demo-project -o policy.yaml # enforce -> default-deny policy
attestral drift policy.yaml examples/demo-project/runtime-events.jsonl --fail-on-drift # detect
Real-world benchmark
Run on TerraGoat (Bridgecrew's deliberately-vulnerable Terraform), same repo, as the rule pack grew (the pack is 294 rules today; this table shows the historical progression, not the current pack size):
| TerraGoat AWS | TerraGoat Azure | TerraGoat GCP | Distinct rules | |
|---|---|---|---|---|
| v0.4.0 (10 rules) | 3 | - | - | 3 |
| v0.6.0 (57 rules) | 7 | 2 | 3 | 12 |
| v0.9.0 (169 rules) | 8 | 3 | 5 | 16 |
The pipeline (ingest, evidence chain, tamper detection, gate, SARIF) is verified on real code. One honest caveat: TerraGoat leans heavily on Terraform variables and modules, and Attestral's HCL resolver does not yet evaluate cross-variable interpolation, so a chunk of TerraGoat's misconfigurations sit behind var. references the scanner can't see through yet. The TerraGoat number is therefore a floor gated by HCL-resolution depth, not a measure of the 163-rule cloud pack's reach. Deeper HCL resolution is on the roadmap; when it lands, these numbers jump without adding a single rule.
What it does not do
A tool you can trust is one that is honest about its edges. Attestral is a design review, not a SAST tool: it reads the declared surface (config, agent wiring, prompts, IaC) and reasons over the system model, but it does not read the inside of a tool's implementation, execute anything against your live agent, or prove exploitability (a reachable path is necessary, not sufficient). It has known blind spots too: HCL cross-variable resolution depth, coarse capability classification, a probabilistic ML tier, and agent-code ingestion that needs a recognizable framework. The full list, including where our own detection breaks under adaptive attack, is in docs/limitations.md. We would rather state the limits than imply coverage we do not have.
Use it in CI
# .github/workflows/attestral.yml
name: attestral
on: [pull_request]
permissions:
contents: read
security-events: write # to upload to the Security tab
jobs:
design-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with: { python-version: "3.12" }
- run: pip install "attestral[terraform]"
# Inline annotations on the offending line, via GitHub code scanning.
- run: attestral scan . --format sarif -o attestral
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: attestral.sarif }
# A clean job summary rendering the reachable attack paths and the
# findings this PR introduced (commit attestral-baseline.json first).
- run: attestral scan . --baseline attestral-baseline.json --format md-summary -o attestral
- run: cat attestral.summary.md >> "$GITHUB_STEP_SUMMARY"
# Hard gate: fail only on net-new high/critical (auto-uses attestral-waivers.yaml).
- run: attestral scan . --baseline attestral-baseline.json --fail-on high --quiet
The action does three things a passing check does not: SARIF puts each finding inline on the offending line, the job summary renders the reachable entry → pivot → impact path so a reviewer sees the story not a list, and --baseline gates on net-new findings only, so a brownfield repo adopts without failing on day-one debt. attestral init scaffolds exactly this; ready-made workflows also live in examples/github-actions/.
Run attestral on every commit
pip install pre-commit
Add to .pre-commit-config.yaml:
repos:
- repo: https://github.com/attestral-labs/attestral
rev: v0.17.0
hooks:
- id: attestral # gate infra/agent config in this repo
- id: attestral-local # optional: audit installed MCP servers
Then pre-commit install. See examples/pre-commit/ for details.
Writing custom rules
Rules are YAML with structured matchers. No eval anywhere, and an unknown matcher fails closed (never matches).
rules:
- id: ORG-001
title: Internal load balancer 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"]
python -c "from attestral.rules import RuleEngine; RuleEngine(['org_rules.yaml'])"
Development
pip install -e ".[dev,terraform,llm]" # add ,ml for the DeBERTa layer (pulls torch)
pytest -q # offline suite; the live judge test skips without a key
ruff check attestral tests
To run the live judge test, set ATTESTRAL_JUDGE_API_KEY (or ANTHROPIC_API_KEY) and re-run pytest -q.
How a change ships
flowchart LR
subgraph inner["inner loop (local)"]
E["edit code / rules / ingesters"] --> T["pytest -q · ruff"]
T --> S["attestral scan examples/*<br/>(eyeball real findings)"]
S --> E
end
S --> PR["pull request"]
PR --> CI["CI: lint + tests on 3.10 / 3.12<br/>+ docs-sync gate"]
CI --> REV["CODEOWNERS review · CLA signed"]
REV --> MAIN["main (protected: no force push,<br/>required checks)"]
MAIN --> TAG["tag vX.Y.Z + CHANGELOG entry"]
TAG --> PUB["publish.yml → PyPI<br/>(Trusted Publishing)"]
The docs-sync gate (tests/test_docs_sync.py) keeps this README honest: it
fails when a pipeline module exists that no diagram shows, when a CLI command
is undocumented, or when the package version has no CHANGELOG.md entry. If
you add a stage, draw it - the suite won't pass until you do.
License
Apache 2.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file attestral-0.23.0.tar.gz.
File metadata
- Download URL: attestral-0.23.0.tar.gz
- Upload date:
- Size: 1.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f903691728514f577cd345ff0339aab33c35d1935e1d9d04cb0cb09fd29cf6bf
|
|
| MD5 |
c84283281f53ff45aca59121328dbee0
|
|
| BLAKE2b-256 |
4f29b246299ee0cdc04b678ae3f4c09ffcd2e357720fb3f46c26ad49a1f6e06a
|
Provenance
The following attestation bundles were made for attestral-0.23.0.tar.gz:
Publisher:
publish.yml on attestral-labs/attestral
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
attestral-0.23.0.tar.gz -
Subject digest:
f903691728514f577cd345ff0339aab33c35d1935e1d9d04cb0cb09fd29cf6bf - Sigstore transparency entry: 2778186145
- Sigstore integration time:
-
Permalink:
attestral-labs/attestral@7e7bda99a5447cf9dbce561dac55838787453fbc -
Branch / Tag:
refs/heads/main - Owner: https://github.com/attestral-labs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7e7bda99a5447cf9dbce561dac55838787453fbc -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file attestral-0.23.0-py3-none-any.whl.
File metadata
- Download URL: attestral-0.23.0-py3-none-any.whl
- Upload date:
- Size: 471.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9cdcb1e82d380a5c15d4d2107b0cea31cdb9485d15d7a4e52039d88a12513abd
|
|
| MD5 |
adb9186ecc6fe19c3d6fc963bd8fa31b
|
|
| BLAKE2b-256 |
105dd338493bfa00f76d4b5f40ee6d6494411daac7d8cbf375e34925fa6b48a0
|
Provenance
The following attestation bundles were made for attestral-0.23.0-py3-none-any.whl:
Publisher:
publish.yml on attestral-labs/attestral
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
attestral-0.23.0-py3-none-any.whl -
Subject digest:
9cdcb1e82d380a5c15d4d2107b0cea31cdb9485d15d7a4e52039d88a12513abd - Sigstore transparency entry: 2778186286
- Sigstore integration time:
-
Permalink:
attestral-labs/attestral@7e7bda99a5447cf9dbce561dac55838787453fbc -
Branch / Tag:
refs/heads/main - Owner: https://github.com/attestral-labs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7e7bda99a5447cf9dbce561dac55838787453fbc -
Trigger Event:
workflow_dispatch
-
Statement type: