AI agent & MCP security

The security scanner for AI agents and MCP servers.

Attestral reads your MCP configs, agent code, system prompts, and tool descriptions, builds one system model of the whole fleet, and reviews the agentic surfaces most scanners never look at, from prompt injection to the toxic flows that only exist across servers and repos. It models your cloud and Kubernetes in the same graph, so it sees the trust boundary between the agent and the infrastructure it can reach.

$ pip install attestral
$ attestral scan --local

No account, no API key, fully offline. It reads the MCP configs your agents already use, Claude Desktop, Cursor, .mcp.json, and reviews every server it finds.

PyPI v0.21.0 Python 3.10+ Zero required deps Runs fully offline Apache 2.0
$ attestral scan examples/vulnerable-agent
Reviewed 6 components across 2 source files: 6 agent / MCP surface

Attack paths (1)
  internal chain: webshellweb  entry · pivot · exit

  [CRITICAL] ATL-202  Lethal trifecta: private data + untrusted input + an exit  (model)
  [CRITICAL] ATL-103  Shell-capable MCP server configured  (shell)
  [CRITICAL] ATL-108  Tool calls auto-approved, no human checkpoint  (shell)
  [HIGH    ] ATL-207  Untrusted input can reach a code-execution tool  (model)
  [HIGH    ] ATL-107  Outbound network reach  (web) · raised from medium
  [HIGH    ] ATL-ML-001  Prompt-injection text in a tool description  (web)
   and 10 more
6 components · 17 findings · 4 critical, 12 high, 1 medium

A real review of the vulnerable-agent example: two small config files, zero cloud credentials required. The attack path and the lethal trifecta are fleet-level, and ATL-107 was raised to high because it sits on that reachable path. Point it at your own machine with attestral scan --local.

Every finding cites a real control
  • OWASP ASI + LLM Top 10
  • MITRE ATLAS
  • NIST 800-53
  • CIS AWS · Azure · GCP · K8s
  • SOC 2 · ASVS
  • SARIF · CycloneDX AI-BOM

How it works

One command. Seven phases. Three layers.

Every scan runs the same pipeline: ingest everything the agent touches, build one system model, trace what can actually reach what, sweep the graph with typed rule packs grounded in current attack research, then seal the evidence and compile the verdict into runtime policy. Follow any phase to watch it run live.

Inside the pipeline, three review layers keep their labels: deterministic rules always run, a local ML tier scores the language surfaces offline, and an LLM judge cross-examines findings when you bring a key. Every finding carries its origin, so a reviewer knows what is reproducible fact, what is a score, and what is reasoning.

0
security checks
high-signal, each with a fixture
0
clouds modeled
AWS · Azure · GCP · Kubernetes
0
standards mapped
OWASP · NIST · MITRE · CIS · SOC 2 · ASVS
0
review layers
deterministic · ML · LLM

What only a whole-fleet model catches

It builds a model of your whole fleet, then reasons across it.

Not a config-by-config scan. Attestral ingests your MCP servers, prompts, cloud, and Kubernetes into one graph of components, edges, and trust boundaries. That is how it sees the path a single injected sentence can take: from an untrusted input, through a shell, across the boundary into your cloud. Hover a node to see its reach, or trace the attack path.

hover any node to see its reach
agent runtime cloud trust boundary orchestratoragent web-fetchnetwork jirasaas_data ops-shellshell notifymessaging deployshell s3 bucketcloud store iam rolecloud
trust boundary reach / dataflow attack path

Because the model knows what every surface can reach, findings are not rated in isolation. A prompt-injection hit is raised to critical only when the surface it sits on can reach a secret or an egress channel, and left low when it reaches nothing worth taking. A cross-server tool-name collision is raised when one side is a lower-trust, mutable package positioned to shadow a trusted tool, and look-alike names (a Cyrillic letter, a zero-width space) are folded so an impersonation cannot hide behind a spelling an exact match would miss.

New in the system model

The system model, deepened.

The same graph of components, edges, and trust boundaries now reasons in five new ways. Each is deterministic, cites a real control, and ships with a fixture in the repo you can run today. Two of them read further than any single-config scan: they walk the graph rather than the capability set, so they name a path and clear when a mitigation sits on it.

1 · Architecture posture grade attestral grade

One defensible letter, A to F, that credits good design.

A scan hands you a list of findings; a grade answers what that list cannot: how the whole design is doing. It deducts for findings and, unlike a finding list, credits the patterns that make a design safe, a human in the loop, scoped tools, constrained egress, so a strong architecture with one issue never scores like a fragile one. Deterministic, explainable, and a CI gate with --fail-under.

