Notification & automation flows (n8n/Node-RED style: trigger → filter → action) #24
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Motstandskraft/spam#24
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
A notification / automation flow builder, conceptually like n8n / Node-RED: users compose triggers → filters (AND/OR) → mapping → actions, and bind them to events in SPAM. v1 actions: Slack / in-web notification / webhook. The model must be extensible so later we can add richer action nodes (base64 decode, small Python scripts, linting/scanning steps, etc.).
Filters must be bindable to clusters, images, and repos (the same scopes SPAM already filters on).
What already exists (foundation)
NOTIFY/LISTENpub/sub on channelspam_events(api/internal/events/notify.go), already feeding SSE streams (/api/app/stream,/api/runs/...). This is the natural trigger source.web/src/routes/(app)/notifications/+page.svelte— currently placeholders only (hardcoded alerts, "Delivery settings" stubs for push/email/webhook). No real delivery backend.internal/jobs(QUEUED→RUNNING→SUCCEEDED/FAILED/RETRY,SELECT … FOR UPDATE SKIP LOCKED) — the right executor for action runs (esp. future code-running nodes that need sandboxing likeinternal/runner).acl.Provideralready filters repos / images / clusters — reuse for "this rule applies to assets in scope X".Proposed architecture
Data model (new
internal/automationpackage)flow—{id, name, enabled, owner_user_id, created_at}flow_trigger—{flow_id, event_type}whereevent_type∈ event taxonomy belowflow_filter— a tree (group node withAND/OR+ leaf predicates). Leaf predicate:{field, op, value}e.g.severity >= HIGH,kev == true,cluster.slug in [...],image.repository matches …,repo.id in [...],internet_exposed == true,tier == fix_now. Store as JSONB for flexibility.flow_action— ordered list:{type, config}. v1 types:slack,web_notification,webhook.configis per-type JSONB (Slack channel/webhook URL + template; webhook URL + headers + HMAC secret viasecretsAES-GCM).flow_run— execution record per fired event (for observability/retry), tied to a job.web_notification— delivered in-app notifications (replaces the placeholder data){user_id/scope, title, body, severity, read_at, source_flow_id, deep_link}.Event taxonomy (triggers)
Define a stable internal event vocabulary emitted via
events.NotifyEvent():finding.new,finding.escalated(e.g. CVE → KEV / now internet-exposed / tier→fix_now)asset.tier_changed,secret.detectedscan.completed,scan.failedsuppression.expired(ties into triage epic)cluster.offline/cluster.onlineEvents must carry enough context (asset_type/id, cluster, severity, vuln_id, tier) for filters to evaluate without extra DB joins where possible.
Execution
spam_events→ matches enabled flows whose trigger == event_type.FLOW_ACTIONjob per action (retry/backoff for free via job framework).web_notification).Extensibility seam
Action is an interface (
Action.Execute(ctx, event, config) error) with a registry. v1 registersslack/web/webhook. Later nodes (base64 transform, Python script, lint/scan) register the same way; untrusted code-running nodes must run in the sandboxed K8s runner (internal/runner), never in-process. Mapping/transform nodes can chain by passing transformed payload to the next node.Edge cases / decisions
SKIP LOCKED) so the listener enqueues and any worker executes exactly once. No in-memory subscriptions.flow_run.secretspkg), never returned in plaintext via API.asset_risk— ensure the event payload or a cheap lookup carries them at fire time.Tasks
events.NotifyEventemissions across scan/triage/secret/cluster pathsslack,web_notification,webhookwith retry/DLQ via jobsweb_notification(replace placeholders)Relates to: triage epic (
suppression.expired,finding.escalatedevents), public scan API (scan.completed).