From f84bf82f6cebf66422e72b85ddb5f498c9a18e09 Mon Sep 17 00:00:00 2001 From: anti Date: Fri, 24 Apr 2026 15:48:14 -0400 Subject: [PATCH] docs(webhook): roadmap tick + threat-model component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DEVELOPMENT.md: tick the "Real-time alerting" roadmap item with a note that Slack/Telegram-specific senders remain per-destination follow-ups (they accept generic webhook payloads already). - THREAT_MODEL.md: new Component 2 — DECNET↔External webhook destination. DFD, full STRIDE table, WH-01 (secret at rest) and WH-02 (half-dead-receiver retry waste) registered as accepted risks pointing at DEBT-037 for post-MVP hardening. Checklist lists two open items: OpenAPI schema omits `secret`, and http:// URL rejection at admin time. --- development/DEVELOPMENT.md | 2 +- development/THREAT_MODEL.md | 80 +++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/development/DEVELOPMENT.md b/development/DEVELOPMENT.md index c38cd3f1..0392bbd4 100644 --- a/development/DEVELOPMENT.md +++ b/development/DEVELOPMENT.md @@ -55,7 +55,7 @@ ## Detection & Intelligence -- [ ] **Real-time alerting** — Webhook/Slack/Telegram notifications for first-hits. +- [x] **Real-time alerting via webhooks** — Admin-configurable outbound webhooks (SIEM/SOAR integration: Wazuh/Shuffle/TheHive/n8n) with HMAC-SHA256 signing, topic-pattern filtering, and bounded retry. Slack/Telegram-specific senders remain as per-destination work (they accept generic webhook payloads already). - [ ] **Threat intel enrichment** — Auto-lookup IPs against AbuseIPDB, Shodan, and GreyNoise. - [ ] **Attack campaign clustering** — Group sessions by signatures and timing patterns. - [ ] **GeoIP mapping** — Visualize attacker origin and ASN data on a map. diff --git a/development/THREAT_MODEL.md b/development/THREAT_MODEL.md index a51dfbef..9e55cd01 100644 --- a/development/THREAT_MODEL.md +++ b/development/THREAT_MODEL.md @@ -339,6 +339,85 @@ regardless of component: | Component | ID | Summary | |-----------|----|---------| | Dashboard↔API | DA-01..DA-09 | See component section. | +| DECNET↔Webhook destination | WH-01..WH-02 | See component section. | + +--- + +## Component 2 — DECNET ↔ External webhook destination + +**Status:** modeled alongside the webhook MVP (2026-04-24). +**Scope:** outbound HTTP POSTs from the `decnet webhook` worker to an +operator-configured URL (typically Shuffle / TheHive / Wazuh / n8n). +In scope: the data crossing the master→receiver boundary, the signing +& secret storage, and the failure behavior of the egress path. Out of +scope: the receiver's own security posture, anything downstream of +the receiver (Shuffle→Slack, TheHive→Cortex, …). + +### DFD + +``` + Master host + ┌─────────────────────────────────────┐ + │ ┌────────────────────┐ │ + │ │ WebhookSubscription│ (DB row) │ + │ │ url, secret, │ │ + │ │ topic_patterns │ │ + │ └─────────┬──────────┘ │ + │ │ read │ + │ ┌─────────▼────────┐ bus events │ + │ │ decnet webhook │◄──── bus ◄─── other workers (attacker.*, decky.*, system.*) + │ │ worker │ │ + │ └─────────┬────────┘ │ + │ │ HMAC-signed POST │ + └───────────│─────────────────────────┘ + │ + ══ TRUST BOUNDARY ═══════════════════════════════════ + │ + ▼ + External receiver (Shuffle / TheHive / Wazuh / n8n / ...) +``` + +### STRIDE enumeration + +| Cat | Threat | Status | Notes | +|-----|--------|--------|-------| +| S | Receiver accepts a forged event impersonating DECNET | M | Every POST carries `X-DECNET-Signature: sha256=` computed with HMAC-SHA256 over the canonical body (orjson with sorted keys); per-subscription secret. Receiver recomputes + compares. See `decnet/webhook/client.py::sign`. | +| S | Attacker controls a webhook URL the admin added and forges callbacks back to DECNET | X | DECNET does not accept inbound webhook POSTs; only egress. Receiver→DECNET is not a surface in this component. | +| T | Payload tampering in transit | T / M | TLS termination is the operator's responsibility (same stance as Dashboard↔API). If the URL is `http://` on a hostile network, the HMAC still detects tampering — a recomputed signature would fail on any altered byte. Operators MUST use `https://`; the router does not enforce this pre-v1 (see WH-01). | +| T | Secret leak lets attackers forge events in-band | A | Secret rotation is a manual PATCH. In-flight window where a rotated secret is observed by both old + new verifiers is the operator's coordination problem. Encrypt-at-rest on the DB column is deferred — see DEBT-037 §7. | +| R | DECNET denies having sent an event | M | `last_success_at` + `last_failure_at` stamps on the row; structured log per delivery with `event_id`. No persisted per-event audit log pre-v1 — see DEBT-037 §3. | +| I | Secret leaks via API GET/LIST response | M | `WebhookResponse` deliberately omits the `secret` field. `WebhookCreateResponse` carries the secret exactly once on create for copy-out. PATCH-to-rotate, no read-back. | +| I | Webhook URL + secret leak via DB dump | A | Plaintext at-rest on SQLite/MySQL. Same trust assumption as the JWT secret (which is env-sourced, not DB-stored). See WH-01 and DEBT-037 §7. | +| I | Attacker-controlled event content reaches receiver | T | Event payloads pass through DECNET untransformed — the receiver must sanitize before rendering (e.g. XSS if Shuffle pipes to a browser-facing Slack block without escaping). Out of scope for the DECNET side. Document in operator docs. | +| D | Slow / unreachable receiver ties up egress | M / A | Bounded concurrency (`Semaphore(10)`), per-delivery timeout (10s), and bounded retry (3 attempts, `[1,2,4]` × jitter) keep one slow destination from starving others. Half-dead receivers still waste retry budget — see WH-02. Circuit breaker deferred to DEBT-037 §1. | +| D | Huge payload floods receiver | A | Payload shape is whatever the bus event carries; no per-destination batching / coalescing. On high-volume topics this is a known concern — see DEBT-037 §4 for post-MVP batch delivery. | +| E | Viewer role manipulates webhook config | M | All CRUD routes under `/api/v1/webhooks` are `Depends(require_admin)`. Verified by `tests/api/test_rbac_contract.py` (every admin-classified route asserts viewer → 403). | +| E | Admin adds a URL pointing at an internal-only DECNET service (SSRF-style) | A | Admin role is trusted; protecting admin from self-inflicted SSRF is out of scope under the current trust model. Revisit if we ever delegate subscription CRUD to a less-trusted role. | + +### Accepted risks (DECNET↔Webhook) + +| ID | Threat | Why accepted | Revisit when | +|----|--------|--------------|--------------| +| WH-01 | Webhook secret + URL stored plaintext in the DB | Matches the existing pre-v1 posture (JWT secret is env-sourced; there's no operator expectation that DB-at-rest is encrypted). Encrypting one column in isolation invents a KEK lifecycle we don't have. | Comprehensive DB-at-rest encryption lands, OR regulated-industry customer engagement. Tracked in DEBT-037 §7. | +| WH-02 | Half-dead receiver wastes the full retry budget (1+2+4 ≈ 7s with jitter) per delivery before the worker gives up | Admin role is trusted; this is operator-observable via `consecutive_failures` on the subscription row. A sticky-failure receiver disabled itself via operator action is fine pre-v1. | Circuit breaker lands (DEBT-037 §1) — auto-disable after N consecutive failures, require admin re-enable. | + +### Needs-verification checklist (DECNET↔Webhook) + +- [x] HMAC-SHA256 signing over canonical (orjson sorted-keys) body — verified by `tests/webhook/test_client.py::test_deliver_receiver_can_verify_signature`. +- [x] Secret never leaks via GET/LIST response — `tests/api/webhooks/test_crud.py::test_list_strips_secret` + `::test_get_single_strips_secret`. +- [x] Admin-only CRUD — inherited invariant from `test_rbac_contract.py`; new webhook routes auto-classified as admin. +- [x] 4xx no-retry, 5xx/429/network retry — `tests/webhook/test_client.py::test_deliver_no_retry_on_4xx` + retry tests. +- [x] Bounded concurrency + timeout per delivery — `Semaphore(10)` + 10s httpx timeout in `worker.py`. +- [ ] Secret-field omission on the OpenAPI schema (not just the response body). Verify that `/openapi.json` shows `WebhookResponse` without `secret` so SDK consumers don't accidentally deserialize into a shape that expects it. +- [ ] Reject `http://` URLs at admin time (WH-01 adjunct). A viewer can't create subscriptions, but even an admin typo into a plaintext URL bypasses the HMAC-alone-detects-tampering assumption. Consider a router-level check warning on non-`https` + a `DECNET_WEBHOOK_ALLOW_INSECURE` opt-out for dev boxes. + +### Out of scope (this component) + +- The receiver's auth, storage, or downstream routing. +- Post-MVP hardening (circuit breaker, dead-letter, batch, templates, at-rest encryption) — all tracked in DEBT-037. +- Frontend UI for subscription CRUD — a separate commit series. + +--- ## Components not yet modeled @@ -365,3 +444,4 @@ In priority order: | 2026-04-24 | F4/T (ORM sort injection), F4/D (unbounded `limit`), F4/D (deep `offset`) all moved from **?** to **M**. Limit caps were already universal; sort is pattern-validated on the only surface that exposes it; added `le=2147483647` to the two offset params that were unbounded (`api_list_topologies.py`, `api_get_transcript.py`). | ANTI | | 2026-04-24 | F5/I moved from **?** to **M** via `response_model=...` on every dict-returning mutation (`MessageResponse` + purpose-built models). F4/D "expensive `LIKE`" moved from **?** to **A** under new accepted risk DA-09 — admin-only surface, operator-scope rate limiting, `limit` cap. FTS5 kept as a performance TODO, not a security blocker. | ANTI | | 2026-04-24 | F6/I and F6/D both moved from **?** to **M**. F6/I: documented the viewer-safe-by-construction invariant for both SSE streams (every emitted event type wraps data already viewer-readable via REST). F6/D: added `decnet/web/sse_limits.py::sse_connection_slot` — per-user counter + async lock + 429 on overflow, wired into both SSE generators. `DECNET_SSE_MAX_PER_USER` env knob, default 5. | ANTI | +| 2026-04-24 | Component 2 added — DECNET↔External webhook destination. Covers the new `decnet webhook` worker + `/api/v1/webhooks` admin CRUD. HMAC-SHA256 signing, 4xx no-retry + 5xx/429 retry with jittered backoff, admin-only CRUD, secret never leaks post-create. Two accepted risks registered (WH-01 secret at rest, WH-02 half-dead-receiver retry waste) paired with DEBT-037 pointers. | ANTI |