Skip to content

Releases: lance0/prefixd

v0.18.1

Choose a tag to compare

@lance0 lance0 released this 12 May 14:48

What's New

Generic webhook adapter transforms

Per-field transforms applied after JSONPath extraction in the generic webhook adapter (ADR 020). Operators can now reshape detector payloads without writing a shim or pre-processing pipeline.

Three transform variants:

  • unit_conversion — multiply an extracted numeric value (bps, pps, confidence) by a constant. Useful for Mbps→bps, kpps→pps, %→ratio.
  • regex_extract — pull a capture group out of an extracted vector string. Maps free-form alert descriptions ("Detected: udp_flood at 250Gbps") onto prefixd vector names.
  • computed — replace a numeric field's value with scale * Π(extract(path_i)). Lets operators derive fields not directly present in the payload (e.g. bps = packets × avg_size × 8).

Example:

transforms:
  bps:
    type: unit_conversion
    multiplier: 1000000        # detector reports Mbps
  vector:
    type: regex_extract
    pattern: "(\\w+)_flood"
    group: 0
  pps:
    type: computed
    paths: ["$.metrics.packets", "$.metrics.duration_inv"]
    scale: 1.0

All transforms are validated at config load (POST /v1/config/reload) — invalid regex, NaN multipliers, type mismatches, and unsupported field names are rejected immediately. Misconfigurations never silently produce zero events at request time.

Tests

17 new tests (14 unit + 3 end-to-end integration). Full suite: 250 unit + 133 integration + 16 postgres + 87 frontend all pass.

Migration

None required. transforms defaults to an empty map; webhook adapters without it behave exactly as before.

Dependencies

  • New: regex = "1"

Full Changelog

v0.18.0...v0.18.1

v0.18.0

Choose a tag to compare

@lance0 lance0 released this 11 May 21:53

What's New

Confidence decay for signal groups (ADR 022)

Exponential time decay on per-event confidence contributions when computing derived_confidence. Each event's effective weight is multiplied by 0.5 ^ (age_seconds / half_life_seconds), so older corroborating evidence smoothly loses influence without ever being discarded.

  • Global config: correlation.confidence_decay_half_life_seconds: u32 (default 0, disabled). Validated 0 ≤ H ≤ 10 × window_seconds.
  • Per-playbook override: correlation_override.confidence_decay_half_life_seconds: Option<u32>. Some(0) explicitly disables decay for the playbook even when the global is non-zero; None falls through.
  • Reconcile loop step. New refresh_decayed_confidence iterates every open signal group each tick (default 30 s) and recomputes derived_confidence from current events. No-op when decay is disabled.
  • One-shot corroboration_met. Once a group authorizes mitigation, decay never revokes that authorization — the flag is sticky.
  • Metric: prefixd_signal_group_decay_refreshes_total counter. Alert on "decay loop not running" by watching for the counter going flat.
  • UI: Group detail page surfaces "decayed, half-life Ns" next to derived_confidence when decay is active for the group's effective playbook.

Default is disabled — zero behavior change for existing deployments. Operators opt in by setting a non-zero value in correlation.yaml.

See ADR 022 for full rationale, alternatives considered, and consequences.

Tests

17 new tests (7 engine unit + 7 config unit + 3 integration) covering decay math, override resolution, validation, disabled paths, and sticky corroboration. Full suite: 236 unit + 130 integration + 16 postgres backend tests + 87 frontend tests all pass.

Migration

None required. Default confidence_decay_half_life_seconds: 0 preserves v0.17.x behavior bit-for-bit.

Full Changelog

v0.17.1...v0.18.0

v0.17.1

Choose a tag to compare

@lance0 lance0 released this 11 May 16:46

Patch release: batched Rust dependency updates. No runtime behavior change.

Changed

  • hmac 0.12 → 0.13 and sha2 0.10 → 0.11, shipped together because hmac 0.13 pulls in digest 0.11 which locks the version with sha2. Required adding use hmac::KeyInit; in src/correlation/webhook.rs and src/alerting/generic.rsnew_from_slice moved off the Mac trait.
  • clap 4.6.0 → 4.6.1, rand 0.10.0 → 0.10.1 — patch bumps.

Closes #106, #118, #119, #120.

