Metadata-Version: 2.5
Name: palisade-sec
Version: 0.3.2
Summary: A linter for LLM security: statically detects prompt-injection-to-sink vulnerabilities in Python and JavaScript/TypeScript codebases.
Project-URL: Homepage, https://github.com/arpankernel/palisade
Project-URL: Issues, https://github.com/arpankernel/palisade/issues
Author: Palisade contributors
License: MIT
License-File: LICENSE
Keywords: linter,llm,prompt-injection,security,static-analysis,taint
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.11
Requires-Dist: pydantic>=2.5
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.0
Requires-Dist: typer>=0.12
Provides-Extra: js
Requires-Dist: tree-sitter-javascript>=0.23; extra == 'js'
Requires-Dist: tree-sitter-typescript>=0.23; extra == 'js'
Requires-Dist: tree-sitter>=0.23; extra == 'js'
Description-Content-Type: text/markdown

# Palisade

**A linter for LLM security.** Palisade statically detects prompt-injection
vulnerabilities in Python and JavaScript/TypeScript codebases — untrusted
input flowing through an LLM into a dangerous sink — in CI, before they ship.

```
untrusted input  →  LLM  →  exec / shell / raw SQL   (no sanitizer)   ⇒  finding
```

No API key. No signup. No network calls. Pure static analysis.

```bash
uvx palisade-sec scan .
```

