Skip to content

feat(engine): operator-configured, redacted breach notifier (off by default) (JEF-144)#62

Merged
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-144-operator-configured-redacted-breach-notifier-off-by-default
Jun 25, 2026
Merged

feat(engine): operator-configured, redacted breach notifier (off by default) (JEF-144)#62
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-144-operator-configured-redacted-breach-notifier-off-by-default

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

What & why

Surfacing was pull-only — a solo operator never learned protector decided a breach unless watching the dashboard. This adds an opt-in outbound notifier that POSTs a redacted breach-decision summary to an operator-configured sink (PROTECTOR_ENGINE_NOTIFY_URL), the inverse of the falcosidekick ingest, documented to target an in-cluster sink (Alertmanager / ntfy / gotify).

This is the one sanctioned outbound path, recorded as ADR-0018 (the notification trust boundary).

Behaviour (matches the acceptance criteria)

  • URL set ⇒ exactly one notification per new breach decision, deduped on the journal's decision identity (JEF-141). The notifier fires at the same point the engine appends a new breach line — a decisive verdict whose summary changed for that entry — so a steady-state cluster notifies once, never per pass.
  • No URL ⇒ zero outbound calls, byte-identical to today. Disabled by default; an empty/blank URL is also disabled.
  • Redacted by default: decision kind, entry workload (a workload key, not a secret), the ATT&CK outcome (distinct tactic/technique IDs + an objective count), the sanitized verdict text, and the enforcement posture. No secret names, no per-peer graph, no CVE list. Richer per-objective ATT&CK detail is behind an explicit opt-in (PROTECTOR_ENGINE_NOTIFY_VERBOSE) and still excludes secrets/peers/CVEs.
  • Shadow vs armed unambiguous: "would isolate" (shadow) vs "isolated" (armed).
  • Verdict prose sanitized before egress (it can carry advisory-derived third-party text per ADR-0015), reusing adjudicate::sanitize.
  • Bounded + fail-safe: reuses the timeout-only client from model.rs (never an unbounded reqwest::Client::new()); a failed POST is logged once and dropped. Never affects a verdict, the journal, or actuation. Engine stays shadow; no action-class default changes.

Where

  • engine/src/engine/notify.rs (new) — BreachNotifier, BreachNotice, pure redacted_payload.
  • engine/src/engine/mod.rs — fires the notifier at the journal write site; with_notifier builder; from_env() wired in run_watch only.
  • engine/src/engine/model.rstimeout_only_client made pub(crate) to reuse the bounded-client pattern.
  • docs/adr/0018-...md + docs/adr/README.md index.

Tests

Unit (notify): redaction (no secret names / no peer graph / no CVE list in the default payload), verbose still excludes secrets, shadow-vs-armed wording, verdict sanitized before egress, disabled = zero outbound, enabled uses a bounded client (fails fast vs an unroutable address), truthy parsing.
Engine integration (local axum sink): one notification per decision (dedupe across passes) with a redacted body, and zero outbound calls when no URL is set.

Gates: cargo fmt --check, cargo check -p protector, cargo clippy -p protector --all-targets -- -D warnings, cargo test (180 engine tests pass, behavior crate clean).

Closes JEF-144

🤖 Generated with Claude Code

…efault) (JEF-144)

Surfacing was pull-only: a solo operator never learned protector decided a
breach unless watching the dashboard. Add an opt-in outbound notifier that
POSTs a redacted breach-decision summary to an operator-configured sink
(PROTECTOR_ENGINE_NOTIFY_URL) — the one sanctioned outbound path (ADR-0018).

- Off by default: unset URL ⇒ zero outbound calls, byte-identical to today.
- Redacted by default: decision kind, entry workload, ATT&CK outcome (distinct
  tactic/technique IDs + an objective count), sanitized verdict text, and the
  shadow-vs-armed posture. No secret names, no peer graph, no CVE list. Richer
  per-objective ATT&CK detail is behind PROTECTOR_ENGINE_NOTIFY_VERBOSE.
- Deduped on the journal's decision identity (JEF-141): fired at the same point
  the engine appends a new breach line, so one decision is one notification.
- Shadow ("would isolate") vs armed ("isolated") explicit in the message.
- Bounded, fail-safe client (reuses model.rs timeout_only_client); a hung sink
  can't stall the engine loop and a failed POST is logged once and dropped.

Records ADR-0018 (the notification trust boundary) and indexes it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
@thejefflarson thejefflarson force-pushed the thejefflarson/jef-144-operator-configured-redacted-breach-notifier-off-by-default branch from 4e652aa to dad0220 Compare June 25, 2026 00:32
@thejefflarson thejefflarson merged commit 15a1d77 into main Jun 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant