CLI reference
palisade-sec [--version] <command> [args]Commands split into two layers. The offline core makes no network calls,
needs no API key, and sends no telemetry. The judgment layer needs the
[judge] extra (pip install 'palisade-sec[judge]', or
uvx --from 'palisade-sec[judge]' palisade-sec ...) and calls an endpoint you
configure in the environment or a .env - TypeSafe by default, or any
OpenAI-compatible endpoint. Everything is MIT and free to run; the split is
keyless-and-offline versus bring-your-own-endpoint. Full setup in the
judgment layer guide.
| Command | Needs [judge] + a judgment endpoint? |
|---|---|
scan, map, baseline, fix, redteam (synthesis) | No - offline, keyless |
audit | Yes - exits 2 with a hint if the extra or key is missing |
review | Only for the AI-judged layer; runs taint-only without them, and says so on stderr |
redteam --execute | Yes - exits 2 with a hint if the extra or key is missing |
Exit codes (the contract)
| Code | Meaning |
|---|---|
0 | Success, or nothing new. Includes “findings exist but --ci not set” and “all findings baselined under --ci”. |
1 | A gate tripped: scan --ci / review --ci found a new HIGH finding, redteam --execute --ci saw an attack land, or audit --ci produced a BLOCK decision. |
2 | Usage, target or setup error: the path does not exist, an explicit --config/--rules is missing, a --ci run scanned 0 files, the judgment layer is missing its [judge] extra or key, an output path is a symlink (refused), redteam --execute --ci had attacks that errored, or a connected surface refused to connect (bad webhook, expired token, missing scope, pr with no GitHub token). |
3 | Internal error - a bug in Palisade, not a finding. Please report it. |
A scan that read 0 files never prints a green tick: it warns “Nothing was
scanned … This is not a clean result.” in every output format, and under
--ci it exits 2 rather than passing a gate that checked nothing (a
JS/TS-only repo without the [js] extra is the usual cause).
Output files (palisade-report.md, palisade-fixes.md,
palisade-review.md, .palisade/baseline.json, or an explicit --output)
are never written through a symlink: a scanned repository could plant one at
those names, so Palisade refuses and exits 2 instead.
palisade-sec scan [PATH]
Scan a file or directory (default .) for source → LLM → sink paths.
| Flag | Effect |
|---|---|
--all | Show MED/LOW findings too. Default view: HIGH + “risky” downgraded findings. |
--json | Emit the stable JSON document (below) to stdout instead of terminal output. |
--sarif | Emit SARIF 2.1.0 to stdout, for GitHub code scanning / any AppSec pipeline. |
--report | Also write palisade-report.md - a shareable mini threat model grouped by severity. |
--ci | Exit 1 if any (new, when combined with --baseline) HIGH finding exists; exit 2 if 0 files were scanned. |
--baseline FILE | Diff against a baseline; only new findings are reported/counted. Stale entries are noted. |
--rules DIR | Load additional/overriding YAML rules from a directory (same id overrides a builtin). |
--config FILE | Explicit config file (.palisade.toml format). |
--assume-params-untrusted | Library mode: parameters of public (non-underscore) functions become untrusted sources (param:<name> in traces). |
File selection: *.py/*.pyi always; *.js/*.mjs/*.cjs/*.jsx/*.ts/*.tsx
when the [js] extra is installed (otherwise skipped with a note).
Always excluded: .venv, venv, site-packages, .git, build, dist,
node_modules, caches, plus .gitignore patterns. tests/**, test_*.py,
*_test.py, and conftest.py are skipped unless include_tests = true.
Unparseable files are skipped with a warning, never a crash.
SARIF / GitHub code scanning
scan --sarif emits SARIF 2.1.0 (severity high->error, med->warning, low->note;
sink as the primary location, source and LLM boundary as related locations,
line-shift-resilient partialFingerprints). A five-line workflow puts findings
in the GitHub Security tab:
permissions: { contents: read, security-events: write }steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 - run: uvx palisade-sec scan . --sarif > palisade.sarif - uses: github/codeql-action/upload-sarif@v3 with: { sarif_file: palisade.sarif }palisade-sec baseline [PATH]
Fingerprint current findings so CI fails only on new ones.
| Flag | Effect |
|---|---|
--output FILE | Baseline path (default <PATH>/.palisade/baseline.json). |
--rules DIR / --config FILE | As in scan. |
Fingerprints are sha256(rule + source file + normalized source snippet + sink file + normalized sink snippet) - line-shift resilient by
construction. The file is sorted and deterministic (diff-friendly); commit
it. Duplicate findings collapse to one fingerprint with a count.
palisade-sec fix [PATH]
Write a remediation plan: for each finding, a rule-tailored guardrail plus a pytest asserting the guardrail blocks the canonical attack and preserves the happy path. Deterministic templates, fully offline, and the scanned project is never modified.
| Flag | Effect |
|---|---|
--output FILE | Plan path (default palisade-fixes.md). |
--all | Cover MED/LOW findings too. |
--rules / --config / --assume-params-untrusted | As in scan. |
Guardrail families: AST allowlist (exec/eval), argv + executable allowlist (shell), single-SELECT parser check (SQL), host allowlist + private-IP block (HTTP/SSRF).
palisade-sec map [PATH]
Inventory the codebase’s AI surface: LLM call sites (with provider and model),
prompts (static vs dynamic), agent tools (with the capabilities their bodies
exercise), agents/chains, retrieval sites, and dangerous config flags
(allow_dangerous_code=True, …). Deterministic and offline - no network,
no key. It is the foundation the judgment checks build on.
| Flag | Effect |
|---|---|
--json | Emit the inventory as JSON (summary + artifacts). |
--config FILE | As in scan. |
palisade-sec audit [PATH] (judgment layer)
Run the semantic checks over grounded artifacts and route each to pass / review / block:
- excessive agency - over agent tools the map found: can the tool take an irreversible action, is it gated, how much harm if a manipulated model calls it.
- taint exploitability - over each verified
source → LLM → sinkfinding: how realistically exploitable is that specific path, and how severe.
Every question is anchored to a fact the static analyzer verified. Needs the
[judge] extra and reads the backend from the environment or .env; if either
is missing it exits 2 with a clear message. An
unverified (generic) backend never emits BLOCK on judgment alone - such a
decision downgrades to REVIEW.
| Flag | Effect |
|---|---|
--json | Emit findings as JSON (schema_version: 1). |
--ci | Opt-in gate: exit 1 if any finding is a BLOCK decision. |
--config FILE | As in scan. |
--policy FILE | Policy YAML (thresholds and criteria). See below. |
Policy: the thresholds are yours
audit and review route on thresholds you can set, rather than ones baked
into the tool. Resolution order:
--policy PATH.palisade/policy.yamlin the scanned tree[tool.palisade.semantic]in that tree’spyproject.toml- the built-in defaults
# .palisade/policy.yaml - a fintech's appetitechecks: excessive_agency: action_threshold: 0.45 # block above this probability review_threshold: 0.20 # flag for a human between the two gate_threshold: 0.60 # count a tool as "gated" only above this severity_block: 1 # harm >= this turns a review into a block taint_exploitability: action_threshold: 0.40 severity_block: 1A partial file overlays the defaults per check and per field, so setting one
threshold does not silently reset the rest. Unknown keys are an error, not a
shrug: action_treshold fails loudly with a warning and the run falls back to
the defaults, rather than leaving a gate at a threshold nobody chose.
One field is confined on purpose. criteria is editable English that is
interpolated into the question sent to the judge:
checks: excessive_agency: criteria: irreversible: "any movement of customer money, or any write to prod"Because that text becomes part of a model prompt, a policy file discovered
inside the tree being scanned may set thresholds but not criteria - it is
dropped with a warning naming the file. Otherwise a repository you scan could
ship a policy reading “nothing here is ever irreversible” and argue the judge
out of its own finding, which is the exact attack class this tool detects. Only
--policy, where you named the file yourself, may set criteria.
palisade-sec review [PATH] (judgment layer)
Compose scan + map + the semantic checks + red-team synthesis into one
prioritized report with a posture score: a number 0..100 and a named band
(Critical / High / Moderate / Low), derived from the tier counts and printed
with the breakdown beside it. It is a posture over detected findings
(likelihood × impact), not a safety score. If no judgment backend is
configured (or the [judge] extra is not installed), review runs taint-only
and says why on stderr, so --json stays parseable.
review judges each finding once per run and emits both the composed posture
and the audit view (audit_findings in --json) from that single pass, so a
run needs only one judgment pass and audit/review never disagree within it.
The model is probabilistic, so judged numbers and the posture score vary across
separate runs; the score is deterministic within a run, not across runs.
| Flag | Effect |
|---|---|
--json | Emit the report as JSON (schema_version: 1), including the posture block. |
--report | Also write palisade-review.md. |
--ci | Exit 1 on a new HIGH taint finding (baseline-diffed); exit 2 if 0 files were scanned. Gates only on deterministic taint findings: review’s judged signals never gate. Their calibration is preliminary: measured on a 10-case seed corpus (n=4 to 6 per signal), not a benchmark result; the judged layer stays advisory. For an explicit gate on judged decisions, opt in with audit --ci. |
--baseline FILE | Baseline to diff --ci against. |
--config FILE | As in scan. |
--policy FILE | Policy YAML, as in audit. Only consulted when a backend is configured. |
palisade-sec redteam [PATH]
Synthesize a targeted adversarial attack suite from the AI System Map, and optionally execute it against a live target you own.
Default is advisory and OFFLINE: it generates attacks aimed at the discovered tools, prompts, and agents (tool coercion, instruction override, data exfiltration, jailbreak, system-prompt leak) but does NOT run them.
| Flag | Effect |
|---|---|
--json | Emit the suite (or, with --execute, the run report) as JSON. |
--variants N | Attack variants per target (1-5). |
--execute | Fire the suite at a live target. Requires --approve. |
--approve | Required with --execute: you authorize firing adversarial inputs. |
--target URL | Target endpoint (or set PALISADE_REDTEAM_TARGET; key via PALISADE_REDTEAM_KEY). |
--ci | With --execute: exit 1 if any attack lands; exit 2 if attacks errored. |
--config FILE | As in scan. |
Execution drives the endpoint you provide, in your environment - Palisade
never executes your code. It scores landed attacks with the judgment backend
from .env (deterministic tool-invocation checks plus a model for behavioral
judgment), so --execute needs the [judge] extra and a configured backend.
Run it only against systems you own and are authorized to test.
palisade-sec connect github|slack|llm
Set Palisade up from the terminal; credentials go to your OS keychain (with
the keyring extra) or a 0600 file. Full guide: connect.
| Command | Notes |
|---|---|
connect github [--token T] [--no-gh] | Reuses the gh CLI’s token when logged in, else the OAuth device flow. Verified before storing. |
connect slack [--webhook URL] [--no-test] | Incoming webhook; posts a test message first. |
connect llm --provider typesafe|anthropic|openai_compatible [--key K] [--endpoint U] [--model M] [--workspace-id ID] [--no-verify] | For audit / review only. --workspace-id is required for an Anthropic organization key, which is not scoped to a workspace; a key created inside a workspace needs no flag. Also readable as ANTHROPIC_WORKSPACE_ID. |
connections | What is connected, from where, redacted. |
disconnect github|slack|llm | Remove stored credentials. |
The environment always wins over stored values, so CI is unchanged.
palisade-sec pr [PATH]
Opens a draft pull request containing the fix plan. Options: --repo owner/name (default: the git remote), --base, --branch, --plan-path,
--baseline, --no-draft, --dry-run. The branch name is derived from the
findings, so re-running updates the same pull request. Exits 0 when there
is nothing to open one for, 2 when GitHub is not connected.
palisade-sec notify [PATH] --slack
Scans and posts a Block Kit summary to the connected webhook. Options:
--baseline (post only new findings), --link URL (button target),
--dry-run (print the message instead of sending it). Nothing is posted
without this command.
Judgment configuration
audit, review, and redteam --execute read their backend from environment
variables, or from a .env in the current working directory (see
.env.example, or the template in the
judgment layer guide). The process environment wins over
.env. Keys are never logged.
| Variable | Meaning |
|---|---|
PALISADE_JUDGE_BACKEND | typesafe (default) or openai_compatible. |
PALISADE_JUDGE_ENDPOINT | Base URL. Defaults to the TypeSafe API for typesafe; required for openai_compatible. |
PALISADE_JUDGE_MODEL | Model id. Defaults to jev-latest for typesafe; required for openai_compatible. |
TYPESAFE_API_KEY | Key for the typesafe backend. |
PALISADE_JUDGE_API_KEY | Key for the openai_compatible backend. |
Install the extra with pip install 'palisade-sec[judge]'. TypeSafe answers
are treated as verified; a generic OpenAI-compatible endpoint is validated against a
strict schema and treated as best-effort/unverified, so it can never BLOCK or
raise a Critical posture on judgment alone. Calibration of the judged signals is
preliminary: measured on a 10-case seed corpus (n=4 to 6 per signal), not a
benchmark result; the judged layer stays advisory.
Inline suppressions
Some findings cannot be fixed today: the code is genuinely sandboxed, the risk is accepted, or the fix is scheduled. Without a way to silence one, “one unfixable finding disables the tool” and the whole scanner gets removed from CI. Suppress it in place instead:
exec(code) # palisade: ignore[PI-EXEC] - runs in a locked-down sandboxeval(code); // palisade: ignore[PI-EXEC] - input is schema-validated upstream| Form | Effect |
|---|---|
# palisade: ignore[PI-EXEC] | silences that rule on this finding |
# palisade: ignore[PI-EXEC,PI-SQL] | silences any of the listed rules |
# palisade: ignore | silences every rule on this finding |
- reason or : reason after the brackets | recorded and reported |
The comment goes on the sink line, or the line directly above it. #
and // are both accepted, so the same syntax works in Python and JS/TS.
Rule ids are case-insensitive.
Suppressions are deliberately loud, because a silent one is how a vulnerability quietly comes back:
- suppressed findings are counted, not discarded, and reported in the
terminal summary and in
summary.suppressed_inline - each one appears in the
suppressionsarray of--jsonwith its rule, location, severity and reason - a comment that stops matching anything is reported as stale, so dead suppressions get cleaned up instead of masking a future finding
A suppression lives in the code being scanned, so anyone who can edit the
code can silence a finding. That is the same trust model as # noqa, and
the reason suppressions are counted and attributable rather than invisible.
Review them in code review like any other change.
Configuration
pyproject.toml under [tool.palisade], or the same keys in
.palisade.toml at the scan root (--config overrides discovery). Invalid
config → warning + defaults, never a crash.
[tool.palisade]paths_ignore = ["migrations/*", "sandbox/*"] # glob patterns, relative to rootinclude_tests = false # scan tests/** toomax_hops = 3 # inter-procedural depth boundassume_params_untrusted = false # library mode defaultrules_dir = "security/palisade-rules" # extra rules directoryCLI flags override config; an explicit assume_params_untrusted=False from
an API caller overrides both.
JSON schema
scan --json emits one document. schema_version gates compatibility -
parse defensively on any other value.
{ "schema_version": 1, "tool": "palisade-sec 0.5.2", "summary": { "files_scanned": 6, "high": 4, "med": 1, "low": 0, "baseline_suppressed": 0, // known findings hidden by --baseline "suppressed_inline": 0 // findings silenced by `palisade: ignore` }, "suppressions": [ // each silenced finding, never hidden {"rule": "PI-EXEC", "file": "app.py", "line": 42, "severity": "high", "reason": "sandboxed", "suppressed_at": 42} ], "findings": [ // sorted: severity, file, line, rule { "rule": "PI-EXEC", "title": "Prompt injection reaching code execution", "severity": "high", // high | med | low "confidence": "HIGH", // HIGH | MEDIUM | LOW (path directness) "risky_partial_defense": false, // true when downgraded (see below) "file": "app.py", // sink location = finding location "line": 64, "fingerprint": "9f2c4a1b8e3d5f07", // baseline identity, line-independent "count": 1, // duplicates collapsed into this entry "trace": { "source": { "file": "app.py", "line": 56, "snippet": "spec = request.json[\"spec\"]", "matched": "request.json" }, "llm": { "file": "app.py", "line": 57, "snippet": "resp = client.chat.completions.create(", "matched": "chat.completions.create" }, "sink": { "file": "app.py", "line": 64, "snippet": "exec(code)", "matched": "exec" } }, "partial_defenses": [ // non-empty ⇒ severity was downgraded to med { "pattern": "is_blocked_code", "kind": "partial_defense", "file": "app.py", "line": 95 } // kind: "partial_defense" (denylist/confirmation gate) // | "unverified_sanitizer" (sanitizer in name only) ], "attack": "...", "fix": "...", "references": ["CVE-...", "..."], "cwe": ["CWE-94", "CWE-1426", "CWE-1427"], "owasp_llm": ["LLM01:2025", "LLM05:2025"] } ], "skipped": ["broken.py: parse error, file skipped (...)"], "warnings": ["invalid rule file skipped: bad.yaml: ..."], "notes": ["1 JS/TS file(s) skipped - install ... palisade-sec[js] ..."]}Notes for consumers:
--jsonalways includes all severities; the HIGH-first display filter applies to terminal output only.- In library mode,
trace.source.matchedisparam:<name>; for route handlers it’s the matched source pattern (request.json,req.body, …). notesmay include an inter-procedural truncation notice on very deep call chains - recall, not precision, is what truncation affects.
Baseline file format
{ "schema_version": 1, "tool": "palisade-sec 0.5.2", "findings": { "<fingerprint>": { "rule": "PI-EXEC", "file": "app.py", "severity": "high", "count": 1 } }}Sorted by fingerprint; safe to merge in git. A missing or corrupt baseline degrades gracefully: a warning, and all findings treated as new.