F
examples/vulnerable-agent
score 0/100 · a lethal trifecta (ATL-202) and 4 criticals cap it at D; no positive pattern held.
A
examples/hardened-agent
score 100/100 · six good patterns credited, +12 for architecture.
$ attestral grade examples/vulnerable-agent
  F  posture grade F  (score 0/100)
  ceiling: lethal trifecta (ATL-202) + 4 critical cap the grade at D

$ attestral grade examples/hardened-agent --fail-under B
  A  posture grade A  (score 100/100)
  + no trifecta · human-in-loop · scoped tools · egress constrained
  +12 credit for good architecture · gate passes
2 · Multi-hop taint ATL-224

It walks the graph, not just the capability set.

Confidentiality and integrity labels propagate hop by hop along declared edges, so the finding names the ordered source, hops, and sink, and catches a flow that spans more than two components. It clears the moment a declassifier sits on the path, while the co-presence alarm (ATL-202) keeps ringing.

reader database formatter filesystem notifier messaging
ATL-224 fires - private data reaches an egress sink across 3 hops, and ATL-202 fires too. ATL-224 clears - a declassifier now mediates the path, while ATL-202 still rings on co-presence.
$ attestral scan examples/taint-multihop
  HIGH  ATL-224  multi-hop flow reaches an incompatible sink
    reader (database) -> formatter (filesystem) -> notifier (messaging)
$ attestral scan examples/taint-declassified  # broker on the path
  ATL-224 clears · ATL-202 still fires
OWASP-ASI02/03:2026 · NIST AC-4 · SC-7 · MITRE ATLAS AML.T0051
3 · Agent skills supply chain ATL-177 / 178

A SKILL.md that lies about its own risk.

Attestral parses packaged skill manifests. It flags a skill that presents itself as low risk while its grant hands over shell or wildcard access, and one that sources its content from a mutable external location, the skill-supply-chain surface a config review never reads.

tierL1 · low risk
safetrue
allowed-tools[ Bash, * ]
sourcehttps://cdn.example/skill.md
the low-risk label contradicts a shell + wildcard grant, and the body loads from a URL that can change after review.
$ attestral scan examples/skill-manifest-ast
  HIGH   ATL-177  skill claims low risk but grants broad capability
    (agent_instruction.release-deployer)
  MEDIUM ATL-178  skill sources content from an external location
    (agent_instruction.remote-fetcher)
OWASP Agentic-Skills Top 10 · AST02/03/04:2026 · OWASP-ASI03:2026
4 · Memory & A2A surfaces ATL-225 / 226

Two agents, one writable memory store.

Memory stores become components with read and write edges, so Attestral sees a cross-agent poisoning channel: an agent that ingests untrusted input writes a store a different agent later reads. Separately, it flags an unauthenticated A2A agent card that advertises privileged skills.

agent A untrusted web input agent B acts on retrieval memory store shared · writable write read
$ attestral scan examples/memory-poisoning-flow
  HIGH  ATL-225  cross-agent memory-poisoning channel
    (memory_store.poisoned-crew-py.crew)

$ attestral scan examples/a2a-unauth-card
  HIGH  ATL-226  unauthenticated A2A card exposes privileged skills
    (a2a_agent.ops-runner)
OWASP-ASI06/07:2026 · A2A Protocol Spec 1.0 · MITRE ATLAS AML.T0051
5 · Cross-session drift DRF-013

A task split across sessions, then recombined.

Per-session enforcement is blind to a task decomposed into individually-permissible requests across fresh sessions. Attestral correlates telemetry by a shared id, actor, or target, and fires when correlated sessions jointly exercise a trifecta-style flow no single session held.

session A reads saas_data session B egress network joint capability read + egress corr id job-42
$ attestral drift policy.yaml runtime-events-attack.jsonl
  HIGH  DRF-013  capability recombined across correlated sessions
    sess-A read [saas_data] + sess-B egress [network]
    linked by correlation id 'job-42' · no single session held both
    decomposition attack (arXiv 2608.17445)
Runtime drift · correlates sessions the guard only ever sees one at a time

Honest by default. These deepen the design-time review; they do not replace it. The taint walk and the reachability behind it are sound over-approximations over declared capability and declared edges, necessary, not sufficient, for exploitation, and a posture grade summarizes the reviewed design, not the running system or the code inside each tool.