Deferred

  • password-hash 0.5 → 0.6 (#116) not bumped. argon2 0.6 is still RC (latest stable: 0.6.0-rc.8); argon2 0.5 still depends on password-hash 0.5, so the bump would create a dual-version transitive with no security gain. Will revisit when argon2 0.6 stabilizes.

Verification

  • 222 unit + 127 integration + 16 postgres tests green
  • cargo fmt --check + cargo clippy --all-targets -- -D warnings clean
  • HMAC and SHA-256 callers exercised by the webhook signature + generic-alerting test suites

Full Changelog: v0.17.0...v0.17.1

v0.17.0

Choose a tag to compare

@lance0 lance0 released this 29 Apr 19:08

Corroborating signals v2 (PR B) — follow-up to v0.16.0 / ADR 021. Ships the four review-deferred items as a coordinated set, plus the doc/security cleanup needed to merge them.

What's New

  • Playbook-aware late finalization. Migration 012_signal_groups_playbook.sql adds nullable signal_groups.playbook_name, populated by the daemon on the next primary event for each group via COALESCE. The corroborator-side aggregate recompute now re-resolves the playbook by name from live state and is allowed to flip corroboration_met=true using the override min_sources/threshold. Conservative fallback preserved: a NULL or stale playbook_name keeps the v0.16.0 no-flip behavior so the next primary event picks up the flag.
  • New gauge prefixd_corroborator_cache_size{source} updated by the reconcile loop after each sweep, with stale labels explicitly zeroed when a source's cache drains between ticks. Operators can alert on caches growing without bound.
  • Cached-corroborators admin endpoint and dashboard panel. GET /v1/signals/corroborator/cache (admin-only) returns { now, total, by_source[], signals[] } filtered to unattached + unexpired rows, with optional ?source= and ?limit= (clamped to 1..1000). New "Cache" tab on the Correlation page renders per-source counts plus a dense table of cached signals with relative ingested/expires timestamps.

Breaking Changes

  • prefixd_corroborator_expired_total regains its {source} label set. PromQL queries written against the v0.16.0 unlabelled counter must add sum() to recover the previous shape. Attribution is now collected in the same DELETE … RETURNING source GROUP BY source query that performs the delete (no read-then-delete race).
  • CorroboratorResponse.cached field removed. Always-true; status ∈ {attached, cached} is the canonical discriminator. Branch on status === "cached" instead.

Security

  • RUSTSEC-2026-0104 (rustls-webpki reachable panic in CRL parsing): bumped to 0.103.13.
  • RUSTSEC-2026-0066 / 0112 / 0113 (astral-tokio-tar PAX header desync, validation gaps, unpack_in symlink chmod): bumped testcontainers 0.26 → 0.27 + testcontainers-modules 0.14 → 0.15 (dev-deps only).
  • Lock-only patches for axum 0.8.9, tokio 1.52.1, uuid 1.23.1, axum-macros 0.5.1.

Verification

  • 222 unit + 127 integration + 16 postgres + 87 frontend tests green
  • cargo fmt --check + cargo clippy --all-targets -- -D warnings clean
  • Docker compose smoke test: full stack up, migrations 1–12 applied, playbook_name column + index present, end-to-end corroborator cache → primary event → drain → cache_size gauge zeroing all working

Full Changelog: v0.16.0...v0.17.0

v0.16.0

Choose a tag to compare

@lance0 lance0 released this 19 Apr 22:01

What's New

Corroborating signals (ADR 021)

A new class of correlation signals that strengthen open signal groups without ever triggering mitigations on their own. Targets coarse telemetry (router CPU, interface utilization, per-customer NetFlow, PoP-level metrics) that shouldn't name a victim IP but is valuable alongside a real detector.

  • Configuration. New mode: corroborating + match_dimensions: [pop, customer_id, service_id, interface] on any entry in correlation.yaml's sources map. Declared dimensions are authoritative — a source declared for [pop] can never attach via an undeclared customer_id/service_id/interface even if those happen to match. Validator rejects misconfiguration on both PUT /v1/config/correlation and daemon boot.
  • Ingest endpoint. POST /v1/signals/corroborator accepts dimension-tagged signals (no victim_ip). Matches open signal groups via OR-semantics across declared dimensions only, with optional vector narrower. Unmatched signals cache for up to window_seconds and drain when a matching primary event arrives.
  • Engine invariant. A signal group composed entirely of corroborators never reaches corroboration_met=true; at least one primary event is required. Enforced in check_corroboration_with_primary and in the corroborator-side aggregate recompute.
  • Dashboard. Per-source mode + dimension picker on the Correlation Config tab; corroborating badge on signal group detail; Signal Sources tab now merges activity from corroborator traffic so mode: corroborating sources no longer render as "never seen".
  • CLI. New prefixdctl send-corroborator --source router-cpu --pop iad1 ....
  • Activity endpoint. GET /v1/signals/corroborator/activity?minutes=N returns per-source (last_seen, count) aggregated across live cache and attached rows.
  • Metrics. prefixd_corroborator_ingested_total{source}, _attached_total{source}, _expired_total (unlabelled; counts only unattached cache misses, not attached-then-GC'd rows).
  • Interface dimension. New optional interface field on inventory Asset entries feeds into IpContext.interface and primary_dimensions, so interface-only corroborators (a common gNMI / SNMP shape) have a real matchable dimension.

Database migrations

  • 009primary_dimensions JSONB on signal_groups, corroborator denormalization on signal_group_events, new corroborating_signals cache table.
  • 010corroborator_ingested_at for accurate per-row timestamps on corroborator attachments.
  • 011 — Backfills primary_dimensions for pre-upgrade open signal groups from their associated mitigations so corroborators can attach to in-flight incidents immediately after upgrade.

Bug fixes and hardening

  • POST /v1/events now rejects corroborating-only sources at handler entry (before any DB writes).
  • Corroborator matching now strictly filters against declared match_dimensions (not just a presence check) on both the ingest and cache-drain paths.
  • CorrelationConfig::load + Settings::load run validate() on YAML parsing — daemon refuses to boot with invalid correlation config.
  • CORROBORATOR_EXPIRED_TOTAL narrowed to true cache misses; attached rows are still GC'd but do not inflate the counter.
  • Mock repository behavior tightened (full-struct update_signal_group, real dimension filter in find_open_groups_by_dimensions) so tests reflect production behavior.
  • Frontend null-safe ingested_at rendering; SourceDialog auto-clears match_dimensions when switching mode back to primary.

Known limits (deferred to PR B)

  • Late corroborator finalization (needs playbook-override-aware recompute on the corroborator path).
  • Per-source label on prefixd_corroborator_expired_total.
  • CorroboratorResponse.cached field cleanup.
  • Dashboard "cached corroborators" panel + /v1/signals/corroborator/cache listing endpoint.
  • prefixd_corroborator_cache_size{source} gauge metric.

See ROADMAP → Correlation Engine → "Corroborating signals v2 (PR B)".

References

Full Changelog: v0.15.0...v0.16.0

v0.15.0

Choose a tag to compare

@lance0 lance0 released this 19 Apr 01:59

What's New

Generic webhook adapter for arbitrary detectors

A new endpoint — POST /v1/signals/webhook/{name} — lets you integrate any detector, telemetry source, or commercial DDoS appliance that can POST JSON, without writing Rust. Declare adapters in `correlation.yaml` with JSONPath field mappings, and prefixd maps the payload into the standard event pipeline (correlation, guardrails, playbooks, FlowSpec).

Highlights:

  • JSONPath field mapping (RFC 9535 via `serde_json_path`) — extract `victim_ip`, `vector`, `bps`, `pps`, `confidence`, `source_id`, `top_dst_ports`, `action`, and `timestamp` from any JSON shape
  • Three auth modes — HMAC-SHA256 (constant-time compare via `subtle`, secret loaded from env var, never in YAML), bearer (reuses global session auth), or none (lab use only)
  • Array batching — `root_path: "$.alerts[*]"` iterates a JSON array, producing one event per match; partial failures are per-event, overall status stays 200
  • Vector normalization — `vector_map` translates detector-specific strings (`UDP_FLOOD` → `udp_flood`); `default_vector` handles unknowns
  • Confidence scaling — `confidence_scale: 100` accepts 0-100 detector scales and normalizes to 0.0-1.0
  • Full CRUD UI — manage adapters from the Correlation → Config tab
  • Hot-reload — adapter changes apply via `POST /v1/config/reload`, no restart

Docs:

Webhook adapter config validation

`CorrelationConfig::validate()` now rejects `confidence_scale <= 0` or non-finite, empty `auth.secret_env` / `auth.header`, and non-`sha256` HMAC algorithms — misconfiguration surfaces as a 400 on PUT/reload instead of runtime 500s.

Fixed

  • Rust 1.95 CI compatibility — Addressed new clippy lints (`collapsible_match`, `cloned_ref_to_slice_refs`, `field_reassign_with_default`) and match-exhaustiveness in `gobgp.rs` guard patterns.
  • Webhook `action` validation — Invalid `action` values (e.g. `"resolved"`, typos) now produce a per-event mapping error instead of silently defaulting to `"ban"`. Missing/null still defaults to `"ban"`.

Security

  • `rustls-webpki` 0.103.10 → 0.103.12 (RUSTSEC-2026-0098 / RUSTSEC-2026-0099)
  • `RUSTSEC-2026-0097` (`rand`) added to `cargo-audit` ignore list pending upstream fix.

New dependencies

  • `serde_json_path 0.7` — RFC 9535 JSONPath, pure-Rust, no C deps
  • `subtle 2` — constant-time comparison for HMAC verification

Tests

  • 204 unit + 110 integration + 16 postgres backend tests pass (+8 webhook unit tests over v0.14.1)
  • 78 frontend tests pass (+10 validator tests)

Contributors

  • @lance0 — generic webhook adapter, validation, CI fixes, docs

Full Changelog: v0.14.1...v0.15.0

v0.14.1

Choose a tag to compare

@lance0 lance0 released this 03 Apr 18:11

Fixed

  • Prometheus metrics wired up — All event, mitigation, announcement, reconciliation, guardrail, and BGP session metrics were defined but never incremented. Now properly instrumented across all handler and reconciliation paths. (contributed by @bswinnerton)
  • MITIGATIONS_ACTIVE gauge — Reconciliation loop now recomputes with correct action_type and pop labels each tick
  • BGP_SESSION_UP gauge — Updated each reconciliation tick from actual peer session state

Security

  • aws-lc-sys 0.36.0 → 0.39.1 — CRL scope check logic error (high), PKCS7 signature/cert chain bypass (high), AES-CCM timing side-channel (medium), X.509 name constraints bypass
  • rustls-webpki 0.103.9 → 0.103.10 — CRL Distribution Point matching logic
  • picomatch → 4.0.4 — ReDoS via extglob quantifiers (high), method injection in POSIX character classes (moderate)

Dependencies

  • uuid 1.21.0 → 1.22.0
  • rustls 0.23.36 → 0.23.37
  • ipnet 2.11.0 → 2.12.0
  • @radix-ui/react-popover 1.1.4 → 1.1.15
  • recharts 3.7.0 → 3.8.0

Full Changelog: v0.14.0...v0.14.1

v0.14.0

Choose a tag to compare

@lance0 lance0 released this 20 Mar 01:18

What's New

Multi-Signal Correlation Engine

Combine weak signals from multiple detectors into high-confidence mitigation decisions. Events targeting the same (victim_ip, vector) within a time window are grouped into signal groups with configurable source weights, corroboration thresholds, and per-playbook overrides.

  • Signal groups with derived confidence, source counting, and corroboration status
  • Correlation explainability on mitigations (why a decision was made)
  • Per-playbook overrides for min_sources and confidence_threshold
  • Signal group expiry via the reconciliation loop

Signal Adapters

  • Alertmanager webhook (POST /v1/signals/alertmanager) — maps labels/annotations to attack events, handles batched alerts, resolved alerts trigger withdraw, fingerprint dedup
  • FastNetMon webhook (POST /v1/signals/fastnetmon) — classifies vector from traffic breakdown, configurable confidence mapping, attack_uuid dedup

Correlation Config API

  • GET /v1/config/correlation — current config (secrets redacted)
  • PUT /v1/config/correlation — update config (admin only, validates, writes YAML, hot-reloads)

Correlation Dashboard

  • Correlation page with Signals, Groups, and Config tabs
  • Signal group detail page with contributing events and source breakdown
  • Correlation context section on mitigation detail page

Infrastructure

  • Docker configs mount changed from read-only to writable (dashboard config editors work out of the box)
  • Default configs/correlation.yaml included as example config

Documentation

Testing

  • 179 backend unit tests, 99 integration, 16 postgres, 9 e2e, 64 frontend
  • All passing, clippy clean, Docker containers verified end-to-end

Breaking Changes

None. Correlation is opt-in via correlation.enabled: true. Default behavior preserves existing single-source flow.

Full Changelog: v0.13.0...v0.14.0

v0.13.0

Choose a tag to compare

@lance0 lance0 released this 19 Mar 17:04

What's New

  • Event batchingPOST /v1/events/batch accepts up to 100 events in a single request with partial success semantics (202/207). Sequential processing through the full pipeline (validation, guardrails, policy, FlowSpec announce).

  • Post-attack incident reportsGET /v1/reports/incident?mitigation_id=X or ?ip=X generates a markdown incident report with summary table, timeline, events, mitigations, and audit trail. Dashboard "Report" buttons on mitigation detail and IP history pages with copy/download dialog.

  • FlowSpec NLRI fuzz/property tests — 8 proptest property-based tests for prefix parsing, NLRI roundtrip, and action roundtrip. Two cargo-fuzz targets for offline fuzzing with libFuzzer.

Bug Fixes

  • WebSocket rejected all connections when auth_mode is none — Dashboard showed "Disconnected" permanently in no-auth deployments.
  • Mitigation detail page showed "Not Found" on Next.js 16 — Dynamic route params became async in Next.js 15+.
  • Dark mode outline button hover invisible — Export CSV, Refresh, and other outline-variant buttons had nearly invisible hover states in dark mode.

Full Changelog: v0.12.0...v0.13.0

v0.12.0

Choose a tag to compare

@lance0 lance0 released this 18 Mar 19:30

What's New

  • Cursor-based pagination — All list endpoints now use cursor-based pagination (?cursor=<opaque>&limit=N). Responses include next_cursor and has_more fields. Breaking: offset parameter removed (see ADR 016).
  • Date range filtering — All list endpoints accept ?start=<ISO8601>&end=<ISO8601> for time-bounded queries.
  • Bulk acknowledgePOST /v1/mitigations/acknowledge marks mitigations as reviewed (sets acknowledged_at/acknowledged_by). Filterable via ?acknowledged=true|false.
  • Per-destination event routing — Each alerting destination can specify its own events list to override the global filter. Backward-compatible (ADR 017).
  • Notification preferencesGET/PUT /v1/preferences stores per-operator toast settings (muted events, quiet hours UTC). Dashboard toasts respect preferences; quiet hours suppress non-critical events only.

Breaking Changes

Offset pagination removed

?offset=N no longer works on /v1/mitigations, /v1/events, or /v1/audit. Use cursor-based pagination:

# Page 1 (no cursor = first page)
curl '/v1/mitigations?limit=50'
# Response: {"mitigations": [...], "next_cursor": "MjAyNi0w...", "has_more": true}

# Page 2
curl '/v1/mitigations?limit=50&cursor=MjAyNi0w...'

If you use prefixdctl, replace --offset with --cursor.

Audit response shape changed

GET /v1/audit now returns {"entries": [...], "count": N, "next_cursor": ..., "has_more": ...} instead of a bare array.

Migrations

Two new migrations run automatically on startup:

  • 005: acknowledged_at/acknowledged_by columns on mitigations
  • 006: notification_preferences table

Back up your database before upgrading: docker compose exec postgres pg_dump -U prefixd prefixd > backup.sql

Bug Fixes

  • Migration 005 uses IF NOT EXISTS for idempotent column adds
  • Notification preferences: quiet hours always serialized as explicit null
  • Half-configured quiet hours rejected (both start and end required, or both null)
  • Preferences fetched eagerly on session start
  • Bun pinned to 1.3.10 in Dockerfile (1.3.11 segfaults during next build in CI)

See upgrading.md for detailed migration guide.

Full Changelog: v0.11.0...v0.12.0