Skip to content

kenlacroix/palisade

Palisade

Attack-surface monitoring for self-hosted and AI-infra services.

CI Release Agent: Go Control plane: FastAPI Web: React + TS License: Apache 2.0

trypalisade.dev · Live demo · Quickstart · Architecture · Design decisions

Palisade demo

What's new in v0.1.3 — the first signed release. Agent binaries ship with minisign-signed checksums that the install script verifies against a pinned public key (fail-closed with PALISADE_REQUIRE_SIGNATURE). Release notes · Changelog

A pull-only agent enrolls once, discovers listening services on-host, and runs CVE detections locally — only normalized findings ever leave the host. A FastAPI control plane serves a signed detection catalog, ingests findings, scores posture (with real 30-day trends), alerts on new/regressed findings, and drafts new detections from CVE advisories with an LLM.

  • Agent-on-host, data stays put — discovery and scanning run locally; the host's raw surface never leaves.
  • Signed detection catalog — Ed25519-signed bundles the agent verifies before running any detection; fails closed.
  • Version-aware matching — detections fire by service and version range, so litellm <1.40.2 skips 1.41.0.
  • Posture, trends & alerts — real 30-day posture scoring with channel/rule alerting on new and regressed findings.
  • AI in the loop — draft detections from CVE advisories and triage findings off the request path.
  • Multi-tenant by design — users, orgs, session auth, RBAC, and Postgres row-level isolation per org_id.

Architecture

Component Path Stack
Control plane control-plane/ FastAPI + SQLAlchemy + Alembic (sqlite by default, Postgres in compose)
Agent agent/ Go 1.22, stdlib-only, runs on each monitored host
Web UI web/ React + TypeScript + Vite + Tailwind
Detections detections/ YAML specs validated against detection.schema.json

The loop: agent enrollheartbeat → control plane issues a discover job → agent reports assets → heartbeat issues a scan job (detections matched by service and version range) → agent pulls the signed catalog bundle, verifies it, runs detections on-host → reports findings → control plane scores posture, evaluates alert rules (delivering matching alerts in the background), and (optionally) AI-triages each finding off the request path.

The web UI is multi-tenant: log in (demo@palisade.local / palisade in the demo), and all /v1 read endpoints are scoped to your active org with role-based access (owner/admin/member/viewer).

Try it in one command

Just want to click around? The same build is live, read-only, at app.trypalisade.dev — log in as demo@palisade.local / palisade. The control plane is home-hosted behind a Cloudflare Tunnel on intentionally-disposable demo data (see control-plane/deploy/README.mdBlast radius & isolation).

The fastest way to see the whole product running in your own environment. Brings up the control plane, web UI, Postgres, and an agent that auto-enrolls and scans a bundled deliberately-vulnerable target — on top of a pre-seeded org so every screen (Dashboard, Assets, Findings, Detections, Alerts) is populated the moment it loads.

make demo                       # docker compose: api + web + postgres + agent + target
# open http://localhost:8080 — log in as demo@palisade.local / palisade
make demo-down                  # tear down (removes volumes for a clean re-run)

Within ~20–40s the agent enrolls, discovers the target, and a real critical finding (litellm-proxy-preauth-sqli, CVE-2026-42208) appears alongside the seeded data — the full enroll → discover → scan → finding → posture loop, end to end, no manual steps.

Two flags drive the demo (set automatically by make demo):

Var Effect
PALISADE_SEED_DEMO=1 Populate the demo org with realistic assets, findings, 30-day posture trend, alerts, and audit history at bootstrap (idempotent).
PALISADE_DEMO_MODE=1 Make the public demo read-only for logged-in users (agent ingest still writes); surfaces a "live demo" banner in the UI.

Both default off, so dev and self-hosted production behave exactly as before unless you opt in. To populate a local sqlite run without Docker, set PALISADE_SEED_DEMO=1 before starting the control plane.

To monitor your own hosts instead of the bundled target: mint an enroll token in the UI (Add agent), then run the agent binary on each host (palisade enroll --token <T> --server <url>palisade run).

Screens

The portal with PALISADE_SEED_DEMO=1 (every screen populated) and PALISADE_DEMO_MODE=1 (the read-only "live demo" banner).

