Metadata-Version: 2.4
Name: agent-cve-scanners
Version: 0.5.1
Summary: Offline scanners for the code patterns behind 53 CVEs in 9 AI-agent frameworks, plus MCP tool pinning. No dependencies, no telemetry.
Author: Erlend Christoffer Hagen Hårsaker
License-Expression: AGPL-3.0-only
Project-URL: Homepage, https://github.com/Ech333/agent-cve-scanners
Project-URL: Issues, https://github.com/Ech333/agent-cve-scanners/issues
Keywords: security,cve,ai-agents,llm-security,langchain,llamaindex,crewai,mcp,static-analysis
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# agent-cve-scanners

Free, offline scanners that check an AI-agent codebase for the code and dependency patterns behind
**68 published CVEs** (plus 4 GitHub-only advisories) in nine agent frameworks and in MCP client and
server code, and a small tool that pins MCP tool definitions so you notice when a server changes a tool
after you approved it.

| Scanner | Framework | CVEs covered | GitHub-only | Tracked, not covered |
|---|---|---|---|---|
| `autogpt_cve_scanner` | AutoGPT Platform | 19 | 4 | 8 + 4 |
| `crewai_cve_scanner` | CrewAI | 5 | 0 | 1 |
| `flowise_cve_scanner` | Flowise | 4 | 0 | 3 |
| `google_adk_cve_scanner` | Google Agent Development Kit | 2 | 0 | 1 |
| `langchain_cve_scanner` | LangChain | 7 | 0 | 0 |
| `langgraph_cve_scanner` | LangGraph | 7 | 0 | 2 |
| `llamaindex_cve_scanner` | LlamaIndex | 7 | 0 | 2 |
| `mcp_client_cve_scanner` | MCP client / host applications | 12 | 0 | 1 |
| `mcp_server_cve_scanner` | MCP servers over HTTP (Python, TypeScript) | 3 | 0 | 0 |
| `n8n_cve_scanner` | n8n | 2 | 0 | 0 |
| `semantic_kernel_cve_scanner` | Microsoft Semantic Kernel | 2 | 0 | 0 |
| `mcp_tool_pinning` | any MCP server | - | - | - |

