Announcing LLMArmor: Free OWASP LLM Top 10 Scanner
Today we’re releasing LLMArmor, a free, open-source static analysis scanner for Python codebases that detects OWASP LLM Top 10 security vulnerabilities. You can install it right now and scan your first project in under a minute.
pip install llmarmorllmarmor scan ./your-app/Why we built it
Section titled “Why we built it”The OWASP LLM Top 10 list has become the de-facto standard for thinking about LLM application security. But most of the tooling that addresses it falls into one of two camps:
- Dynamic red-teaming tools (garak, Promptfoo) that probe a running model with attack prompts
- Commercial runtime firewalls (Lakera Guard, Protect AI) that proxy live API requests
Neither approach catches the security issues that exist in your code before a single user interaction ever happens. We kept seeing developers accidentally write code like this:
messages = [ {"role": "system", "content": f"You are {user_role}. {config_prompt}"}, {"role": "user", "content": user_input},]response = client.chat.completions.create(model="gpt-4o", messages=messages)That’s an OWASP LLM01 prompt injection vulnerability — an attacker who controls user_role can override the system prompt entirely. A runtime firewall won’t catch it because the vulnerability is in how the message is constructed, not in what the message says.
LLMArmor is built to find these code-level issues at commit time, before they ever reach production.
How it works
Section titled “How it works”LLMArmor uses two complementary analysis layers:
Layer 1: Regex pattern matching
Section titled “Layer 1: Regex pattern matching”Fast line-by-line pattern matching for common vulnerability patterns. Catches obvious cases like f-string interpolation of variables with user/input/request in their name directly into LLM messages, and hardcoded API keys matching sk-, sk-ant-, AIza, hf_ patterns.
Layer 2: AST taint analysis
Section titled “Layer 2: AST taint analysis”Python’s ast module builds a full syntax tree. LLMArmor tracks which variables are tainted — assigned from a user-controlled source — and follows them through the code. A variable is tainted when it comes from:
request.json["field"],request.form.get("field"),request.POST["query"]input("Enter: ")sys.argv[1]websocket.receive()- A function parameter (including
@tooldecorated functions, where the LLM controls the arguments)
Taint propagates through direct assignments (alias = tainted) but not through function calls (clean = sanitize(tainted) does not taint clean). This keeps false positives low.
Deduplication
Section titled “Deduplication”When both layers detect the same issue on the same line, only one finding is reported.
What it detects today
Section titled “What it detects today”LLMArmor covers 7 of the 10 OWASP LLM risks with varying coverage depths:
| OWASP Risk | Rule | Status |
|---|---|---|
| Prompt Injection | LLM01 | 🟢 Strong |
| Sensitive Info Disclosure | LLM02 | 🟡 Partial |
| Improper Output Handling | LLM05 | 🟡 Partial |
| Insecure Plugin Design | LLM06 | 🟡 Partial |
| System Prompt Leakage | LLM07 | 🟡 Partial |
| Excessive Agency | LLM08 | 🟢 Strong |
| Unbounded Consumption | LLM10 | 🟡 Partial |
Supply Chain (LLM03), Data Poisoning (LLM04), and Misinformation (LLM09) are out of scope for static analysis.
A real finding example
Section titled “A real finding example”Here’s a finding LLMArmor produces on a vulnerable LangChain snippet:
from flask import requestfrom langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
def handle_query(system_prompt): user_input = request.json["query"] # taint source messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input}, # LLM01 if user_input reaches system role ] result = llm.invoke(messages) return eval(result.content) # LLM05: tainted output to eval()Running llmarmor scan app.py produces:
LLM05 — Improper Output Handling [CRITICAL] app.py:12 eval(result.content) LLM output passed to eval() — code execution risk. Fix: never pass LLM output to eval()/exec(). Validate and parse the output explicitly. Ref: https://owasp.org/www-project-top-10-for-large-language-model-applications/CI/CD integration
Section titled “CI/CD integration”LLMArmor exits with a structured code:
0— no MEDIUM+ findings (clean)1— at least one HIGH or MEDIUM finding2— at least one CRITICAL finding
This makes it easy to gate pipelines:
name: LLM Security Scanon: [push, pull_request]
jobs: llmarmor: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - run: pip install llmarmor - run: llmarmor scan . -f sarif > llmarmor.sarif - name: Upload to GitHub Code Scanning uses: github/codeql-action/upload-sarif@v3 with: sarif_file: llmarmor.sarifThe SARIF output integrates with GitHub’s Code Scanning dashboard — findings appear inline on pull requests, just like CodeQL results.
What’s next
Section titled “What’s next”We’re actively working on expanding coverage:
- Broader LLM02 (Sensitive Info Disclosure) patterns beyond API keys
- LLM06 (Insecure Plugin Design) coverage for more agent frameworks
- VS Code extension for inline findings during development
- Support for JavaScript/TypeScript LLM applications
Contributions are welcome. The rule engine is designed to make adding new patterns straightforward.
Get started
Section titled “Get started”pip install llmarmorllmarmor scan ./your-llm-app/