feat(docs): Add comprehensive documentation for Vexer, Vulnerability Explorer, and Zastava modules
- Introduced AGENTS.md, README.md, TASKS.md, and implementation_plan.md for Vexer, detailing mission, responsibilities, key components, and operational notes. - Established similar documentation structure for Vulnerability Explorer and Zastava modules, including their respective workflows, integrations, and observability notes. - Created risk scoring profiles documentation outlining the core workflow, factor model, governance, and deliverables. - Ensured all modules adhere to the Aggregation-Only Contract and maintain determinism and provenance in outputs.
This commit is contained in:
22
docs/modules/notify/AGENTS.md
Normal file
22
docs/modules/notify/AGENTS.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Notify agent guide
|
||||
|
||||
## Mission
|
||||
Notify evaluates operator-defined rules against platform events and dispatches channel-specific payloads with full auditability.
|
||||
|
||||
## Key docs
|
||||
- [Module README](./README.md)
|
||||
- [Architecture](./architecture.md)
|
||||
- [Implementation plan](./implementation_plan.md)
|
||||
- [Task board](./TASKS.md)
|
||||
|
||||
## How to get started
|
||||
1. Open ../../implplan/SPRINTS.md and locate the stories referencing this module.
|
||||
2. Review ./TASKS.md for local follow-ups and confirm status transitions (TODO → DOING → DONE/BLOCKED).
|
||||
3. Read the architecture and README for domain context before editing code or docs.
|
||||
4. Coordinate cross-module changes in the main /AGENTS.md description and through the sprint plan.
|
||||
|
||||
## Guardrails
|
||||
- Honour the Aggregation-Only Contract where applicable (see ../../ingestion/aggregation-only-contract.md).
|
||||
- Preserve determinism: sort outputs, normalise timestamps (UTC ISO-8601), and avoid machine-specific artefacts.
|
||||
- Keep Offline Kit parity in mind—document air-gapped workflows for any new feature.
|
||||
- Update runbooks/observability assets when operational characteristics change.
|
||||
35
docs/modules/notify/README.md
Normal file
35
docs/modules/notify/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# StellaOps Notify
|
||||
|
||||
Notify evaluates operator-defined rules against platform events and dispatches channel-specific payloads with full auditability.
|
||||
|
||||
## Responsibilities
|
||||
- Process event streams and apply tenant-scoped routing rules.
|
||||
- Render connector-specific payloads (email, Slack, Teams, webhook, custom).
|
||||
- Enforce throttling, digests, and delivery retries.
|
||||
- Surface delivery/audit data for UI and CLI consumers.
|
||||
|
||||
## Key components
|
||||
- `StellaOps.Notify.WebService` (rules API + preview).
|
||||
- `StellaOps.Notify.Worker` (delivery engine).
|
||||
- Connector libraries under `StellaOps.Notify.Connectors.*`.
|
||||
|
||||
## Integrations & dependencies
|
||||
- MongoDB for rule/channel storage.
|
||||
- Redis/NATS for delivery queues.
|
||||
- CLI/UI for authoring and monitoring notifications.
|
||||
|
||||
## Operational notes
|
||||
- Schema fixtures in ./resources/schemas & ./resources/samples.
|
||||
- Connector-specific monitoring dashboards.
|
||||
- Offline runner guidance inside operations playbook.
|
||||
|
||||
## Related resources
|
||||
- ./resources/schemas
|
||||
- ./resources/samples
|
||||
|
||||
## Backlog references
|
||||
- NOTIFY-SVC-38..40 (Notify backlog) referenced in `docs/README.md`.
|
||||
- DOCS-NOTIFY updates tracked in ../../TASKS.md when available.
|
||||
|
||||
## Epic alignment
|
||||
- **Epic 11 – Notifications Studio:** deliver notifications workspace, preview tooling, immutable delivery ledger, and tenant-scoped throttling/digest controls.
|
||||
9
docs/modules/notify/TASKS.md
Normal file
9
docs/modules/notify/TASKS.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Task board — Notify
|
||||
|
||||
> Local tasks should link back to ./AGENTS.md and mirror status updates into ../../TASKS.md when applicable.
|
||||
|
||||
| ID | Status | Owner(s) | Description | Notes |
|
||||
|----|--------|----------|-------------|-------|
|
||||
| NOTIFY-DOCS-0001 | DOING (2025-10-29) | Docs Guild | Validate that ./README.md aligns with the latest release notes. | See ./AGENTS.md |
|
||||
| NOTIFY-OPS-0001 | TODO | Ops Guild | Review runbooks/observability assets after next sprint demo. | Sync outcomes back to ../../TASKS.md |
|
||||
| NOTIFY-ENG-0001 | TODO | Module Team | Cross-check implementation plan milestones against ../../implplan/SPRINTS.md. | Update status via ./AGENTS.md workflow |
|
||||
515
docs/modules/notify/architecture.md
Normal file
515
docs/modules/notify/architecture.md
Normal file
@@ -0,0 +1,515 @@
|
||||
> **Scope.** Implementation‑ready architecture for **Notify** (aligned with Epic 11 – Notifications Studio): a rules‑driven, tenant‑aware notification service that consumes platform events (scan completed, report ready, rescan deltas, attestation logged, admission decisions, etc.), evaluates operator‑defined routing rules, renders **channel‑specific messages** (Slack/Teams/Email/Webhook), and delivers them **reliably** with idempotency, throttling, and digests. It is UI‑managed, auditable, and safe by default (no secrets leakage, no spam storms).
|
||||
|
||||
---
|
||||
|
||||
## 0) Mission & boundaries
|
||||
|
||||
**Mission.** Convert **facts** from Stella Ops into **actionable, noise‑controlled** signals where teams already live (chat/email/webhooks), with **explainable** reasons and deep links to the UI.
|
||||
|
||||
**Boundaries.**
|
||||
|
||||
* Notify **does not make policy decisions** and **does not rescan**; it **consumes** events from Scanner/Scheduler/Vexer/Feedser/Attestor/Zastava and routes them.
|
||||
* Attachments are **links** (UI/attestation pages); Notify **does not** attach SBOMs or large blobs to messages.
|
||||
* Secrets for channels (Slack tokens, SMTP creds) are **referenced**, not stored raw in Mongo.
|
||||
|
||||
---
|
||||
|
||||
## 1) Runtime shape & projects
|
||||
|
||||
```
|
||||
src/
|
||||
├─ StellaOps.Notify.WebService/ # REST: rules/channels CRUD, test send, deliveries browse
|
||||
├─ StellaOps.Notify.Worker/ # consumers + evaluators + renderers + delivery workers
|
||||
├─ StellaOps.Notify.Connectors.* / # channel plug-ins: Slack, Teams, Email, Webhook (v1)
|
||||
│ └─ *.Tests/
|
||||
├─ StellaOps.Notify.Engine/ # rules engine, templates, idempotency, digests, throttles
|
||||
├─ StellaOps.Notify.Models/ # DTOs (Rule, Channel, Event, Delivery, Template)
|
||||
├─ StellaOps.Notify.Storage.Mongo/ # rules, channels, deliveries, digests, locks
|
||||
├─ StellaOps.Notify.Queue/ # bus client (Redis Streams/NATS JetStream)
|
||||
└─ StellaOps.Notify.Tests.* # unit/integration/e2e
|
||||
```
|
||||
|
||||
**Deployables**:
|
||||
|
||||
* **Notify.WebService** (stateless API)
|
||||
* **Notify.Worker** (horizontal scale)
|
||||
|
||||
**Dependencies**: Authority (OpToks; DPoP/mTLS), MongoDB, Redis/NATS (bus), HTTP egress to Slack/Teams/Webhooks, SMTP relay for Email.
|
||||
|
||||
> **Configuration.** Notify.WebService bootstraps from `notify.yaml` (see `etc/notify.yaml.sample`). Use `storage.driver: mongo` with a production connection string; the optional `memory` driver exists only for tests. Authority settings follow the platform defaults—when running locally without Authority, set `authority.enabled: false` and supply `developmentSigningKey` so JWTs can be validated offline.
|
||||
>
|
||||
> `api.rateLimits` exposes token-bucket controls for delivery history queries and test-send previews (`deliveryHistory`, `testSend`). Default values allow generous browsing while preventing accidental bursts; operators can relax/tighten the buckets per deployment.
|
||||
|
||||
> **Plug-ins.** All channel connectors are packaged under `<baseDirectory>/plugins/notify`. The ordered load list must start with Slack/Teams before Email/Webhook so chat-first actions are registered deterministically for Offline Kit bundles:
|
||||
>
|
||||
> ```yaml
|
||||
> plugins:
|
||||
> baseDirectory: "/var/opt/stellaops"
|
||||
> directory: "plugins/notify"
|
||||
> orderedPlugins:
|
||||
> - StellaOps.Notify.Connectors.Slack
|
||||
> - StellaOps.Notify.Connectors.Teams
|
||||
> - StellaOps.Notify.Connectors.Email
|
||||
> - StellaOps.Notify.Connectors.Webhook
|
||||
> ```
|
||||
>
|
||||
> The Offline Kit job simply copies the `plugins/notify` tree into the air-gapped bundle; the ordered list keeps connector manifests stable across environments.
|
||||
|
||||
> **Authority clients.** Register two OAuth clients in StellaOps Authority: `notify-web-dev` (audience `notify.dev`) for development and `notify-web` (audience `notify`) for staging/production. Both require `notify.read` and `notify.admin` scopes and use DPoP-bound client credentials (`client_secret` in the samples). Reference entries live in `etc/authority.yaml.sample`, with placeholder secrets under `etc/secrets/notify-web*.secret.example`.
|
||||
|
||||
---
|
||||
|
||||
## 2) Responsibilities
|
||||
|
||||
1. **Ingest** platform events from internal bus with strong ordering per key (e.g., image digest).
|
||||
2. **Evaluate rules** (tenant‑scoped) with matchers: severity changes, namespaces, repos, labels, KEV flags, provider provenance (VEX), component keys, admission decisions, etc.
|
||||
3. **Control noise**: **throttle**, **coalesce** (digest windows), and **dedupe** via idempotency keys.
|
||||
4. **Render** channel‑specific messages using safe templates; include **evidence** and **links**.
|
||||
5. **Deliver** with retries/backoff; record outcome; expose delivery history to UI.
|
||||
6. **Test** paths (send test to channel targets) without touching live rules.
|
||||
7. **Audit**: log who configured what, when, and why a message was sent.
|
||||
|
||||
---
|
||||
|
||||
## 3) Event model (inputs)
|
||||
|
||||
Notify subscribes to the **internal event bus** (produced by services, escaped JSON; gzip allowed with caps):
|
||||
|
||||
* `scanner.scan.completed` — new SBOM(s) composed; artifacts ready
|
||||
* `scanner.report.ready` — analysis verdict (policy+vex) available; carries deltas summary
|
||||
* `scheduler.rescan.delta` — new findings after Feedser/Vexer deltas (already summarized)
|
||||
* `attestor.logged` — Rekor UUID returned (sbom/report/vex export)
|
||||
* `zastava.admission` — admit/deny with reasons, namespace, image digests
|
||||
* `feedser.export.completed` — new export ready (rarely notified directly; usually drives Scheduler)
|
||||
* `vexer.export.completed` — new consensus snapshot (ditto)
|
||||
|
||||
**Canonical envelope (bus → Notify.Engine):**
|
||||
|
||||
```json
|
||||
{
|
||||
"eventId": "uuid",
|
||||
"kind": "scanner.report.ready",
|
||||
"tenant": "tenant-01",
|
||||
"ts": "2025-10-18T05:41:22Z",
|
||||
"actor": "scanner-webservice",
|
||||
"scope": { "namespace":"payments", "repo":"ghcr.io/acme/api", "digest":"sha256:..." },
|
||||
"payload": { /* kind-specific fields, see below */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Examples (payload cores):**
|
||||
|
||||
* `scanner.report.ready`:
|
||||
|
||||
```json
|
||||
{
|
||||
"reportId": "report-3def...",
|
||||
"verdict": "fail",
|
||||
"summary": {"total": 12, "blocked": 2, "warned": 3, "ignored": 5, "quieted": 2},
|
||||
"delta": {"newCritical": 1, "kev": ["CVE-2025-..."]},
|
||||
"links": {"ui": "https://ui/.../reports/report-3def...", "rekor": "https://rekor/..."},
|
||||
"dsse": { "...": "..." },
|
||||
"report": { "...": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
Payload embeds both the canonical report document and the DSSE envelope so connectors, Notify, and UI tooling can reuse the signed bytes without re-serialising.
|
||||
|
||||
* `scanner.scan.completed`:
|
||||
|
||||
```json
|
||||
{
|
||||
"reportId": "report-3def...",
|
||||
"digest": "sha256:...",
|
||||
"verdict": "fail",
|
||||
"summary": {"total": 12, "blocked": 2, "warned": 3, "ignored": 5, "quieted": 2},
|
||||
"delta": {"newCritical": 1, "kev": ["CVE-2025-..."]},
|
||||
"policy": {"revisionId": "rev-42", "digest": "27d2..."},
|
||||
"findings": [{"id": "finding-1", "severity": "Critical", "cve": "CVE-2025-...", "reachability": "runtime"}],
|
||||
"dsse": { "...": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
* `zastava.admission`:
|
||||
|
||||
```json
|
||||
{ "decision":"deny|allow", "reasons":["unsigned image","missing SBOM"],
|
||||
"images":[{"digest":"sha256:...","signed":false,"hasSbom":false}] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4) Rules engine — semantics
|
||||
|
||||
**Rule shape (simplified):**
|
||||
|
||||
```yaml
|
||||
name: "high-critical-alerts-prod"
|
||||
enabled: true
|
||||
match:
|
||||
eventKinds: ["scanner.report.ready","scheduler.rescan.delta","zastava.admission"]
|
||||
namespaces: ["prod-*"]
|
||||
repos: ["ghcr.io/acme/*"]
|
||||
minSeverity: "high" # min of new findings (delta context)
|
||||
kev: true # require KEV-tagged or allow any if false
|
||||
verdict: ["fail","deny"] # filter for report/admission
|
||||
vex:
|
||||
includeRejectedJustifications: false # notify only on accepted 'affected'
|
||||
actions:
|
||||
- channel: "slack:sec-alerts" # reference to Channel object
|
||||
template: "concise"
|
||||
throttle: "5m"
|
||||
- channel: "email:soc"
|
||||
digest: "hourly"
|
||||
template: "detailed"
|
||||
```
|
||||
|
||||
**Evaluation order**
|
||||
|
||||
1. **Tenant check** → discard if rule tenant ≠ event tenant.
|
||||
2. **Kind filter** → discard early.
|
||||
3. **Scope match** (namespace/repo/labels).
|
||||
4. **Delta/severity gates** (if event carries `delta`).
|
||||
5. **VEX gate** (drop if event’s finding is not affected under policy consensus unless rule says otherwise).
|
||||
6. **Throttling/dedup** (idempotency key) — skip if suppressed.
|
||||
7. **Actions** → enqueue per‑channel job(s).
|
||||
|
||||
**Idempotency key**: `hash(ruleId | actionId | event.kind | scope.digest | delta.hash | day-bucket)`; ensures “same alert” doesn’t fire more than once within throttle window.
|
||||
|
||||
**Digest windows**: maintain per action a **coalescer**:
|
||||
|
||||
* Window: `5m|15m|1h|1d` (configurable); coalesces events by tenant + namespace/repo or by digest group.
|
||||
* Digest messages summarize top N items and counts, with safe truncation.
|
||||
|
||||
---
|
||||
|
||||
## 5) Channels & connectors (plug‑ins)
|
||||
|
||||
Channel config is **two‑part**: a **Channel** record (name, type, options) and a Secret **reference** (Vault/K8s Secret). Connectors are **restart-time plug-ins** discovered on service start (same manifest convention as Concelier/Excititor) and live under `plugins/notify/<channel>/`.
|
||||
|
||||
**Built‑in v1:**
|
||||
|
||||
* **Slack**: Bot token (xoxb‑…), `chat.postMessage` + `blocks`; rate limit aware (HTTP 429).
|
||||
* **Microsoft Teams**: Incoming Webhook (or Graph card later); adaptive card payloads.
|
||||
* **Email (SMTP)**: TLS (STARTTLS or implicit), From/To/CC/BCC; HTML+text alt; DKIM optional.
|
||||
* **Generic Webhook**: POST JSON with HMAC signature (Ed25519 or SHA‑256) in headers.
|
||||
|
||||
**Connector contract:** (implemented by plug-in assemblies)
|
||||
|
||||
```csharp
|
||||
public interface INotifyConnector {
|
||||
string Type { get; } // "slack" | "teams" | "email" | "webhook" | ...
|
||||
Task<DeliveryResult> SendAsync(DeliveryContext ctx, CancellationToken ct);
|
||||
Task<HealthResult> HealthAsync(ChannelConfig cfg, CancellationToken ct);
|
||||
}
|
||||
```
|
||||
|
||||
**DeliveryContext** includes **rendered content** and **raw event** for audit.
|
||||
|
||||
**Test-send previews.** Plug-ins can optionally implement `INotifyChannelTestProvider` to shape `/channels/{id}/test` responses. Providers receive a sanitised `ChannelTestPreviewContext` (channel, tenant, target, timestamp, trace) and return a `NotifyDeliveryRendered` preview + metadata. When no provider is present, the host falls back to a generic preview so the endpoint always responds.
|
||||
|
||||
**Secrets**: `ChannelConfig.secretRef` points to Authority‑managed secret handle or K8s Secret path; workers load at send-time; plug-in manifests (`notify-plugin.json`) declare capabilities and version.
|
||||
|
||||
---
|
||||
|
||||
## 6) Templates & rendering
|
||||
|
||||
**Template engine**: strongly typed, safe Handlebars‑style; no arbitrary code. Partial templates per channel. Deterministic outputs (prop order, no locale drift unless requested).
|
||||
|
||||
**Variables** (examples):
|
||||
|
||||
* `event.kind`, `event.ts`, `scope.namespace`, `scope.repo`, `scope.digest`
|
||||
* `payload.verdict`, `payload.delta.newCritical`, `payload.links.ui`, `payload.links.rekor`
|
||||
* `topFindings[]` with `purl`, `vulnId`, `severity`
|
||||
* `policy.name`, `policy.revision` (if available)
|
||||
|
||||
**Helpers**:
|
||||
|
||||
* `severity_icon(sev)`, `link(text,url)`, `pluralize(n, "finding")`, `truncate(text, n)`, `code(text)`.
|
||||
|
||||
**Channel mapping**:
|
||||
|
||||
* Slack: title + blocks, limited to 50 blocks/3000 chars per section; long lists → link to UI.
|
||||
* Teams: Adaptive Card schema 1.5; fallback text for older channels (surfaced as `teams.fallbackText` metadata alongside webhook hash).
|
||||
* Email: HTML + text; inline table of top N findings, rest behind UI link.
|
||||
* Webhook: JSON with `event`, `ruleId`, `actionId`, `summary`, `links`, and raw `payload` subset.
|
||||
|
||||
**i18n**: template set per locale (English default; Bulgarian built‑in).
|
||||
|
||||
---
|
||||
|
||||
## 7) Data model (Mongo)
|
||||
|
||||
Canonical JSON Schemas for rules/channels/events live in `docs/modules/notify/resources/schemas/`. Sample payloads intended for tests/UI mock responses are captured in `docs/modules/notify/resources/samples/`.
|
||||
|
||||
**Database**: `notify`
|
||||
|
||||
* `rules`
|
||||
|
||||
```
|
||||
{ _id, tenantId, name, enabled, match, actions, createdBy, updatedBy, createdAt, updatedAt }
|
||||
```
|
||||
|
||||
* `channels`
|
||||
|
||||
```
|
||||
{ _id, tenantId, name:"slack:sec-alerts", type:"slack",
|
||||
config:{ webhookUrl?:"", channel:"#sec-alerts", workspace?: "...", secretRef:"ref://..." },
|
||||
createdAt, updatedAt }
|
||||
```
|
||||
|
||||
* `deliveries`
|
||||
|
||||
```
|
||||
{ _id, tenantId, ruleId, actionId, eventId, kind, scope, status:"sent|failed|throttled|digested|dropped",
|
||||
attempts:[{ts, status, code, reason}],
|
||||
rendered:{ title, body, target }, // redacted for PII; body hash stored
|
||||
sentAt, lastError? }
|
||||
```
|
||||
|
||||
* `digests`
|
||||
|
||||
```
|
||||
{ _id, tenantId, actionKey, window:"hourly", openedAt, items:[{eventId, scope, delta}], status:"open|flushed" }
|
||||
```
|
||||
|
||||
* `throttles`
|
||||
|
||||
```
|
||||
{ key:"idem:<hash>", ttlAt } // short-lived, also cached in Redis
|
||||
```
|
||||
|
||||
**Indexes**: rules by `{tenantId, enabled}`, deliveries by `{tenantId, sentAt desc}`, digests by `{tenantId, actionKey}`.
|
||||
|
||||
---
|
||||
|
||||
## 8) External APIs (WebService)
|
||||
|
||||
Base path: `/api/v1/notify` (Authority OpToks; scopes: `notify.admin` for write, `notify.read` for view).
|
||||
|
||||
*All* REST calls require the tenant header `X-StellaOps-Tenant` (matches the canonical `tenantId` stored in Mongo). Payloads are normalised via `NotifySchemaMigration` before persistence to guarantee schema version pinning.
|
||||
|
||||
Authentication today is stubbed with Bearer tokens (`Authorization: Bearer <token>`). When Authority wiring lands, this will switch to OpTok validation + scope enforcement, but the header contract will remain the same.
|
||||
|
||||
Service configuration exposes `notify:auth:*` keys (issuer, audience, signing key, scope names) so operators can wire the Authority JWKS or (in dev) a symmetric test key. `notify:storage:*` keys cover Mongo URI/database/collection overrides. Both sets are required for the new API surface.
|
||||
|
||||
Internal tooling can hit `/internal/notify/<entity>/normalize` to upgrade legacy JSON and return canonical output used in the docs fixtures.
|
||||
|
||||
* **Channels**
|
||||
|
||||
* `POST /channels` | `GET /channels` | `GET /channels/{id}` | `PATCH /channels/{id}` | `DELETE /channels/{id}`
|
||||
* `POST /channels/{id}/test` → send sample message (no rule evaluation); returns `202 Accepted` with rendered preview + metadata (base keys: `channelType`, `target`, `previewProvider`, `traceId` + connector-specific entries); governed by `api.rateLimits:testSend`.
|
||||
* `GET /channels/{id}/health` → connector self‑check (returns redacted metadata: secret refs hashed, sensitive config keys masked, fallbacks noted via `teams.fallbackText`/`teams.validation.*`)
|
||||
|
||||
* **Rules**
|
||||
|
||||
* `POST /rules` | `GET /rules` | `GET /rules/{id}` | `PATCH /rules/{id}` | `DELETE /rules/{id}`
|
||||
* `POST /rules/{id}/test` → dry‑run rule against a **sample event** (no delivery unless `--send`)
|
||||
|
||||
* **Deliveries**
|
||||
|
||||
* `POST /deliveries` → ingest worker delivery state (idempotent via `deliveryId`).
|
||||
* `GET /deliveries?since=...&status=...&limit=...` → list envelope `{ items, count, continuationToken }` (most recent first); base metadata keys match the test-send response (`channelType`, `target`, `previewProvider`, `traceId`); rate-limited via `api.rateLimits.deliveryHistory`. See `docs/modules/notify/resources/samples/notify-delivery-list-response.sample.json`.
|
||||
* `GET /deliveries/{id}` → detail (redacted body + metadata)
|
||||
* `POST /deliveries/{id}/retry` → force retry (admin, future sprint)
|
||||
|
||||
* **Admin**
|
||||
|
||||
* `GET /stats` (per tenant counts, last hour/day)
|
||||
* `GET /healthz|readyz` (liveness)
|
||||
* `POST /locks/acquire` | `POST /locks/release` – worker coordination primitives (short TTL).
|
||||
* `POST /digests` | `GET /digests/{actionKey}` | `DELETE /digests/{actionKey}` – manage open digest windows.
|
||||
* `POST /audit` | `GET /audit?since=&limit=` – append/query structured audit trail entries.
|
||||
|
||||
**Ingestion**: workers do **not** expose public ingestion; they **subscribe** to the internal bus. (Optional `/events/test` for integration testing, admin‑only.)
|
||||
|
||||
---
|
||||
|
||||
## 9) Delivery pipeline (worker)
|
||||
|
||||
```
|
||||
[Event bus] → [Ingestor] → [RuleMatcher] → [Throttle/Dedupe] → [DigestCoalescer] → [Renderer] → [Connector] → [Result]
|
||||
└────────→ [DeliveryStore]
|
||||
```
|
||||
|
||||
* **Ingestor**: N consumers with per‑key ordering (key = tenant|digest|namespace).
|
||||
* **RuleMatcher**: loads active rules snapshot for tenant into memory; vectorized predicate check.
|
||||
* **Throttle/Dedupe**: consult Redis + Mongo `throttles`; if hit → record `status=throttled`.
|
||||
* **DigestCoalescer**: append to open digest window or flush when timer expires.
|
||||
* **Renderer**: select template (channel+locale), inject variables, enforce length limits, compute `bodyHash`.
|
||||
* **Connector**: send; handle provider‑specific rate limits and backoffs; `maxAttempts` with exponential jitter; overflow → DLQ (dead‑letter topic) + UI surfacing.
|
||||
|
||||
**Idempotency**: per action **idempotency key** stored in Redis (TTL = `throttle window` or `digest window`). Connectors also respect **provider** idempotency where available (e.g., Slack `client_msg_id`).
|
||||
|
||||
---
|
||||
|
||||
## 10) Reliability & rate controls
|
||||
|
||||
* **Per‑tenant** RPM caps (default 600/min) + **per‑channel** concurrency (Slack 1–4, Teams 1–2, Email 8–32 based on relay).
|
||||
* **Backoff** map: Slack 429 → respect `Retry‑After`; SMTP 4xx → retry; 5xx → retry with jitter; permanent rejects → drop with status recorded.
|
||||
* **DLQ**: NATS/Redis stream `notify.dlq` with `{event, rule, action, error}` for operator inspection; UI shows DLQ items.
|
||||
|
||||
---
|
||||
|
||||
## 11) Security & privacy
|
||||
|
||||
* **AuthZ**: all APIs require **Authority** OpToks; actions scoped by tenant.
|
||||
* **Secrets**: `secretRef` only; Notify fetches just‑in‑time from Authority Secret proxy or K8s Secret (mounted). No plaintext secrets in Mongo.
|
||||
* **Egress TLS**: validate SSL; pin domains per channel config; optional CA bundle override for on‑prem SMTP.
|
||||
* **Webhook signing**: HMAC or Ed25519 signatures in `X-StellaOps-Signature` + replay‑window timestamp; include canonical body hash in header.
|
||||
* **Redaction**: deliveries store **hashes** of bodies, not full payloads for chat/email to minimize PII retention (configurable).
|
||||
* **Quiet hours**: per tenant (e.g., 22:00–06:00) route high‑sev only; defer others to digests.
|
||||
* **Loop prevention**: Webhook target allowlist + event origin tags; do not ingest own webhooks.
|
||||
|
||||
---
|
||||
|
||||
## 12) Observability (Prometheus + OTEL)
|
||||
|
||||
* `notify.events_consumed_total{kind}`
|
||||
* `notify.rules_matched_total{ruleId}`
|
||||
* `notify.throttled_total{reason}`
|
||||
* `notify.digest_coalesced_total{window}`
|
||||
* `notify.sent_total{channel}` / `notify.failed_total{channel,code}`
|
||||
* `notify.delivery_latency_seconds{channel}` (end‑to‑end)
|
||||
* **Tracing**: spans `ingest`, `match`, `render`, `send`; correlation id = `eventId`.
|
||||
|
||||
**SLO targets**
|
||||
|
||||
* Event→delivery p95 **≤ 30–60 s** under nominal load.
|
||||
* Failure rate p95 **< 0.5%** per hour (excluding provider outages).
|
||||
* Duplicate rate **≈ 0** (idempotency working).
|
||||
|
||||
---
|
||||
|
||||
## 13) Configuration (YAML)
|
||||
|
||||
```yaml
|
||||
notify:
|
||||
authority:
|
||||
issuer: "https://authority.internal"
|
||||
require: "dpop" # or "mtls"
|
||||
bus:
|
||||
kind: "redis" # or "nats"
|
||||
streams:
|
||||
- "scanner.events"
|
||||
- "scheduler.events"
|
||||
- "attestor.events"
|
||||
- "zastava.events"
|
||||
mongo:
|
||||
uri: "mongodb://mongo/notify"
|
||||
limits:
|
||||
perTenantRpm: 600
|
||||
perChannel:
|
||||
slack: { concurrency: 2 }
|
||||
teams: { concurrency: 1 }
|
||||
email: { concurrency: 8 }
|
||||
webhook: { concurrency: 8 }
|
||||
digests:
|
||||
defaultWindow: "1h"
|
||||
maxItems: 100
|
||||
quietHours:
|
||||
enabled: true
|
||||
window: "22:00-06:00"
|
||||
minSeverity: "critical"
|
||||
webhooks:
|
||||
sign:
|
||||
method: "ed25519" # or "hmac-sha256"
|
||||
keyRef: "ref://notify/webhook-sign-key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14) UI touch‑points
|
||||
|
||||
* **Notifications → Channels**: add Slack/Teams/Email/Webhook; run **health**; rotate secrets.
|
||||
* **Notifications → Rules**: create/edit YAML rules with linting; test with sample events; see match rate.
|
||||
* **Notifications → Deliveries**: timeline with filters (status, channel, rule); inspect last error; retry.
|
||||
* **Digest preview**: shows current window contents and when it will flush.
|
||||
* **Quiet hours**: configure per tenant; show overrides.
|
||||
* **DLQ**: browse dead‑letters; requeue after fix.
|
||||
|
||||
---
|
||||
|
||||
## 15) Failure modes & responses
|
||||
|
||||
| Condition | Behavior |
|
||||
| ----------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| Slack 429 / Teams 429 | Respect `Retry‑After`, backoff with jitter, reduce concurrency |
|
||||
| SMTP transient 4xx | Retry up to `maxAttempts`; escalate to DLQ on exhaust |
|
||||
| Invalid channel secret | Mark channel unhealthy; suppress sends; surface in UI |
|
||||
| Rule explosion (matches everything) | Safety valve: per‑tenant RPM caps; auto‑pause rule after X drops; UI alert |
|
||||
| Bus outage | Buffer to local queue (bounded); resume consuming when healthy |
|
||||
| Mongo slowness | Fall back to Redis throttles; batch write deliveries; shed low‑priority notifications |
|
||||
|
||||
---
|
||||
|
||||
## 16) Testing matrix
|
||||
|
||||
* **Unit**: matchers, throttle math, digest coalescing, idempotency keys, template rendering edge cases.
|
||||
* **Connectors**: provider‑level rate limits, payload size truncation, error mapping.
|
||||
* **Integration**: synthetic event storm (10k/min), ensure p95 latency & duplicate rate.
|
||||
* **Security**: DPoP/mTLS on APIs; secretRef resolution; webhook signing & replay windows.
|
||||
* **i18n**: localized templates render deterministically.
|
||||
* **Chaos**: Slack/Teams API flaps; SMTP greylisting; Redis hiccups; ensure graceful degradation.
|
||||
|
||||
---
|
||||
|
||||
## 17) Sequences (representative)
|
||||
|
||||
**A) New criticals after Feedser delta (Slack immediate + Email hourly digest)**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant SCH as Scheduler
|
||||
participant NO as Notify.Worker
|
||||
participant SL as Slack
|
||||
participant SMTP as Email
|
||||
|
||||
SCH->>NO: bus event scheduler.rescan.delta { newCritical:1, digest:sha256:... }
|
||||
NO->>NO: match rules (Slack immediate; Email hourly digest)
|
||||
NO->>SL: chat.postMessage (concise)
|
||||
SL-->>NO: 200 OK
|
||||
NO->>NO: append to digest window (email:soc)
|
||||
Note over NO: At window close → render digest email
|
||||
NO->>SMTP: send email (detailed digest)
|
||||
SMTP-->>NO: 250 OK
|
||||
```
|
||||
|
||||
**B) Admission deny (Teams card + Webhook)**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant ZA as Zastava
|
||||
participant NO as Notify.Worker
|
||||
participant TE as Teams
|
||||
participant WH as Webhook
|
||||
|
||||
ZA->>NO: bus event zastava.admission { decision: "deny", reasons: [...] }
|
||||
NO->>TE: POST adaptive card
|
||||
TE-->>NO: 200 OK
|
||||
NO->>WH: POST JSON (signed)
|
||||
WH-->>NO: 2xx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18) Implementation notes
|
||||
|
||||
* **Language**: .NET 10; minimal API; `System.Text.Json` with canonical writer for body hashing; Channels for pipelines.
|
||||
* **Bus**: Redis Streams (**XGROUP** consumers) or NATS JetStream for at‑least‑once with ack; per‑tenant consumer groups to localize backpressure.
|
||||
* **Templates**: compile and cache per rule+channel+locale; version with rule `updatedAt` to invalidate.
|
||||
* **Rules**: store raw YAML + parsed AST; validate with schema + static checks (e.g., nonsensical combos).
|
||||
* **Secrets**: pluggable secret resolver (Authority Secret proxy, K8s, Vault).
|
||||
* **Rate limiting**: `System.Threading.RateLimiting` + per‑connector adapters.
|
||||
|
||||
---
|
||||
|
||||
## 19) Roadmap (post‑v1)
|
||||
|
||||
* **PagerDuty/Opsgenie** connectors; **Jira** ticket creation.
|
||||
* **User inbox** (in‑app notifications) + mobile push via webhook relay.
|
||||
* **Anomaly suppression**: auto‑pause noisy rules with hints (learned thresholds).
|
||||
* **Graph rules**: “only notify if *not_affected → affected* transition at consensus layer”.
|
||||
* **Label enrichment**: pluggable taggers (business criticality, data classification) to refine matchers.
|
||||
61
docs/modules/notify/implementation_plan.md
Normal file
61
docs/modules/notify/implementation_plan.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Implementation plan — Notify
|
||||
|
||||
## Delivery phases
|
||||
- **Phase 1 – Core rules engine & delivery ledger**
|
||||
Implement rules/channels schema, event ingestion, rule evaluation, idempotent deliveries, and audit logging.
|
||||
- **Phase 2 – Connectors & rendering**
|
||||
Ship Slack/Teams/Email/Webhook connectors, template rendering, localization, throttling, retries, and secret referencing.
|
||||
- **Phase 3 – Console & CLI authoring**
|
||||
Provide UI/CLI for rule authoring, previews, channel health, delivery browsing, digests, and test sends.
|
||||
- **Phase 4 – Governance & observability**
|
||||
Add approvals, RBAC, tenant quotas, Notify metrics/logs/traces, dashboards, Notify-specific alerts, and Notify runbooks.
|
||||
- **Phase 5 – Offline & compliance**
|
||||
Produce Offline Kit bundles (rules/channels/deploy scripts), signed exports, retention policies, and auditing for regulated environments.
|
||||
|
||||
## Work breakdown
|
||||
- **Service & worker**
|
||||
- REST API for rules/channels/delivery history, idempotency middleware, digest scheduler.
|
||||
- Worker pipelines for event intake, rule matching, template rendering, delivery execution, retries, and throttling.
|
||||
- Delivery ledger capturing payload metadata, response, retry state, DSSE signatures.
|
||||
- **Connectors**
|
||||
- Slack/Teams/Email/Webhook plug-ins with configuration validation, rate limiting, error classification.
|
||||
- Secrets referenced via Authority/Secret store; no plaintext storage.
|
||||
- **Console & CLI**
|
||||
- Console module for rules builder, condition editor, preview, test send, delivery insights, digests and schedule configuration.
|
||||
- CLI (`stella notify rule|channel|delivery`) for automation, export/import.
|
||||
- **Integrations**
|
||||
- Event sources: Concelier, Excititor, Policy Engine, Vuln Explorer, Export Center, Attestor, Zastava, Scheduler.
|
||||
- Notify events to Notify (meta) for failure escalations, accepted-risk expiration reminders.
|
||||
- **Observability & ops**
|
||||
- Metrics: delivery success/failure, retry counts, throttle hits, digest generation, channel health.
|
||||
- Logs/traces with tenant, rule ID, channel, correlation ID; dashboards and alerts.
|
||||
- Runbooks for misconfigured channels, throttling, event backlog, incident digest.
|
||||
- **Docs & compliance**
|
||||
- Update Notifications Studio guides, channel runbooks, security/RBAC docs, Offline Kit instructions.
|
||||
- Provide compliance checklist (audit logging, retention, opt-out).
|
||||
|
||||
## Acceptance criteria
|
||||
- Rules evaluate deterministically per event; deliveries idempotent with audit trail and DSSE signatures.
|
||||
- Channel connectors support retries, rate limits, health checks, previews; secrets referenced securely.
|
||||
- Console/CLI support rule creation, testing, digests, delivery browsing, and export/import workflows.
|
||||
- Observability dashboards track delivery health; alerts fire for sustained failures or backlog; runbooks cover remediation.
|
||||
- Offline Kit bundle contains configs, rules, digests, and deployment scripts for air-gapped installs.
|
||||
- Notify respects tenancy and RBAC; governance (approvals, change log) enforced for high-impact rules.
|
||||
|
||||
## Risks & mitigations
|
||||
- **Notification storms:** throttling, digests, dedupe windows, preview/test gating.
|
||||
- **Secret compromise:** secret references only, rotation workflows, audit logging.
|
||||
- **Connector API changes:** versioned adapter layer, nightly health checks, fallback channels.
|
||||
- **Noise vs signal:** simulation previews, metrics, rule scoring, recommended defaults.
|
||||
- **Offline parity:** export/import of rules, connectors, and digests with signed manifests.
|
||||
|
||||
## Test strategy
|
||||
- **Unit:** rule evaluation, template rendering, connector clients, throttling, digests.
|
||||
- **Integration:** end-to-end events from core services, multi-channel routing, retries, audit logging.
|
||||
- **Performance:** burst throttling, digest creation, large rule sets.
|
||||
- **Security:** RBAC tests, tenant isolation, secret reference validation, DSSE signature verification.
|
||||
- **Offline:** export/import round-trips, Offline Kit deployment, manual delivery replay.
|
||||
|
||||
## Definition of done
|
||||
- Notify service, workers, connectors, Console/CLI, observability, and Offline Kit assets shipped with documentation and runbooks.
|
||||
- Compliance checklist appended to docs; ./TASKS.md and ../../TASKS.md updated with progress.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schemaVersion": "notify.channel@1",
|
||||
"channelId": "channel-slack-sec-ops",
|
||||
"tenantId": "tenant-01",
|
||||
"name": "slack:sec-ops",
|
||||
"type": "slack",
|
||||
"displayName": "SecOps Slack",
|
||||
"description": "Primary incident response channel.",
|
||||
"config": {
|
||||
"secretRef": "ref://notify/channels/slack/sec-ops",
|
||||
"target": "#sec-ops",
|
||||
"properties": {
|
||||
"workspace": "stellaops-sec"
|
||||
},
|
||||
"limits": {
|
||||
"concurrency": 2,
|
||||
"requestsPerMinute": 60,
|
||||
"timeout": "PT10S"
|
||||
}
|
||||
},
|
||||
"enabled": true,
|
||||
"labels": {
|
||||
"team": "secops"
|
||||
},
|
||||
"metadata": {
|
||||
"createdByTask": "NOTIFY-MODELS-15-102"
|
||||
},
|
||||
"createdBy": "ops:amir",
|
||||
"createdAt": "2025-10-18T17:02:11+00:00",
|
||||
"updatedBy": "ops:amir",
|
||||
"updatedAt": "2025-10-18T17:45:00+00:00"
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"deliveryId": "delivery-7f3b6c51",
|
||||
"tenantId": "tenant-acme",
|
||||
"ruleId": "rule-critical-slack",
|
||||
"actionId": "slack-secops",
|
||||
"eventId": "4f6e9c09-01b4-4c2a-8a57-3d06de182d74",
|
||||
"kind": "scanner.report.ready",
|
||||
"status": "Sent",
|
||||
"statusReason": null,
|
||||
"rendered": {
|
||||
"channelType": "Slack",
|
||||
"format": "Slack",
|
||||
"target": "#sec-alerts",
|
||||
"title": "Critical findings detected",
|
||||
"body": "{\"text\":\"Critical findings detected\",\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"*Critical findings detected*\\n1 new critical finding across 2 images.\"}},{\"type\":\"context\",\"elements\":[{\"type\":\"mrkdwn\",\"text\":\"Preview generated 2025-10-19T16:23:41.889Z · Trace `trace-58c212`\"}]}]}",
|
||||
"summary": "1 new critical finding across 2 images.",
|
||||
"textBody": "1 new critical finding across 2 images.\nTrace: trace-58c212",
|
||||
"locale": "en-us",
|
||||
"bodyHash": "febf9b2a630d862b07f4390edfbf31f5e8b836529f5232c491f4b3f6dba4a4b2",
|
||||
"attachments": []
|
||||
},
|
||||
"attempts": [
|
||||
{
|
||||
"timestamp": "2025-10-19T16:23:42.112Z",
|
||||
"status": "Succeeded",
|
||||
"statusCode": 200,
|
||||
"reason": null
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"channelType": "slack",
|
||||
"target": "#sec-alerts",
|
||||
"previewProvider": "fallback",
|
||||
"traceId": "trace-58c212",
|
||||
"slack.channel": "#sec-alerts"
|
||||
},
|
||||
"createdAt": "2025-10-19T16:23:41.889Z",
|
||||
"sentAt": "2025-10-19T16:23:42.101Z",
|
||||
"completedAt": "2025-10-19T16:23:42.112Z"
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
"continuationToken": "2025-10-19T16:23:41.889Z|tenant-acme:delivery-7f3b6c51"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"eventId": "8a8d6a2f-9315-49fe-9d52-8fec79ec7aeb",
|
||||
"kind": "scanner.report.ready",
|
||||
"version": "1",
|
||||
"tenant": "tenant-01",
|
||||
"ts": "2025-10-19T03:58:42+00:00",
|
||||
"actor": "scanner-webservice",
|
||||
"scope": {
|
||||
"namespace": "prod-payment",
|
||||
"repo": "ghcr.io/acme/api",
|
||||
"digest": "sha256:79c1f9e5...",
|
||||
"labels": {
|
||||
"environment": "production"
|
||||
},
|
||||
"attributes": {}
|
||||
},
|
||||
"payload": {
|
||||
"delta": {
|
||||
"kev": [
|
||||
"CVE-2025-40123"
|
||||
],
|
||||
"newCritical": 1,
|
||||
"newHigh": 2
|
||||
},
|
||||
"links": {
|
||||
"rekor": "https://rekor.stella.local/api/v1/log/entries/1",
|
||||
"ui": "https://ui.stella.local/reports/sha256-79c1f9e5"
|
||||
},
|
||||
"verdict": "fail"
|
||||
},
|
||||
"attributes": {
|
||||
"correlationId": "scan-23a6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"schemaVersion": "notify.rule@1",
|
||||
"ruleId": "rule-secops-critical",
|
||||
"tenantId": "tenant-01",
|
||||
"name": "Critical digests to SecOps",
|
||||
"description": "Escalate KEV-tagged findings to on-call feeds.",
|
||||
"enabled": true,
|
||||
"match": {
|
||||
"eventKinds": [
|
||||
"scanner.report.ready",
|
||||
"scheduler.rescan.delta"
|
||||
],
|
||||
"namespaces": [
|
||||
"prod-*"
|
||||
],
|
||||
"repositories": [],
|
||||
"digests": [],
|
||||
"labels": [],
|
||||
"componentPurls": [],
|
||||
"minSeverity": "high",
|
||||
"verdicts": [],
|
||||
"kevOnly": true,
|
||||
"vex": {
|
||||
"includeAcceptedJustifications": false,
|
||||
"includeRejectedJustifications": false,
|
||||
"includeUnknownJustifications": false,
|
||||
"justificationKinds": [
|
||||
"component-remediated",
|
||||
"not-affected"
|
||||
]
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"actionId": "email-digest",
|
||||
"channel": "email:soc",
|
||||
"digest": "hourly",
|
||||
"template": "digest",
|
||||
"enabled": true,
|
||||
"metadata": {
|
||||
"locale": "en-us"
|
||||
}
|
||||
},
|
||||
{
|
||||
"actionId": "slack-oncall",
|
||||
"channel": "slack:sec-ops",
|
||||
"template": "concise",
|
||||
"throttle": "PT5M",
|
||||
"metadata": {},
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"labels": {
|
||||
"team": "secops"
|
||||
},
|
||||
"metadata": {
|
||||
"source": "sprint-15"
|
||||
},
|
||||
"createdBy": "ops:zoya",
|
||||
"createdAt": "2025-10-19T04:12:27+00:00",
|
||||
"updatedBy": "ops:zoya",
|
||||
"updatedAt": "2025-10-19T04:45:03+00:00"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"schemaVersion": "notify.template@1",
|
||||
"templateId": "tmpl-slack-concise",
|
||||
"tenantId": "tenant-01",
|
||||
"channelType": "slack",
|
||||
"key": "concise",
|
||||
"locale": "en-us",
|
||||
"body": "{{severity_icon payload.delta.newCritical}} {{summary}}",
|
||||
"description": "Slack concise message for high severity findings.",
|
||||
"renderMode": "markdown",
|
||||
"format": "slack",
|
||||
"metadata": {
|
||||
"version": "2025-10-19"
|
||||
},
|
||||
"createdBy": "ops:zoya",
|
||||
"createdAt": "2025-10-19T05:00:00+00:00",
|
||||
"updatedBy": "ops:zoya",
|
||||
"updatedAt": "2025-10-19T05:45:00+00:00"
|
||||
}
|
||||
73
docs/modules/notify/resources/schemas/notify-channel@1.json
Normal file
73
docs/modules/notify/resources/schemas/notify-channel@1.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"$id": "https://stella-ops.org/schemas/notify/notify-channel@1.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Notify Channel",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"channelId",
|
||||
"tenantId",
|
||||
"name",
|
||||
"type",
|
||||
"config",
|
||||
"enabled",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": {"type": "string", "const": "notify.channel@1"},
|
||||
"channelId": {"type": "string"},
|
||||
"tenantId": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["slack", "teams", "email", "webhook", "custom"]
|
||||
},
|
||||
"displayName": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"config": {"$ref": "#/$defs/channelConfig"},
|
||||
"enabled": {"type": "boolean"},
|
||||
"labels": {"$ref": "#/$defs/stringMap"},
|
||||
"metadata": {"$ref": "#/$defs/stringMap"},
|
||||
"createdBy": {"type": "string"},
|
||||
"createdAt": {"type": "string", "format": "date-time"},
|
||||
"updatedBy": {"type": "string"},
|
||||
"updatedAt": {"type": "string", "format": "date-time"}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"channelConfig": {
|
||||
"type": "object",
|
||||
"required": ["secretRef"],
|
||||
"properties": {
|
||||
"secretRef": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"endpoint": {"type": "string", "format": "uri"},
|
||||
"properties": {"$ref": "#/$defs/stringMap"},
|
||||
"limits": {"$ref": "#/$defs/channelLimits"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"channelLimits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concurrency": {"type": "integer", "minimum": 1},
|
||||
"requestsPerMinute": {"type": "integer", "minimum": 1},
|
||||
"timeout": {
|
||||
"type": "string",
|
||||
"pattern": "^P(T.*)?$",
|
||||
"description": "ISO 8601 duration"
|
||||
},
|
||||
"maxBatchSize": {"type": "integer", "minimum": 1}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"stringMap": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
56
docs/modules/notify/resources/schemas/notify-event@1.json
Normal file
56
docs/modules/notify/resources/schemas/notify-event@1.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"$id": "https://stella-ops.org/schemas/notify/notify-event@1.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Notify Event Envelope",
|
||||
"type": "object",
|
||||
"required": ["eventId", "kind", "tenant", "ts", "payload"],
|
||||
"properties": {
|
||||
"eventId": {"type": "string", "format": "uuid"},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "Event kind identifier (e.g. scanner.report.ready).",
|
||||
"enum": [
|
||||
"scanner.report.ready",
|
||||
"scanner.scan.completed",
|
||||
"scheduler.rescan.delta",
|
||||
"attestor.logged",
|
||||
"zastava.admission",
|
||||
"feedser.export.completed",
|
||||
"vexer.export.completed"
|
||||
]
|
||||
},
|
||||
"version": {"type": "string"},
|
||||
"tenant": {"type": "string"},
|
||||
"ts": {"type": "string", "format": "date-time"},
|
||||
"actor": {"type": "string"},
|
||||
"scope": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"namespace": {"type": "string"},
|
||||
"repo": {"type": "string"},
|
||||
"digest": {"type": "string"},
|
||||
"component": {"type": "string"},
|
||||
"image": {"type": "string"},
|
||||
"labels": {"$ref": "#/$defs/stringMap"},
|
||||
"attributes": {"$ref": "#/$defs/stringMap"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"description": "Event specific body; see individual schemas for shapes.",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"attributes": {"$ref": "#/$defs/stringMap"}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"stringMap": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
96
docs/modules/notify/resources/schemas/notify-rule@1.json
Normal file
96
docs/modules/notify/resources/schemas/notify-rule@1.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"$id": "https://stella-ops.org/schemas/notify/notify-rule@1.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Notify Rule",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"ruleId",
|
||||
"tenantId",
|
||||
"name",
|
||||
"enabled",
|
||||
"match",
|
||||
"actions",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": {"type": "string", "const": "notify.rule@1"},
|
||||
"ruleId": {"type": "string"},
|
||||
"tenantId": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"enabled": {"type": "boolean"},
|
||||
"match": {"$ref": "#/$defs/ruleMatch"},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"$ref": "#/$defs/ruleAction"}
|
||||
},
|
||||
"labels": {"$ref": "#/$defs/stringMap"},
|
||||
"metadata": {"$ref": "#/$defs/stringMap"},
|
||||
"createdBy": {"type": "string"},
|
||||
"createdAt": {"type": "string", "format": "date-time"},
|
||||
"updatedBy": {"type": "string"},
|
||||
"updatedAt": {"type": "string", "format": "date-time"}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"ruleMatch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eventKinds": {"$ref": "#/$defs/stringArray"},
|
||||
"namespaces": {"$ref": "#/$defs/stringArray"},
|
||||
"repositories": {"$ref": "#/$defs/stringArray"},
|
||||
"digests": {"$ref": "#/$defs/stringArray"},
|
||||
"labels": {"$ref": "#/$defs/stringArray"},
|
||||
"componentPurls": {"$ref": "#/$defs/stringArray"},
|
||||
"minSeverity": {"type": "string"},
|
||||
"verdicts": {"$ref": "#/$defs/stringArray"},
|
||||
"kevOnly": {"type": "boolean"},
|
||||
"vex": {"$ref": "#/$defs/ruleMatchVex"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ruleMatchVex": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"includeAcceptedJustifications": {"type": "boolean"},
|
||||
"includeRejectedJustifications": {"type": "boolean"},
|
||||
"includeUnknownJustifications": {"type": "boolean"},
|
||||
"justificationKinds": {"$ref": "#/$defs/stringArray"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ruleAction": {
|
||||
"type": "object",
|
||||
"required": ["actionId", "channel", "enabled"],
|
||||
"properties": {
|
||||
"actionId": {"type": "string"},
|
||||
"channel": {"type": "string"},
|
||||
"template": {"type": "string"},
|
||||
"digest": {"type": "string"},
|
||||
"throttle": {
|
||||
"type": "string",
|
||||
"pattern": "^P(T.*)?$",
|
||||
"description": "ISO 8601 duration"
|
||||
},
|
||||
"locale": {"type": "string"},
|
||||
"enabled": {"type": "boolean"},
|
||||
"metadata": {"$ref": "#/$defs/stringMap"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"stringArray": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"}
|
||||
},
|
||||
"stringMap": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
55
docs/modules/notify/resources/schemas/notify-template@1.json
Normal file
55
docs/modules/notify/resources/schemas/notify-template@1.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"$id": "https://stella-ops.org/schemas/notify/notify-template@1.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Notify Template",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"templateId",
|
||||
"tenantId",
|
||||
"channelType",
|
||||
"key",
|
||||
"locale",
|
||||
"body",
|
||||
"renderMode",
|
||||
"format",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": {"type": "string", "const": "notify.template@1"},
|
||||
"templateId": {"type": "string"},
|
||||
"tenantId": {"type": "string"},
|
||||
"channelType": {
|
||||
"type": "string",
|
||||
"enum": ["slack", "teams", "email", "webhook", "custom"]
|
||||
},
|
||||
"key": {"type": "string"},
|
||||
"locale": {"type": "string"},
|
||||
"body": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"renderMode": {
|
||||
"type": "string",
|
||||
"enum": ["markdown", "html", "adaptiveCard", "plainText", "json"]
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": ["slack", "teams", "email", "webhook", "json"]
|
||||
},
|
||||
"metadata": {"$ref": "#/$defs/stringMap"},
|
||||
"createdBy": {"type": "string"},
|
||||
"createdAt": {"type": "string", "format": "date-time"},
|
||||
"updatedBy": {"type": "string"},
|
||||
"updatedAt": {"type": "string", "format": "date-time"}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"stringMap": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user