Skip to content
calbebopPublic

About

Active security scanner for A2A and MCP servers

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

Batesian

Build Latest release License OpenSSF Scorecard

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.

Batesian probing a local A2A fixture and confirming unenforced authentication

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.

Highlights

  • 60 bundled rules: 22 A2A and 38 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.

Install

Release artifacts

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 --version

Published 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.

Go toolchain

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.

Quick start

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.com

probe 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-001

Coverage

The bundled catalogs contain 22 A2A rules and 38 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
Cache scoping Public MCP cache hints on principal-specific discovery and listings
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 and secure controls, and the protocol coverage matrix for compatibility gates and advisory limits.

Authentication and identity

Bearer token

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.com

BATESIAN_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.

OAuth client credentials

read -rsp "OAuth client secret: " BATESIAN_CLIENT_SECRET && echo
export BATESIAN_CLIENT_SECRET
batesian scan --target https://mcp.example.com \
  --token-url https://auth.example.com/oauth/token \
  --client-id my-client \
  --oauth-scopes mcp:read,mcp:write

BATESIAN_CLIENT_SECRET is used for the client credentials flow. A non-empty --client-secret overrides it, but puts the secret in process arguments.

When 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.

Authorization code with PKCE

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:read

The 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.

Multiple principals

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.yaml
target: https://agent.example.com
principals:
  - name: tenant-a
    token: "${TOKEN_A}"
    tenant: A
    headers:
      X-Tenant-Id: A
  - name: tenant-b
    token: "${TOKEN_B}"
    tenant: B
    headers:
      X-Tenant-Id: B

Set TOKEN_A and TOKEN_B in the environment, then run batesian scan from the config directory. For rules that use a single owner credential, also set BATESIAN_TOKEN to the intended owner's token. An unset or empty principal token variable stops the scan before requests are sent.

Expected protection

Some rules can prove that an operation succeeded but cannot know whether the deployment intended to allow it. Add exact expected_denials to batesian.yaml when you know the boundary:

target: https://agent.example.com
principals:
  - name: tenant-a
    token: "${TOKEN_A}"
    tenant: A
  - name: tenant-b
    token: "${TOKEN_B}"
    tenant: B
expected_denials:
  - operation: a2a.tasks.cancel
    caller: tenant-b
    caller_tenant: B
    owner: tenant-a
    owner_tenant: A
  - operation: mcp.prompts.get
    caller: anonymous
  - operation: mcp.tasks.get
    caller: tenant-b
    caller_tenant: B
    owner: tenant-a
    owner_tenant: A
  - operation: mcp.resources.read
    caller: tenant-b
    caller_tenant: B
    owner: tenant-a
    owner_tenant: A

An observed access matching the operation and caller/owner names is marked confirmed; tenant fields, when supplied, must also match. No policy means the finding remains an indicator. This changes verdicts, not scan traffic or severity. A policy requires a target in the same config file and cannot be reused with a different --target. Each entry covers the entire operation, so do not use it when only selected prompts or tasks are private.

Supported operations are a2a.tasks.get, a2a.tasks.list, a2a.tasks.cancel, mcp.prompts.list, mcp.prompts.get, mcp.completion.complete, mcp.tasks.get, mcp.tasks.update, mcp.tasks.cancel, and mcp.resources.read. The A2A names cover both JSON-RPC and HTTP+JSON. A2A task reads use owner: default for the identity supplied with --token; task cancellation and modern MCP cross-principal checks use configured principal names. Error responses and unverified access are never promoted. A completed access subcheck may still be reported when another subcheck is incomplete.

Safe operation

The opt-in A2A gRPC rule tests task access using two configured principals. Set --a2a-grpc-target grpcs://host:port to the card-advertised address, or add a2a_grpc_target beside target in the config. Plaintext grpc:// is limited to loopback. Other A2A rules use JSON-RPC or HTTP+JSON; see the binding assessment for limits.

  • --dry-run records 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:8080 routes 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-tls disables 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.com to 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 NAME before calling a real MCP tool. Review the tool yourself; server-supplied read-only annotations are only hints.
  • The filesystem suffix check requires a target-bound mcp_filesystem_suffix_probe with a disposable allowed directory and explicit approval of get_file_info. It requests metadata for missing paths only.
  • The filesystem symlink check requires a target-bound mcp_filesystem_symlink_probe with an operator-prepared link and outside file. It calls only approved metadata tools and never reads file contents.
  • The MCP subscription check runs only with a target-bound mcp_subscription_probe in the config. It calls the specified update tool once, so use a disposable resource and arguments.
  • The MCP cross-audience check needs a working target JWT and a target-bound mcp_cross_audience_token issued for another resource. It uses read-only requests and reports accepted other-resource tokens as indicators.
  • The MCP session-principal check requires two bearer-token principals and a target-bound mcp.sessions.use expected denial. It compares read-only listings across stateful sessions and closes both sessions afterward.
  • JSON and SARIF may contain URLs, response snippets, and evidence. Treat them as sensitive security artifacts.

Results and exit behavior

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 when the rule proves a failed boundary, including observed access that matches an explicit expected denial. An indicator identifies a risky condition without claiming a policy violation.

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.

Use --scan-budget 300 to set an optional five-minute budget for rule execution. The default is unlimited. This is separate from the per-request --timeout and can also be set as scan_budget: 300 in the config; an explicit --scan-budget 0 disables a configured budget. OAuth setup and --dry-run are outside the budget. Rules cooperate with cancellation, and bounded cleanup may continue after the deadline. If the budget expires, completed findings remain in the report, interrupted and unstarted rules are marked skipped, JSON sets scan_budget_exhausted, and SARIF sets scanBudgetExhausted in invocation properties. Review incomplete coverage before relying on the result.

CI integration

The following workflow uses v1.8.1 coverage metadata to upload A2A findings and fail when its card check does not complete. The target must serve an Agent Card.

name: Batesian

on: workflow_dispatch

jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
        with:
          go-version: '1.26.10'
      - run: go install github.com/calbebop/batesian/cmd/batesian@v1.8.1
      - name: Scan
        env:
          AGENT_TARGET_URL: ${{ vars.AGENT_TARGET_URL }}
        run: |
          batesian scan --no-config --target "$AGENT_TARGET_URL" \
            --protocol a2a \
            --rule-ids a2a-card-trust-001 \
            --output sarif > results.sarif
      - uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
        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.sarif

Set the AGENT_TARGET_URL repository variable before running this workflow. Update the pinned release after review. This example uses a manual trigger; keep credentialed scans off pull-request code. Select only rules whose authentication, identity, and callback prerequisites the job can provide; skipped or errored rules are not evidence of a clean target. A two-principal example is in CI/CD integration.

Configuration and rule packs

Generate an annotated configuration file with:

batesian init

Run 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, oversized, or unsupported descriptor stops the command before scan traffic.

Contributing and security

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.

References

License

Apache 2.0. See LICENSE.

About

Active security scanner for A2A and MCP servers

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages