Metadata-Version: 2.5
Name: agent-boundary-scan
Version: 1.1.0
Summary: Static analyzer for confused-deputy vulnerabilities in MCP, LangGraph, OpenAI Agents, and CrewAI systems
Project-URL: Homepage, https://github.com/AureliusOctavion/agent-boundary-scan
Project-URL: Repository, https://github.com/AureliusOctavion/agent-boundary-scan
Project-URL: Issues, https://github.com/AureliusOctavion/agent-boundary-scan/issues
Author: Siddharth Sriram
License: MIT
Keywords: confused-deputy,langgraph,llm-agents,mcp,sast,security,static-analysis
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.11
Requires-Dist: networkx>=3.2
Requires-Dist: pydantic>=2.5
Requires-Dist: pyyaml>=6.0
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# agent-boundary-scan

Static analysis that detects **confused-deputy vulnerabilities** in multi-agent and MCP-based AI
systems — unvalidated paths where attacker-influenced data can reach a privileged action (shell
exec, file write, credential access, network egress, agent delegation) with no trust boundary in
between. It reads an agent/tool graph and reports where the unvalidated paths are, the way a SAST
tool flags SQL injection before the query ever runs.

Supports **LangGraph**, the **OpenAI Agents SDK**, **CrewAI**, and **MCP** (both server manifests and
**FastMCP** `@mcp.tool` server source, the latter giving HIGH-confidence analysis of the real tool
bodies). Outputs a console table, JSON, or spec-compliant **SARIF 2.1.0** for GitHub code scanning,
and returns a CI-friendly exit code.

> Status: **v1.0** (research/portfolio project). See [DESIGN.md](./DESIGN.md) for the full
> rationale behind every decision below.

## The problem

Single-agent LLM systems have one trust boundary: the line between the user's prompt and the model.
Multi-agent systems (LangGraph, CrewAI, AutoGen, Google ADK) and MCP tool integrations multiply that
boundary across a graph of agents and tools, and most frameworks wire that graph together without
re-checking authorization at each hop. Concretely: data enters from a source that may be
attacker-influenced — a scraped web page, a document pulled into RAG, a third-party API response, a
poisoned tool result — and can flow, unvalidated, into a node with real-world write power.

This is a structural recurrence of the **confused deputy** problem (Norm Hardy, 1988), catalogued by
MITRE as **CWE-441 (Unintended Proxy or Intermediary)**. What's new is the medium: the "deputy" is
now an LLM interpreting natural language, and the trust boundary is a graph edge instead of a
function call — which makes the vulnerability harder to see by inspection and easier to construct by
accident. The attack and runtime-defense research is mature; the **static** side (inspect the graph
before deployment) is the gap this fills.

## How it works

```mermaid
flowchart LR
    subgraph Extraction
        A1[LangGraph parser]
        A2[OpenAI Agents / CrewAI parsers]
        A3[FastMCP / MCP manifest parser]
    end
    B[Capability + source<br/>classification]
    C[networkx graph]
    subgraph Analysis
        D[Trust-boundary<br/>detection]
        E[Taint path<br/>enumeration]
        F[Rule engine]
        G[Severity + confidence<br/>scoring]
    end
    subgraph Reporting
        H1[Console]
        H2[JSON]
        H3[SARIF]
    end
    A1 --> B
    A2 --> B
    A3 --> B
    B --> C
    C --> D
    C --> E
    D --> E
    E --> F
    F --> G
    G --> H1 & H2 & H3
```

