palisade-sec 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. palisade_sec-0.1.0/.github/workflows/ci.yml +33 -0
  2. palisade_sec-0.1.0/.gitignore +10 -0
  3. palisade_sec-0.1.0/CONTRIBUTING.md +58 -0
  4. palisade_sec-0.1.0/LICENSE +21 -0
  5. palisade_sec-0.1.0/PKG-INFO +177 -0
  6. palisade_sec-0.1.0/README.md +150 -0
  7. palisade_sec-0.1.0/examples/vulnerable-app/README.md +15 -0
  8. palisade_sec-0.1.0/examples/vulnerable-app/agent_pipeline.py +10 -0
  9. palisade_sec-0.1.0/examples/vulnerable-app/app.py +187 -0
  10. palisade_sec-0.1.0/examples/vulnerable-app/db.py +5 -0
  11. palisade_sec-0.1.0/examples/vulnerable-app/executor.py +6 -0
  12. palisade_sec-0.1.0/examples/vulnerable-app/guards.py +36 -0
  13. palisade_sec-0.1.0/examples/vulnerable-app/llm_utils.py +13 -0
  14. palisade_sec-0.1.0/pyproject.toml +58 -0
  15. palisade_sec-0.1.0/src/palisade_sec/__init__.py +7 -0
  16. palisade_sec-0.1.0/src/palisade_sec/baseline.py +73 -0
  17. palisade_sec-0.1.0/src/palisade_sec/cli.py +166 -0
  18. palisade_sec-0.1.0/src/palisade_sec/engine/__init__.py +8 -0
  19. palisade_sec-0.1.0/src/palisade_sec/engine/analyzer.py +517 -0
  20. palisade_sec-0.1.0/src/palisade_sec/engine/findings.py +115 -0
  21. palisade_sec-0.1.0/src/palisade_sec/engine/taint.py +60 -0
  22. palisade_sec-0.1.0/src/palisade_sec/frontends/__init__.py +9 -0
  23. palisade_sec-0.1.0/src/palisade_sec/frontends/ast_python.py +443 -0
  24. palisade_sec-0.1.0/src/palisade_sec/ir/__init__.py +51 -0
  25. palisade_sec-0.1.0/src/palisade_sec/ir/model.py +205 -0
  26. palisade_sec-0.1.0/src/palisade_sec/report/__init__.py +5 -0
  27. palisade_sec-0.1.0/src/palisade_sec/report/json_emitter.py +49 -0
  28. palisade_sec-0.1.0/src/palisade_sec/report/markdown.py +75 -0
  29. palisade_sec-0.1.0/src/palisade_sec/report/terminal.py +109 -0
  30. palisade_sec-0.1.0/src/palisade_sec/rules/README.md +77 -0
  31. palisade_sec-0.1.0/src/palisade_sec/rules/__init__.py +4 -0
  32. palisade_sec-0.1.0/src/palisade_sec/rules/loader.py +54 -0
  33. palisade_sec-0.1.0/src/palisade_sec/rules/pi-exec.yaml +99 -0
  34. palisade_sec-0.1.0/src/palisade_sec/rules/pi-shell.yaml +102 -0
  35. palisade_sec-0.1.0/src/palisade_sec/rules/pi-sql.yaml +97 -0
  36. palisade_sec-0.1.0/src/palisade_sec/rules/schema.py +84 -0
  37. palisade_sec-0.1.0/src/palisade_sec/scanner.py +201 -0
  38. palisade_sec-0.1.0/tests/conftest.py +50 -0
  39. palisade_sec-0.1.0/tests/test_baseline.py +105 -0
  40. palisade_sec-0.1.0/tests/test_cli.py +103 -0
  41. palisade_sec-0.1.0/tests/test_edge_cases.py +436 -0
  42. palisade_sec-0.1.0/tests/test_example_app.py +126 -0
  43. palisade_sec-0.1.0/tests/test_rules.py +59 -0
  44. palisade_sec-0.1.0/uv.lock +389 -0