The columns add up to more than 68 because two CVEs are covered by more than one scanner (68 is the
number of distinct CVEs). "Tracked, not covered" are real advisories a scanner's docstring names and
deliberately does **not** turn into a check - because the flaw is a missing limit rather than a code
shape, the technical detail is not public, or another tool already covers it. They are listed rather
than hidden: [`scanners/data/coverage.json`](https://github.com/Ech333/agent-cve-scanners/blob/main/scanners/data/coverage.json) has the exact ids per scanner and
is rebuilt by `tools/build_coverage.py`.

Every id was checked against its primary source (NVD for CVEs; GitHub's advisory database, including
repository-level advisories, for GHSAs): see [`docs/ADVISORY_VERIFICATION.md`](https://github.com/Ech333/agent-cve-scanners/blob/main/docs/ADVISORY_VERIFICATION.md).
Every covered CVE id was found in NVD or GitHub's advisory database at the last check; the ids that were
not (a ZDI-published CVE, and Bisheng's CVE-2026-33224) are the "tracked, not covered" ones.

## Use it

Python 3.10+, no dependencies.

```bash
python scan.py path/to/your/project                    # readable report
python scan.py path/to/your/project --json             # for CI; exit code 1 when something is flagged
python scan.py path/to/your/project --sarif out.sarif  # SARIF 2.1.0, for GitHub code scanning
```

Pin MCP tool definitions, then check them later:

```bash
python scanners/mcp_tool_pinning.py --help
```

Or install it as a package from a checkout (`pip install .`), which gives you the `agent-cve-scan` and
`mcp-tool-pin` commands.

### What it checks

1. **Code patterns** - nine scanners, one per framework. Calls wrapped over several lines, comments,
   docstrings, import aliases (`import pickle as pk`, `from x import load_prompt as lp`) and nested
   arguments are handled, so `load_prompt(\n    user_path\n)` is seen and `# never use pickle.loads(x)`
   is not.
2. **Dependency versions** - `requirements*.txt`, `pyproject.toml`, `setup.py`/`setup.cfg`,
   `Pipfile.lock`, `poetry.lock`, `uv.lock`, `package.json`, `package-lock.json` and `yarn.lock` are
   compared with the affected version ranges in [`scanners/data/affected.json`](https://github.com/Ech333/agent-cve-scanners/blob/main/scanners/data/affected.json)
   (taken from OSV, rebuilt by `tools/update_affected.py`). An exact pin or lockfile entry inside an
   affected range is a finding. A *range* is a finding only when its lowest allowed version is
   affected; `>=1.5.0` with no ceiling is not, because picking a version is the resolver's job.
   Advisories OSV has no PyPI/npm range for (for example AutoGPT's repository advisories) are not in
   that data and are never guessed. Turn this off with `--no-deps`.
3. **MCP client configs** - `.mcp.json`, `mcp.json`, `claude_desktop_config.json`, `mcp_config.json`,
   `.claude.json`: when a server is launched with `npx`, `bunx`, `pnpm dlx`, `uvx` or `pipx run` and
   names a version (`pkg@1.2.3`, `pkg==1.2.3`), that package and version are checked against the same
   advisory data (for example the MCP Python SDK, LiteLLM, n8n-mcp, dbt-mcp, Flowise). To check your own
   machine, `python scan.py --home-configs` reads only the well-known per-user config files (Claude
   Desktop, Claude Code, Cursor, Windsurf, VS Code), runs only the version check on them, shows paths
   as `~/...`, and says how many servers it could and could not judge: a server launched as plain
   `npx pkg` resolves to "latest" at run time and a static check cannot evaluate it. In a sample of 362 public
   `.mcp.json` files only about 9% of package launches pinned a version and none pinned one with a published advisory
   ([census](https://github.com/Ech333/agent-cve-scanners/blob/main/research/census-2026-10-01.md), a retrieved sample, not a prevalence
   estimate), so for MCP configs this check will usually find nothing to say; it matters more on lockfiles and requirements files.
4. **MCP client checks** (`mcp_client_cve_scanner`) - application code and project config that launch or
   trust MCP servers, for the shapes behind the MCP stdio command-injection CVE family and a Claude Code
   project-trust bypass. Python and TypeScript source for the first two, project-scoped files for the last two:
   - `stdio_command_non_literal` - `StdioServerParameters(...)` / `new StdioClientTransport({...})` whose
     `command` is not a literal, whose `args` is a variable or has a spread, or whose `env` is a variable,
     spreads a user dict, or sets a dangerous key (`NODE_OPTIONS`, `LD_PRELOAD`, `PATH`, ...) to a variable.
     Not flagged: `command=sys.executable`, list literals with a variable element, `{"API_KEY": key}`.
   - `command_allowlist_args_unvalidated` - an `args=[...]` list containing `-c`, `-e`, `--eval` or
     `--eval-string` in MCP client code, which defeats a command-name allowlist. Not reported in committed
     JSON configs, where `bash -c` wrappers are common.
   - `config_auto_approve_trust_bypass` - `enableAllProjectMcpServers: true` or a non-empty
     `enabledMcpjsonServers` in a committed `.claude/settings.json` / `.mcp.json` (CVE-2026-21852).
   - `vendor_api_base_url_override` - `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL` and similar set to a literal in a
     project-scoped file. Official `api.anthropic.com` / `api.openai.com` hosts, `$VAR` references and
     `.env.example`-style files are not reported.
   Advisories behind these include CVE-2025-65720, CVE-2026-30623, CVE-2026-40933 and CVE-2026-21852.
5. **MCP server checks** (`mcp_server_cve_scanner`) - Python and TypeScript code that serves MCP over HTTP:
   - `mcp_dns_rebinding_protection_disabled` (high confidence) - protection switched off or a `*` in the allowed
     hosts/origins (CVE-2025-66416 Python SDK < 1.23.0, CVE-2025-66414 TypeScript SDK < 1.24.0).
   - `mcp_dns_rebinding_protection_missing` (review) - a low-level transport built in a file that binds to
     localhost with no protection configured; `FastMCP()` and `createMcpExpressApp()` protect by default and are
     not flagged, and neither is a file that checks the Host/Origin header or keeps an allowed-hosts list itself. It cannot see authentication or other files, so behind real auth it is a false positive.
   - `mcp_transport_shared_across_clients` (review) - a TypeScript `StreamableHTTPServerTransport` created at module
     scope, so one transport serves every client (CVE-2026-25536, fixed in 1.26.0). Fine for a single-client tool.

**Not what this is:** a general MCP config linter. Plaintext secrets in configs, `http://` transports,
`curl | sh` launch commands, over-broad filesystem roots and unpinned `@latest` references are already
covered by other free tools - for example [mcp-config-audit](https://github.com/jiru-labs/mcp-config-audit),
[mcp-config-lint](https://github.com/basilalshukaili/mcp-config-lint) and
[mcp-drift-check](https://github.com/tomelias10/mcp-drift-check) - and are deliberately not repeated here.
This tool's lane is the question those do not ask: does a server version you pinned have a published advisory.

### Keeping the noise down

```bash
python scan.py . --no-tests                  # drop findings under tests/, examples/, docs/ (they are tagged either way)
python scan.py . --ignore 'vendor/*'         # drop findings by path glob (repeatable)
python scan.py . --write-baseline base.json  # record today's findings (exit 0)
python scan.py . --baseline base.json        # report only findings NOT in base.json - fail CI on new problems
```

Suppress one finding in the source it points at (same line, or a comment line directly above):

```python
data = pickle.loads(blob)  # agent-cve-scan: ignore[langgrinch_insecure_deserialization]
# agent-cve-scan: ignore            <- a bare marker covers every category
```

A baseline entry is keyed by rule + file + the normalised line text, not by line number, so unrelated
edits above a finding do not bring it back.

### CI and pre-commit

```yaml
# .github/workflows/agent-cve-scan.yml (steps)
- uses: actions/checkout@v4
- uses: Ech333/agent-cve-scanners@v0.5.1
  with:
    baseline: .agent-cve-baseline.json   # optional: fail only on new findings
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with: { sarif_file: agent-cve-scan.sarif }
```

The action is a composite action that uses the runner's `python3`: no `pip install`, no network, no telemetry.
For pre-commit, add the hook `agent-cve-scan` from this repository to `.pre-commit-config.yaml`. Inputs, outputs,
permissions and a complete workflow are in [`docs/usage-ci.md`](https://github.com/Ech333/agent-cve-scanners/blob/main/docs/usage-ci.md).

### Policy (dry run first)

`agent-cve-scan . --policy policy.json` checks which MCP servers a project launches against a policy: package
allow/deny globs, a "must be pinned" rule, and optionally "no launches I cannot identify". It is a dry run by
default (violations are printed, the exit code is unchanged); `--enforce` makes them fail the run. The same file
carries a `tools` section (`allowed_tools` / `blocked_tools`) in the field names MCP Gateway's `ToolPolicy`
takes, so a team can start in dry run here and move to runtime enforcement of tool calls later. An invalid policy
is an error (exit 2), never a permissive default. Details and an example: [`docs/policy.md`](https://github.com/Ech333/agent-cve-scanners/blob/main/docs/policy.md).

## What it is and isn't

- **Reads files only.** It never runs your code, never installs anything and never sends anything
  anywhere. No telemetry, no account. The advisory data is bundled; nothing is fetched when you scan.
- **Pattern matching, not proof.** A finding means "this looks like the shape behind CVE-X, go look",
  not a confirmed exploit. A clean result does not mean you are safe. Keep your frameworks updated.
- **Known limits.** It is a text scanner, not a data-flow analyser: it does not know whether a value
  reaching a call is attacker-controlled, only whether it is a literal. It does not resolve transitive
  dependencies that no lockfile in the tree records. Python statements are joined using the standard
  tokenizer; JavaScript/TypeScript use a simpler bracket-matching join, so unusual JS (regex literals
  containing brackets, template-literal tricks) can be missed or mis-joined. Code quoted inside a
  string that is *assigned* (a prompt template, a test fixture) is still scanned; bare docstrings are
  not. Scanning the source of a scanner (including these) reports its own finding messages.
- Findings describe the vulnerable pattern and the fixed version; no exploit code is included.

## Tests

```bash
python -m pytest -q tests
```

## Maintaining the data (not needed to use the scanners)

```bash
python tools/update_affected.py        # affected version ranges, from OSV
python tools/build_coverage.py         # covered vs tracked-not-covered, per scanner
python tools/verify_advisories.py > docs/ADVISORY_VERIFICATION.md   # NVD + GitHub existence check (slow: NVD rate limit)
```

`tools/tracked_advisories.txt` lists advisories followed for the version check even though no code
pattern exists for them (mostly MCP-server and agent-platform advisories from 2026).

## Licence

GNU Affero General Public License v3.0 only (see `LICENSE`). If you change it and offer it to others,
including as a hosted service, you publish your changes under the same licence.

Maintained by Erlend Christoffer Hagen Hårsaker (Norway). Corrections and new advisories welcome as
issues or pull requests.
