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.
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.
Parse
Python via stdlib ast, JS/TS via tree-sitter. Source text only - scanned code is never executed.
Lower
Both languages compile to one normalized taint IR. Import aliases resolve, so sp.run is subprocess.run.
Propagate
Taint flows through f-strings, collections, awaits, class fields, and bounded cross-file calls - 3 hops deep.
Verify defenses
Real sanitizers suppress. Denylists, confirmation gates, and sanitizers-in-name-only downgrade - never silence.
Report
Full trace per finding: source, LLM, sink, attack, fix, CVE refs. Terminal, stable JSON, or markdown threat model.
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.
Injection → code execution
Model output executed as code. The most common real-world LLM vulnerability class.
new Function · vm.runIn*
Injection → OS command
Model output handed to a shell verbatim. Arg-list subprocess.run([...]) stays silent.
child_process.exec / execSync
Injection → raw SQL
Text-to-SQL executed non-parameterized. execute(q, params) stays silent.
pool.query · db.query
Framework wrapper → execution step
Agent frameworks hide the LLM behind wrappers. Palisade knows the shapes: submit_prompt, call_llm, generate_code…
Injection → SSRF / exfiltration
Model-chosen URLs fetched directly - cloud metadata, internal APIs, attacker hosts. Advisory: never gates CI.
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.
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.
- PASSConstant developer prompt → LLM → exec - no untrusted source, no finding
- PASS
subprocess.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
- MEDDenylists. LangChain PAL's
COMMAND_EXECUTION_FUNCTIONSdenylist was bypassed → CVE-2023-36258 - MEDConfirmation gates. "Are you sure?" is not a security boundary
- MEDSanitizers in name only. Vanna's
_sanitize_plotly_codestrippedfig.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.xclass 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
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.
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.
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.
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 →
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
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.
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
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.