Dashboard — posture score, 30-day trend, needs-attention Finding detail — evidence, fingerprint, remediation, refs
Dashboard Finding detail
Assets — discovered services, version, exposure, findings Detections — signed CVE catalog with CVSS and tenant hits
Assets Detections
Alerts — channels, rules, quiet hours, history
Alerts

Quickstart (zero infra, sqlite)

Requires Go 1.22+, Python 3.12, Node 18+.

# 1. control plane
make venv                       # create control-plane/.venv + install deps
make migrate                    # apply migrations (sqlite:///./palisade.db)
cd control-plane && PALISADE_ENROLL_TOKENS=PLS-DEMO \
  ./.venv/bin/uvicorn app.main:app --reload     # http://127.0.0.1:8000/docs

# 2. web UI (separate terminal) — vite proxies /v1 to the control plane
cd web && npm install && npm run dev            # http://127.0.0.1:5173

Or run the whole stack (FastAPI + Postgres) in Docker:

cd control-plane && cp .env.example .env && docker compose up --build

End-to-end demo

DEMO.md walks the full on-host loop: start the control plane, expose a fake-vulnerable LiteLLM target on port 4000, enroll + run the agent, and watch a real critical finding (litellm-proxy-preauth-sqli, CVE-2026-42208) appear via the read APIs, then mute it and watch posture recover.

make smoke        # enroll → discover → assets → scan → findings → posture (temp DB)

Signed catalog bundles

The control plane signs the detection bundle with Ed25519 over a canonical manifest; the agent rebuilds the same manifest and verifies it against a pinned public key before running any detection — integrity over an untrusted channel. A demo keypair ships in .env.example.

# control plane: enable signing with the demo key
cd control-plane
PALISADE_SIGNING_KEY=70kJtI1NajTd1yQXFHVRuBVQfc6P2CAtRroaLCmYYbY= \
  ./.venv/bin/uvicorn app.main:app --reload

The agent pins the matching public key (override with PALISADE_CATALOG_PUBKEY); the bundled default matches the demo seed. Verification policy:

  • empty signature → refuse to scan;
  • "stub" (no signing key set) → proceed in dev mode with a warning;
  • otherwise → verify, and refuse to run detections if it fails.

Generate your own keypair:

cd control-plane && ./.venv/bin/python -c "import os,base64;from app import _ed25519 as e;s=os.urandom(32);print('PALISADE_SIGNING_KEY=',base64.b64encode(s).decode());print('pubkey         =',base64.b64encode(e.publickey(s)).decode())"

Set the printed seed as PALISADE_SIGNING_KEY on the control plane and the pubkey as PALISADE_CATALOG_PUBKEY on the agent.

Draft → review → accept (close the loop)

In the Detections screen, + New from CVE URL drafts a detection from an advisory with an LLM (requires ANTHROPIC_API_KEY on the control plane; otherwise the endpoint returns 503). Review the draft, then Accept & ship to persist it — this bumps the catalog version so agents pull it on their next bundle. Equivalent API call (admin+ session bearer required; see DEMO.md for $TOKEN):

curl -s -X POST http://127.0.0.1:8000/v1/detections \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{
  "id":"acme-rce","title":"ACME RCE","cve":"CVE-2026-9999","severity":"high",
  "category":"web","engine":"nuclei","match":{"service":"acme","versions":"<2.0.0"},
  "http":[{"method":"GET","path":"/x","matchers":[{"type":"status","status":[200]}]}],
  "remediation":"upgrade to >=2.0.0","references":["https://example.com"],"cvss":7.5
}'   # -> {"id":"acme-rce","version":<bumped>}

Version-aware scan matching

Detections target assets by service and version range. match.versions accepts comma/space-separated constraints (<1.40.2, >=11.1.4 <15.2.3); a litellm <1.40.2 detection fires on 1.39.0 but not 1.41.0. Unknown/missing asset versions fail open (scanned anyway) so a vuln is never silently skipped.

AI triage

When ANTHROPIC_API_KEY is set, new findings are scored after ingest (triage_priority / triage_score / triage_rationale, surfaced on the finding read API). Best-effort and run in a background task off the ingest request path — it never blocks or fails ingestion, and no-ops without a key. PALISADE_TRIAGE_MODEL overrides the model (default claude-haiku-4-5-20251001).

Alerting

Define channels (telegram / email / webhook) and rules (min_severity + on_events [new|regressed] → channel) from the Alerts screen or the API. On finding ingest, matching rules fire and alerts are delivered in a background task; an alert history is kept and surfaced at GET /v1/alerts. Channel secrets are redacted on read.