Real systems · aggregate, no repo named

Scanned in the wild: 33 popular MCP servers.

The rest of the evaluation is a synthetic suite with an answer key. This is different: Attestral run against 33 of the most widely used public MCP servers, at pinned commits. It reviews the documented launch configuration, the mcpServers block users copy from a README, and every hit is a configuration default, not an exploited vulnerability. The aggregate below carries no embargo; per-repo results stay under responsible disclosure until each maintainer has been notified.

33servers scanned
23shipped a modelable config
3were clean (good counter-examples)

What those 23 shipped configs contain (percentages are of the 23):

PatternRule% of 23
Lethal trifecta · private data + untrusted input + an exit, in one fleetATL-20222%
Auto-installs an unpinned package at launch (npx -y / uvx)ATL-10552%
Remote MCP server with no authentication (superseded: under 1% once the check became OAuth-aware, see below)ATL-10948%
Secret passed to the server via envATL-10443%
Outbound network / browser accessATL-10726%
Mutable @latest / :latest tag (rug-pull surface)ATL-10622%
Shell execution + outbound network in one fleetATL-2039%
Toxic flow (untrusted input can reach a sensitive action)ATL-2079%

The headline is the compositional one. The lethal trifecta and toxic-flow hits are fleet-level: they exist only once you model several servers, and in one case a committed sub-agent, together. That is the finding no config-by-config scanner produces, and it showed up in nearly a quarter of the configs that had enough to model.

We have since widened this to 390 servers, and the compositional rate held at one in three. Two per-server numbers above were corrected on the way: the no-auth rate was an OAuth false positive and fell to under 1% once the check became OAuth-aware, and monorepo README merges can manufacture a false "fleet." The trifecta survived every correction.

A named per-repo gallery, one screenshot of a genuine trifecta in a project you recognize, publishes as a second wave, after each maintainer has been given a responsible-disclosure window. We would rather be fair to maintainers than fast to a headline.

Don't take the demo's word for it. Paste your own config.

Drop in an .mcp.json or claude_desktop_config.json and twenty-seven of the agentic checks from the rule pack, the same typed matchers the engine runs, review it right here. Everything happens in this tab: the page makes no network request with what you paste. The CLI goes further, with the cloud packs, attack paths, prompt surfaces, and the evidence chain; the playground runs whole fleet scenarios in the browser.

The example is examples/vulnerable-agent/.mcp.json from the repo, the same file the demo above scans.

$ attestral scan <pasted config>
waiting for a config. Load the example, or paste the mcpServers block your agent already uses.

Why not just Checkov or Trivy?

Keep using them. They are excellent at cloud infrastructure, and attestral is not out to replace them; it scans the same Terraform and Kubernetes with a solid CIS-grounded pack of its own. But those tools were built before agents had tools. They do not parse an .mcp.json, they cannot tell that a tool description is quietly asking the model to read your SSH key, and they have no concept of the trust boundary between an agent and the shell it can call. Attestral does one thing they structurally can't: it builds a system model of your whole agent fleet, so it catches the toxic flows that only exist across servers, where private data meets an exit and one injected sentence walks out with your secrets.

Capability Checkov · Trivy · tfsec Attestral
Cloud IaC misconfiguration Mature and deep. Their home turf. A CIS-grounded pack across AWS, Azure, GCP, and Kubernetes.
MCP server configs (shell access, transport, secrets) Not modeled. First-class components in the system model.
System prompts and tool descriptions Not parsed. Reviewed, and scored for prompt injection.
Agent-to-tool trust boundaries No concept of one. The center of the graph attestral builds.
Prompt injection in agentic text Out of scope. Local ML classifier, on by default, fully offline.
Toxic flows across the tool fleet (lethal trifecta) No fleet model. Named source and sink servers, mapped over the graph.
Memory poisoning & risky agent skills Not modeled. World-writable CLAUDE.md, vector stores, SKILL.md grants.
Runtime drift (the running system diverging from the reviewed design) & tool rug-pulls (a tool silently changed after you approved it) Static only. Attested design compiles to a policy; drift is caught.
Known MCP CVEs & hook config-injection Not tracked. Vulnerable package versions and malicious .claude/settings.json hooks (CVE-2025-6514, CVE-2025-59536).
Tamper-evident audit evidence Report only. SHA-256 evidence chain you verify offline.

The honest version: run Checkov and Trivy for cloud depth, and run attestral for everything that lives above the infrastructure, the agent, its tools, and the prompts that drive them. It happens to cover the cloud underneath too.

293 checks, and the balance is the strategy.

We do not chase rule count. Every check earns its place with a real control and a fixture. The agentic and cross-boundary rules are the hard-to-copy core, the surfaces nobody else models; the cloud packs are deliberately good enough that you need no second scanner, never an arms race.

MCP / Agentichardest to copy
0
Cross-boundaryreachability
0
AWSCIS-grounded
0
AzureCIS-grounded
0
GCPCIS-grounded
0
KubernetesPod Security
0

104 agentic and cross-boundary checks that are genuinely hard to copy, over 189 cloud checks across AWS, Azure, GCP, and Kubernetes, mapped to 21 named control families across the six standards it cites. Depth where it counts most, parity where it just has to be solid. The newest checks reach further than a config scan: the information-flow lattice that turns the lethal trifecta into a discharge-able property (above), known-CVE scanning of the agent's own dependency manifest (requirements.txt / pyproject / package.json, the surface a config review never reads), and the cross-boundary flows a system model uniquely sees: untrusted input written into agent long-term memory, a sampling-capable server driving tools on another with no human checkpoint, and indirect injection reaching cloud credentials.

Recall you cannot self-grade. The in-repo benchmark scores 174/174, but its labels come from our own fixtures, so we also measure against eight published 2025-2026 CVE advisories labelled from the advisory, not our output. That number is allowed to fall below 100% and does; each miss is itemised with a concrete path to close it. It is how a tool whose product is trust should report its reach.

Proven on real Terraform, tuned against first-scan noise. A pinned regression suite runs the cloud packs against Bridgecrew's deliberately-vulnerable TerraGoat corpus, so what fires today keeps firing tomorrow. The ingester also reads security-group CIDRs direction-aware: the world-open egress block nearly every production repo ships never reads as "open to the world", so a first scan of real infrastructure does not open with a false alarm. A benign real-Terraform design sits in the CI benchmark to keep it that way.

The finding that lives in no single file.

Every server below is individually defensible, and a config-by-config scanner would wave them all through. The risk is compositional: the moment one agent session can read private data and reach an exit, a single injected sentence anywhere in its inputs becomes an exfiltration. Toggle the fleet and watch the pack's combination rules evaluate it live, using the same capability groups attestral scan applies to your real configs.

The exfiltration chain

reads private datanothing yet
reaches an exitnothing yet
toggle servers above

Command and control

executes commandsnothing yet
reaches the networknothing yet
toggle servers above

The iframe backdoor

reads private datanothing yet
renders an app UI with external originsnothing yet
toggle servers above

Fleet-level findings only: the per-server findings these toggles would also raise (a shell server is ATL-103 all by itself) are left out here to keep the composition visible. On a real scan the full attack path is also synthesized end to end, entry to pivot to impact.

The flagship

...and no single repo.

Real agent estates span repositories: a data reader in one, an ops runner in another, a notifier in a third. Each repo passes its own review. The attack chain completes only across the boundary, and only a model that spans them can see it. Assign each tool to a repo and watch every per-repo scan stay green while attestral fleet lights up.

attestral scan repo-a

attestral scan repo-b

attestral fleet repo-a repo-b

ATL-213 fires only when the fleet completes a chain that no single repo completes alone. Move all three tools into one repo and it goes silent, because that repo's own scan already catches the flow. It is never noise, and it is the one finding a per-repo scanner structurally cannot produce.

Three review layers. Run each one.

Regulated buyers can't accept "the AI said so." Attestral separates what is deterministic from what is model-reasoned, and every finding carries its origin. Pick a layer and watch it run against the same insecure agent, real commands, the output the CLI actually prints.

origin: deterministic

Typed rules, fail-closed

293 typed YAML matchers over the system model, spanning AWS/Azure/GCP/K8s and the agentic surfaces nobody else models, plus cross-server attack-path synthesis. No eval anywhere; an unknown matcher never matches. Fully reproducible, free, offline.

origin: ml

Injection text a rule can't see

A DeBERTa-v3 prompt-injection classifier scores the language surfaces an agent reads and can be steered by, tool and server descriptions, system prompts, offline. Three tiers (zero-dep heuristic, ONNX, DeBERTa) emit the same finding shape, so the evidence chain is identical whichever scored.

optional · offlineRead the deep dive →
origin: llm

Cross-examined to cut noise

