Open source · MIT · No API key · v0.3.3

A linter for LLM security.

Palisade statically catches prompt-injection paths - untrusted input flowing through an LLM into exec, a shell, raw SQL, or an outbound fetch - in CI, before they ship. Python and JavaScript/TypeScript.

untrusted input ──▶ LLM ──▶ dangerous sink (no sanitizer) ⇒ finding
Star on GitHub ↗
pure static analysis - never executes your code · no network calls · no telemetry
palisade-sec · scan
uvx palisade-sec scan .HIGH app.py:34 [PI-SQL] Prompt injection reaching raw SQL source: question = request.json["question"] (app.py:24) llm: resp = client.chat.completions.create( (app.py:25) sink: cur.execute(sql) (app.py:34) No sanitizer on path. Confidence: HIGH · CVE-2024-5565 (Vanna.ai)HIGH app.py:64 [PI-EXEC] Prompt injection reaching code execution source: spec = request.json["spec"] (app.py:56) llm: resp = client.chat.completions.create( (app.py:57) sink: exec(code) (app.py:64) Attack: crafted input makes the model emit Python that runs on your server. Fix: never exec model output; sandbox + strict allowlist.MED app.py:97 [PI-EXEC] (risky: partial defense only - denylists are bypassable)Found 2 high, 1 med finding(s) in 6 file(s). exit 1 (--ci)
0
real files scanned · zero false positives
0 CVE
exact vanna cve-2024-5565 line found
~0
1,576-file python + ts monorepo
0 rules
python · js/ts - same engine, same rules
How it works

Taint analysis, not grep.

A finding requires the complete source → LLM → sink data-flow path with no real sanitizer in between. Calling an LLM is never a finding. That single rule is why the noise floor is zero.

01FRONTEND

Parse

Python via stdlib ast, JS/TS via tree-sitter. Source text only - scanned code is never executed.

02IR

Lower

Both languages compile to one normalized taint IR. Import aliases resolve, so sp.run is subprocess.run.

03ENGINE

Propagate

Taint flows through f-strings, collections, awaits, class fields, and bounded cross-file calls - 3 hops deep.

04JUDGE

Verify defenses

Real sanitizers suppress. Denylists, confirmation gates, and sanitizers-in-name-only downgrade - never silence.

05EMIT

Report

Full trace per finding: source, LLM, sink, attack, fix, CVE refs. Terminal, stable JSON, or markdown threat model.

What it catches

Five rules. Every one backed by a real CVE class.

Rules are plain YAML - sources, LLM signatures, sinks, sanitizers. Adding coverage for your framework is a data change, never an engine change.

HIGH
PI-EXEC

Injection → code execution

Model output executed as code. The most common real-world LLM vulnerability class.

exec · eval · compile · PythonREPL
new Function · vm.runIn*
CVE-2024-12366 · CVE-2025-3248 · CVE-2023-36258
HIGH
PI-SHELL

Injection → OS command

Model output handed to a shell verbatim. Arg-list subprocess.run([...]) stays silent.

os.system · subprocess(shell=True)
child_process.exec / execSync
Open Interpreter class (by design)
HIGH
PI-SQL

Injection → raw SQL

Text-to-SQL executed non-parameterized. execute(q, params) stays silent.

cursor.execute · executemany · Model.raw
pool.query · db.query
CVE-2024-5565 · CVE-2024-5826
HIGH
PI-FRAMEWORK-EXEC

Framework wrapper → execution step

Agent frameworks hide the LLM behind wrappers. Palisade knows the shapes: submit_prompt, call_llm, generate_code

*.execute_code · *.run_code · *.execute_plan
Vanna · PandasAI pipeline class
MED · ADVISORY
PI-HTTP

Injection → SSRF / exfiltration

Model-chosen URLs fetched directly - cloud metadata, internal APIs, attacker hosts. Advisory: never gates CI.

requests.* · httpx.* · urlopen (URL arg only)
OWASP LLM Top-10 · SSRF
YOURS
PI-CUSTOM

Your framework, one YAML file

Your codebase routes LLM calls through self.inference()? Add one line to llm_signatures, pass --rules ./dir. Same id overrides a builtin.

sources · llm_signatures · sinks · sanitizers
The contract

Precision is the product.

A noisy security tool trains developers to ignore it. Every "will not flag" below is pinned by a permanent test - the false-positive tests are the most important tests in the repo.

Stays silent - safe shapes
  • PASSConstant developer prompt → LLM → exec - no untrusted source, no finding
  • PASSsubprocess.run([...]) with an arg list and no shell
  • PASSParameterized SQL - execute(q, params), pool.query(text, values)
  • PASSpydantic / marshmallow validation on the path
  • PASSVerified project sanitizers - allowlists and guards that actually raise
  • PASSLLM output that is only logged, printed, or returned
Still flagged - real CVEs shipped through these
  • MEDDenylists. LangChain PAL's COMMAND_EXECUTION_FUNCTIONS denylist was bypassed → CVE-2023-36258
  • MEDConfirmation gates. "Are you sure?" is not a security boundary
  • MEDSanitizers in name only. Vanna's _sanitize_plotly_code stripped fig.show() - and shipped CVE-2024-5565. Palisade verifies the body, not the name
  • HIGHMulti-hop paths. Source in one file, LLM in a second, sink in a third - traced across the call graph
  • HIGHHidden flows. json.loads(output)["cmd"], f-strings, parts.append(...), self.x class fields
"A false positive is worse than a miss - noise trains developers to ignore all security warnings."Design philosophy #1, enforced by the test suite
The receipts

Point it at the repo behind a real CVE.

We scanned the actual vulnerable releases of the projects that motivated Palisade - and published the misses alongside the hits.

vanna v0.5.5 · the release behind CVE-2024-5565
palisade-sec scan vanna-0.5.5 --assume-params-untrustedMED src/vanna/base/base.py:1998 [PI-FRAMEWORK-EXEC] source: param:question - ask() (base.py:1594) llm: self.submit_prompt(message_log) sink: exec(plotly_code, globals(), ldict) Unverified sanitizer: _sanitize_plotly_code - matches a sanitizer name, but its body shows no allowlist/validation shape. the CVE sink. Zero other findings across the 45-file repo.
HIT

The exact CVE line, nothing else

Builtin rules + library mode land on base.py:1998 - the sink NVD lists for CVE-2024-5565 - and correctly call out the cosmetic sanitizer that failed in the wild.

HELD

Zero false positives at scale

Vanna, PandasAI, and Langflow - 2,040 real files, including Langflow's 1,576-file Python + TypeScript tree - with no crashes, no skipped files, and not one wrong flag.

HONEST

Misses are documented, not hidden

PandasAI's dynamic pipeline dispatch beats bounded static taint. We say so, and it defines the roadmap. Read the full proof-scan report →

Adopt in an afternoon

Scan. Gate. Fix.

Baseline your existing debt, fail CI only on new findings, and generate a guardrail + regression test for every finding you burn down.

Gate CI on new findings

github actions
# .github/workflows/security.yml
- uses: astral-sh/setup-uv@v5
- run: |
    uvx palisade-sec scan . --ci \
      --baseline .palisade/baseline.json
Line-shift-resilient fingerprints - refactors never churn the baseline.

Fix with proof

palisade-sec fix
$ palisade-sec fix .
→ palisade-fixes.md

# per finding: a tailored guardrail
# + a pytest proving it blocks the
# canonical attack. Offline. Never
# edits your code.
AST allowlists, argv allowlists, SELECT-only SQL validation, SSRF guards.

Built for AI agents

llms.txt · agents.md
$ palisade-sec scan . --json
{ "schema_version": 1, ... }

# stable schema · exit-code contract
# remediation policy · fingerprint
# diffing - the full agent contract
FAQ

The questions security teams ask first.

No. scan is pure static analysis: it parses source text and never executes, imports, or evals it - a live test in the suite proves scanned code cannot run. There are no network calls, no telemetry, no accounts, and no API key. The only writes are .palisade/ and files you explicitly request.

Bandit flags exec() anywhere; Semgrep matches patterns you write. Palisade runs LLM-aware taint analysis: it only fires on a complete untrusted-input → LLM → sink data-flow path, understands LLM SDK response shapes, and judges defenses - denylists and cosmetic sanitizers downgrade instead of silencing. It's the layer those tools don't model, and it composes fine with both.

Zero across 2,040 files of Vanna, PandasAI, and Langflow - including Langflow's 1,576-file Python + TypeScript tree. The mechanism: findings require the full path, safe shapes (arg-list subprocess, parameterized SQL, verified sanitizers, constant prompts) are recognized, and every reported FP becomes a permanent must-stay-silent test. Misses are documented publicly in the proof-scan report.

Yes - install with the extra: pip install "palisade-sec[js]" or uvx --from "palisade-sec[js]" palisade-sec scan .. A tree-sitter frontend lowers JS/TS into the same taint IR with zero engine changes, so the same YAML rules match Express req.body, eval, new Function, child_process.exec, and pool.query.

Library mode: --assume-params-untrusted treats the parameters of public functions as untrusted sources - a library's callers are the untrusted world. That's exactly how Palisade finds the real CVE-2024-5565 sink in vanna v0.5.5 with builtin rules and nothing else.

No, and we won't pretend otherwise. Palisade is one layer against one class of vulnerability - code-level injection-to-sink paths. Keep your runtime guardrails, permission boundaries, and sandboxes. Palisade complements them, before merge.

Free forever · MIT

Ship the guardrail,
not the incident.

One command. No key, no signup, no code leaving your machine. Findings in under 30 seconds.