![palisade-sec scanning the example app](https://raw.githubusercontent.com/arpankernel/palisade/main/docs/demo.svg)

<details><summary>Same output as text</summary>

```
HIGH  app.py:31  [PI-EXEC] Prompt injection reaching code execution
  ↳ source:  question = request.json["question"]        (app.py:31)
  ↳ llm:     resp = client.chat.completions.create(     (app.py:32)
  ↳ sink:    exec(code)                                 (app.py:40)
  No sanitizer on path.  Confidence: HIGH
  Attack: crafted input makes the model emit Python that executes on your server.
  Fix:    never exec model output; sandbox + strict allowlist (denylists are bypassable).
  Refs:   CVE-2024-12366 (PandasAI); CVE-2025-3248 (Langflow, CISA KEV)
```

</details>

## Why

This exact pattern is behind real, exploited CVEs: **Langflow**
(CVE-2025-3248, on CISA KEV, exploited in the wild), **PandasAI**
(CVE-2024-12366, CVSS 9.8), **Vanna.ai** (CVE-2024-5565), **LangChain**
PAL/LLMMath chains (CVE-2023-36258, CVE-2023-29374). Almost nobody defends it
at the code level: existing tools are runtime proxies (paid, in the traffic
path) or guardrail libraries you have to know to wire in. Palisade is the
missing piece — **free, static, LLM-dataflow-aware, and CI-native**, like
ruff or semgrep but for the OWASP LLM Top-10 #1 risk.

## What it detects

| Rule | Path | Real-world precedent |
|------|------|----------------------|
| `PI-EXEC` | input → LLM → `exec` / `eval` / `new Function` / `vm.runIn*` | PandasAI, Langflow, LangChain PAL |
| `PI-SHELL` | input → LLM → `os.system` / `subprocess(shell=True)` / `child_process.exec` | Open Interpreter (by design) |
| `PI-SQL` | input → LLM → raw non-parameterized SQL (`cursor.execute`, `pool.query`) | Vanna.ai |
| `PI-FRAMEWORK-EXEC` | input → framework LLM wrapper (`submit_prompt`, `generate_code`, ...) → execution step | Vanna.ai, PandasAI |
| `PI-HTTP` | input → LLM → model-chosen URL fetched (SSRF/exfil; advisory) | OWASP LLM Top-10 |

Sources cover Flask (`request.*`), FastAPI (`@app.post` route params and
pydantic bodies), Express (`req.body`/`req.query`), CLIs (`input()`,
`sys.argv`, `process.argv`) — and, in library mode, public function
parameters. **Scanning the real vanna v0.5.5 with
`--assume-params-untrusted` flags exactly the CVE-2024-5565 sink
(`base.py:1998`) and nothing else.**

Palisade runs **taint analysis, not grep**: it only reports a *complete*
`source → LLM → sink` data-flow path with no sanitizer in between.

- Constant developer prompt → LLM → `exec`? **Silent** — no untrusted source.
- `subprocess.run([...])` with an arg list? **Silent** — safe sink shape.
- Parameterized `cursor.execute(q, params)`? **Silent.**
- Allowlist / pydantic validation on the path? **Silent** — sanitized.
- Denylist or human-confirmation gate? **Flagged MED "risky"** — real CVEs
  were exploited despite exactly those defenses. That is deliberate.
- A "sanitizer" in name only — a project function matching `sanitize`/
  `validate` whose body never actually validates? **Flagged MED "unverified
  sanitizer"** — Vanna's cosmetic `_sanitize_plotly_code` shipped
  CVE-2024-5565 straight through such a function.
- Several rules matching one `source → sink` path? **One finding** — the
  most specific rule wins; no duplicate noise.

## Install & run

```bash
# one-shot, no install
uvx palisade-sec scan path/to/project

# or
pipx run palisade-sec scan .

# or as a dev dependency
uv add --dev palisade-sec

# with the JavaScript/TypeScript frontend (tree-sitter)
uvx --from "palisade-sec[js]" palisade-sec scan .
```

Python is scanned out of the box; `.js`/`.ts`/`.tsx` files are scanned when
the `[js]` extra is installed (otherwise they're skipped with a note).

Useful flags:

```bash
palisade-sec scan . --all          # also show MED/LOW findings
palisade-sec scan . --json         # stable machine-readable output
palisade-sec scan . --report       # write palisade-report.md
palisade-sec scan . --rules ./my-rules   # add your own YAML rules
palisade-sec scan . --assume-params-untrusted   # library mode, see below
palisade-sec fix .                 # remediation plan: guardrail + test per finding
```

### `palisade-sec fix`

`fix` turns findings into a remediation plan (`palisade-fixes.md`): for each
finding, a rule-tailored guardrail (AST allowlist for exec, arg-list +
executable allowlist for shell, SELECT-only parser check for SQL, host
allowlist + private-IP block for SSRF) **plus a pytest asserting the
guardrail blocks the canonical attack**. Deterministic and offline — it
never modifies your code and never calls an LLM.

### Scanning libraries

Apps read untrusted input from `request.*` / `input()` / `sys.argv`. A
*library* has no visible caller — its public parameters ARE the untrusted
world (Vanna's `ask(question)`, CVE-2024-5565). Library mode treats the
parameters of public (non-underscore) functions as untrusted sources:

```bash
palisade-sec scan path/to/library --assume-params-untrusted
```

If the library routes LLM calls through its own wrapper method, add the
wrapper to a custom rule's `llm_signatures` (e.g. `"*.submit_prompt"`) — see
the rules guide.

## CI

Gate pull requests on **new** findings only — adopt Palisade on an imperfect
codebase without a wall of pre-existing failures:

```bash
palisade-sec baseline .                 # once; commit .palisade/baseline.json
palisade-sec scan . --ci --baseline .palisade/baseline.json
```

`--ci` exits non-zero only if a **new HIGH** finding appears. Fingerprints are
line-number independent, so refactors don't churn the baseline.

GitHub Actions:

```yaml
- uses: astral-sh/setup-uv@v5
- run: uvx palisade-sec scan . --ci --baseline .palisade/baseline.json
```

## Configuration

`pyproject.toml`:

```toml
[tool.palisade]
paths_ignore = ["migrations/*", "sandbox/*"]
include_tests = false   # tests/** and conftest.py are skipped by default
max_hops = 3            # inter-procedural depth bound
assume_params_untrusted = false   # library mode (see "Scanning libraries")
```

Or the same keys in `.palisade.toml`.

## Custom rules

Rules are plain YAML validated by a pydantic schema — sources, LLM call
signatures, sinks, sanitizers, partial defenses. Adding coverage for a new
framework is a small PR with **no engine changes**. See
[`src/palisade_sec/rules/README.md`](https://github.com/arpankernel/palisade/blob/main/src/palisade_sec/rules/README.md) for
the 5-minute guide.

## Architecture

```
source ──▶ language frontends ──────────────────▶ normalized taint IR
           Python (stdlib ast)                          │
           JS/TS (tree-sitter, optional extra)          │
                              language-agnostic engine ─┤ taint propagation,
                              sanitizer resolution, confidence scoring
                                                        │
             YAML rules ──▶ findings ──▶ baseline diff ──▶ terminal / json / md
```

The frontend/IR split is the scalability story — proven, not promised: the
JS/TS frontend landed with **zero engine changes**, and the same YAML rules
match both languages (`chat.completions.create`, `eval`,
`child_process.exec` are just dotted paths). Go and more come the same way.

## Safety of the tool itself

- Palisade **never executes, imports, or evaluates scanned code** — it only
  parses source text with `ast.parse`.
- `scan` makes **no network calls** and needs no API key or account.
- No telemetry. Nothing leaves your machine.

## An honest note on scope

Palisade is **one layer** of defense against **one class** of vulnerability.
A clean scan means no *detected* injection-to-sink path — it does not mean
your application is secure. Keep your runtime guardrails, permissions
boundaries, and sandboxes; Palisade complements them, before merge.

## License

MIT