BASE=http://127.0.0.1:8000; UAUTH="Authorization: Bearer $TOKEN"   # see DEMO.md for $TOKEN
# webhook channel + a rule that fires on any high+ new/regressed finding
CH=$(curl -s -X POST $BASE/v1/alert-channels -H "$UAUTH" -H 'content-type: application/json' \
  -d '{"type":"webhook","name":"local","config":{"url":"http://127.0.0.1:9000/hook"}}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
curl -s -X POST $BASE/v1/alert-rules -H "$UAUTH" -H 'content-type: application/json' \
  -d "{\"name\":\"high+\",\"min_severity\":\"high\",\"on_events\":[\"new\",\"regressed\"],\"channel_id\":\"$CH\"}"

Tests

make test                                         # Go agent tests + smoke + detection validation
cd control-plane && ./.venv/bin/python -m app.api_test          # new endpoint coverage (signed path)
cd control-plane && ./.venv/bin/python -m app.smoke_test        # full loop (unsigned path)
cd agent && go test ./...                                       # agent unit tests incl. manifest verify

The control plane has a suite of app/*_test.py modules; CI runs each in its own process with pytest (several rebind module-global state at import). Most also run as plain scripts via python -m. Run make lint before committing.

Configuration

All knobs live in control-plane/app/config.py, read from env — see control-plane/.env.example and control-plane/README.md for the full table. Key vars:

Var Default Notes
DATABASE_URL sqlite:///./palisade.db Compose sets the Postgres URL.
PALISADE_ENROLL_TOKENS PLS-DEMO Comma-separated, single-use enroll tokens (each mints one agent into the token's org).
PALISADE_DEMO_USER_EMAIL demo@palisade.local Demo org owner seeded at bootstrap.
PALISADE_DEMO_USER_PASSWORD palisade Demo user password.
PALISADE_SESSION_TTL_S 604800 (7d) Web UI bearer-session lifetime, seconds.
PALISADE_SIGNING_KEY unset (demo key) Ed25519 seed (base64) for bundle signing. Unset signs with the public demo key and warns — set it in production.
PALISADE_CATALOG_PUBKEY demo key Agent-side pinned bundle pubkey (base64); must match the signing key.
PALISADE_ALLOW_UNSIGNED unset Agent dev escape hatch — if set, runs unsigned/stub bundles instead of refusing. Never set in production.
ANTHROPIC_API_KEY unset Enables AI drafting + finding triage.
PALISADE_DETECTIONS_DIR repo detections/ Source of seeded detection YAMLs.

Status

Implemented: enroll/heartbeat/scan loop, Ed25519-signed catalog bundles (agent verifies before running any detection and fails closed; detections/README.md covers keygen/rotation), version-aware matching, AI drafting + accept loop, CVSS, background AI triage, posture scoring with real 30-day trends, multi-tenancy (users/sessions/orgs + RBAC, single-use enroll tokens, Postgres row-level security per org_id), alerting (channels/rules/history), agent mTLS (enroll issues a client cert from an internal CA; verified at a TLS-terminating proxy, with the bearer agent_secret as the plaintext-demo fallback), and a SECURITY DEFINER path so the cross-tenant catalog aggregate (tenants_hit / tenants_total) is correct under RLS on Postgres, a durable Arq + Redis queue/worker for AI triage and alert delivery (with an in-process BackgroundTasks fallback when REDIS_URL is unset), and a pluggable module detection engine (a compiled spec_ref registry in the agent; first module is the Next.js middleware bypass, CVE-2025-29927), per-org encryption of evidence at rest (AES-256-GCM under a per-org wrapped data key, migration 0005), and per-rule alert quiet hours (deferred delivery released when the window closes, migration 0007). Not yet built (see SPEC.md): the production ops layer — IaC, a live deploy, public status page, observability dashboards, and runbooks.

Contributing

See CONTRIBUTING.md for dev setup, make lint / make test, and the PR flow. Release history is in CHANGELOG.md.

License

Apache License 2.0 — © 2026 Kenneth Lacroix.


About

Attack-surface monitoring for self-hosted & AI-infra services: a pull-only agent runs CVE detections on-host (only findings leave), a FastAPI control plane serves a signed catalog, scores posture, and AI-drafts new detections.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors