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 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.
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.
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:
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
- 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, soload_prompt(\n user_path\n)is seen and# never use pickle.loads(x)is not. - Dependency versions -
requirements*.txt,pyproject.toml,setup.py/setup.cfg,Pipfile.lock,poetry.lock,uv.lock,package.json,package-lock.jsonandyarn.lockare compared with the affected version ranges inscanners/data/affected.json(taken from OSV, rebuilt bytools/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.0with 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. - MCP client configs -
.mcp.json,mcp.json,claude_desktop_config.json,mcp_config.json,.claude.json: when a server is launched withnpx,bunx,pnpm dlx,uvxorpipx runand 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-configsreads 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 plainnpx pkgresolves to "latest" at run time and a static check cannot evaluate it. In a sample of 362 public.mcp.jsonfiles only about 9% of package launches pinned a version and none pinned one with a published advisory (census, 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. - 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({...})whosecommandis not a literal, whoseargsis a variable or has a spread, or whoseenvis 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- anargs=[...]list containing-c,-e,--evalor--eval-stringin MCP client code, which defeats a command-name allowlist. Not reported in committed JSON configs, wherebash -cwrappers are common.config_auto_approve_trust_bypass-enableAllProjectMcpServers: trueor a non-emptyenabledMcpjsonServersin a committed.claude/settings.json/.mcp.json(CVE-2026-21852).vendor_api_base_url_override-ANTHROPIC_BASE_URL,OPENAI_BASE_URLand similar set to a literal in a project-scoped file. Officialapi.anthropic.com/api.openai.comhosts,$VARreferences 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.
- 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()andcreateMcpExpressApp()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 TypeScriptStreamableHTTPServerTransportcreated 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,
mcp-config-lint and
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
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):
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
# .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.
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.
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
python -m pytest -q tests
Maintaining the data (not needed to use the scanners)
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.
Metadata
Release files for agent-cve-scanners 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agent_cve_scanners-0.5.1.tar.gz | 132.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agent_cve_scanners-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 249.6 kB
Release files / agent_cve_scanners-0.5.1.tar.gz
| Download URL | agent_cve_scanners-0.5.1.tar.gz |
|---|---|
| Size | 132.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c83b09e855aef3d243188963f76e25162e8ae798b290cadcbe3181c557ba896e
|
|
BLAKE2b-256 checksum How to use checksums |
397262ef8e8383b0e0e9791446d703b0e06bc9a076ac234ff5a0279010dbe85e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / agent_cve_scanners-0.5.1-py3-none-any.whl
| Download URL | agent_cve_scanners-0.5.1-py3-none-any.whl |
|---|---|
| Size | 117.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
506dd46f37f23c51851c922d62d5478a60c6614b8133f35a330e8671d82048c8
|
|
BLAKE2b-256 checksum How to use checksums |
5fc9ffb96dba3fd08d79b1e7054c9bae03b676e9d6a8a409599042ecb46b94c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log