1. **Extraction** parses LangGraph source, OpenAI Agents SDK source, FastMCP server source
   (`@mcp.tool`), and MCP manifests into a normalized IR of nodes and edges. A `.py` file is parsed
   against each framework (each ignores files that aren't its own). For LangGraph it AST-walks graph
   construction (`StateGraph`, `add_node`, `add_edge`,
   `add_conditional_edges`), the four tool-wiring idioms (`ToolNode`, `create_react_agent`,
   `create_agent`, in-body `llm.bind_tools`), and modern multi-agent control flow
   (`Command(goto=...)` and `Send(...)` handoffs, `compile(interrupt_before/after=[...])` pause
   points). Each node's calls — **including those in local helper functions it calls**
   (interprocedural) — are recorded as evidence. Anything too dynamic to resolve is reported as
   `unresolved` rather than silently dropped.
2. **Classification** assigns each node capabilities and a source category using a signal hierarchy:
   AST-detected calls (HIGH) > MCP annotation hints / known dangerous tool classes like
   `CodeInterpreterTool`/`ShellTool` (MEDIUM) > name/description keywords (LOW). Confidence is
   reported alongside every finding.
3. **Graph** construction assembles a `networkx` digraph. A flat MCP manifest (no edges) gets a
   synthesized client hub wired to every tool, modeling that one session can call them all.
4. **Analysis** detects trust boundaries, enumerates source→sink paths that don't cross one, and
   applies five rules (four graph-reachability + intra-tool value taint).
5. **Reporting** renders findings as console / JSON / SARIF and sets the exit code.

## What it detects

| Rule | What it flags | CWE (informal) |
|---|---|---|
| **confused-deputy** | An unmitigated path from an untrusted source to a privileged sink | CWE-441 |
| **lethal-trifecta** | One subgraph with untrusted input **+** private data **+** external egress ([Willison](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/)) | CWE-668 |
| **cyclic-delegation** | An agent-to-agent delegation cycle that can bypass per-hop capability limits | CWE-441 |
| **guardrail-only** | A path whose only control is a prompt-based/sanitizer guard, not a structural boundary | CWE-863 |
| **tool-injection** | A tool whose own input flows unsanitized to a dangerous sink (intra-tool value taint) | CWE-78 / 918 / 22 |

Rules 1–4 are **cross-agent reachability** (can untrusted data *reach* a sink across the graph);
rule 5 is **intra-tool value taint** (is a single tool's own input passed to a dangerous sink — a
classic injection bug). See [DESIGN.md §5a](./DESIGN.md) for why the inter-node analysis is
reachability rather than value-tracking, grounded in the AgentFlow / VIPER-MCP literature.

**Capabilities:** `CODE_EXEC`, `FILE_WRITE`, `FILE_READ`, `NETWORK_EGRESS`, `CREDENTIAL_ACCESS`,
`AGENT_DELEGATION`, `EXTERNAL_ACTION` (financial/physical/publishing actions), `READ_ONLY_QUERY`.
**Sources:** `UNTRUSTED_EXTERNAL`, `USER_INPUT`, `TRUSTED_INTERNAL`.

**What counts as a trust boundary** (a path crossing one is *mitigated*): a human-in-the-loop
`interrupt()`; a deterministic validator with a rejection path (pydantic/jsonschema validation, or a
regex/allowlist check followed by `raise`); or a node you explicitly trust in the config. A bare
sanitizer (`re.sub`, `.strip`) or an LLM "is this safe?" check is **not** a boundary.

## Installation

Requires Python 3.11+.

```bash
pip install agent-boundary-scan
```

Or from source:

```bash
git clone https://github.com/AureliusOctavion/agent-boundary-scan.git
cd agent-boundary-scan
pip install -e .
```

## Usage

```bash
agent-boundary-scan scan <path>          # a .py file, a .json manifest, or a directory
agent-boundary-scan scan src/ -f sarif -o results.sarif
agent-boundary-scan scan graph.py --fail-on CRITICAL
```

| Option | Meaning |
|---|---|
| `-f, --format` | `console` (default), `json`, or `sarif` |
| `-o, --output` | Write the report to a file instead of stdout |
| `--fail-on` | Severity gate for the exit code: `CRITICAL \| HIGH \| MEDIUM \| LOW` (default `HIGH`) |
| `--config` | Path to an `.agent-boundary-scan.yml` (default: found next to the target) |

**Exit codes:** `0` clean, `1` a finding at or above the `--fail-on` severity was found, `2`
usage/scan error. That makes `scan` usable directly as a CI gate.

### Console output

```
1 finding(s) across 1 file(s): 1 critical

1. [CRITICAL] confused-deputy  (confidence: high, CWE-441)  confused_deputy_langgraph.py:31
   Untrusted data (from: fetch_page) can reach privileged sink 'run_command' (CODE_EXEC) with no
   trust boundary in between. Example path: fetch_page -> run_command.
   path: fetch_page -> run_command
```

### SARIF (for GitHub code scanning)

Severity maps to the SARIF `level` plus a numeric `security-severity` (so GitHub ranks findings);
confidence maps to `properties.precision` (the CodeQL convention).

```json
{
  "ruleId": "confused-deputy",
  "level": "error",
  "message": { "text": "Untrusted data (from: fetch_page) can reach privileged sink 'run_command' (CODE_EXEC) ..." },
  "properties": { "security-severity": "9.0", "precision": "high", "cwe": "CWE-441" },
  "locations": [ { "physicalLocation": {
    "artifactLocation": { "uri": "confused_deputy_langgraph.py" },
    "region": { "startLine": 31 } } } ]
}
```

### Configuration (`.agent-boundary-scan.yml`)

```yaml
# Nodes to trust as boundaries even if the static heuristics don't recognize them.
# Every path cleared this way is labeled "boundary trusted via user override" in output.
trusted_boundaries:
  - my_module.custom_validator
  - approval_node

# Severity at or above which `scan` exits non-zero (overridable with --fail-on).
fail_on_severity: HIGH
```

Keeping overrides in a checked-in file (rather than an in-code decorator) means they work for
MCP-manifest-only scans too, and every trust decision is visible in one PR-reviewable place.

### GitHub Action

A reusable action ships in this repo. It installs the scanner, runs it, and gates the build; set
`upload-sarif: true` (with `security-events: write`) to feed findings into GitHub code scanning:

```yaml
permissions:
  security-events: write   # only needed for upload-sarif
jobs:
  agent-boundary-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: AureliusOctavion/agent-boundary-scan@v1
        with:
          path: .
          fail-on: HIGH
          upload-sarif: true
```

Inputs: `path` (default `.`), `fail-on` (default `HIGH`), `output` (SARIF path), `config`,
`upload-sarif` (default `false`), `python-version` (default `3.11`).

### pre-commit hook

```yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/AureliusOctavion/agent-boundary-scan
    rev: v1.1.0
    hooks:
      - id: agent-boundary-scan
```

Scans the whole tree on commit (the analysis is graph-based). Override the target or gate with
`args: ["scan", "src/", "--fail-on", "CRITICAL"]`.

## Limitations (read these)

This is a **triage/assist** tool, not a proof of safety. A clean scan means "no obvious structural
red flags, plus here is where I couldn't see" — never "safe". Key false-negative/false-positive
risks (full discussion in [DESIGN.md §7](./DESIGN.md)):

- **Heuristic classification** (LOW-confidence, keyword-based) both under- and over-classifies;
  confidence is always reported so you can weigh it.
- **Interprocedural analysis is module-local and bare-name only** — a node is scanned together with
  local helpers it calls, but danger reached only through imported code or method calls (`obj.foo()`)
  is still under-reported.
- **MCP-only scans** have no function bodies to inspect, so confidence is capped at MEDIUM/LOW, and
  the co-reachability model assumes all of a manifest's tools are exposed to one agent (can
  over-flag).