An LLM judge re-examines each finding and votes confirmed / false-positive / needs-review with a confidence, so the borderline ones (a localhost dev endpoint read as non-TLS) get caught. A confident false positive is auto-waived, never deleted: it stays on the evidence chain with the judge's reasoning.

optional · your API keyRead the deep dive →

Tamper with the record. Watch it break.

Every finding commits to a SHA-256 chain: entry N hashes its own canonical JSON together with the hash of entry N-1, from a zero genesis. The head is the integrity commitment for the run. This recomputes live in your browser, the same construction attestral verify uses. Edit one entry and every hash after it stops matching.

chain VALID

That is integrity: the chain is internally consistent. A determined editor could recompute every hash and forge a fresh head, and this check alone would still pass. So attestral sign adds authenticity: an Ed25519 signature over the head, in a DSSE envelope (the Sigstore / in-toto envelope). attestral verify --public-key then catches a recomputed forgery, because re-sealing the chain needs the reviewer's private key. Tamper-evident, and now provably the chain they sealed.

Then the policy watches. Drift gets caught.

The attested design compiled to a default-deny policy. Now runtime telemetry is diffed against it, in batch or as a continuous sidecar: attestral drift --stdin reads a live telemetry pipe and --watch tails the log, streaming drift the moment it happens. What the review saw passes, and what it never saw is drift, each tagged with the rule that caught it: an unattested server, a rug-pull where the served tool schema no longer matches the attested manifest, a runaway loop. This is the loop no scanner and no gateway closes on its own.

compiled policy · default deny
jiraALLOW
filesystemALLOW
webALLOW
shellDENY
bound to review chain head c31f8e…9bd4

Honest by default

What it does not do.

A tool you can trust is one that is honest about its edges. In a field full of overclaiming, the tool that documents where it stops is the one a skeptical engineer keeps. So here are the limits, stated plainly, not buried in a footnote.

Design review, not SAST

It reads the declared surface

Config, agent wiring, prompts, IaC, not the inside of a tool's implementation. It tells you the capability a tool grants and the flows it enables; it will not find a logic bug inside that tool's code. Every scan says so up front, so a clean result never reads as "nothing here".

Reachable, not proven

Necessary, not sufficient

A reachable attack path means the design allows the flow over declared capability. It does not prove the model would follow an injection, or that no guardrail sits in the path. We reframed away from the word "proof" for exactly this reason. A reachable HIGH is worth prioritizing, not already exploited.

Known blind spots

And where we break

HCL cross-variable resolution depth, coarse capability classification, a probabilistic ML tier, agent code that needs a recognizable framework. We track a benign false-positive rate in CI and publish where a defense-aware attacker can dodge us. The full list is in docs/limitations.md.

Attestral Labs

Security built for the agentic era, not bolted onto it.

Agents got tools before anyone secured them. The industry's answer was to bolt an agent tab onto scanners built for a pre-agent world. Attestral is built the other way around: a system model of the agent, its tools, and the cloud they can reach comes first, and everything else follows from that graph.

Be #1, uncontested

Agentic security is our identity

MCP, prompt injection, tool poisoning, excessive agency, the OWASP Agentic and MITRE ATLAS surfaces. Depth here is the moat, and it is where we invest first.

Reachability across the fleet

Architecture, not a linter

One system model lets us reason about agent-to-cloud reachability, secrets crossing a boundary, reachable attack paths, and compile-to-policy drift. That is cross-server reachability across a trust boundary, which a per-file scanner cannot express, no matter how many rules it adds.

Parity, not volume

Your cloud, covered too

189 high-signal CIS checks across AWS, Azure, GCP, and Kubernetes. Good enough that most teams need no second scanner, never an arms race.

Open source, forever. The core is Apache 2.0 and always will be. The vision is a single artifact that carries a design from review, to proof, to runtime policy, so that "is this agent safe" stops being an opinion and becomes something anyone can verify. See the work on GitHub.

Start in sixty seconds.

No account, no server, no API key. The whole attest → prove → enforce → detect loop runs on your laptop, under Apache 2.0.

$ pip install attestral
$ attestral scan ./my-project              # attest: one system model, reviewed
$ attestral verify review.json           # prove: chain VALID
$ attestral compile ./my-project -o policy.yaml  # enforce: default-deny policy
$ attestral drift policy.yaml events.jsonl  # detect: runtime vs. reviewed design

Gate every deploy in CI with the GitHub Action:

# .github/workflows/drift.yml
- uses: attestral-labs/attestral@v1
  with:
    policy: policy.yaml
    events: events.jsonl