diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d0cadd9..c831208 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,13 +5,13 @@ "url": "https://github.com/crowdsecurity" }, "metadata": { - "description": "CrowdSec skills and tooling for Claude Code — operational automation for installing, configuring, and debugging CrowdSec." + "description": "CrowdSec skills and tooling for Claude Code — operational automation for installing, configuring, and debugging CrowdSec, plus a Service API skill for the premium Console cloud API." }, "plugins": [ { "name": "crowdsec", "source": "./", - "description": "Operational skill for installing, configuring, operating, and debugging CrowdSec (cscli, LAPI/CAPI, hub, bouncers, WAF/AppSec) across bare-metal, Docker, and Kubernetes.", + "description": "CrowdSec skills for Claude Code: an operational skill (engine, cscli, hub, bouncers, WAF/AppSec across bare-metal, Docker, Kubernetes) and a Service API skill (premium Console cloud API — blocklists, allowlists, firewall integrations, metrics, decisions).", "version": "0.2.3", "author": { "name": "CrowdSec", @@ -30,7 +30,12 @@ "bouncer", "fail2ban", "devops", - "sre" + "sre", + "console", + "service-api", + "sapi", + "blocklist", + "integration" ] } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index cd1d0d0..da7f746 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,9 +2,10 @@ "name": "crowdsec", "version": "0.2.3", "skills": [ - "./skills/crowdsec" + "./skills/crowdsec", + "./skills/crowdsec-service-api" ], - "description": "Operational skill for installing, configuring, operating, and debugging CrowdSec (cscli, LAPI/CAPI, hub, bouncers, WAF/AppSec) across bare-metal, Docker, and Kubernetes.", + "description": "CrowdSec skills for Claude Code: an operational skill (install/configure/operate/debug the engine, cscli, bouncers, WAF across bare-metal/Docker/Kubernetes) and a Service API skill (drive the premium Console cloud API — blocklists, allowlists, firewall integrations, metrics, decisions).", "author": { "name": "CrowdSec", "url": "https://github.com/crowdsecurity" @@ -18,6 +19,12 @@ "waf", "appsec", "bouncer", - "fail2ban" + "fail2ban", + "console", + "service-api", + "sapi", + "blocklist", + "integration", + "cloud" ] } diff --git a/CLAUDE.md b/CLAUDE.md index ece1767..2e01d92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,13 @@ If you are uncertain about any fact, statistic, date, or piece of technical info ## Content structure +The plugin ships **two skills** under `skills/`: `crowdsec` (operational — local +engine/cscli/bouncers/WAF) and `crowdsec-service-api` (the premium Console cloud REST API). Each has +its own router `SKILL.md` and `references/`. The layout below describes the `crowdsec` skill; the +`crowdsec-service-api` layout follows in its own subsection. + +### `crowdsec` skill layout + `SKILL.md` is the router — a symptom/intent-indexed table that points into `references/`. All depth lives in `references//`, organized by the axis that fits the area: @@ -61,6 +68,31 @@ All depth lives in `references//`, organized by the axis that fits the are area's organizing axis — update the table above in the *same* change. This section is the authoritative map of the layout; let it drift and it stops being trustworthy. +### `crowdsec-service-api` skill layout + +`SKILL.md` is the router **plus** the "acting on the user's behalf" operating contract (key +resolution, `/info` validation, read-vs-mutate confirm gate). `references/` is organized by **API +resource group** — the axis that fits a REST API — one file each: + +| File | Covers | +|---|---| +| `authentication.md` | Key creation, `x-api-key`, key resolution (env → `~/.config/crowdsec/sapi_key`), `/info`, Python SDK pointer. | +| `blocklists.md` | CRUD, add/remove/bulk IPs + expiration, download, subscribers, shares, search. | +| `allowlists.md` | CRUD, items + expiration, subscribers (cross-links the `crowdsec` skill's *local* allowlists). | +| `integrations.md` | Firewall/appliance feeds: create (entity_type/output_format), Basic-auth content pull + pagination, decisions stream, vendor table. | +| `decisions.md` | Org-level decisions + aggregated (short; prefer blocklists). | +| `metrics.md` | `/metrics/remediation` ROI (raw vs computed). | + +CTI (the tracker surface — cves/vendors/fingerprints/tags/products/tracker-*) is **deferred to a +future iteration**: it needs a CTI-scoped key (a blocklist/decision key gets `403`). When added, +restore a `cti.md` row here. + +Conventions differ from the `crowdsec` skill in two ways: **no platform axis** (the "environment" is +HTTPS, not systemd/docker/k8s — so no per-platform prefixes), and recipes are `curl` the skill runs +on the user's behalf, every mutating one gated behind explicit confirmation. Verification uses +`env: sapi` in the `verified:` block. When you add/rename a resource file here, update this table in +the same change. + ## Testing - **Nothing ships unverified.** Every command and every expected outcome must have been diff --git a/README.md b/README.md index 1b2419d..c899900 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,22 @@ --- -This is an [Agent Skill](https://docs.claude.com/en/docs/claude-code/skills) that turns Claude/Codex/... into a -hands-on CrowdSec operator. Ask it to stand up an engine, wire a bouncer, enable -the WAF, or figure out why nothing's getting blocked — it knows the `cscli` -commands, the config layout, the failure modes, and the safe way through each of -them across **bare-metal/systemd, Docker, OpnSense and Kubernetes/Helm**. +This plugin bundles **two [Agent Skills](https://docs.claude.com/en/docs/claude-code/skills)**: + +- **`crowdsec`** — a hands-on CrowdSec operator. Stand up an engine, wire a + bouncer, enable the WAF, or figure out why nothing's getting blocked. It knows + the `cscli` commands, the config layout, the failure modes, and the safe way + through each across **bare-metal/systemd, Docker, OpnSense and Kubernetes/Helm**. +- **`crowdsec-service-api`** — drives the premium **Console Service API** (cloud) + on your behalf with your API key: create and populate blocklists/allowlists, + wire firewall/appliance integrations, pull remediation ROI metrics, and manage + org-level decisions — every state change gated behind an explicit confirmation. ## What it covers +**`crowdsec` (operational):** + | Area | Covered | |---|---| | **Install** | bare-metal/systemd · Docker · Kubernetes/Helm · OpnSense · Console enrollment | @@ -34,6 +41,16 @@ them across **bare-metal/systemd, Docker, OpnSense and Kubernetes/Helm**. | **Operate** | health checks & smoke tests · upgrades & rollback · multi-server / remote LAPI / mTLS | | **Debug** | logs not parsing · no alerts firing · bouncer not blocking · specific errors | +**`crowdsec-service-api` (premium cloud API):** + +| Area | Covered | +|---|---| +| **Blocklists** | create · add/remove/bulk IPs (with expiry) · download · share across orgs · subscribe engines/bouncers | +| **Allowlists** | create · items with expiry · subscribe by engine/tag/org | +| **Integrations** | firewall/appliance feeds (Palo Alto, Fortinet, Cisco, F5, Sophos, pfSense/OPNsense…) · paginated Basic-auth content pull | +| **Metrics** | remediation ROI (traffic dropped, bytes/egress saved, attacks prevented) | +| **Decisions** | org-level decisions + aggregated (read/manage) | + ## 🚀 Install The skill loads automatically once installed. Just talk to @@ -80,6 +97,9 @@ Once installed, Claude picks the skill up whenever your prompt involves CrowdSec - _"CrowdSec doesn't detect attacks on my nginx server, why?"_ - _"There's a decision for this IP but it's not being blocked."_ - _"Migrate my fail2ban jails to CrowdSec."_ +- _"Create a Console blocklist and push these IPs from my SIEM to it."_ (Service API) +- _"Wire a Palo Alto external dynamic list to my CrowdSec blocklist."_ (Service API) +- _"Show me the remediation ROI metrics for last month."_ (Service API) ## What it does **not** do diff --git a/skills/crowdsec-service-api/SKILL.md b/skills/crowdsec-service-api/SKILL.md new file mode 100644 index 0000000..7880601 --- /dev/null +++ b/skills/crowdsec-service-api/SKILL.md @@ -0,0 +1,124 @@ +--- +name: crowdsec-service-api +description: Use when the user wants to drive the CrowdSec Console **Service API (SAPI)** — the premium cloud REST API at admin.api.crowdsec.net — to programmatically manage blocklists (add/remove/bulk IPs, share, subscribe engines), allowlists, firewall/appliance integrations (Palo Alto, Fortinet, Cisco, F5, Sophos, pfSense/OPNsense…), remediation ROI metrics, and org-level decisions. Acts on the user's behalf with their API key. This is the cloud/API skill — for the local engine, cscli, and bouncers use the `crowdsec` skill. +verified: + - date: 2026-07-29 + version: "1.70.52" + env: sapi + notes: "key resolution + GET /info validation + list recipes against production SAPI" +--- + +# CrowdSec Service API (SAPI) — cloud blocklist / allowlist / integration automation + +SAPI is the **premium** REST API behind the CrowdSec Console. It manages +**cloud-side** objects (private blocklists, allowlists, decisions, firewall integrations) +that then push down to enrolled engines and bouncers. It is **not** the local +engine API — there is no `cscli` here, only HTTPS. + +- **Base URL:** `https://admin.api.crowdsec.net/v1` +- **Auth:** `x-api-key: ` header on every call. (One exception: the + integration *content* endpoint uses HTTP Basic with credentials minted at + integration creation — see [references/integrations.md](./references/integrations.md).) +- **Interactive API docs:** · spec + + +## Boundary — this skill vs the `crowdsec` skill + +| You want to… | Use | +|---|---| +| Create/manage a **private blocklist** in the cloud, push IPs to it via API | this skill | +| Wire a firewall/appliance (Palo Alto, Fortinet…) to a cloud **integration** | this skill | +| Manage **cloud allowlists**, subscribe engines/tags/orgs to lists | this skill | +| Create/manage **org-level decisions** (targeted or ad-hoc bans, non-IP scopes, per tag/entity) | this skill | +| Pull **remediation ROI metrics** | this skill | +| Install / run / debug the **local engine**, `cscli`, bouncers, WAF | the `crowdsec` skill | +| **Enroll** an engine into the Console (`cscli console enroll`) | the `crowdsec` skill → `references/install/console.md` | +| Configure a **local** allowlist/whitelist on one engine | the `crowdsec` skill → `references/configure/allowlists.md` | + +Cloud allowlists/blocklists here only take effect on an engine once that engine +is enrolled **and** subscribed (and `console_management` is enabled locally). The +enrollment half lives in the `crowdsec` skill. + +## Acting on the user's behalf — operating contract + +This skill runs real cloud mutations. Follow this every session. + +**1 — Resolve the key** (never echo it, never write it into the repo): +```bash +KEY="${CROWDSEC_SAPI_KEY:-$(cat ~/.config/crowdsec/sapi_key 2>/dev/null)}" +[ -n "$KEY" ] || echo "No key: export CROWDSEC_SAPI_KEY or write it to ~/.config/crowdsec/sapi_key (chmod 0600)" +``` + +**2 — Validate before acting** — one read call confirms the key and shows *which +tenant* is about to change: +```bash +curl -s -H "x-api-key: $KEY" https://admin.api.crowdsec.net/v1/info +# → {"organization_id":"…","subscription_type":"…","api_key_name":"…"} +``` + +**3 — Classify read vs mutate.** `GET` / download / `POST …/search` are safe — +run them directly. Every **`POST` / `PATCH` / `DELETE` that changes state** needs +**explicit user confirmation first**; show the exact URL and JSON body you will +send, then wait. + +**4 — Extra-danger operations** — warn in plain words *before* the confirm, +because subscribed engines **enforce** these lists, so a change can block or +unblock real traffic and is hard to undo: + +| Operation | Why it's dangerous | +|---|---| +| `POST …/ips/bulk_overwrite` | Replaces the **entire** blocklist content. | +| `DELETE /blocklists/{id}` · `/allowlists/{id}` · `/integrations/{id}` | Removes the object and everyone's subscription/feed to it. | +| `POST …/ips/delete` | Un-blocks IPs fleet-wide. | +| `POST /decisions` with `target.type: org` | Bans fleet-wide across every enrolled engine in the org. | +| `…/shares` / unshare | Grants/revokes another **organization** access. | +| any `…/subscribers` change | Changes which engines/bouncers enforce the list. | + +**5 — Clean up** any throwaway objects you created while testing, and remind the +user to rotate a key that was pasted into chat. + +## Step — Detect the intent + +| Cue from user | Go to | +|---|---| +| "test my key", "what org / plan am I on" | [references/authentication.md](./references/authentication.md) | +| "create a blocklist", "push IPs from my SIEM/SOAR", "expire IPs", "share a blocklist with another org", "subscribe my engine to a list" | [references/blocklists.md](./references/blocklists.md) | +| "cloud allowlist via API", "allow my office/CDN across the fleet" | [references/allowlists.md](./references/allowlists.md) | +| "connect Palo Alto / Fortinet / Cisco / F5 / Sophos / pfSense / OPNsense", "firewall integration", "pull IP list in vendor format", "paginate the feed" | [references/integrations.md](./references/integrations.md) | +| "remediation metrics", "how much did CrowdSec save / block", "ROI dashboard" | [references/metrics.md](./references/metrics.md) | +| "org-level decisions via API", "aggregated decisions" | [references/decisions.md](./references/decisions.md) | + +## Step — curl cheat sheet + +All assume `KEY` is set (see operating contract). `jq` optional for readability. + +| Purpose | Command | +|---|---| +| Who am I / validate key | `curl -s -H "x-api-key: $KEY" $B/info` | +| List blocklists | `curl -s -H "x-api-key: $KEY" "$B/blocklists"` | +| List allowlists | `curl -s -H "x-api-key: $KEY" "$B/allowlists"` | +| List integrations | `curl -s -H "x-api-key: $KEY" "$B/integrations"` | +| List / find decisions | `curl -s -H "x-api-key: $KEY" "$B/decisions"` · `…?ips=1.2.3.4` | +| Remediation metrics | `curl -s -H "x-api-key: $KEY" "$B/metrics/remediation?start_date=$FROM&end_date=$TO"` | +| Add IPs to a blocklist *(mutating — confirm)* | `curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/blocklists/$ID/ips" -d '{"ips":["1.2.3.4"]}'` | +| Create a decision *(mutating — confirm)* | `curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/decisions" -d '{"duration":"4h","origin":"cscli","scenario":"manual","scope":"Ip","type":"ban","value":"1.2.3.4","target":{"type":"org","value":""}}'` | + +where `B=https://admin.api.crowdsec.net/v1`. + +## Hard don'ts + +- Don't run any mutating call without showing the user the URL + body and getting + a yes (see operating contract §3–4). +- Don't use `…/ips/bulk_overwrite` when the user means "add a few IPs" — that's + `…/ips`. `bulk_overwrite` wipes the list first. +- Don't print, log, or commit the API key. Resolve it from the env var or + `~/.config/crowdsec/sapi_key` only. +- Don't assume a cloud allowlist/blocklist is enforced just because the API call + succeeded — the engine must be enrolled, subscribed, and have + `console_management` on (that's the `crowdsec` skill's job to verify). + +## Docs + +Canonical: . Each +`references/` file cites the specific upstream page and the live OpenAPI operation +it derives from. diff --git a/skills/crowdsec-service-api/references/allowlists.md b/skills/crowdsec-service-api/references/allowlists.md new file mode 100644 index 0000000..bd106cd --- /dev/null +++ b/skills/crowdsec-service-api/references/allowlists.md @@ -0,0 +1,84 @@ +--- +verified: + - date: 2026-07-29 + version: "1.70.52" + env: sapi + notes: "create, items create + list (scope/value fields); delete needs unsubscribe first (409 otherwise)" + - date: 2026-07-30 + version: "1.70.52" + env: sapi + notes: "closed loop: subscribe org -> engine on crowdsec-vm shows it 'Managed by Console: yes', cscli allowlists check confirms the item allowlisted (~40s sync); unsubscribe then delete 204" +--- + +# SAPI — Allowlists + +Canonical docs: +OpenAPI: the `/allowlists` group — + +A cloud **allowlist** is a named set of IPs/ranges (each with optional expiration and reason) +that subscribed engines treat as "never act on this". Same subscription model as +blocklists. + +> **Cloud vs local:** this is the *org-wide, Console-pushed* allowlist. A single +> engine's local allowlist/whitelist is different — see the `crowdsec` skill, +> `references/configure/allowlists.md`. A cloud allowlist only reaches an engine +> that is enrolled, **subscribed here**, and has `console_management` enabled +> locally (`crowdsec` skill → `references/install/console.md`). +> +> Mutating calls are marked ⚠. `B=https://admin.api.crowdsec.net/v1`, `KEY` set. + +## Lifecycle + +### Create ⚠ / list / delete ⚠ +`name` is required; `description` optional. +```bash +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/allowlists" -d '{ + "name": "corp-egress", "description": "office + CDN egress IPs" +}' +curl -s -H "x-api-key: $KEY" "$B/allowlists" # list (paginated) +curl -s -H "x-api-key: $KEY" -X DELETE "$B/allowlists/$ID" # removes list + subscriptions +``` +Delete returns **`409 Conflict`** while the list still has subscribers. Either +unsubscribe first (see Subscribers below), or **force it** — +`DELETE "$B/allowlists/$ID?force=true"`. On subscribed engines the allowlist shows +in `cscli allowlists list` as *Managed by Console: yes* and syncs within ~40s. + +## Items — the IPs/ranges + +`items` and `description` are required; `expiration` optional (RFC3339). +```bash +# add items ⚠ +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/allowlists/$ID/items" -d '{ + "items": ["203.0.113.10", "198.51.100.0/24"], + "description": "office ranges" +}' + +curl -s -H "x-api-key: $KEY" "$B/allowlists/$ID/items" # list items +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' \ + -X PATCH "$B/allowlists/$ID/items/" -d '{"expiration":"2026-12-31T00:00:00Z"}' # ⚠ update +curl -s -H "x-api-key: $KEY" -X DELETE "$B/allowlists/$ID/items/" # ⚠ delete item +``` +Items accept a single IP or a CIDR range. + +## Subscribers — who applies the allowlist ⚠ + +`entity_type` is required, one of: `org`, `tag`, `engine`, `firewall_integration`, +`remediation_component_integration`, `remediation_component`, `log_processor`. As +with blocklists, `org` takes **no `ids`** (whole org); the other types take `ids`. +```bash +# a specific tag +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/allowlists/$ID/subscribers" -d '{ + "entity_type": "tag", "ids": [""] +}' +# the entire org (no ids) +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/allowlists/$ID/subscribers" -d '{"entity_type":"org"}' +curl -s -H "x-api-key: $KEY" "$B/allowlists/$ID/subscribers" # list +curl -s -H "x-api-key: $KEY" -X DELETE "$B/allowlists/$ID/subscribers/" # ⚠ unsubscribe +``` + +## Use case — fleet-wide "don't block us" + +Put office/CDN/monitoring ranges in one allowlist, subscribe by `tag` (or `org` for +everything). New engines that inherit the tag pick it up automatically — no +per-engine config. Set `expiration` on temporary items (a vendor's scan window, +a short-lived NAT) so they clean themselves up. diff --git a/skills/crowdsec-service-api/references/authentication.md b/skills/crowdsec-service-api/references/authentication.md new file mode 100644 index 0000000..707b619 --- /dev/null +++ b/skills/crowdsec-service-api/references/authentication.md @@ -0,0 +1,73 @@ +--- +verified: + - date: 2026-07-29 + version: "1.70.52" + env: sapi + notes: "GET /info key validation against production SAPI (ENTERPRISE key)" +--- + +# SAPI — Authentication & key handling + +Canonical docs: · quickstart · Python SDK +OpenAPI: `GET /info` — + +SAPI is a **premium** feature. Every call authenticates with the `x-api-key` +header. The only exception is the integration *content* endpoint, which uses HTTP +Basic auth with credentials minted per integration — see +[integrations.md](./integrations.md). + +## Create an API key + +In the Console (): **Settings → Service API Keys → +Create API Key**. Name it, set permissions, create. **The key is shown once** — +copy it immediately. + +## Key resolution (how this skill reads it) + +Resolve from the environment variable first, then an optional local file. Never +echo, log, or commit the key. + +```bash +B=https://admin.api.crowdsec.net/v1 +KEY="${CROWDSEC_SAPI_KEY:-$(cat ~/.config/crowdsec/sapi_key 2>/dev/null)}" +``` + +Store it out of the repo, readable only by you: +```bash +install -m 0600 /dev/stdin ~/.config/crowdsec/sapi_key <<<'YOUR-KEY' +``` + +## Validate — always the first call + +```bash +curl -s -H "x-api-key: $KEY" "$B/info" +# → {"organization_id":"…","subscription_type":"ENTERPRISE","api_key_name":"…"} +``` + +`subscription_type` confirms the plan; `organization_id` confirms **which tenant** +subsequent mutations will change. A `401 {"message":"Unauthorized"}` means the key +is wrong, revoked, or the header name is misspelled (it is `x-api-key`). + +## Typed alternative — Python SDK + +For scripted/automated use there is an official SDK instead of raw curl: + +```bash +pip install crowdsec-service-api +``` +```python +from crowdsec_service_api import Info, Server, ApiKeyAuth +client = Info(base_url=Server.production_server.value, auth=ApiKeyAuth(api_key=KEY)) +print(client.get_me_info()) +``` + +SDK reference: . It +exposes `Blocklists`, `Allowlists`, `Integrations`, `Metrics`, `Info` and raises +`httpx.HTTPStatusError` on API errors. + +## Security notes + +- Treat the key like a password. If it lands in a shell history, a ticket, or a + chat, **rotate it** (Console → Settings → Service API Keys). +- One key = one organization's blast radius. Mutations affect every engine/bouncer + subscribed to the lists in that org. diff --git a/skills/crowdsec-service-api/references/blocklists.md b/skills/crowdsec-service-api/references/blocklists.md new file mode 100644 index 0000000..7fec9e4 --- /dev/null +++ b/skills/crowdsec-service-api/references/blocklists.md @@ -0,0 +1,142 @@ +--- +verified: + - date: 2026-07-29 + version: "1.70.52" + env: sapi + notes: "create/list/get, add IPs (201), ips/delete, download (204 eventual-consistency observed); delete needs unsubscribe first (409 otherwise); search not exercised" + - date: 2026-07-30 + version: "1.70.52" + env: sapi + notes: "closed loop: subscribe org (remediation required) -> PAPI force_pull -> engine active decision origin=lists on crowdsec-vm; unsubscribe then delete 204" +--- + +# SAPI — Blocklists + +Canonical docs: +OpenAPI: the `/blocklists` group — + +A **blocklist** is a named, cloud-hosted list of IPs (each with optional +expiration). You add IPs via API, then **subscribe** engines / bouncers / firewall +integrations / whole orgs to it — they pull and **enforce** it. You can also +**share** a blocklist read/write with another organization. + +> Mutating calls below are marked ⚠ — show the user the URL + body and get a yes +> before sending (see SKILL.md operating contract). All snippets assume +> `B=https://admin.api.crowdsec.net/v1` and `KEY` set. + +## Lifecycle + +### Create ⚠ +`name` and `description` are required. +```bash +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/blocklists" -d '{ + "name": "siem-high-confidence", + "description": "IPs pushed from our SIEM", + "label": "SIEM feed", + "tags": ["siem"] +}' +``` +The response `id` is what every other call uses (`ID=`). + +### List / get +```bash +curl -s -H "x-api-key: $KEY" "$B/blocklists?page=1&page_size=100" # add subscribed_only=true to see only what you consume +curl -s -H "x-api-key: $KEY" "$B/blocklists/$ID" +``` +Useful list filters: `subscribed_only=true`, `exclude_subscribed=true`, +`include_filter=private,shared`, `category=…`. + +### Update ⚠ / Delete ⚠ +```bash +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X PATCH "$B/blocklists/$ID" -d '{"description":"new desc"}' +curl -s -H "x-api-key: $KEY" -X DELETE "$B/blocklists/$ID" # removes the list AND every subscription/feed to it +``` +Delete returns **`409 Conflict`** while the list still has subscribers. Either +unsubscribe everyone first (see Subscribers below), or **force it** — +`DELETE "$B/blocklists/$ID?force=true"` drops the list and all its subscriptions. + +## Content — the IPs + +| Action | Call | Notes | +|---|---|---| +| **Add** IPs | `POST /blocklists/$ID/ips` | Additive. `{"ips":[…],"expiration":"…"}`; `expiration` optional (RFC3339). | +| **Remove** IPs ⚠ | `POST /blocklists/$ID/ips/delete` | `{"ips":[…]}` — un-blocks fleet-wide. | +| **Replace all** ⚠⚠ | `POST /blocklists/$ID/ips/bulk_overwrite` | Wipes the list, then sets exactly these IPs. Not for "add a few". | +| **Download** | `GET /blocklists/$ID/download` | Published content (raw, one IP/line). Safe. | + +A successful add returns `201`. Content is **eventually consistent**: right after +an add, `GET …/download` can still return `204 No Content` and `stats.count` `0` +for a short processing interval before the IPs are published. Don't treat an empty +download immediately after an add as a failure — re-check after a moment. + +Add with a 24h expiry: +```bash +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/blocklists/$ID/ips" -d '{ + "ips": ["1.2.3.4", "5.6.7.8"], + "expiration": "'"$(date -u -d tomorrow +%FT%TZ)"'" +}' +``` + +## Subscribers — who enforces the list ⚠ + +Subscribe entities so they pull this blocklist. Two fields are **required**: +`entity_type` (one of `engine`, `firewall_integration`, +`remediation_component_integration`, `tag`, `org`) and `remediation` (the action +the subscribers apply, e.g. `ban`). `ids` depends on the type: + +| `entity_type` | `ids` | +|---|---| +| `engine` / `firewall_integration` / `remediation_component_integration` / `tag` | required — the specific entity ids to subscribe | +| `org` | **must be omitted** — subscribes the whole org (sending `ids` errors with *"Organization type subscription can't have ids"*) | + +```bash +# subscribe specific engines +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/blocklists/$ID/subscribers" -d '{ + "entity_type": "engine", + "ids": ["", ""], + "remediation": "ban" +}' + +# subscribe the entire org (no ids) +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/blocklists/$ID/subscribers" -d '{ + "entity_type": "org", "remediation": "ban" +}' + +curl -s -H "x-api-key: $KEY" "$B/blocklists/$ID/subscribers" # list +curl -s -H "x-api-key: $KEY" -X DELETE "$B/blocklists/$ID/subscribers/" # ⚠ unsubscribe +``` + +An enrolled engine only enforces a subscribed blocklist once it has +`console_management` enabled locally (`crowdsec` skill → `references/install/console.md`); +it then pulls the decisions over PAPI within a poll cycle. + +To feed a **firewall appliance**, subscribe the *integration* (`entity_type: +firewall_integration`, `ids:[]`) — the device then pulls the +blocklist's IPs in its vendor format. See [integrations.md](./integrations.md). + +## Sharing across organizations ⚠ + +```bash +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/blocklists/$ID/shares" -d '{ + "organizations": [ {"organization_id": "", "permission": "read"} ] +}' +# permission: "read" (subscribe/view) or "write" (also add/remove IPs) +curl -s -H "x-api-key: $KEY" -X DELETE "$B/blocklists/$ID/shares/" # unshare +``` + +## Discover existing blocklists — search (read-only) + +`POST /blocklists/search` finds catalog blocklists (CrowdSec/third-party/custom) +you can subscribe to, filtered by pricing tier, country, classification, etc. +```bash +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/blocklists/search" -d '{ + "query": "ssh", "pricing_tiers": ["premium"], "min_ips": 100, "page": 1, "page_size": 20 +}' +``` + +## Use case — SIEM/SOAR feed + +1. Create one blocklist (once). 2. Subscribe your engines/integrations (once). +3. On each detection, `POST …/ips` with a short `expiration`. Expired IPs drop off +automatically — no cleanup job needed. Use `…/ips/delete` only to pull an IP early; +reserve `bulk_overwrite` for a full authoritative re-sync from the source of truth. diff --git a/skills/crowdsec-service-api/references/decisions.md b/skills/crowdsec-service-api/references/decisions.md new file mode 100644 index 0000000..b972442 --- /dev/null +++ b/skills/crowdsec-service-api/references/decisions.md @@ -0,0 +1,98 @@ +--- +verified: + - date: 2026-07-30 + version: "1.70.52" + env: sapi + notes: "list + aggregated reads; create org-targeted ban (POST -> 200 {uuid}) confirmed present in /decisions. DELETE of an org decision NOT confirmed: /decisions/{uuid} and /decisions/aggregated/{id} both return 204 but the decision persisted (it isn't in the aggregated set); relies on duration to expire" +--- + +# SAPI — Decisions (org-level) + +Canonical docs: +OpenAPI: the `/decisions` group — + +Org-scoped decisions in the cloud (distinct from a single engine's *local* +decisions, which live behind `cscli` in the `crowdsec` skill). A decision is one +remediation (`ban`, `captcha`…) on a `scope`/`value` (an IP, range, country, AS), +`target`ed at your `org`, a `tag`, or a single `entity`, for a `duration`. + +**Decisions vs blocklists** — both end up as enforced decisions on subscribed +engines; pick by shape of the task: + +| Use **decisions** for | Use **[blocklists](./blocklists.md)** for | +|---|---| +| A targeted / ad-hoc ban (one IP, right now) | A maintained IP feed (SIEM/SOAR, threat intel) | +| Non-IP scopes — range, country, AS | Bulk IP sets you add/expire/bulk-overwrite | +| Aiming at one `tag`/`entity` instead of a whole subscription | Sharing a curated list across orgs / vendor integrations | +| Org-wide decision **visibility** (list + aggregate) | — | + +> **Access:** the `/decisions` group needs a **decision-scoped key**. A key scoped +> only to blocklist/allowlist management gets `403 {"message":"Forbidden"}` on +> every call here — an entitlement gap, not a bad request. A blocklist-only key +> silently fails. +> +> Mutating calls marked ⚠. `B=https://admin.api.crowdsec.net/v1`, `KEY` set. + +## List (read-only) + +```bash +curl -s -H "x-api-key: $KEY" "$B/decisions?page=1&size=50" +curl -s -H "x-api-key: $KEY" "$B/decisions?ips=1.2.3.4" # find decisions for an IP +``` +Returns the paginated envelope `{items, total, page, size, links}`; each item is a +full decision (`uuid`, `origin`, `scenario`, `scope`, `type`, `value`, `duration`, +`target`, geo fields). Note `id` is `0` for org-targeted decisions — the +identifier that matters is `uuid` (see delete). Filters: `ips=`, `instance_ids=`, +`tag_ids=`, `remediation_types=`, `alert_ids=`, `decision_ids=`, `created_at_from=`, +`sort_by=` (default `created_at`), `sort_order=` (default `desc`) — repeatable +where plural. + +## Aggregated (read-only) + +```bash +curl -s -H "x-api-key: $KEY" "$B/decisions/aggregated" +``` +Collapses decisions by `scope`/`type`/`value`. Each item's `id` is a +**URL-encoded JSON composite key** — decoded, `{"organization_id":…,"scope":"ip","type":"ban","value":"1.2.3.4"}`. +In testing this view listed only CAPI/community-origin decisions, not a freshly +`POST`ed org decision — so don't rely on it to find something you just created. + +## Create ⚠ + +Required: `duration`, `origin`, `scenario`, `scope`, `type`, `value`, and `target` +(`type` ∈ `org`/`tag`/`entity`, `value` = the org id / tag id / entity id). `scope` +uses the usual CrowdSec values (`Ip`, `Range`, `Country`, `AS`); `type` is `ban`, +`captcha`, etc. Get your org id from [`/info`](./authentication.md). + +```bash +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/decisions" -d '{ + "duration": "4h", + "origin": "cscli", + "scenario": "manual", + "scope": "Ip", + "type": "ban", + "value": "1.2.3.4", + "target": { "type": "org", "value": "" } +}' +# → {"uuid":"…"} +``` +An `org` target pushes to **every** enrolled engine in the org (each enforces it +within a poll cycle) — treat it like a blocklist mutation and confirm first. + +## Delete ⚠ — and its caveat + +The API exposes two delete paths: + +```bash +curl -s -H "x-api-key: $KEY" -X DELETE "$B/decisions/" # per decision +curl -s -H "x-api-key: $KEY" -X DELETE "$B/decisions/aggregated/" # per aggregated key +``` + +**Caveat (observed, v1.70.52):** for an **org-targeted** decision, *neither* +reliably removed it — both returned `204`, but the decision stayed in +`GET /decisions` (its list `id` is `0`, and it never appeared under +`/decisions/aggregated`, so the aggregated id can't be built). Treat org-decision +deletion as **unconfirmed**: prefer a bounded `duration` and let it expire, rather +than assuming the delete took. Per-`entity`/`tag` decision deletion by `uuid` is +plausible but was **not** verified here — check the response *and* re-read +`GET /decisions` before telling the user it's gone. diff --git a/skills/crowdsec-service-api/references/integrations.md b/skills/crowdsec-service-api/references/integrations.md new file mode 100644 index 0000000..20adad9 --- /dev/null +++ b/skills/crowdsec-service-api/references/integrations.md @@ -0,0 +1,98 @@ +--- +verified: + - date: 2026-07-29 + version: "1.70.52" + env: sapi + notes: "create (plain_text) returns endpoint + basic-auth credentials{username,password}, delete 204; content pull / update / stream not exercised (no subscribed content)" +--- + +# SAPI — Integrations (firewall / appliance feeds) + +Canonical docs: +OpenAPI: the `/integrations` group — + +An **integration** is a pull endpoint that exposes the IPs of the blocklists +subscribed to it, **rendered in a firewall vendor's format**. A device (Palo Alto +EDL, Fortinet threat feed, etc.) polls it on a schedule. The content endpoint uses +**HTTP Basic auth** with credentials minted at creation — the only non-`x-api-key` +call in SAPI. + +> Mutating calls marked ⚠. `B=https://admin.api.crowdsec.net/v1`, `KEY` set. + +## Create ⚠ + +Required: `name`, `entity_type`, `output_format`. + +- `entity_type` ∈ `firewall_integration`, `remediation_component_integration`. +- `output_format` ∈ `plain_text`, `paloalto`, `fortigate`, `checkpoint`, `cisco`, + `f5`, `juniper`, `mikrotik`, `pfsense`, `opnsense`, `sophos`, `remediation_component`. + +```bash +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/integrations" -d '{ + "name": "edge-paloalto", + "description": "Palo Alto external dynamic list", + "entity_type": "firewall_integration", + "output_format": "paloalto" +}' +``` +The response contains `endpoint` (the content URL) and `credentials` +(`username`/`password` for Basic auth) — **shown at creation**; regenerate later if +lost (see Update). + +## Wire a blocklist to the integration ⚠ + +The integration is empty until you subscribe lists to it. From +[blocklists.md](./blocklists.md), subscribe the integration to a blocklist: +```bash +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X POST "$B/blocklists/$BLID/subscribers" -d '{ + "entity_type": "firewall_integration", "ids": [""] +}' +``` + +## Pull the content (Basic auth) — what the appliance does + +```bash +curl -s -u ":" "$B/integrations//content?page=1&page_size=1500" +``` +Query params: `page` (default 1), `page_size`, `pull_limit`, `enable_ip_aggregation`. +**Paginate** for appliances with a max-entries cap: fetch `page=1,2,…` until a +short/empty page. Point the device's feed/EDL URL at this endpoint with the Basic +credentials. + +There is also a decisions **stream** (deltas rather than the full list), for +remediation components that maintain state: +```bash +curl -s -u ":" "$B/integrations//v1/decisions/stream" +``` + +## Update ⚠ / Delete ⚠ + +```bash +# rotate the Basic-auth credentials +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X PATCH "$B/integrations/" -d '{"regenerate_credentials": true}' +# change format or add an entry cap +curl -s -H "x-api-key: $KEY" -H 'Content-Type: application/json' -X PATCH "$B/integrations/" -d '{"output_format":"fortigate","pull_limit":50000}' +curl -s -H "x-api-key: $KEY" -X DELETE "$B/integrations/" # device feed goes empty/401 +``` + +## Vendor format cheat sheet + +| Appliance | `output_format` | Consumes via | +|---|---|---| +| Palo Alto | `paloalto` | External Dynamic List (EDL) | +| Fortinet FortiGate | `fortigate` | External/Threat Feed connector | +| Check Point | `checkpoint` | Custom threat feed | +| Cisco | `cisco` | — | +| F5 | `f5` | — | +| Juniper | `juniper` | — | +| MikroTik | `mikrotik` | address-list | +| pfSense / OPNsense | `pfsense` / `opnsense` | firewall alias / URL table | +| Sophos | `sophos` | — | +| Anything / scripts | `plain_text` | one IP per line | + +## Use case — appliance without a native CrowdSec bouncer + +For a firewall that can't run a bouncer, an integration turns any subscribed +blocklist into a vendor-native feed the device already knows how to poll — no agent +on the box. Curate the IPs once (blocklist), expose them many ways (one integration +per appliance). diff --git a/skills/crowdsec-service-api/references/metrics.md b/skills/crowdsec-service-api/references/metrics.md new file mode 100644 index 0000000..7354af5 --- /dev/null +++ b/skills/crowdsec-service-api/references/metrics.md @@ -0,0 +1,41 @@ +--- +verified: + - date: 2026-07-29 + version: "1.70.52" + env: sapi + notes: "GET /metrics/remediation returns raw/computed/stats; units request/byte/packet; computed.saved keys log_lines/storage/egress_traffic" +--- + +# SAPI — Remediation metrics (ROI) + +Canonical docs: +OpenAPI: `GET /metrics/remediation` — + +Read-only. Returns what your remediation actually did over a window: traffic +dropped vs processed, plus computed savings — the numbers for an ROI dashboard or +report. `B=https://admin.api.crowdsec.net/v1`, `KEY` set. + +## Call + +```bash +FROM=2026-07-01T00:00:00Z ; TO=2026-07-29T00:00:00Z +curl -s -H "x-api-key: $KEY" "$B/metrics/remediation?start_date=$FROM&end_date=$TO" +``` +Dates are ISO 8601 / RFC3339. Optional filters narrow the scope: +`engine_ids=`, `integration_ids=`, `tags=` (repeatable). + +## What you get + +| Group | Fields | Meaning | +|---|---|---| +| **raw.dropped** | requests, bytes, packets | Blocked at the remediation layer. | +| **raw.processed** | requests, bytes, packets | Total seen (dropped + allowed). | +| **computed.saved** | log_lines, storage, egress_traffic | Estimated resources not spent because traffic was dropped. | +| **computed** | dropped, prevented | Aggregate blocked / attacks prevented. | + +## Use case — monthly ROI report + +Query month-over-month, chart `computed.saved.egress_traffic` and +`raw.dropped.requests`, and attribute per team with `tags=`. Turn "we blocked N +requests" into a concrete savings figure (log lines, storage, egress) for the +report. diff --git a/skills/crowdsec/SKILL.md b/skills/crowdsec/SKILL.md index f5f75e8..8d38070 100644 --- a/skills/crowdsec/SKILL.md +++ b/skills/crowdsec/SKILL.md @@ -19,7 +19,8 @@ thing. This skill uses both interchangeably. | Deploy the WAF (AppSec component) | this skill | | Debug "logs not parsing" / "no alerts" / "bouncer not blocking" | this skill | | Migrate from fail2ban | this skill | -| **Write** a parser, scenario, or WAF (AppSec) rule | out of scope — this skill is operational only | +| **Write** a parser, scenario, or WAF (AppSec) rule | the `crowdsec-local-mcp` mcp | +| Drive the **cloud Service API** (manage blocklists / allowlists / firewall integrations / metrics / decisions programmatically) | the `crowdsec-service-api` skill | ## Step 1 — Detect the environment diff --git a/skills/crowdsec/references/install/console.md b/skills/crowdsec/references/install/console.md index fedc37c..b7bf942 100644 --- a/skills/crowdsec/references/install/console.md +++ b/skills/crowdsec/references/install/console.md @@ -111,6 +111,15 @@ local-vs-Console allowlist distinction. register bouncers normally with `cscli bouncers add` and they surface in the Console via the enrolled LAPI. +## Managing the Console programmatically + +This page covers **enrolling** an engine. To manage Console objects by API — +create blocklists/allowlists, push IPs, wire firewall integrations, pull +remediation metrics — use the **`crowdsec-service-api`** skill (the premium cloud +Service API at `admin.api.crowdsec.net`). Enroll here first; a cloud list only +reaches this engine once it's enrolled, subscribed there, and has +`console_management` enabled (§2). + ## Next step Confirm detection + sharing end-to-end with diff --git a/skills/crowdsec/scripts/check-verification.py b/skills/crowdsec/scripts/check-verification.py index 5703d81..4190788 100644 --- a/skills/crowdsec/scripts/check-verification.py +++ b/skills/crowdsec/scripts/check-verification.py @@ -21,12 +21,10 @@ import sys from pathlib import Path -# skills/crowdsec/scripts/check-verification.py -> skills/crowdsec +# skills/crowdsec/scripts/check-verification.py -> skills/crowdsec (the default --root) SKILL_ROOT = Path(__file__).resolve().parent.parent -REFERENCES = SKILL_ROOT / "references" -SKILL_MD = SKILL_ROOT / "SKILL.md" -CANONICAL_ENVS = {"systemd", "docker", "k8s"} +CANONICAL_ENVS = {"systemd", "docker", "k8s", "sapi"} REQUIRED_KEYS = ("date", "version", "env") OPTIONAL_KEYS = ("notes",) ALLOWED_KEYS = set(REQUIRED_KEYS) | set(OPTIONAL_KEYS) @@ -114,14 +112,25 @@ def main() -> int: default=180, help="flag verified entries older than this many days (default: 180)", ) + parser.add_argument( + "--root", + type=Path, + default=SKILL_ROOT, + help="skill directory to check (default: this script's own skill). " + "Point it at a sibling skill, e.g. --root skills/crowdsec-service-api", + ) args = parser.parse_args() + skill_root = args.root.resolve() + references = skill_root / "references" + skill_md = skill_root / "SKILL.md" + today = dt.date.today() - docs = sorted(REFERENCES.rglob("*.md")) - if SKILL_MD.exists(): - docs.append(SKILL_MD) + docs = sorted(references.rglob("*.md")) + if skill_md.exists(): + docs.append(skill_md) if not docs: - print(f"No reference docs found under {REFERENCES}", file=sys.stderr) + print(f"No reference docs found under {references}", file=sys.stderr) return 1 rows: list[tuple[str, str, str, str, str]] = [] @@ -130,7 +139,7 @@ def main() -> int: errors: list[str] = [] for doc in docs: - rel = doc.relative_to(SKILL_ROOT).as_posix() + rel = doc.relative_to(skill_root).as_posix() try: frontmatter = split_frontmatter(doc.read_text(encoding="utf-8")) entries = parse_verified(frontmatter) if frontmatter is not None else None