# Scanner Orchestrator Events (ORCH-SVC-38-101) Last updated: 2025-10-26 The Notifications Studio initiative (NOTIFY-SVC-38-001) and orchestrator backlog (ORCH-SVC-38-101) standardise how platform services emit lifecycle events. This document describes the Scanner WebService contract for the new **orchestrator envelopes** (`scanner.event.*`) and how they supersede the legacy Redis-backed `scanner.report.ready` / `scanner.scan.completed` events. ## 1. Envelope overview Orchestrator events share a deterministic JSON envelope: | Field | Type | Notes | |-------|------|-------| | `eventId` | `uuid` | Globally unique identifier generated per occurrence. | | `kind` | `string` | Event identifier; Scanner emits `scanner.event.report.ready` and `scanner.event.scan.completed`. | | `version` | `integer` | Schema version. Initial release uses `1`. | | `tenant` | `string` | Tenant that owns the scan/report. Mirrors Authority claims. | | `occurredAt` | `date-time` | UTC instant when the underlying state transition happened (e.g., report persisted). | | `recordedAt` | `date-time` | UTC instant when the event was durably written. Optional but recommended. | | `source` | `string` | Producer identifier (`scanner.webservice`). | | `idempotencyKey` | `string` | Deterministic key for duplicate suppression (see §4). | | `correlationId` | `string` | Maps back to the API request or scan identifier. | | `traceId` / `spanId` | `string` | W3C trace context propagated into downstream telemetry. | | `scope` | `object` | Describes the affected artefact. Requires `repo` and `digest`; optional `namespace`, `component`, `image`. | | `attributes` | `object` | Flat string map for frequently queried metadata (e.g., policy revision). | | `payload` | `object` | Event-specific body (see §2). | Canonical schemas live under `docs/events/scanner.event.*@1.json`. Samples that round-trip through `NotifyCanonicalJsonSerializer` are stored in `docs/events/samples/`. ## 2. Event kinds and payloads ### 2.1 `scanner.event.report.ready` Emitted once a signed report is persisted and attested. Payload highlights: - `reportId` / `scanId` — identifiers for the persisted report and originating scan. Until Scan IDs are surfaced by the API, `scanId` mirrors `reportId` so downstream correlators can stabilise on a single key. - **Attributes:** `reportId`, `policyRevisionId`, `policyDigest`, `verdict` — pre-sorted for deterministic routing. - **Links:** - `ui` → `/ui/reports/{reportId}` on the current host. - `report` → `{apiBasePath}/{reportsSegment}/{reportId}` (defaults to `/api/v1/reports/{reportId}`). - `policy` → `{apiBasePath}/{policySegment}/revisions/{revisionId}` when a revision is present. - `attestation` → `/ui/attestations/{reportId}` when a DSSE envelope is included. - `imageDigest` — OCI image digest associated with the analysis. - `generatedAt` — report generation timestamp (ISO-8601 UTC). - `verdict` — `pass`, `warn`, or `fail` after policy evaluation. - `summary` — blocked/warned/ignored/quieted counters (all non-negative integers). - `delta` — newly critical/high counts and optional `kev` array. - `quietedFindingCount` — mirrors `summary.quieted`. - `policy` — revision metadata (`digest`, `revisionId`) surfaced for routing. - `links` — UI/report/policy URLs suitable for operators. - `dsse` — embedded DSSE envelope (payload, type, signature list). - `report` — canonical report document; identical to the DSSE payload. Schema: `docs/events/scanner.event.report.ready@1.json` Sample: `docs/events/samples/scanner.event.report.ready@1.sample.json` ### 2.2 `scanner.event.scan.completed` Emitted after scan execution finishes (success or policy failure). Payload highlights: - `reportId` / `scanId` / `imageDigest` — identifiers mirroring the report-ready event. As with the report-ready payload, `scanId` currently mirrors `reportId` as a temporary shim. - **Attributes:** `reportId`, `policyRevisionId`, `policyDigest`, `verdict`. - **Links:** same as above (`ui`, `report`, `policy`) with `attestation` populated when DSSE metadata exists. - `verdict`, `summary`, `delta`, `policy` — same semantics as above. - `findings` — array of surfaced findings with `id`, `severity`, optional `cve`, `purl`, and `reachability`. - `links`, `dsse`, `report` — same structure as §2.1 (allows Notifier to reuse signatures). Schema: `docs/events/scanner.event.scan.completed@1.json` Sample: `docs/events/samples/scanner.event.scan.completed@1.sample.json` ### 2.3 Relationship to legacy events | Legacy Redis event | Replacement orchestrator event | Notes | |--------------------|-------------------------------|-------| | `scanner.report.ready` | `scanner.event.report.ready` | Adds versioning, idempotency, trace context. Payload is a superset of the legacy fields. | | `scanner.scan.completed` | `scanner.event.scan.completed` | Same data plus explicit scan identifiers and orchestrator metadata. | Legacy schemas remain for backwards-compatibility during migration, but new integrations **must** target the orchestrator variants. ## 3. Deterministic serialization - Producers must serialise events using `NotifyCanonicalJsonSerializer` to guarantee consistent key ordering and whitespace. - Timestamps (`occurredAt`, `recordedAt`, `payload.generatedAt`) use `DateTimeOffset.UtcDateTime.ToString("O")`. - Payload arrays (`delta.kev`, `findings`) should be pre-sorted (e.g., alphabetical CVE order) so hash-based consumers remain stable. - Optional fields are omitted rather than emitted as `null`. ## 4. Idempotency and correlation Idempotency keys dedupe repeated publishes and align with the orchestrator’s outbox pattern: | Event kind | Idempotency key template | |------------|-------------------------| | `scanner.event.report.ready` | `scanner.event.report.ready::` | | `scanner.event.scan.completed` | `scanner.event.scan.completed::` | Keys are ASCII lowercase; components should be trimmed and validated before concatenation. Retries must reuse the same key. `correlationId` should match the scan identifier that appears in REST responses (`scanId`). Re-using the same value across the pair of events allows Notifier and orchestrator analytics to stitch lifecycle data together. ## 5. Versioning and evolution - Increment the `version` field and the `@` suffix for **breaking** changes (field removals, type changes, semantic shifts). - Additive optional fields may remain within version 1; update the JSON schema and samples accordingly. - When introducing `@2`, keep the `@1` schema/docs in place until orchestrator subscribers confirm migration. ## 6. Consumer checklist 1. Validate incoming payloads against the schema for the targeted version. 2. Use `idempotencyKey` for dedupe, not `eventId`. 3. Map `traceId`/`spanId` into telemetry spans to preserve causality. 4. Prefer `payload.report` → `policy.revisionId` when populating templates; the top-level `attributes` are convenience duplicates for quick routing. 5. Reserve the legacy Redis events for transitional compatibility only; downstream systems should subscribe to the orchestrator bus exposed by ORCH-SVC-38-101. ## 7. Implementation status and next actions - **Scanner WebService** — `SCANNER-EVENTS-16-301` (blocked) and `SCANNER-EVENTS-16-302` (doing) track the production of these envelopes. The remaining blocker is the .NET 10 preview OpenAPI/Auth dependency drift that currently breaks `dotnet test`. Once Gateway and Notifier owners land the replacement packages, rerun the full test suite and capture fresh fixtures under `docs/events/samples/`. - **Gateway/Notifier consumers** — subscribe to the orchestrator stream documented in ORCH-SVC-38-101. When the Scanner tasks unblock, regenerate notifier contract tests against the sample events included here. - **Docs cadence** — update this file and the matching JSON schemas whenever payload fields change. Use the rehearsal checklist in `docs/ops/launch-cutover.md` to confirm downstream validation before the production cutover. Record gaps or newly required fields in `docs/ops/launch-readiness.md` so they land in the launch checklist. --- **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.