up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-28 00:45:16 +02:00
parent 3b96b2e3ea
commit 1c6730a1d2
95 changed files with 14504 additions and 463 deletions

View File

@@ -0,0 +1,88 @@
# advisory.linkset.updated@1 · Event contract
Purpose: unblock CONCELIER-LNM-21-005 by freezing the platform event shape for linkset changes emitted by Concelier. This is the only supported event for linkset churn; downstreams subscribe for graph overlays, policy evaluations, and replay bundles.
## Envelope & transport
- Subject: `concelier.advisory.linkset.updated.v1`
- Type/version: `advisory.linkset.updated@1`
- Transport: NATS (primary), Redis Stream `concelier:advisory.linkset.updated:v1` (fallback). Both carry the same DSSE envelope.
- DSSE payloadType: `application/vnd.stellaops.advisory.linkset.updated.v1+json`.
- Signature: Ed25519 via Platform Events signer; attach Rekor UUID when available. Offline kits treat the envelope as the source of truth.
## Payload (JSON)
| Field | Type | Rules |
| --- | --- | --- |
| `eventId` | string (uuid) | Generated by publisher; idempotency key. |
| `tenantId` | string | `urn:tenant:{uuid}`; required for multi-tenant routing. |
| `linksetId` | string (ObjectId) | Mongo `_id` of the linkset document. |
| `advisoryId` | string | Upstream advisory identifier (e.g., CVE, GHSA, vendor id). |
| `source` | string | Linkset source/adapter identifier (lowercase). |
| `observationIds` | string[] | Array of observation ObjectIds included in this linkset; sorted ASCII. |
| `delta` | object | Change description: `{ type, observationsAdded, observationsRemoved, confidenceChanged, conflictsChanged }`. |
| `confidence` | number (01, nullable) | Correlation confidence score; null if not computed. |
| `conflicts` | object[] | Array of `{ field, reason, sourceIds[] }` conflict summaries; sorted by field then reason. |
| `provenance` | object | `{ observationHashes[], toolVersion?, policyHash? }` for replay/audit. |
| `createdAt` | string (ISO-8601 UTC) | Timestamp when linkset was built. |
| `replayCursor` | string | Monotone cursor for offline bundle ordering (tick from createdAt). |
| `builtByJobId` | string (optional) | Job ID that built this linkset. |
| `traceId` | string (optional) | Propagated from ingest job/request; aids join with logs/metrics. |
### Delta object
| Field | Type | Rules |
| --- | --- | --- |
| `type` | string | `"created"` or `"updated"`. |
| `observationsAdded` | string[] | Observation IDs added since previous version. |
| `observationsRemoved` | string[] | Observation IDs removed since previous version. |
| `confidenceChanged` | boolean | True if confidence score changed. |
| `conflictsChanged` | boolean | True if conflicts array changed. |
### Determinism & ordering
- Arrays sorted ASCII; objects field-sorted when hashing.
- `eventId` + `replayCursor` provide exactly-once consumer handling; duplicates must be ignored when observation hashes unchanged.
- No judgments: only raw facts, delta descriptions, and provenance pointers; any derived severity/merge content is forbidden.
### Error contracts for Scheduler
- Retryable NATS/Redis failures use backoff capped at 30s; after 5 attempts, emit `concelier.events.dlq` with the same envelope and `error` field describing transport failure.
- Consumers must NACK on schema validation failure; publisher logs `ERR_EVENT_SCHEMA` and quarantines the offending linkset id.
## Sample payload
```json
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"tenantId": "urn:tenant:acme-corp",
"linksetId": "6744abcd1234567890abcdef",
"advisoryId": "CVE-2024-1234",
"source": "nvd",
"observationIds": ["674400001234567890abcdef", "674400001234567890abcde0"],
"delta": {
"type": "created",
"observationsAdded": ["674400001234567890abcdef", "674400001234567890abcde0"],
"observationsRemoved": [],
"confidenceChanged": true,
"conflictsChanged": false
},
"confidence": 0.85,
"conflicts": [],
"provenance": {
"observationHashes": ["sha256:abc123", "sha256:def456"],
"toolVersion": "1.0.0",
"policyHash": null
},
"createdAt": "2025-11-27T12:00:00.000Z",
"replayCursor": "638688000000000000",
"builtByJobId": "job-12345",
"traceId": "trace-67890"
}
```
## Schema
`advisory.linkset.updated@1.schema.json` provides a JSON Schema (draft 2020-12) for runtime validation; any additional fields are rejected.
## Implementation (LNM-21-005)
- Event type defined in `StellaOps.Concelier.Core.Linksets.AdvisoryLinksetUpdatedEvent`.
- Publisher interface: `IAdvisoryLinksetEventPublisher`.
- Outbox interface: `IAdvisoryLinksetEventOutbox`.
- Configuration: `AdvisoryLinksetEventPublisherOptions`.
## Change control
- Add-only. Adjusting delta types or conflict codes requires new version `advisory.linkset.updated@2` and a sprint note.

View File

@@ -78,6 +78,33 @@ _Frozen v1 (add-only) — approved 2025-11-17 for CONCELIER-LNM-21-001/002/101._
- **Deterministic keying:** `_id` derived from `hash(tenantId|source|advisoryId|provenance.sourceArtifactSha)` to keep inserts idempotent in replay.
- **Normalization guardrails:** version ranges must be stored as raw-from-source; no inferred merges.
## Append-Only Contract (AOC) — LNM-21-004
The Aggregation-Only Contract (AOC) ensures observations are immutable after creation. This is enforced by `IAdvisoryObservationWriteGuard`.
### Write disposition rules
| Existing Hash | New Hash | Disposition | Action |
|--------------|----------|-------------|--------|
| null/empty | any | `Proceed` | Insert new observation |
| X | X (identical) | `SkipIdentical` | Idempotent re-insert, no write |
| X | Y (different) | `RejectMutation` | Reject with `AppendOnlyViolationException` |
### Supersession model
When an advisory source publishes a revised version of an advisory:
1. A **new observation** is created with its own unique `observationId` and `contentHash`.
2. The new observation MAY carry a `supersedesId` pointing to the previous observation.
3. The **original observation remains immutable** — it is never updated or deleted.
4. Linksets are rebuilt to include all non-superseded observations; superseded observations remain queryable for audit but excluded from active linkset aggregation.
### Implementation checklist (LNM-21-004)
- [x] `IAdvisoryObservationWriteGuard` interface with `ValidateWrite(observation, existingContentHash)` method.
- [x] `AdvisoryObservationWriteGuard` implementation enforcing append-only semantics.
- [x] `AppendOnlyViolationException` for mutation rejections.
- [x] DI registration via `AddConcelierAocGuards()` extension.
- [x] Unit tests covering Proceed/SkipIdentical/RejectMutation scenarios.
- [x] Legacy merge logic deprecated with `[Obsolete]` and gated by `NoMergeEnabled` feature flag (defaults to `true`).
- [x] Roslyn analyzer `StellaOps.Concelier.Analyzers.NoMergeApiAnalyzer` emits warnings for merge API usage.
## Linkset document
```json
{