7.7 KiB
Notifications Rules
Imposed rule: Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.
Rules decide which platform events deserve a notification, how aggressively they should be throttled, and which channels/actions should run. They are tenant-scoped contracts that guarantee deterministic routing across Notify.Worker replicas.
1. Rule lifecycle
- Authoring. Operators create or update rules through the Notify WebService (
POST /rules,PATCH /rules/{id}) or UI. Payloads are normalised to the currentNotifyRuleschema version. - Evaluation. Notify.Worker evaluates enabled rules per incoming event. Tenancy is enforced first, followed by match filters, VEX gates, throttles, and digest handling.
- Delivery. Matching actions are enqueued with an idempotency key to prevent storm loops. Throttle rejections and digest coalescing are recorded in the delivery ledger.
- Audit. Every change carries
createdBy/updatedByplus timestamps; the delivery ledger referencesruleId/actionIdfor traceability.
2. Rule schema reference
| Field | Type | Notes |
|---|---|---|
ruleId |
string | Stable identifier; clients may provide UUID/slug. |
tenantId |
string | Must match the tenant header supplied when the rule is created. |
name |
string | Display label shown in UI and audits. |
description |
string? | Optional operator-facing note. |
enabled |
bool | Disabled rules remain stored but skipped during evaluation. |
labels |
map<string,string> | Sorted, trimmed key/value tags supporting filtering. |
metadata |
map<string,string> | Reserved for automation; stored verbatim (sorted). |
match |
NotifyRuleMatch |
Declarative filters applied before actions execute. |
actions[] |
NotifyRuleAction |
Ordered set of channel dispatchers; minimum one. |
createdBy/createdAt |
string?, instant | Populated automatically when omitted. |
updatedBy/updatedAt |
string?, instant | Defaults to creation values when unspecified. |
schemaVersion |
string | Auto-upgraded during persistence; use for migrations. |
Rules are immutable snapshots; updates produce a full document write so workers observing change streams can refresh caches deterministically.
3. Match filters
NotifyRuleMatch narrows which events trigger the rule. All string collections are trimmed, deduplicated, and sorted to guarantee deterministic evaluation.
| Field | Type | Behaviour |
|---|---|---|
eventKinds[] |
string | Lower-cased; supports any canonical Notify event (scanner.report.ready, scheduler.rescan.delta, zastava.admission, etc.). Empty list matches all kinds. |
namespaces[] |
string | Exact match against event.scope.namespace. Supports glob-style filters via upstream enrichment (planned). |
repositories[] |
string | Matches event.scope.repo. |
digests[] |
string | Lower-cased; matches event.scope.digest. |
labels[] |
string | Matches event attributes or delta labels (kev, critical, license, …). |
componentPurls[] |
string | Matches component identifiers inside the event payload when provided. |
minSeverity |
string? | Lower-cased severity gate (e.g., medium, high, critical). Evaluated on new findings inside event deltas; events lacking severity bypass this gate unless set. |
verdicts[] |
string | Accepts scan/report verdicts (fail, warn, block, escalate, deny). |
kevOnly |
bool? | When true, only KEV-tagged findings fire. |
vex |
object | Additional gating aligned with VEX consensus; see below. |
3.1 VEX gates
NotifyRuleMatchVex offers fine-grained control when VEX findings accompany events:
| Field | Default | Effect |
|---|---|---|
includeAcceptedJustifications |
true |
Include findings marked not_affected/acceptable in consensus. |
includeRejectedJustifications |
false |
Surface findings the consensus rejected. |
includeUnknownJustifications |
false |
Allow findings without explicit justification. |
justificationKinds[] |
[] |
Optional allow-list of justification codes (e.g., exploit_observed, component_not_present). |
If the VEX block filters out every applicable finding, the rule is treated as a non-match and no actions run.
4. Actions, throttles, and digests
Each rule requires at least one action. Actions are deduplicated and sorted by actionId, so prefer deterministic identifiers.
| Field | Type | Notes |
|---|---|---|
actionId |
string | Stable identifier unique within the rule. |
channel |
string | Reference to a channel (channelId) configured in /channels. |
template |
string? | Template key to use for rendering; falls back to channel default when omitted. |
digest |
string? | Digest window key (instant, 5m, 15m, 1h, 1d). instant bypasses coalescing. |
throttle |
ISO8601 duration? | Optional throttle TTL (PT300S, PT1H). Prevents duplicate deliveries when the same idempotency hash appears before expiry. |
locale |
string? | BCP-47 tag (stored lower-case). Template lookup falls back to channel locale then en-us. |
enabled |
bool | Disabled actions skip rendering but remain stored. |
metadata |
map<string,string> | Connector-specific hints (priority, layout, etc.). |
4.1 Evaluation order
- Verify channel exists and is enabled; disabled channels mark the delivery as
Dropped. - Apply throttle idempotency key:
hash(ruleId|actionId|event.kind|scope.digest|delta.hash|dayBucket). Hits are logged asThrottled. - If the action defines a digest window other than
instant, append the event to the open window and defer delivery until flush. - When delivery proceeds, the renderer resolves the template, locale, and metadata before invoking the connector.
5. Example rule payload
{
"ruleId": "rule-critical-soc",
"tenantId": "tenant-dev",
"name": "Critical scanner verdicts",
"description": "Route KEV-tagged critical findings to SOC Slack with zero delay.",
"enabled": true,
"match": {
"eventKinds": ["scanner.report.ready"],
"labels": ["kev", "critical"],
"minSeverity": "critical",
"verdicts": ["fail", "block"],
"kevOnly": true
},
"actions": [
{
"actionId": "act-slack-critical",
"channel": "chn-slack-soc",
"template": "tmpl-critical",
"digest": "instant",
"throttle": "PT300S",
"locale": "en-us",
"metadata": {
"priority": "p1"
}
}
],
"labels": {
"owner": "soc"
},
"metadata": {
"revision": "12"
}
}
Dry-run calls (POST /rules/{id}/test) accept the same structure along with a sample Notify event payload to exercise match logic without invoking connectors.
6. Operational guidance
- Keep rule scopes narrow (namespace/repository) before relying on severity gates; this minimises noise and improves digest summarisation.
- Always configure a throttle window for instant actions to protect against repeated upstream retries.
- Use rule labels to organise dashboards and access control (e.g.,
owner:soc,env:prod). - Prefer tenant-specific rule IDs so Offline Kit exports remain deterministic across environments.
- If a rule depends on derived metadata (e.g., policy verdict tags), list those dependencies in the rule description for audit readiness.
Imposed rule reminder: Work of this type or tasks of this type on this component must also be applied everywhere else it should be applied.