Restructure solution layout by module
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			This commit is contained in:
		| @@ -1,147 +1,147 @@ | ||||
| # 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 | ||||
|  | ||||
| 1. **Authoring.** Operators create or update rules through the Notify WebService (`POST /rules`, `PATCH /rules/{id}`) or UI. Payloads are normalised to the current `NotifyRule` schema version. | ||||
| 2. **Evaluation.** Notify.Worker evaluates enabled rules per incoming event. Tenancy is enforced first, followed by match filters, VEX gates, throttles, and digest handling. | ||||
| 3. **Delivery.** Matching actions are enqueued with an idempotency key to prevent storm loops. Throttle rejections and digest coalescing are recorded in the delivery ledger. | ||||
| 4. **Audit.** Every change carries `createdBy`/`updatedBy` plus timestamps; the delivery ledger references `ruleId`/`actionId` for 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`](#3-match-filters) | Declarative filters applied before actions execute. | | ||||
| | `actions[]` | [`NotifyRuleAction`](#4-actions-throttles-and-digests) | 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 | ||||
|  | ||||
| 1. Verify channel exists and is enabled; disabled channels mark the delivery as `Dropped`. | ||||
| 2. Apply throttle idempotency key: `hash(ruleId|actionId|event.kind|scope.digest|delta.hash|dayBucket)`. Hits are logged as `Throttled`. | ||||
| 3. If the action defines a digest window other than `instant`, append the event to the open window and defer delivery until flush. | ||||
| 4. When delivery proceeds, the renderer resolves the template, locale, and metadata before invoking the connector. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 5. Example rule payload | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "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. | ||||
| # 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 | ||||
|  | ||||
| 1. **Authoring.** Operators create or update rules through the Notify WebService (`POST /rules`, `PATCH /rules/{id}`) or UI. Payloads are normalised to the current `NotifyRule` schema version. | ||||
| 2. **Evaluation.** Notify.Worker evaluates enabled rules per incoming event. Tenancy is enforced first, followed by match filters, VEX gates, throttles, and digest handling. | ||||
| 3. **Delivery.** Matching actions are enqueued with an idempotency key to prevent storm loops. Throttle rejections and digest coalescing are recorded in the delivery ledger. | ||||
| 4. **Audit.** Every change carries `createdBy`/`updatedBy` plus timestamps; the delivery ledger references `ruleId`/`actionId` for 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`](#3-match-filters) | Declarative filters applied before actions execute. | | ||||
| | `actions[]` | [`NotifyRuleAction`](#4-actions-throttles-and-digests) | 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 | ||||
|  | ||||
| 1. Verify channel exists and is enabled; disabled channels mark the delivery as `Dropped`. | ||||
| 2. Apply throttle idempotency key: `hash(ruleId|actionId|event.kind|scope.digest|delta.hash|dayBucket)`. Hits are logged as `Throttled`. | ||||
| 3. If the action defines a digest window other than `instant`, append the event to the open window and defer delivery until flush. | ||||
| 4. When delivery proceeds, the renderer resolves the template, locale, and metadata before invoking the connector. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 5. Example rule payload | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "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. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user