Skip to content

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.

Terminal window
pip install llmarmor
llmarmor scan ./your-app/

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:

  1. Dynamic red-teaming tools (garak, Promptfoo) that probe a running model with attack prompts
  2. 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.

LLMArmor uses two complementary analysis layers:

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.

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 @tool decorated 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.

When both layers detect the same issue on the same line, only one finding is reported.

LLMArmor covers 7 of the 10 OWASP LLM risks with varying coverage depths:

OWASP RiskRuleStatus
Prompt InjectionLLM01🟢 Strong
Sensitive Info DisclosureLLM02🟡 Partial
Improper Output HandlingLLM05🟡 Partial
Insecure Plugin DesignLLM06🟡 Partial
System Prompt LeakageLLM07🟡 Partial
Excessive AgencyLLM08🟢 Strong
Unbounded ConsumptionLLM10🟡 Partial

Supply Chain (LLM03), Data Poisoning (LLM04), and Misinformation (LLM09) are out of scope for static analysis.

Here’s a finding LLMArmor produces on a vulnerable LangChain snippet:

app.py
from flask import request
from 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/

LLMArmor exits with a structured code:

  • 0 — no MEDIUM+ findings (clean)
  • 1 — at least one HIGH or MEDIUM finding
  • 2 — at least one CRITICAL finding

This makes it easy to gate pipelines:

.github/workflows/llmarmor.yml
name: LLM Security Scan
on: [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.sarif

The SARIF output integrates with GitHub’s Code Scanning dashboard — findings appear inline on pull requests, just like CodeQL results.

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.

Terminal window
pip install llmarmor
llmarmor scan ./your-llm-app/