- **MCP annotation hints** are server-supplied claims, treated as untrusted per the MCP spec.
- **Cyclic-delegation** deliberately excludes agent↔tool ReAct loops (can miss a cycle through an
  agent that also wraps tools).

## Prior art

- **pajaMAS** — Trail of Bits (Triedman et al., 2025): MAS-hijacking control-flow attacks.
- **InjecAgent** (ACL Findings 2024) and **AgentDojo** (NeurIPS 2024): indirect-prompt-injection
  benchmarks for tool-using agents.
- **Prompt Flow Integrity** (Kim, Choi, Lee, 2025) and **MiniScope** (2025): runtime enforcement /
  least-privilege reconstruction — complementary to this tool's static, pre-deploy analysis.
- **OWASP Top 10 for Agentic Applications** (2026) and Simon Willison's **lethal trifecta** (2025).
- **"Capability Gates Are Not Authorization"** ([arXiv 2606.28679](https://arxiv.org/abs/2606.28679),
  2026): frameworks gate *which* tools a model sees but don't re-authorize the specific arguments of
  a specific call — the gap confused-deputy paths exploit.
- **AgentFlow** ([arXiv 2607.01640](https://arxiv.org/abs/2607.01640), 2026): the first static
  analysis framework for agent programs — independently validates this tool's approach (LLM as a
  taint conduit; reachability over an over-approximating dependency graph).
- **VIPER-MCP** ([arXiv 2605.21392](https://arxiv.org/abs/2605.21392), 2026): intra-tool taint of MCP
  servers (106 zero-day CVEs) — the basis for the `tool-injection` rule.

Fixtures under [`fixtures/`](./fixtures) recreate the *patterns* (not verbatim code) of these
scenarios; the test suite asserts every vulnerable fixture is flagged and every safe fixture is not.

## Future work

- **Deeper interprocedural resolution** — follow method calls and imported helpers (needs light
  type/import resolution), beyond today's module-local bare-name expansion.
- **Compiled subgraphs as nodes** — inline a `builder.compile()` used as an `add_node` action
  (Command/Send handoffs and `interrupt_before/after` pause points are already handled).
- **MCP resources & prompts** (`@mcp.resource`, `@mcp.prompt`) — resources as untrusted sources.
- **Full argument-level dataflow** — track the untrusted *value* into the sink's dangerous argument
  (today's taint is graph-reachability with self-node and per-sink precision refinements).
- **OpenAI Agents SDK guardrails** — recognize `tool_input_guardrails` / `output_guardrails` hooks
  as trust boundaries (surfaced by the real-corpus benchmark; see [benchmark/](./benchmark)).
- **AutoGen / ag2, Google ADK, Pydantic AI** extraction, and cross-framework tracing.
- **CrewAI Task/context chaining** (v1 models agents, tools, and delegation, not task data-flow).
- **SARIF codeFlows** — render the full source→sink chain as thread-flow locations.
- Correlating an MCP manifest with the server implementation source when available (upgrades
  MCP-only confidence from MEDIUM to HIGH).

## Development

```bash
pip install -e ".[dev]"
pytest -q          # 152 tests
ruff check .       # lint
```

CI (GitHub Actions) runs lint + tests on Python 3.11 and 3.12.

## License

MIT
