Protocol-aware security testing for A2A and MCP.
Batesian is a single-binary CLI that tests deployed AI agent infrastructure with real protocol traffic. It exercises authorization, identity, discovery, task isolation, callbacks, sessions, and tool boundaries, then reports evidence as a confirmed exploit or a risk indicator. Table, JSON, and SARIF output support local testing and CI.
This README describes the current main branch, which may differ from the latest release. Use a release tag for its exact behavior and see the changelog for changes by version.
Caution
Run Batesian only against systems you own or have explicit written permission to test. Scans send attack-shaped traffic and some rules can change server-side state. Use scan --dry-run to inspect the request plan without sending traffic.
The recording uses a deliberately vulnerable local fixture. Recreate it with ./docs/render-demo.ps1 from PowerShell; the demo tape defines the capture. The script removes its containers and temporary binary when it finishes.
- 50 bundled rules: 21 A2A and 29 MCP checks focused on protocol-specific weaknesses.
- Active verification: rules evaluate live responses, state transitions, and out-of-band callbacks instead of relying on banners or static heuristics.
- Evidence-aware results: confirmed behavior is separated from indicators that require manual validation.
- Honest coverage: skipped, unsupported, and errored rules remain visible instead of being reported as clean.
- CI-ready output: SARIF 2.1.0 integrates with GitHub code scanning; JSON supports custom pipelines.
- Controlled execution: dry runs, exact rule filters, proxy support, strict configuration, and explicit approval for mutating MCP tools.
Download the latest archive from GitHub Releases.
| Platform | Architectures | Archive |
|---|---|---|
| Linux | x86_64, arm64 | .tar.gz |
| macOS | x86_64, arm64 | .tar.gz |
| Windows | x86_64 | .zip |
# Example: Linux x86_64
tar -xzf batesian_<version>_linux_x86_64.tar.gz
sudo install -m 0755 batesian /usr/local/bin/batesian
batesian --versionPublished releases include SHA-256 checksums, a Sigstore bundle for the checksum file, a per-archive SBOM, and SLSA build provenance. Check each release's SBOM for its format; the current release pipeline produces CycloneDX.
The current main branch requires Go 1.26.10 or newer. A release tag's go.mod specifies its own minimum:
go install github.com/calbebop/batesian/cmd/batesian@latest@latest installs the latest tagged release. To install the current main branch instead, use @main. Pin a reviewed commit or release tag in CI.
List the available checks, probe the target, review the attack plan, and then run a scan:
batesian rules --protocol mcp --severity critical,high
batesian probe --target https://agent.example.com --protocol a2a
batesian scan --target https://agent.example.com --dry-run
batesian scan --target https://agent.example.comprobe performs lightweight reconnaissance and produces table or JSON output. scan executes the applicable attack rules and supports table, JSON, or SARIF output.
Run a smaller rule set with protocol, severity, tag, or rule-ID filters:
batesian scan --target https://mcp.example.com \
--protocol mcp \
--severity critical,high
batesian scan --target https://agent.example.com \
--rule-ids a2a-push-ssrf-001,a2a-task-idor-001The bundled catalogs contain 21 A2A rules and 29 MCP rules. Every descriptor maps to compiled attack logic, CWE references, and remediation guidance.
| Area | Examples |
|---|---|
| OAuth and token handling | Audience and scope validation, DCR, token replay, metadata fetches, redirect confusion |
| Agent identity and discovery | Agent Card trust, JWS validation, host injection, extension downgrade, extended-card access |
| Task and tenant isolation | Task IDOR, cross-principal cancellation and enumeration, context fixation, delegation integrity |
| Push notifications | Callback SSRF, control-plane binding, missing callback authentication |
| MCP authorization surfaces | Unauthenticated tools, resources, prompts, completions, and logging controls |
| Transport boundaries | Origin validation, header/body routing, protocol downgrade, SSE replay, session misuse |
| Tool and manifest integrity | Argument traversal, hidden characters, description injection, duplicate-name ambiguity, manifest drift |
| Deployment exposure | Shadow listeners, secret leakage, and known vulnerable component versions |
The suite is validated against vulnerable fixtures and third-party reference implementations. See validation results for expected findings, secure controls, and known applicability limits.
Prefer the environment variable so credentials stay out of process arguments. Read it without echoing or recording the value in shell history:
read -rsp "Batesian token: " BATESIAN_TOKEN && echo
export BATESIAN_TOKEN
batesian scan --target https://mcp.example.comBATESIAN_TOKEN works with both scan and probe. A non-empty --token overrides it. scan checks the token in batesian.yaml before the environment; probe does not load YAML config.
batesian scan --target https://mcp.example.com \
--token-url https://auth.example.com/oauth/token \
--client-id my-client \
--client-secret "$CLIENT_SECRET" \
--oauth-scopes mcp:read,mcp:writeWhen MCP rules are selected, Batesian sends the target's canonical URI as the
OAuth resource in token requests. Use --oauth-resource https://mcp.example.com/mcp
if the resource server's canonical URI differs from --target. A2A-only scans
omit resource unless this flag is set. --oauth-audience remains a separate,
provider-specific parameter.
OAuth token responses must declare a Bearer token_type; other or missing
types are rejected before the token can be used in a scan.
Batesian prints the authorization URL to stderr for manual sign-in. It contains a one-time state value, so keep it private and avoid saving terminal logs during sign-in.
batesian scan --target https://mcp.example.com \
--auth-url https://auth.example.com/authorize \
--token-url https://auth.example.com/oauth/token \
--oauth-issuer https://auth.example.com \
--client-id my-client \
--oauth-scopes mcp:readThe PKCE flow sends the same resource in both the authorization and token
requests.
Set --oauth-issuer to the exact issuer in the authorization server metadata,
which may include a path. Batesian rejects callbacks with a missing or different
iss when this flag is set. It also rejects an iss received without a
configured issuer. Servers that omit iss remain compatible when the flag is
unset. This manual endpoint flow does not discover or validate authorization
server metadata; verify the issuer and endpoints before using it.
Cross-tenant and cross-principal checks require distinct identities. Add routing headers when the target derives tenant identity from a header rather than the token:
batesian scan --target https://agent.example.com \
--principal name=tenant-a,token="$TOKEN_A",tenant=A,header=X-Tenant-Id:A \
--principal name=tenant-b,token="$TOKEN_B",tenant=B,header=X-Tenant-Id:BIn batesian.yaml, principal tokens can use ${TOKEN_A}-style environment references. An unset or empty variable stops the scan before requests are sent.
--dry-runrecords a redacted request plan and sends no network traffic. Header values, URL userinfo and queries, and request body values are hidden; hosts and paths remain visible. Requests that depend on live responses, acquired tokens, or callbacks cannot be expanded fully until a real scan.--proxy 127.0.0.1:8080routes scan and OAuth traffic through an intercepting proxy. Environment proxy variables are honored when the flag is absent; Go excludes loopback targets from environment proxies.--skip-tlsdisables certificate validation and should be limited to controlled labs or intercepting proxies.- Target-advertised OAuth endpoints are restricted to the target origin. Use
--oauth-origin https://login.example.comto approve an additional exact origin. - OAuth probes attempt to remove registered clients when RFC 7592 management is available. External OOB metadata checks leave the client registered so delayed callbacks remain observable; remove it manually after verification.
- Stateful rules may create A2A and MCP tasks, A2A push configurations, and OAuth clients. Scan reports list accepted and unverified objects with cleanup status even when no finding is produced. The push SSRF, callback-auth, and binding rules attempt to delete configurations after readback confirms the registered callback. External OOB probes retain registrations for manual verification. Other push configs may remain. Task cancellation is not deletion. Review leftover objects after a scan.
- The scope-confusion rule invokes a mutating tool only when its exact name is approved with
--mcp-scope-tool. This also asserts that the limited principal should be denied access to that tool. - Traversal and task rules require
--mcp-invoke-tool NAMEbefore calling a real MCP tool. Review the tool yourself; server-supplied read-only annotations are only hints. - JSON and SARIF may contain URLs, response snippets, and evidence. Treat them as sensitive security artifacts.
| Output | Intended use |
|---|---|
table |
Interactive review in a terminal |
json |
Automation and custom policy gates |
sarif |
GitHub code scanning and SARIF-compatible platforms |
For A2A probes, JSON agent_card is a parsed, redacted view, not the original response bytes. agent_card_provenance records the fetched URL and SHA-256 of those bytes.
A finding is marked confirmed only when the rule observes exploit evidence. An indicator identifies a risky condition without claiming successful exploitation.
scan exits non-zero for command-level failures, including invalid configuration or filters that select no rules. Findings and individual rule skips do not change the process exit code. Check rule outcomes in JSON or SARIF before treating a scan as complete; a zero exit code alone is not a clean bill of health. SARIF invocation metadata records completed, skipped, and errored rules and sets executionSuccessful to false when coverage is incomplete.
The following workflow uses current main coverage metadata to upload A2A findings and fail when selected rules do not complete:
name: Batesian
on: workflow_dispatch
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/setup-go@v7
with:
go-version: '1.26.10'
- run: go install github.com/calbebop/batesian/cmd/batesian@v1.8.1
- name: Scan
env:
BATESIAN_TOKEN: ${{ secrets.BATESIAN_TOKEN }}
run: batesian scan --target https://agent.example.com --protocol a2a --output sarif > results.sarif
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: results.sarif
- name: Require complete coverage
run: |
jq -e '
(.runs | length) == 1 and
(.runs[0].invocations | length) == 1 and
(.runs[0].invocations[0] |
.executionSuccessful == true and
.properties.rulesSelected > 0 and
.properties.rulesCompleted == .properties.rulesSelected and
.properties.rulesSkipped == 0 and
.properties.rulesErrored == 0)
' results.sarifUpdate the pinned release after review. This example uses a manual trigger so credentials are not exposed to pull-request code. Older releases may not include coverage metadata. Select only protocols and rules whose authentication, identity, and callback prerequisites the job can provide; skipped or errored rules are not evidence of a clean target. More deployment patterns are documented in CI/CD integration.
Generate an annotated configuration file with:
batesian initRun init from the project directory. It creates batesian.yaml there and will not overwrite an existing file; it does not print the YAML to stdout. Explicit or discovered configuration must be readable, contain one YAML document, and use known keys.
scan searches the current directory and its parents for a config file and reports the selected path before OAuth or scan traffic begins (stderr for JSON and SARIF). Pre-scan errors also name the selected file. Use --config path to select a file or --no-config to use only flags and environment variables. The two flags cannot be combined. --no-config affects file discovery, not protocol discovery.
--rules-dir supplements the built-in catalog with strict YAML descriptors. Descriptors select compiled executors and provide report metadata; they cannot define arbitrary network behavior. A rule pack loads atomically, so one unreadable, invalid, or oversized descriptor stops the command instead of producing a partial scan.
Rules, fixes, and documentation are welcome. See CONTRIBUTING.md for the rule architecture, test requirements, and local workflow. Vulnerable fixtures and their port assignments are documented in testdata/README.md.
Report vulnerabilities in Batesian privately according to SECURITY.md.
- A2A Protocol Specification
- MCP Authorization Specification
- MCP Security Best Practices
- Unit 42: Agent Session Smuggling in A2A Systems
- OWASP GenAI Security Project
Apache 2.0. See LICENSE.