@@ -0,0 +1,33 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install
20
+ run: uv sync --dev
21
+ - name: Lint
22
+ run: uv run ruff check .
23
+ - name: Format check
24
+ run: uv run ruff format --check src tests
25
+ - name: Tests
26
+ run: uv run pytest -q
27
+ - name: Self-check — the example app must fail CI mode
28
+ run: |
29
+ if uv run palisade-sec scan examples/vulnerable-app --ci; then
30
+ echo "expected non-zero exit on the vulnerable example app" && exit 1
31
+ fi
32
+ - name: Self-check — palisade's own source must be clean
33
+ run: uv run palisade-sec scan src --ci
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ palisade-report.md
10
+ .palisade/
@@ -0,0 +1,58 @@
1
+ # Contributing to Palisade
2
+
3
+ Thanks for helping make LLM-security review as routine as running a linter.
4
+
5
+ ## The fastest way to contribute: rules
6
+
7
+ Most coverage gaps are rule gaps, not engine gaps. Adding a framework's
8
+ sources/LLM signatures/sinks is a small YAML PR with no engine changes —
9
+ see [`src/palisade_sec/rules/README.md`](src/palisade_sec/rules/README.md)
10
+ for the 5-minute guide. Every rule PR needs two fixtures: one that must be
11
+ flagged, one same-shaped safe variant that must stay silent.
12
+
13
+ ## Ground rules (from the design philosophy)
14
+
15
+ 1. **Precision over recall.** A false positive is worse than a miss. If your
16
+ change flags something new, it needs a matching must-NOT-flag test.
17
+ 2. **Taint, not grep.** Findings require a complete source → LLM → sink path.
18
+ 3. **The engine operates only on the IR.** Never leak Python `ast` types into
19
+ `engine/` or rules; language specifics belong in `frontends/`.
20
+ 4. **The tool never runs scanned code.** `ast.parse` only. No network calls
21
+ in `scan`. Ever.
22
+ 5. **Partial defenses (denylists, confirmation gates) downgrade to MED —
23
+ they never suppress.** Real CVEs shipped with exactly those defenses.
24
+
25
+ ## Dev setup
26
+
27
+ ```bash
28
+ git clone https://github.com/arpankernel/palisade && cd palisade
29
+ uv sync
30
+ uv run pytest # the example-app FP tests are the ones that gate merges
31
+ uv run ruff check .
32
+ uv run palisade-sec scan examples/vulnerable-app --all
33
+ ```
34
+
35
+ ## Project layout
36
+
37
+ ```
38
+ src/palisade_sec/
39
+ ├── frontends/ # language -> IR lowering (Python: stdlib ast)
40
+ ├── ir/ # normalized taint IR (language-agnostic)
41
+ ├── engine/ # taint propagation, sanitizer resolution, confidence
42
+ ├── rules/ # YAML rules + pydantic schema (community entry point)
43
+ ├── report/ # terminal / json / markdown emitters
44
+ ├── baseline.py # CI baseline (fail only on NEW findings)
45
+ └── cli.py # Typer CLI
46
+ examples/vulnerable-app/ # the acceptance fixtures — deliberately unsafe
47
+ ```
48
+
49
+ ## Adding a language frontend (post-v1)
50
+
51
+ A frontend lowers source into `ir.Module` — see
52
+ `frontends/ast_python.py` for the reference implementation. If your frontend
53
+ emits correct IR, every existing rule and the whole engine work unchanged.
54
+ Open an issue first so we can coordinate on tree-sitter setup.
55
+
56
+ ## Reporting vulnerabilities in Palisade itself
57
+
58
+ Email the maintainers privately rather than opening a public issue.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Palisade contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.5
2
+ Name: palisade-sec
3
+ Version: 0.1.0
4
+ Summary: A linter for LLM security: statically detects prompt-injection-to-sink vulnerabilities in Python codebases.
5
+ Project-URL: Homepage, https://github.com/arpankernel/palisade
6
+ Project-URL: Issues, https://github.com/arpankernel/palisade/issues
7
+ Author: Palisade contributors
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: linter,llm,prompt-injection,security,static-analysis,taint
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: pydantic>=2.5
23
+ Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: rich>=13.0
25
+ Requires-Dist: typer>=0.12
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Palisade
29
+
30
+ **A linter for LLM security.** Palisade statically detects prompt-injection
31
+ vulnerabilities in Python codebases — untrusted input flowing through an LLM
32
+ into a dangerous sink — in CI, before they ship.
33
+
34
+ ```
35
+ untrusted input → LLM → exec / shell / raw SQL (no sanitizer) ⇒ finding
36
+ ```
37
+
38
+ No API key. No signup. No network calls. Pure static analysis.
39
+
40
+ ```bash
41
+ uvx palisade-sec scan .
42
+ ```
43
+
44
+ ```
45
+ HIGH app.py:31 [PI-EXEC] Prompt injection reaching code execution
46
+ ↳ source: question = request.json["question"] (app.py:31)
47
+ ↳ llm: resp = client.chat.completions.create( (app.py:32)
48
+ ↳ sink: exec(code) (app.py:40)
49
+ No sanitizer on path. Confidence: HIGH
50
+ Attack: crafted input makes the model emit Python that executes on your server.
51
+ Fix: never exec model output; sandbox + strict allowlist (denylists are bypassable).
52
+ Refs: CVE-2024-12366 (PandasAI); CVE-2025-3248 (Langflow, CISA KEV)
53
+ ```
54
+
55
+ ## Why
56
+
57
+ This exact pattern is behind real, exploited CVEs: **Langflow**
58
+ (CVE-2025-3248, on CISA KEV, exploited in the wild), **PandasAI**
59
+ (CVE-2024-12366, CVSS 9.8), **Vanna.ai** (CVE-2024-5565), **LangChain**
60
+ PAL/LLMMath chains (CVE-2023-36258, CVE-2023-29374). Almost nobody defends it
61
+ at the code level: existing tools are runtime proxies (paid, in the traffic
62
+ path) or guardrail libraries you have to know to wire in. Palisade is the
63
+ missing piece — **free, static, LLM-dataflow-aware, and CI-native**, like
64
+ ruff or semgrep but for the OWASP LLM Top-10 #1 risk.
65
+
66
+ ## What it detects (v1)
67
+
68
+ | Rule | Path | Real-world precedent |
69
+ |------|------|----------------------|
70
+ | `PI-EXEC` | input → LLM → `exec` / `eval` / `compile` / `PythonREPL` | PandasAI, Langflow, LangChain PAL |
71
+ | `PI-SHELL` | input → LLM → `os.system` / `subprocess(shell=True)` | Open Interpreter (by design) |
72
+ | `PI-SQL` | input → LLM → raw non-parameterized SQL | Vanna.ai |
73
+
74
+ Palisade runs **taint analysis, not grep**: it only reports a *complete*
75
+ `source → LLM → sink` data-flow path with no sanitizer in between.
76
+
77
+ - Constant developer prompt → LLM → `exec`? **Silent** — no untrusted source.
78
+ - `subprocess.run([...])` with an arg list? **Silent** — safe sink shape.
79
+ - Parameterized `cursor.execute(q, params)`? **Silent.**
80
+ - Allowlist / pydantic validation on the path? **Silent** — sanitized.
81
+ - Denylist or human-confirmation gate? **Flagged MED "risky"** — real CVEs
82
+ were exploited despite exactly those defenses. That is deliberate.
83
+
84
+ ## Install & run
85
+
86
+ ```bash
87
+ # one-shot, no install
88
+ uvx palisade-sec scan path/to/project
89
+
90
+ # or
91
+ pipx run palisade-sec scan .
92
+
93
+ # or as a dev dependency
94
+ uv add --dev palisade-sec
95
+ ```
96
+
97
+ Useful flags:
98
+
99
+ ```bash
100
+ palisade-sec scan . --all # also show MED/LOW findings
101
+ palisade-sec scan . --json # stable machine-readable output
102
+ palisade-sec scan . --report # write palisade-report.md
103
+ palisade-sec scan . --rules ./my-rules # add your own YAML rules
104
+ ```
105
+
106
+ ## CI
107
+
108
+ Gate pull requests on **new** findings only — adopt Palisade on an imperfect
109
+ codebase without a wall of pre-existing failures:
110
+
111
+ ```bash
112
+ palisade-sec baseline . # once; commit .palisade/baseline.json
113
+ palisade-sec scan . --ci --baseline .palisade/baseline.json
114
+ ```
115
+
116
+ `--ci` exits non-zero only if a **new HIGH** finding appears. Fingerprints are
117
+ line-number independent, so refactors don't churn the baseline.
118
+
119
+ GitHub Actions:
120
+
121
+ ```yaml
122
+ - uses: astral-sh/setup-uv@v5
123
+ - run: uvx palisade-sec scan . --ci --baseline .palisade/baseline.json
124
+ ```
125
+
126
+ ## Configuration
127
+
128
+ `pyproject.toml`:
129
+
130
+ ```toml
131
+ [tool.palisade]
132
+ paths_ignore = ["migrations/*", "sandbox/*"]
133
+ include_tests = false # tests/** and conftest.py are skipped by default
134
+ max_hops = 3 # inter-procedural depth bound
135
+ ```
136
+
137
+ Or the same keys in `.palisade.toml`.
138
+
139
+ ## Custom rules
140
+
141
+ Rules are plain YAML validated by a pydantic schema — sources, LLM call
142
+ signatures, sinks, sanitizers, partial defenses. Adding coverage for a new
143
+ framework is a small PR with **no engine changes**. See
144
+ [`src/palisade_sec/rules/README.md`](src/palisade_sec/rules/README.md) for
145
+ the 5-minute guide.
146
+
147
+ ## Architecture
148
+
149
+ ```
150
+ source ──▶ language frontend (Python: stdlib ast) ──▶ normalized taint IR
151
+
152
+ language-agnostic engine ─┤ taint propagation,
153
+ sanitizer resolution, confidence scoring
154
+
155
+ YAML rules ──▶ findings ──▶ baseline diff ──▶ terminal / json / md
156
+ ```
157
+
158
+ The frontend/IR split is the scalability story: JS/TS/Go land later as new
159
+ frontends (tree-sitter) with zero engine changes.
160
+
161
+ ## Safety of the tool itself
162
+
163
+ - Palisade **never executes, imports, or evaluates scanned code** — it only
164
+ parses source text with `ast.parse`.
165
+ - `scan` makes **no network calls** and needs no API key or account.
166
+ - No telemetry. Nothing leaves your machine.
167
+
168
+ ## An honest note on scope
169
+
170
+ Palisade is **one layer** of defense against **one class** of vulnerability.
171
+ A clean scan means no *detected* injection-to-sink path — it does not mean
172
+ your application is secure. Keep your runtime guardrails, permissions
173
+ boundaries, and sandboxes; Palisade complements them, before merge.
174
+
175
+ ## License
176
+
177
+ MIT
@@ -0,0 +1,150 @@
1
+ # Palisade
2
+
3
+ **A linter for LLM security.** Palisade statically detects prompt-injection
4
+ vulnerabilities in Python codebases — untrusted input flowing through an LLM
5
+ into a dangerous sink — in CI, before they ship.
6
+
7
+ ```
8
+ untrusted input → LLM → exec / shell / raw SQL (no sanitizer) ⇒ finding
9
+ ```
10
+
11
+ No API key. No signup. No network calls. Pure static analysis.
12
+
13
+ ```bash
14
+ uvx palisade-sec scan .
15
+ ```
16
+
17
+ ```
18
+ HIGH app.py:31 [PI-EXEC] Prompt injection reaching code execution
19
+ ↳ source: question = request.json["question"] (app.py:31)
20
+ ↳ llm: resp = client.chat.completions.create( (app.py:32)
21
+ ↳ sink: exec(code) (app.py:40)
22
+ No sanitizer on path. Confidence: HIGH
23
+ Attack: crafted input makes the model emit Python that executes on your server.
24
+ Fix: never exec model output; sandbox + strict allowlist (denylists are bypassable).
25
+ Refs: CVE-2024-12366 (PandasAI); CVE-2025-3248 (Langflow, CISA KEV)
26
+ ```
27
+
28
+ ## Why
29
+
30
+ This exact pattern is behind real, exploited CVEs: **Langflow**
31
+ (CVE-2025-3248, on CISA KEV, exploited in the wild), **PandasAI**
32
+ (CVE-2024-12366, CVSS 9.8), **Vanna.ai** (CVE-2024-5565), **LangChain**
33
+ PAL/LLMMath chains (CVE-2023-36258, CVE-2023-29374). Almost nobody defends it
34
+ at the code level: existing tools are runtime proxies (paid, in the traffic
35
+ path) or guardrail libraries you have to know to wire in. Palisade is the
36
+ missing piece — **free, static, LLM-dataflow-aware, and CI-native**, like
37
+ ruff or semgrep but for the OWASP LLM Top-10 #1 risk.
38
+
39
+ ## What it detects (v1)
40
+
41
+ | Rule | Path | Real-world precedent |
42
+ |------|------|----------------------|
43
+ | `PI-EXEC` | input → LLM → `exec` / `eval` / `compile` / `PythonREPL` | PandasAI, Langflow, LangChain PAL |
44
+ | `PI-SHELL` | input → LLM → `os.system` / `subprocess(shell=True)` | Open Interpreter (by design) |
45
+ | `PI-SQL` | input → LLM → raw non-parameterized SQL | Vanna.ai |
46
+
47
+ Palisade runs **taint analysis, not grep**: it only reports a *complete*
48
+ `source → LLM → sink` data-flow path with no sanitizer in between.
49
+
50
+ - Constant developer prompt → LLM → `exec`? **Silent** — no untrusted source.
51
+ - `subprocess.run([...])` with an arg list? **Silent** — safe sink shape.
52
+ - Parameterized `cursor.execute(q, params)`? **Silent.**
53
+ - Allowlist / pydantic validation on the path? **Silent** — sanitized.
54
+ - Denylist or human-confirmation gate? **Flagged MED "risky"** — real CVEs
55
+ were exploited despite exactly those defenses. That is deliberate.
56
+
57
+ ## Install & run
58
+
59
+ ```bash
60
+ # one-shot, no install
61
+ uvx palisade-sec scan path/to/project
62
+
63
+ # or
64
+ pipx run palisade-sec scan .
65
+
66
+ # or as a dev dependency
67
+ uv add --dev palisade-sec
68
+ ```
69
+
70
+ Useful flags:
71
+
72
+ ```bash
73
+ palisade-sec scan . --all # also show MED/LOW findings
74
+ palisade-sec scan . --json # stable machine-readable output
75
+ palisade-sec scan . --report # write palisade-report.md
76
+ palisade-sec scan . --rules ./my-rules # add your own YAML rules
77
+ ```
78
+
79
+ ## CI
80
+
81
+ Gate pull requests on **new** findings only — adopt Palisade on an imperfect
82
+ codebase without a wall of pre-existing failures:
83
+
84
+ ```bash
85
+ palisade-sec baseline . # once; commit .palisade/baseline.json
86
+ palisade-sec scan . --ci --baseline .palisade/baseline.json
87
+ ```
88
+
89
+ `--ci` exits non-zero only if a **new HIGH** finding appears. Fingerprints are
90
+ line-number independent, so refactors don't churn the baseline.
91
+
92
+ GitHub Actions:
93
+
94
+ ```yaml
95
+ - uses: astral-sh/setup-uv@v5
96
+ - run: uvx palisade-sec scan . --ci --baseline .palisade/baseline.json
97
+ ```
98
+
99
+ ## Configuration
100
+
101
+ `pyproject.toml`:
102
+
103
+ ```toml
104
+ [tool.palisade]
105
+ paths_ignore = ["migrations/*", "sandbox/*"]
106
+ include_tests = false # tests/** and conftest.py are skipped by default
107
+ max_hops = 3 # inter-procedural depth bound
108
+ ```
109
+
110
+ Or the same keys in `.palisade.toml`.
111
+
112
+ ## Custom rules
113
+
114
+ Rules are plain YAML validated by a pydantic schema — sources, LLM call
115
+ signatures, sinks, sanitizers, partial defenses. Adding coverage for a new
116
+ framework is a small PR with **no engine changes**. See
117
+ [`src/palisade_sec/rules/README.md`](src/palisade_sec/rules/README.md) for
118
+ the 5-minute guide.
119
+
120
+ ## Architecture
121
+
122
+ ```
123
+ source ──▶ language frontend (Python: stdlib ast) ──▶ normalized taint IR
124
+
125
+ language-agnostic engine ─┤ taint propagation,
126
+ sanitizer resolution, confidence scoring
127
+
128
+ YAML rules ──▶ findings ──▶ baseline diff ──▶ terminal / json / md
129
+ ```
130
+
131
+ The frontend/IR split is the scalability story: JS/TS/Go land later as new
132
+ frontends (tree-sitter) with zero engine changes.
133
+
134
+ ## Safety of the tool itself
135
+
136
+ - Palisade **never executes, imports, or evaluates scanned code** — it only
137
+ parses source text with `ast.parse`.
138
+ - `scan` makes **no network calls** and needs no API key or account.
139
+ - No telemetry. Nothing leaves your machine.
140
+
141
+ ## An honest note on scope
142
+
143
+ Palisade is **one layer** of defense against **one class** of vulnerability.
144
+ A clean scan means no *detected* injection-to-sink path — it does not mean
145
+ your application is secure. Keep your runtime guardrails, permissions
146
+ boundaries, and sandboxes; Palisade complements them, before merge.
147
+
148
+ ## License
149
+
150
+ MIT
@@ -0,0 +1,15 @@
1
+ # vulnerable-app (fixtures)
2
+
3
+ A deliberately unsafe Flask app used as Palisade's acceptance fixtures.
4
+ **Do not deploy or copy patterns from this app.**
5
+
6
+ It contains, on purpose:
7
+ - 4 exploitable source → LLM → sink paths (exec, raw SQL, shell, and a
8
+ multi-hop exec across three files) that must be flagged **HIGH**;
9
+ - 1 denylist-gated exec that must be flagged **MED "risky"** — partial
10
+ defenses do not count as safe;
11
+ - 6 safe variants (sanitizer, arg-list subprocess, parameterized SQL,
12
+ constant prompt, log-only output, enum-constrained output) that must stay
13
+ **silent** — the false-positive tests are the most important in the repo.
14
+
15
+ `tests/test_example_app.py` pins every one of these expectations.
@@ -0,0 +1,10 @@
1
+ """Multi-hop fixture: source in app.py, LLM in llm_utils.py, sink in
2
+ executor.py — this module is the middle hop."""
3
+
4
+ from executor import execute_plan
5
+ from llm_utils import ask_llm
6
+
7
+
8
+ def run_agent(goal: str) -> str:
9
+ plan = ask_llm(f"Write Python code to accomplish: {goal}")
10
+ return execute_plan(plan)
@@ -0,0 +1,187 @@
1
+ """Deliberately vulnerable Flask app — Palisade's acceptance fixtures.
2
+
3
+ DO NOT DEPLOY. Every route here is either a real LLM prompt-injection
4
+ vulnerability (must be flagged), a safe variant (must stay silent), or a
5
+ partially-defended variant (must be flagged MED "risky").
6
+ """
7
+
8
+ import subprocess
9
+
10
+ from flask import Flask, jsonify, request
11
+ from openai import OpenAI
12
+
13
+ from db import get_db
14
+ from agent_pipeline import run_agent
15
+ from guards import is_blocked_code, validate_code
16
+
17
+ app = Flask(__name__)
18
+ client = OpenAI()
19
+
20
+ MODEL = "gpt-4o-mini"
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # VULNERABLE — must be flagged HIGH
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ @app.route("/calc", methods=["POST"])
29
+ def calc():
30
+ """VULN 1 (PI-EXEC, PandasAI-style): NL question -> LLM Python -> exec."""
31
+ question = request.json["question"]
32
+ resp = client.chat.completions.create(
33
+ model=MODEL,
34
+ messages=[
35
+ {"role": "system", "content": "Reply with Python code only."},
36
+ {"role": "user", "content": f"Write Python that answers: {question}"},
37
+ ],
38
+ )
39
+ code = resp.choices[0].message.content
40
+ exec(code) # noqa: S102 — the vulnerability under test
41
+ return jsonify(status="done")
42
+
43
+
44
+ @app.route("/ask-db")
45
+ def ask_db():
46
+ """VULN 2 (PI-SQL, Vanna-style): text-to-SQL -> raw execute."""
47
+ question = request.args.get("q", "")
48
+ resp = client.chat.completions.create(
49
+ model=MODEL,
50
+ messages=[{"role": "user", "content": f"Write a SQL query for: {question}"}],
51
+ )
52
+ sql = resp.choices[0].message.content
53
+ cur = get_db().cursor()
54
+ cur.execute(sql)
55
+ return jsonify(rows=cur.fetchall())
56
+
57
+
58
+ @app.route("/ops", methods=["POST"])
59
+ def ops():
60
+ """VULN 3 (PI-SHELL): task description -> LLM shell command -> shell."""
61
+ task = request.form["task"]
62
+ resp = client.chat.completions.create(
63
+ model=MODEL,
64
+ messages=[{"role": "user", "content": f"Give me one shell command to: {task}"}],
65
+ )
66
+ command = resp.choices[0].message.content
67
+ subprocess.run(command, shell=True)
68
+ return jsonify(status="ran")
69
+
70
+
71
+ @app.route("/agent", methods=["POST"])
72
+ def agent():
73
+ """VULN 4 (PI-EXEC, multi-hop): source here, LLM in llm_utils.py,
74
+ sink in executor.py — three functions across three files."""
75
+ goal = request.json["goal"]
76
+ result = run_agent(goal)
77
+ return jsonify(result=result)
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # PARTIAL DEFENSE — must be flagged MED "risky" (not HIGH, not silent)
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ @app.route("/calc-guarded", methods=["POST"])
86
+ def calc_guarded():
87
+ """A denylist gate before exec, LangChain-PAL-style. Real CVEs were
88
+ exploited despite exactly this defense."""
89
+ question = request.json["question"]
90
+ resp = client.chat.completions.create(
91
+ model=MODEL,
92
+ messages=[{"role": "user", "content": f"Write Python that answers: {question}"}],
93
+ )
94
+ code = resp.choices[0].message.content
95
+ if is_blocked_code(code):
96
+ return jsonify(error="blocked"), 400
97
+ exec(code) # noqa: S102 — still exploitable: denylists are bypassable
98
+ return jsonify(status="done")
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # SAFE — must NOT be flagged
103
+ # ---------------------------------------------------------------------------
104
+
105
+
106
+ @app.route("/calc-safe", methods=["POST"])
107
+ def calc_safe():
108
+ """SAFE (a): same shape as /calc, but the code is validated against a
109
+ strict AST allowlist before running."""
110
+ question = request.json["question"]
111
+ resp = client.chat.completions.create(
112
+ model=MODEL,
113
+ messages=[{"role": "user", "content": f"Write Python that answers: {question}"}],
114
+ )
115
+ code = validate_code(resp.choices[0].message.content)
116
+ exec(code) # noqa: S102 — sanitized above
117
+ return jsonify(status="done")
118
+
119
+
120
+ @app.route("/ping", methods=["POST"])
121
+ def ping():
122
+ """SAFE (b): LLM output used as ONE ARGUMENT in an arg-list subprocess
123
+ call — no shell, no injection into a command line."""
124
+ where = request.json["where"]
125
+ resp = client.chat.completions.create(
126
+ model=MODEL,
127
+ messages=[{"role": "user", "content": f"Hostname most relevant to: {where}"}],
128
+ )
129
+ host = resp.choices[0].message.content.strip()
130
+ subprocess.run(["ping", "-c", "1", host], check=False)
131
+ return jsonify(status="ok")
132
+
133
+
134
+ @app.route("/lookup", methods=["POST"])
135
+ def lookup():
136
+ """SAFE (c): LLM output bound via a PARAMETERIZED query."""
137
+ description = request.json["description"]
138
+ resp = client.chat.completions.create(
139
+ model=MODEL,
140
+ messages=[{"role": "user", "content": f"Product name for: {description}"}],
141
+ )
142
+ product = resp.choices[0].message.content
143
+ cur = get_db().cursor()
144
+ cur.execute("SELECT * FROM products WHERE name = ?", (product,))
145
+ return jsonify(rows=cur.fetchall())
146
+
147
+
148
+ def nightly_report():
149
+ """SAFE (d): constant developer prompt — no untrusted source at all."""
150
+ resp = client.chat.completions.create(
151
+ model=MODEL,
152
+ messages=[{"role": "user", "content": "Write Python that prints yesterday's date."}],
153
+ )
154
+ code = resp.choices[0].message.content
155
+ exec(code) # noqa: S102 — developer-controlled prompt, not user input
156
+
157
+
158
+ @app.route("/chat", methods=["POST"])
159
+ def chat():
160
+ """SAFE (e): LLM output is only logged and returned — never executed."""
161
+ message = request.json["message"]
162
+ resp = client.chat.completions.create(
163
+ model=MODEL,
164
+ messages=[{"role": "user", "content": message}],
165
+ )
166
+ answer = resp.choices[0].message.content
167
+ app.logger.info("chat answer: %s", answer)
168
+ print(answer)
169
+ return jsonify(answer=answer)
170
+
171
+
172
+ @app.route("/action", methods=["POST"])
173
+ def action():
174
+ """SAFE (f): LLM output constrained to a literal enum before the sink."""
175
+ wish = request.json["wish"]
176
+ resp = client.chat.completions.create(
177
+ model=MODEL,
178
+ messages=[{"role": "user", "content": f"One of list|status|uptime for: {wish}"}],
179
+ )
180
+ verb = resp.choices[0].message.content.strip()
181
+ if verb in ("list", "status", "uptime"):
182
+ subprocess.run(verb, shell=True)
183
+ return jsonify(status="ok")
184
+
185
+
186
+ if __name__ == "__main__":
187
+ app.run(debug=True)
@@ -0,0 +1,5 @@
1
+ import sqlite3
2
+
3
+
4
+ def get_db():
5
+ return sqlite3.connect("app.db")
@@ -0,0 +1,6 @@
1
+ """Multi-hop fixture: the dangerous sink lives here."""
2
+
3
+
4
+ def execute_plan(code: str) -> str:
5
+ exec(code) # noqa: S102 — the vulnerability under test (multi-hop)
6
+ return "ok"