SecureAI-Scan
The AI security scanner that proves its findings.
SecureAI-Scan finds LLM, MCP, Agent Skill, and RAG vulnerabilities in TypeScript, JavaScript, and Python — and shows you the evidence: the exact source → flow → sink path for every dataflow finding, resolved through real imports, not keyword matching.
It's the first scanner mapped to all three OWASP AI security frameworks — the LLM Top 10, the Top 10 for Agentic Applications (2026), and the MCP Top 10 — with a coverage matrix in every threat model showing exactly which risks are checked and which are runtime concerns.
▌ HIGH AI001 Prompt injection via user input
PROVEN LLM01 Prompt Injection
source src/chat.ts:8 request data `req.body.input`
flow src/chat.ts:13 passed as `systemPrompt`
sink src/chat.ts:10 openai.chat.completions.create — system role (OpenAI)
fix Keep system prompts static; pass user input as a user-role message.
Is this for you? SecureAI-Scan is scoped deliberately to LLM, MCP, and RAG/agent risks — prompt injection, tool poisoning, unsafe output handling, vector-store access control, agent-skill poisoning. It is not a general SAST or secrets scanner, and doesn't try to be one; a known-malicious package with no LLM-shaped payload (e.g. a hardcoded exfiltration address in an email API call) is caught by the offline advisory list (DEP003), not a pattern rule. If your codebase talks to an LLM, an MCP server, a vector store, or ships Agent Skills, this is built for you.
Contents
- Why this scanner is different
- Get started in 30 seconds
- Commands
- GitHub Action
- Rules
- MCP server (use it from Claude)
- Claude Skill
- The precision contract
- Testing & benchmarking
Why this scanner is different
- Evidence tiers, not noise. Every finding is
proven(traced dataflow or parsed config fact),likely(resolved sink, one heuristic hop), orheuristic. A default scan shows only proven + likely. Heuristics are opt-in via--paranoid. - Import-resolved detection. A call is only an "LLM call" if it resolves to a real SDK import (
openai,@anthropic-ai/sdk,ai,@google/genai, LangChain, Bedrock, …). Your Google Maps client will never be flagged as an LLM again. - Precision-gated, and benchmarked against real repos. The test suite asserts every vulnerable fixture fires and every safe fixture stays clean — a false positive on the safe corpus fails the build. Beyond that, every detection change is run against real public repos (OpenAI/Anthropic/Vercel AI SDKs, official MCP servers, LlamaIndex) before shipping. See Testing & benchmarking for the actual before/after numbers.
- SARIF for GitHub code scanning.
--output report.sarifputs findings inline on pull requests and in the Security tab. - AI-BOM.
secureai-scan bom .inventories every SDK, model ID, vector store, agent framework, and MCP server in your repo — zero-false-positive by construction, mapped to OWASP LLM Top 10 / EU AI Act documentation needs. - MCP config scanning. Parses
.mcp.json,claude_desktop_config.json,.cursor/mcp.json: unpinnednpx -yservers, inline secrets, plaintext HTTP transports. - MCP tool-poisoning detection. Catches the pattern behind the WhatsApp MCP rug-pull and postmark-mcp backdoor — invisible Unicode, agent-directed injection phrases, and cross-tool shadowing in tool names/descriptions, statically, before you ever run the server.
- MCP command-injection detection. Flags MCP stdio transport
command/argsbuilt from request data — the pattern behind the 2026 MCP STDIO RCE disclosure. - Agent Skill poisoning detection. The same invisible-Unicode, injection-phrase, and shadowing checks applied to
SKILL.mdfiles — Agent Skills load into context wholesale, so a poisoned skill is a poisoned tool description by another name. - Known-malicious package advisories, version-aware. Checks every dependency and every MCP-launched package against a curated advisory list (documented backdoors, critical CVEs) — offline, on every scan, no flag required. Clears a finding once you've actually upgraded past the affected range; stays flagged on any ambiguous or unpinned version, never silently.
- Local-first. Nothing leaves your machine.
Get started in 30 seconds
npx --yes secureai-scan@latest scan .
TypeScript, JavaScript, Python, MCP config files, and Agent Skill (SKILL.md) files are scanned automatically — one command, no config.
Prefer to ask questions first? Try the free SecureAI-Scan AI Security Advisor on ChatGPT.
About to run an MCP server you found on GitHub or Twitter? Paste its tool description into MCP X-Ray first — checks it for hidden Unicode, injected instructions, and known-malicious packages in your browser, no install.
Commands
The one you need 95% of the time:
secureai-scan scan .
Everything else is there when you need it. secureai-scan scan . --help shows all of this in the terminal, grouped the same way:
Everyday
| Flag | What it does |
|---|---|
| (none) | proven + likely findings — the default, no flags needed |
--paranoid |
also include heuristic-tier findings |
-s, --severity <level> |
only show findings at/above low|medium|high|critical |
--output <file> |
write a full report — .sarif (GitHub code scanning), .json, .md, or .html |
Scope which rules run
| Flag | What it does |
|---|---|
-r, --rules <list> |
run only these rule IDs, e.g. AI001,MCP007 |
--only-ai / --only-mcp / --only-vec / --only-skl |
run only one rule category |
--check-dependencies |
also check package.json/requirements.txt against the npm/PyPI registry for typos and hallucinated packages (DEP001/DEP002). Auto-enabled if you select those rules directly via -r — you never need to remember to pass both. Not needed for DEP003 (known-malicious packages), which always runs offline |
CI / workflow
| Flag | What it does |
|---|---|
--fail-on <severity> |
exit 1 if findings at/above this severity exist |
--baseline <file> |
track only new/changed issues against a saved baseline |
--policy <file> |
load thresholds, skipped paths, and blocked rules from a .secureai-policy.json (auto-detected if present — secureai-scan init creates one) |
Advanced
| Flag | What it does |
|---|---|
--min-confidence <0-1> |
finer-grained than --paranoid: hide findings below an exact confidence score (0.9 proven / 0.65 likely / 0.35 heuristic) |
--limit <n> |
max rule groups shown in the terminal (default 10) — full detail always goes to --output |
--debug |
print every file scanned and which rules ran |
Other commands:
secureai-scan bom . --output AI_BOM.md # AI Bill of Materials
secureai-scan explain AI001 # why + exploit + fix example, for any rule
secureai-scan threat-model . # THREAT_MODEL.md with the OWASP coverage matrix
secureai-scan init # policy file + CI workflow, one-time setup
Suppress a reviewed finding in code:
// secureai-ignore AI001: reviewed, input sanitized via allowlist
GitHub Action
name: SecureAI-Scan
on: [pull_request]
permissions:
contents: read
security-events: write
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: akanthed/SecureAI-Scan@main
with:
fail-on: high
Findings appear as inline annotations on the PR and in the repository's Security tab. (secureai-scan init generates an equivalent workflow using the CLI directly.)
Scanning clean? Add the badge to your own README:
[](https://github.com/akanthed/SecureAI-Scan)
Rules
33 rules, every one mapped to the OWASP LLM Top 10 (2025) — plus, where applicable, the OWASP Top 10 for Agentic Applications (2026, ASI) and the OWASP MCP Top 10 (2025) — and, where relevant, an EU AI Act article. threat-model output includes a full coverage matrix across all three frameworks.
| Rule | What it proves | OWASP |
|---|---|---|
| AI001 | User input flows into a system/developer prompt (traced source → sink) | LLM01 |
| AI002 | Prompt content or secrets written to logs (in files that use an LLM SDK) | LLM02 |
| AI003 | LLM call in a request handler with no auth check before it | LLM10 |
| AI004 | Whole user/session object serialized into a prompt (field-picking is not flagged) | LLM02 |
| AI005 | LLM output reaches eval/exec/SQL/HTML sinks | LLM05 |
| AI006 | High-impact tools (delete, pay, deploy, …) exposed without an approval gate | LLM06 |
| AI007 | Retrieved RAG content interpolated into privileged prompts | LLM01 |
| AI008 | Secrets embedded in system prompt text | LLM07 |
| AI009 | Unbounded user input / missing token limits | LLM10 |
| AI010 | Fetched external content flows into prompts | LLM01 |
| AI011 | Agent output elevated to system-role in downstream calls | LLM06 |
| AI012 | LLM output parsed without schema validation | LLM05 |
| MCP001 | MCP tool metadata reaches the system prompt without validation | LLM01 |
| MCP002 | MCP server URL constructed from user input | LLM03 |
| MCP003 | MCP tool results elevated to system-role | LLM05 |
| MCP004 | MCP server launched as an unpinned npx -y package |
LLM03 |
| MCP005 | Secret inlined in a committed MCP config | LLM02 |
| MCP006 | MCP server over plaintext HTTP | LLM03 |
| MCP007 | Invisible/bidi Unicode hidden in MCP tool names or descriptions | LLM01 · MCP03 |
| MCP008 | Agent-directed injection phrases in MCP tool descriptions | LLM01 · MCP03 |
| MCP009 | A tool description that steers calls to a different tool (shadowing) | LLM01 · MCP03 |
| MCP010 | MCP stdio server command/args constructed from user input (RCE) | LLM03 · MCP05 |
| SKL001 | Invisible/bidi Unicode in an Agent Skill file (SKILL.md) |
LLM01 |
| SKL002 | Agent-directed injection phrasing in a skill's description or body | LLM01 |
| SKL003 | A skill's content steers when/how a different skill is used (shadowing) | LLM01 |
| VEC001 | Vector search without a tenant/user filter | LLM08 |
| VEC002 | Unbounded or user-controlled search limit | LLM10 |
| VEC003 | User content ingested into a shared vector store | LLM04 |
| VEC004 | Ingestion without tenant/namespace tagging | LLM08 |
| DEP001 | Dependency name not found in the registry (opt-in --check-dependencies) |
LLM03 |
| DEP002 | Dependency name one edit away from a popular package (opt-in) | LLM03 |
| DEP003 | Dependency with a documented malicious release or critical CVE — checked offline on every scan, version-range aware (postmark-mcp, mcp-remote CVE-2025-6514, …) | LLM03 · MCP04 |
secureai-scan explain <RULE_ID> gives the exploit walkthrough and a before/after code example for any rule.
MCP server (use it from Claude)
The package ships an MCP server exposing scan_repository, explain_rule, and generate_bom:
{
"mcpServers": {
"secureai-scan": {
"command": "node",
"args": ["/path/to/secureai-scan/mcp-server/index.js"]
}
}
}
Claude Skill
For Claude Code / Claude.ai users, skills/secureai-scan/SKILL.md teaches Claude when to run a scan (reviewing AI/LLM code, or checking an MCP server/Agent Skill before you install it) and how to read the results — no separate process to run, unlike the MCP server above. Copy the skills/secureai-scan/ directory into your .claude/skills/ to use it.
The precision contract
False positives kill scanners. SecureAI-Scan's rule engine follows three hard rules:
- Sinks are resolved through imports. If an identifier resolves to a module that is not an LLM SDK, it is definitively not an LLM call — no matter what it's named.
- Evidence is labeled, never blended. A traced dataflow and a word-proximity match are not the same thing, so they never share a tier.
- The safe corpus gates every release.
test-fixtures/safe/contains the patterns that used to cause false positives (redacted PII payloads, Google Maps clients, env-var API keys next to LLM clients, ordinary response logging, OAuth metadata fields, streaming-responsechunks, fiction/narrative prompt text). Any finding there fails the suite.
Testing & benchmarking
Three layers, because one alone isn't enough to trust a scanner's claims — precision and recall are different failure modes, and both get checked.
1. Fixture corpus — precision + recall, runs on every build.
npm test
test-fixtures/vulnerable/ and test-fixtures/safe/ are scanned together: every vulnerable fixture must fire its expected rule at proven/likely evidence (recall), every safe fixture must produce zero proven/likely findings (precision). Fast and deterministic — but it only proves the scanner behaves on code written specifically to test it.
2. Real-world regression benchmark — against public repos we didn't write.
npm run regression # scan the full curated repo set
npm run regression -- --fresh # re-clone everything first
npm run regression -- openai-node # scan just one repo by name
scripts/regression-scan.js clones a curated, diverse set of real public repos (OpenAI/Anthropic/Vercel AI SDKs, the official MCP servers and TypeScript SDK, LlamaIndex — spanning TS and Python, SDK-consumer example code and SDK-author source) and scans each with the built CLI. There's no fixed pass/fail threshold — upstream repos change — so every proven/likely finding gets read against its source line by hand. Anything that isn't a genuine issue is a rule bug, fixed at the root cause and locked in permanently as a new test-fixtures/safe/ fixture.
Real numbers from the last run (findings at default evidence level, no --paranoid):
| Repo | Before | After | What was wrong |
|---|---|---|---|
| vercel/ai | 773 | 1 | examples/, top-level tests/, and hyphenated ecosystem-tests/-style directories weren't recognized as lower-trust paths; chunks (a common streaming-response variable) was treated as unambiguous RAG evidence |
| openai/openai-node | 47 | 0 | Same path-detection gap, applied to the SDK's own examples//ecosystem-tests/ |
| anthropics/anthropic-sdk-typescript | 2 | 0 | Same path-detection gap on a top-level tests/ directory |
| modelcontextprotocol/typescript-sdk | 3 | 0 | token_endpoint/tokenType-style OAuth metadata fields flagged as leaked secrets |
| run-llama/llama_index | 18 | 15 | A Python check flagged any description= field containing "system prompt" as proven MCP tool poisoning, regardless of context. The remaining 15 are VEC001 hits on the library's own generic retriever definitions — scanning a vector-DB SDK's own source, not application code, so a filter can't exist to check; an honest, inherent limit, not a bug |
The single remaining vercel/ai finding, and all modelcontextprotocol/servers / anthropics/anthropic-sdk-python results (zero, even at --paranoid), are genuine — reviewed by hand, not assumed.
3. Vulnerable-vs-patched validation — proves recall, not just precision.
The two layers above only check that the scanner stays quiet on safe code. DEP003's advisory checks are validated the other way: pin a package to a documented-vulnerable version and confirm it's flagged, then pin it to the patched version and confirm it isn't.
node --test test/dependency-guard.test.js
covers: [email protected] (CVE-2025-6514, vulnerable) flagged / [email protected] (patched) clear; [email protected] (before the backdoor) clear / [email protected] (after — no legitimate patch exists for a malicious package) still flagged; an unpinned ^0.1.16 range still flagged despite being patchable, since we can't prove what actually resolves. Building this test caught a real gap: DEP003 used to match advisories by package name only, never actually comparing the declared version against the advisory's affected range — fixed in src/scanner/semver.ts, which clears a finding only when an exact version pin is provably outside the affected range, and fails toward flagging on anything ambiguous.
License
MIT © Akshay Kanthed
No comments yet
Be the first to share your take.