Add MergeUsageAnalyzer to detect legacy merge service usage
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented MergeUsageAnalyzer to flag usage of AdvisoryMergeService and AddMergeModule. - Created AnalyzerReleases.Shipped.md and AnalyzerReleases.Unshipped.md for release documentation. - Added tests for MergeUsageAnalyzer to ensure correct diagnostics for various scenarios. - Updated project files for analyzers and tests to include necessary dependencies and configurations. - Introduced a sample report structure for scanner output.
This commit is contained in:
72
NuGet.config
72
NuGet.config
@@ -1,25 +1,25 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<config>
|
<config>
|
||||||
<add key="restoreIgnoreFailedSources" value="true" />
|
<add key="restoreIgnoreFailedSources" value="true" />
|
||||||
</config>
|
</config>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<add key="local" value="local-nuget" />
|
<add key="local" value="local-nuget" />
|
||||||
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
|
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
|
||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
<packageSourceMapping>
|
<packageSourceMapping>
|
||||||
<packageSource key="local">
|
<packageSource key="local">
|
||||||
<package pattern="Mongo2Go" />
|
<package pattern="Mongo2Go" />
|
||||||
<package pattern="Microsoft.IdentityModel.Tokens" />
|
<package pattern="Microsoft.IdentityModel.Tokens" />
|
||||||
<package pattern="Microsoft.Extensions.Http.Polly" />
|
<package pattern="Microsoft.Extensions.Http.Polly" />
|
||||||
<package pattern="Microsoft.Extensions.Caching.Memory" />
|
<package pattern="Microsoft.Extensions.Caching.Memory" />
|
||||||
<package pattern="Microsoft.Extensions.Configuration" />
|
<package pattern="Microsoft.Extensions.Configuration" />
|
||||||
<package pattern="Microsoft.Extensions.Configuration.Binder" />
|
<package pattern="Microsoft.Extensions.Configuration.Binder" />
|
||||||
<package pattern="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
<package pattern="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||||
<package pattern="Microsoft.Extensions.Hosting" />
|
<package pattern="Microsoft.Extensions.Hosting" />
|
||||||
<package pattern="Microsoft.Extensions.Hosting.Abstractions" />
|
<package pattern="Microsoft.Extensions.Hosting.Abstractions" />
|
||||||
<package pattern="Microsoft.Extensions.Http" />
|
<package pattern="Microsoft.Extensions.Http" />
|
||||||
<package pattern="Microsoft.Extensions.Logging.Abstractions" />
|
<package pattern="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
<package pattern="Microsoft.Extensions.Options" />
|
<package pattern="Microsoft.Extensions.Options" />
|
||||||
@@ -28,9 +28,23 @@
|
|||||||
<package pattern="Microsoft.IdentityModel.Logging" />
|
<package pattern="Microsoft.IdentityModel.Logging" />
|
||||||
<package pattern="Microsoft.IdentityModel.Abstractions" />
|
<package pattern="Microsoft.IdentityModel.Abstractions" />
|
||||||
<package pattern="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
<package pattern="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
<package pattern="Google.Protobuf" />
|
<package pattern="Google.Protobuf" />
|
||||||
|
<package pattern="Microsoft.CodeAnalysis.*" />
|
||||||
|
<package pattern="Microsoft.Bcl.AsyncInterfaces" />
|
||||||
|
<package pattern="Humanizer.Core" />
|
||||||
|
<package pattern="System.Collections.Immutable" />
|
||||||
|
<package pattern="System.Composition*" />
|
||||||
|
<package pattern="System.IO.Pipelines" />
|
||||||
|
<package pattern="System.Memory" />
|
||||||
|
<package pattern="System.Numerics.Vectors" />
|
||||||
|
<package pattern="System.Reflection.Metadata" />
|
||||||
|
<package pattern="System.Runtime.CompilerServices.Unsafe" />
|
||||||
|
<package pattern="System.Text.Encoding.CodePages" />
|
||||||
|
<package pattern="System.Threading.Channels" />
|
||||||
|
<package pattern="System.Threading.Tasks.Extensions" />
|
||||||
|
<package pattern="NETStandard.Library" />
|
||||||
<package pattern="Grpc.*" />
|
<package pattern="Grpc.*" />
|
||||||
</packageSource>
|
</packageSource>
|
||||||
<packageSource key="dotnet-public">
|
<packageSource key="dotnet-public">
|
||||||
<package pattern="Microsoft.Extensions.*" />
|
<package pattern="Microsoft.Extensions.*" />
|
||||||
<package pattern="Microsoft.AspNetCore.*" />
|
<package pattern="Microsoft.AspNetCore.*" />
|
||||||
@@ -38,8 +52,8 @@
|
|||||||
<package pattern="Microsoft.OpenApi*" />
|
<package pattern="Microsoft.OpenApi*" />
|
||||||
<package pattern="System.Diagnostics.*" />
|
<package pattern="System.Diagnostics.*" />
|
||||||
</packageSource>
|
</packageSource>
|
||||||
<packageSource key="nuget.org">
|
<packageSource key="nuget.org">
|
||||||
<package pattern="*" />
|
<package pattern="*" />
|
||||||
</packageSource>
|
</packageSource>
|
||||||
</packageSourceMapping>
|
</packageSourceMapping>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -22,3 +22,4 @@ Document follow-up actions for CONCELIER-CORE-AOC-19-004 as we unwind the final
|
|||||||
- 2025-11-05: Catalogued residual normalization paths tied to the legacy Merge service and outlined `noMergeEnabled` feature-toggle work to keep AOC ingestion fully merge-free.
|
- 2025-11-05: Catalogued residual normalization paths tied to the legacy Merge service and outlined `noMergeEnabled` feature-toggle work to keep AOC ingestion fully merge-free.
|
||||||
- 2025-11-05 19:20Z: Observation factory/linkset now preserve upstream ordering and duplicates; canonicalisation shifts to downstream services.
|
- 2025-11-05 19:20Z: Observation factory/linkset now preserve upstream ordering and duplicates; canonicalisation shifts to downstream services.
|
||||||
- 2025-11-06: Documented post-merge rollout plan and annotated sprint trackers with analyzer gating updates.
|
- 2025-11-06: Documented post-merge rollout plan and annotated sprint trackers with analyzer gating updates.
|
||||||
|
- 2025-11-06 23:30Z: Concelier core/linkset query paths now keep alias/reference casing & whitespace intact; alias filters switched to case-insensitive regex so raw data and lookups remain compatible.
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
# Scanner Orchestrator Events (ORCH-SVC-38-101)
|
# Scanner Orchestrator Events (ORCH-SVC-38-101)
|
||||||
|
|
||||||
Last updated: 2025-10-26
|
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.
|
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
|
## 1. Envelope overview
|
||||||
|
|
||||||
Orchestrator events share a deterministic JSON envelope:
|
Orchestrator events share a deterministic JSON envelope:
|
||||||
|
|
||||||
| Field | Type | Notes |
|
| Field | Type | Notes |
|
||||||
|-------|------|-------|
|
|-------|------|-------|
|
||||||
| `eventId` | `uuid` | Globally unique identifier generated per occurrence. |
|
| `eventId` | `uuid` | Globally unique identifier generated per occurrence. |
|
||||||
| `kind` | `string` | Event identifier; Scanner emits `scanner.event.report.ready` and `scanner.event.scan.completed`. |
|
| `kind` | `string` | Event identifier; Scanner emits `scanner.event.report.ready` and `scanner.event.scan.completed`. |
|
||||||
| `version` | `integer` | Schema version. Initial release uses `1`. |
|
| `version` | `integer` | Schema version. Initial release uses `1`. |
|
||||||
| `tenant` | `string` | Tenant that owns the scan/report. Mirrors Authority claims. |
|
| `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). |
|
| `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. |
|
| `recordedAt` | `date-time` | UTC instant when the event was durably written. Optional but recommended. |
|
||||||
| `source` | `string` | Producer identifier (`scanner.webservice`). |
|
| `source` | `string` | Producer identifier (`scanner.webservice`). |
|
||||||
| `idempotencyKey` | `string` | Deterministic key for duplicate suppression (see §4). |
|
| `idempotencyKey` | `string` | Deterministic key for duplicate suppression (see §4). |
|
||||||
| `correlationId` | `string` | Maps back to the API request or scan identifier. |
|
| `correlationId` | `string` | Maps back to the API request or scan identifier. |
|
||||||
| `traceId` / `spanId` | `string` | W3C trace context propagated into downstream telemetry. |
|
| `traceId` / `spanId` | `string` | W3C trace context propagated into downstream telemetry. |
|
||||||
| `scope` | `object` | Describes the affected artefact. Requires `repo` and `digest`; optional `namespace`, `component`, `image`. |
|
| `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). |
|
| `attributes` | `object` | Flat string map for frequently queried metadata (e.g., policy revision). |
|
||||||
| `payload` | `object` | Event-specific body (see §2). |
|
| `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/`.
|
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. Event kinds and payloads
|
||||||
|
|
||||||
### 2.1 `scanner.event.report.ready`
|
### 2.1 `scanner.event.report.ready`
|
||||||
|
|
||||||
Emitted once a signed report is persisted and attested. Payload highlights:
|
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.
|
- `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.
|
- **Attributes:** `reportId`, `policyRevisionId`, `policyDigest`, `verdict` — pre-sorted for deterministic routing.
|
||||||
- **Links:**
|
- **Links:**
|
||||||
- `report.ui` → `/ui/reports/{reportId}` on the current host.
|
- `report.ui` → `/ui/reports/{reportId}` on the current host.
|
||||||
- `report.api` → `{apiBasePath}/{reportsSegment}/{reportId}` (defaults to `/api/v1/reports/{reportId}`).
|
- `report.api` → `{apiBasePath}/{reportsSegment}/{reportId}` (defaults to `/api/v1/reports/{reportId}`).
|
||||||
@@ -41,83 +41,84 @@ Emitted once a signed report is persisted and attested. Payload highlights:
|
|||||||
- `policy.api` → `{apiBasePath}/{policySegment}/revisions/{revisionId}` when a revision is present.
|
- `policy.api` → `{apiBasePath}/{policySegment}/revisions/{revisionId}` when a revision is present.
|
||||||
- `attestation.ui` → `/ui/attestations/{reportId}` when a DSSE envelope is included.
|
- `attestation.ui` → `/ui/attestations/{reportId}` when a DSSE envelope is included.
|
||||||
- `attestation.api` → `{apiBasePath}/{reportsSegment}/{reportId}/attestation` when a DSSE envelope is included.
|
- `attestation.api` → `{apiBasePath}/{reportsSegment}/{reportId}/attestation` when a DSSE envelope is included.
|
||||||
- `imageDigest` — OCI image digest associated with the analysis.
|
- UI routes honour the configurable `scanner:console` options (`basePath`, `reportsSegment`, `policySegment`, `attestationsSegment`) so operators can move links under `/console` without code changes.
|
||||||
- `generatedAt` — report generation timestamp (ISO-8601 UTC).
|
- `imageDigest` — OCI image digest associated with the analysis.
|
||||||
- `verdict` — `pass`, `warn`, or `fail` after policy evaluation.
|
- `generatedAt` — report generation timestamp (ISO-8601 UTC).
|
||||||
- `summary` — blocked/warned/ignored/quieted counters (all non-negative integers).
|
- `verdict` — `pass`, `warn`, or `fail` after policy evaluation.
|
||||||
- `delta` — newly critical/high counts and optional `kev` array.
|
- `summary` — blocked/warned/ignored/quieted counters (all non-negative integers).
|
||||||
- `quietedFindingCount` — mirrors `summary.quieted`.
|
- `delta` — newly critical/high counts and optional `kev` array.
|
||||||
- `policy` — revision metadata (`digest`, `revisionId`) surfaced for routing.
|
- `quietedFindingCount` — mirrors `summary.quieted`.
|
||||||
- `links` — UI/report/policy URLs suitable for operators.
|
- `policy` — revision metadata (`digest`, `revisionId`) surfaced for routing.
|
||||||
- `dsse` — embedded DSSE envelope (payload, type, signature list).
|
- `links` — UI/report/policy URLs suitable for operators.
|
||||||
- `report` — canonical report document; identical to the DSSE payload.
|
- `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`
|
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`
|
|
||||||
|
### 2.2 `scanner.event.scan.completed`
|
||||||
Emitted after scan execution finishes (success or policy failure). Payload highlights:
|
|
||||||
|
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`.
|
- `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 (`report.*`, `policy.*`) with `attestation.*` populated when DSSE metadata exists.
|
- **Links:** same as above (`report.*`, `policy.*`) with `attestation.*` populated when DSSE metadata exists.
|
||||||
- `verdict`, `summary`, `delta`, `policy` — same semantics as above.
|
- `verdict`, `summary`, `delta`, `policy` — same semantics as above.
|
||||||
- `findings` — array of surfaced findings with `id`, `severity`, optional `cve`, `purl`, and `reachability`.
|
- `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).
|
- `links`, `dsse`, `report` — same structure as §2.1 (allows Notifier to reuse signatures).
|
||||||
|
|
||||||
Schema: `docs/events/scanner.event.scan.completed@1.json`
|
Schema: `docs/events/scanner.event.scan.completed@1.json`
|
||||||
Sample: `docs/events/samples/scanner.event.scan.completed@1.sample.json`
|
Sample: `docs/events/samples/scanner.event.scan.completed@1.sample.json`
|
||||||
|
|
||||||
### 2.3 Relationship to legacy events
|
### 2.3 Relationship to legacy events
|
||||||
|
|
||||||
| Legacy Redis event | Replacement orchestrator event | Notes |
|
| 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.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. |
|
| `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.
|
Legacy schemas remain for backwards-compatibility during migration, but new integrations **must** target the orchestrator variants.
|
||||||
|
|
||||||
## 3. Deterministic serialization
|
## 3. Deterministic serialization
|
||||||
|
|
||||||
- Producers must serialise events using `NotifyCanonicalJsonSerializer` to guarantee consistent key ordering and whitespace.
|
- Producers must serialise events using `NotifyCanonicalJsonSerializer` to guarantee consistent key ordering and whitespace.
|
||||||
- Timestamps (`occurredAt`, `recordedAt`, `payload.generatedAt`) use `DateTimeOffset.UtcDateTime.ToString("O")`.
|
- 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.
|
- 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`.
|
- Optional fields are omitted rather than emitted as `null`.
|
||||||
|
|
||||||
## 4. Idempotency and correlation
|
## 4. Idempotency and correlation
|
||||||
|
|
||||||
Idempotency keys dedupe repeated publishes and align with the orchestrator’s outbox pattern:
|
Idempotency keys dedupe repeated publishes and align with the orchestrator’s outbox pattern:
|
||||||
|
|
||||||
| Event kind | Idempotency key template |
|
| Event kind | Idempotency key template |
|
||||||
|------------|-------------------------|
|
|------------|-------------------------|
|
||||||
| `scanner.event.report.ready` | `scanner.event.report.ready:<tenant>:<reportId>` |
|
| `scanner.event.report.ready` | `scanner.event.report.ready:<tenant>:<reportId>` |
|
||||||
| `scanner.event.scan.completed` | `scanner.event.scan.completed:<tenant>:<scanId>` |
|
| `scanner.event.scan.completed` | `scanner.event.scan.completed:<tenant>:<scanId>` |
|
||||||
|
|
||||||
Keys are ASCII lowercase; components should be trimmed and validated before concatenation. Retries must reuse the same key.
|
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.
|
`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
|
## 5. Versioning and evolution
|
||||||
|
|
||||||
- Increment the `version` field and the `@<version>` suffix for **breaking** changes (field removals, type changes, semantic shifts).
|
- Increment the `version` field and the `@<version>` 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.
|
- 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.
|
- When introducing `@2`, keep the `@1` schema/docs in place until orchestrator subscribers confirm migration.
|
||||||
|
|
||||||
## 6. Consumer checklist
|
## 6. Consumer checklist
|
||||||
|
|
||||||
1. Validate incoming payloads against the schema for the targeted version.
|
1. Validate incoming payloads against the schema for the targeted version.
|
||||||
2. Use `idempotencyKey` for dedupe, not `eventId`.
|
2. Use `idempotencyKey` for dedupe, not `eventId`.
|
||||||
3. Map `traceId`/`spanId` into telemetry spans to preserve causality.
|
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.
|
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.
|
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
|
## 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/`.
|
- **Scanner WebService** — `SCANNER-EVENTS-16-301` (blocked) and `SCANNER-EVENTS-16-302` (done) track the production of these envelopes. Dispatcher link customisation landed and samples updated; full `dotnet test` suite now succeeds after Surface cache ctor drift was patched and DSSE fixtures re-synced (2025-11-06).
|
||||||
- **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.
|
- **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/modules/devops/runbooks/launch-cutover.md` to confirm downstream validation before the production cutover. Record gaps or newly required fields in `docs/modules/devops/runbooks/launch-readiness.md` so they land in the launch checklist.
|
- **Docs cadence** — update this file and the matching JSON schemas whenever payload fields change. Use the rehearsal checklist in `docs/modules/devops/runbooks/launch-cutover.md` to confirm downstream validation before the production cutover. Record gaps or newly required fields in `docs/modules/devops/runbooks/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.
|
**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.
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ Follow the sprint files below in order. Update task status in both `SPRINTS` and
|
|||||||
- [Ops & Offline](./SPRINT_190_ops_offline.md)
|
- [Ops & Offline](./SPRINT_190_ops_offline.md)
|
||||||
- [Documentation & Process](./SPRINT_200_documentation_process.md)
|
- [Documentation & Process](./SPRINT_200_documentation_process.md)
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
> 2025-11-03: ATTESTOR-72-003 moved to DOING (Attestor Service Guild) – running live TTL validation against local MongoDB/Redis processes (manual hosts, no Docker).
|
||||||
> 2025-11-03: ATTESTOR-72-003 moved to DOING (Attestor Service Guild) – running live TTL validation against local MongoDB/Redis processes (manual hosts, no Docker).
|
|
||||||
> 2025-11-03: ATTESTOR-72-003 marked DONE (Attestor Service Guild) – Mongo/Redis TTL expiry logs archived under `docs/modules/attestor/evidence/2025-11-03-*.txt` with summary in `docs/modules/attestor/ttl-validation.md`.
|
> 2025-11-03: ATTESTOR-72-003 marked DONE (Attestor Service Guild) – Mongo/Redis TTL expiry logs archived under `docs/modules/attestor/evidence/2025-11-03-*.txt` with summary in `docs/modules/attestor/ttl-validation.md`.
|
||||||
> 2025-11-03: AIAI-31-004B moved to DOING (Advisory AI Guild) – starting prompt assembler/guardrail plumbing, cache persistence contract, and DSSE provenance wiring.
|
> 2025-11-03: AIAI-31-004B moved to DOING (Advisory AI Guild) – starting prompt assembler/guardrail plumbing, cache persistence contract, and DSSE provenance wiring.
|
||||||
> 2025-11-03: PLG7.RFC marked DONE (Auth Plugin Guild, Security Guild) – LDAP plugin RFC accepted; review log stored at `docs/notes/2025-11-03-authority-plugin-ldap-review.md`, follow-up PLG7.IMPL-001..005 queued.
|
> 2025-11-03: PLG7.RFC marked DONE (Auth Plugin Guild, Security Guild) – LDAP plugin RFC accepted; review log stored at `docs/notes/2025-11-03-authority-plugin-ldap-review.md`, follow-up PLG7.IMPL-001..005 queued.
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ CONCELIER-ATTEST-73-002 `Transparency metadata` | TODO | Ensure Conseiller expos
|
|||||||
CONCELIER-CONSOLE-23-001 `Advisory aggregation views` | TODO | Expose `/console/advisories` endpoints returning aggregation groups (per linkset) with source chips, provider-reported severity columns (no local consensus), and provenance metadata for Console list + dashboard cards. Support filters by source, ecosystem, published/modified window, tenant enforcement. Dependencies: CONCELIER-LNM-21-201, CONCELIER-LNM-21-202. | Concelier WebService Guild, BE-Base Platform Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
|
CONCELIER-CONSOLE-23-001 `Advisory aggregation views` | TODO | Expose `/console/advisories` endpoints returning aggregation groups (per linkset) with source chips, provider-reported severity columns (no local consensus), and provenance metadata for Console list + dashboard cards. Support filters by source, ecosystem, published/modified window, tenant enforcement. Dependencies: CONCELIER-LNM-21-201, CONCELIER-LNM-21-202. | Concelier WebService Guild, BE-Base Platform Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
|
||||||
CONCELIER-CONSOLE-23-002 `Dashboard deltas API` | TODO | Provide aggregated advisory delta counts (new, modified, conflicting) for Console dashboard + live status ticker; emit structured events for queue lag metrics. Ensure deterministic counts across repeated queries. Dependencies: CONCELIER-CONSOLE-23-001, CONCELIER-LNM-21-203. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
|
CONCELIER-CONSOLE-23-002 `Dashboard deltas API` | TODO | Provide aggregated advisory delta counts (new, modified, conflicting) for Console dashboard + live status ticker; emit structured events for queue lag metrics. Ensure deterministic counts across repeated queries. Dependencies: CONCELIER-CONSOLE-23-001, CONCELIER-LNM-21-203. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
|
||||||
CONCELIER-CONSOLE-23-003 `Search fan-out helpers` | TODO | Deliver fast lookup endpoints for CVE/GHSA/purl search (linksets, observations) returning evidence fragments for Console global search; implement caching + scope guards. Dependencies: CONCELIER-CONSOLE-23-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
|
CONCELIER-CONSOLE-23-003 `Search fan-out helpers` | TODO | Deliver fast lookup endpoints for CVE/GHSA/purl search (linksets, observations) returning evidence fragments for Console global search; implement caching + scope guards. Dependencies: CONCELIER-CONSOLE-23-001. | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService/TASKS.md)
|
||||||
CONCELIER-CORE-AOC-19-004 `Remove ingestion normalization` | DOING (2025-10-28) | Strip normalization/dedup/severity logic from ingestion pipelines, delegate derived computations to Policy Engine, and update exporters/tests to consume raw documents only.<br>2025-10-29 19:05Z: Audit completed for `AdvisoryRawService`/Mongo repo to confirm alias order/dedup removal persists; identified remaining normalization in observation/linkset factory that will be revised to surface raw duplicates for Policy ingestion. Change sketch + regression matrix drafted under `docs/dev/aoc-normalization-removal-notes.md` (pending commit).<br>2025-10-31 20:45Z: Added raw linkset projection to observations/storage, exposing canonical+raw views, refreshed fixtures/tests, and documented behaviour in models/doc factory.<br>2025-10-31 21:10Z: Coordinated with Policy Engine (POLICY-ENGINE-20-003) on adoption timeline; backfill + consumer readiness tracked in `docs/dev/raw-linkset-backfill-plan.md`.<br>2025-11-05 14:20Z: Resumed work to map remaining normalization hooks tied to Merge service and capture requirements for the upcoming `noMergeEnabled` feature toggle.<br>2025-11-05 19:05Z: Hardened no-merge feature flag wiring by suppressing obsolete diagnostics and extending gating tests.<br>2025-11-06 16:10Z: Updated AOC references/backfill plan with raw-vs-canonical guidance and noted analyzer guardrails introduced under MERGE-LNM-21-002. Dependencies: CONCELIER-CORE-AOC-19-002, POLICY-AOC-19-003. | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
|
CONCELIER-CORE-AOC-19-004 `Remove ingestion normalization` | DONE (2025-11-06) | Strip normalization/dedup/severity logic from ingestion pipelines, delegate derived computations to Policy Engine, and update exporters/tests to consume raw documents only.<br>2025-10-29 19:05Z: Audit completed for `AdvisoryRawService`/Mongo repo to confirm alias order/dedup removal persists; identified remaining normalization in observation/linkset factory that will be revised to surface raw duplicates for Policy ingestion. Change sketch + regression matrix drafted under `docs/dev/aoc-normalization-removal-notes.md` (pending commit).<br>2025-10-31 20:45Z: Added raw linkset projection to observations/storage, exposing canonical+raw views, refreshed fixtures/tests, and documented behaviour in models/doc factory.<br>2025-10-31 21:10Z: Coordinated with Policy Engine (POLICY-ENGINE-20-003) on adoption timeline; backfill + consumer readiness tracked in `docs/dev/raw-linkset-backfill-plan.md`.<br>2025-11-05 14:20Z: Resumed work to map remaining normalization hooks tied to Merge service and capture requirements for the upcoming `noMergeEnabled` feature toggle.<br>2025-11-05 19:05Z: Hardened no-merge feature flag wiring by suppressing obsolete diagnostics and extending gating tests.<br>2025-11-06 16:10Z: Updated AOC references/backfill plan with raw-vs-canonical guidance and noted analyzer guardrails introduced under MERGE-LNM-21-002.<br>2025-11-06 23:40Z: Raw observations now flow unaltered (casing + whitespace preserved) with case-insensitive filters/tests refreshed; docs aligned. Tests: `StellaOps.Concelier.Models/Core/Storage.Mongo.Tests` green on .NET 10 preview. Dependencies: CONCELIER-CORE-AOC-19-002, POLICY-AOC-19-003. | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
|
||||||
CONCELIER-CORE-AOC-19-013 `Authority tenant scope smoke coverage` | TODO | Extend Concelier smoke/e2e fixtures to configure `requiredTenants` and assert cross-tenant rejection with updated Authority tokens. Dependencies: AUTH-AOC-19-002. | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
|
CONCELIER-CORE-AOC-19-013 `Authority tenant scope smoke coverage` | TODO | Extend Concelier smoke/e2e fixtures to configure `requiredTenants` and assert cross-tenant rejection with updated Authority tokens. Dependencies: AUTH-AOC-19-002. | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core/TASKS.md)
|
||||||
|
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ Depends on: Sprint 110.B - Concelier.VI
|
|||||||
Summary: Ingestion & Evidence focus on Concelier (phase VII).
|
Summary: Ingestion & Evidence focus on Concelier (phase VII).
|
||||||
Task ID | State | Task description | Owners (Source)
|
Task ID | State | Task description | Owners (Source)
|
||||||
--- | --- | --- | ---
|
--- | --- | --- | ---
|
||||||
MERGE-LNM-21-002 | DOING (2025-11-03) | Refactor or retire `AdvisoryMergeService` and related pipelines, ensuring callers transition to observation/linkset APIs; add compile-time analyzer preventing merge service usage.<br>2025-11-03: Began dependency audit and call-site inventory ahead of deprecation plan; cataloging service registrations/tests referencing merge APIs.<br>2025-11-05 14:42Z: Drafting `concelier:features:noMergeEnabled` gating, merge job allowlist handling, and deprecation/telemetry changes prior to analyzer rollout.<br>2025-11-06 16:10Z: Landed analyzer project (`CONCELIER0002`), wired into Concelier WebService/tests, and updated docs to direct suppressions through explicit migration notes. | BE-Merge (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
|
MERGE-LNM-21-002 | DOING (2025-11-03) | Refactor or retire `AdvisoryMergeService` and related pipelines, ensuring callers transition to observation/linkset APIs; add compile-time analyzer preventing merge service usage.<br>2025-11-03: Began dependency audit and call-site inventory ahead of deprecation plan; cataloging service registrations/tests referencing merge APIs.<br>2025-11-05 14:42Z: Drafting `concelier:features:noMergeEnabled` gating, merge job allowlist handling, and deprecation/telemetry changes prior to analyzer rollout.<br>2025-11-06 16:10Z: Landed analyzer project (`CONCELIER0002`), wired into Concelier WebService/tests, and updated docs to direct suppressions through explicit migration notes.<br>2025-11-06 23:45Z: Analyzer enforcement merged; DI removal + flag defaults pending. Analyzer test project blocked by offline feed (`Microsoft.Bcl.AsyncInterfaces >= 8.0` missing) — rerun once nuget mirror refreshed. | BE-Merge (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
|
||||||
MERGE-LNM-21-003 Determinism/test updates | QA Guild, BE-Merge | Replace merge determinism suites with observation/linkset regression tests verifying no data mutation and conflicts remain visible. Dependencies: MERGE-LNM-21-002. | MERGE-LNM-21-002 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
|
MERGE-LNM-21-003 Determinism/test updates | QA Guild, BE-Merge | Replace merge determinism suites with observation/linkset regression tests verifying no data mutation and conflicts remain visible. Dependencies: MERGE-LNM-21-002. | MERGE-LNM-21-002 (src/Concelier/__Libraries/StellaOps.Concelier.Merge/TASKS.md)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ SCANNER-ENV-01 | TODO (2025-11-06) | Replace ad-hoc environment reads with `Stel
|
|||||||
SCANNER-ENV-02 | TODO (2025-11-06) | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration. Dependencies: SCANNER-ENV-01.<br>2025-11-02: WebService bootstrap now consumes Surface.Env helpers for cache roots and feature flag toggles; configuration doc draft pending.<br>2025-11-05 14:55Z: Picking up configuration/documentation work and aligning API readiness checks with Surface.Env validation outputs.<br>2025-11-05 19:18Z: Added unit test for Surface.Env cache root binding and ensured configurator registration.<br>2025-11-06 17:05Z: Surface.Env design doc expanded with warning catalogue and release notes, README refreshed.<br>2025-11-06 07:45Z: Helm/Compose templates ship `SCANNER_SURFACE_*` defaults across dev/stage/prod/airgap/mirror profiles with rollout guidance in deploy docs.<br>2025-11-06 07:55Z: Paused; follow-up automation tracked under `DEVOPS-OPENSSL-11-001/002` and readiness tests outstanding. | Scanner WebService Guild, Ops Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
SCANNER-ENV-02 | TODO (2025-11-06) | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration. Dependencies: SCANNER-ENV-01.<br>2025-11-02: WebService bootstrap now consumes Surface.Env helpers for cache roots and feature flag toggles; configuration doc draft pending.<br>2025-11-05 14:55Z: Picking up configuration/documentation work and aligning API readiness checks with Surface.Env validation outputs.<br>2025-11-05 19:18Z: Added unit test for Surface.Env cache root binding and ensured configurator registration.<br>2025-11-06 17:05Z: Surface.Env design doc expanded with warning catalogue and release notes, README refreshed.<br>2025-11-06 07:45Z: Helm/Compose templates ship `SCANNER_SURFACE_*` defaults across dev/stage/prod/airgap/mirror profiles with rollout guidance in deploy docs.<br>2025-11-06 07:55Z: Paused; follow-up automation tracked under `DEVOPS-OPENSSL-11-001/002` and readiness tests outstanding. | Scanner WebService Guild, Ops Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
||||||
SCANNER-ENV-03 | TODO | Adopt Surface.Env helpers for plugin configuration (cache roots, CAS endpoints, feature toggles). Dependencies: SCANNER-ENV-02. | BuildX Plugin Guild (src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md)
|
SCANNER-ENV-03 | TODO | Adopt Surface.Env helpers for plugin configuration (cache roots, CAS endpoints, feature toggles). Dependencies: SCANNER-ENV-02. | BuildX Plugin Guild (src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md)
|
||||||
SCANNER-EVENTS-16-301 | BLOCKED (2025-10-26) | Emit orchestrator-compatible envelopes (`scanner.event.*`) and update integration tests to verify Notifier ingestion (no Redis queue coupling). | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
SCANNER-EVENTS-16-301 | BLOCKED (2025-10-26) | Emit orchestrator-compatible envelopes (`scanner.event.*`) and update integration tests to verify Notifier ingestion (no Redis queue coupling). | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
||||||
SCANNER-EVENTS-16-302 | DOING (2025-10-26) | Extend orchestrator event links (report/policy/attestation) once endpoints are finalised across gateway + console. Dependencies: SCANNER-EVENTS-16-301. | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
SCANNER-EVENTS-16-302 | DONE (2025-11-06) | Extend orchestrator event links (report/policy/attestation) once endpoints are finalised across gateway + console. Dependencies: SCANNER-EVENTS-16-301.<br>2025-11-06 22:55Z: Dispatcher honours configurable console/API segments; docs and samples refreshed; added regression test for custom segments. `dotnet test` previously blocked by legacy Surface cache ctor signature (tracked under Surface task).<br>2025-11-06 23:30Z: Report DSSE fixtures re-synced; Surface cache ctor drift repaired; `dotnet test src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests --no-build` now green end-to-end. | Scanner WebService Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
||||||
SCANNER-GRAPH-21-001 | TODO | Provide webhook/REST endpoint for Cartographer to request policy overlays and runtime evidence for graph nodes, ensuring determinism and tenant scoping. | Scanner WebService Guild, Cartographer Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
SCANNER-GRAPH-21-001 | TODO | Provide webhook/REST endpoint for Cartographer to request policy overlays and runtime evidence for graph nodes, ensuring determinism and tenant scoping. | Scanner WebService Guild, Cartographer Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
||||||
SCANNER-LNM-21-001 | TODO | Update `/reports` and `/policy/runtime` payloads to consume advisory/vex linksets, exposing source severity arrays and conflict summaries alongside effective verdicts. | Scanner WebService Guild, Policy Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
SCANNER-LNM-21-001 | TODO | Update `/reports` and `/policy/runtime` payloads to consume advisory/vex linksets, exposing source severity arrays and conflict summaries alongside effective verdicts. | Scanner WebService Guild, Policy Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
||||||
SCANNER-LNM-21-002 | TODO | Add evidence endpoint for Console to fetch linkset summaries with policy overlay for a component/SBOM, including AOC references. Dependencies: SCANNER-LNM-21-001. | Scanner WebService Guild, UI Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
SCANNER-LNM-21-002 | TODO | Add evidence endpoint for Console to fetch linkset summaries with policy overlay for a component/SBOM, including AOC references. Dependencies: SCANNER-LNM-21-001. | Scanner WebService Guild, UI Guild (src/Scanner/StellaOps.Scanner.WebService/TASKS.md)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ Do not proceed to Phase 1 until all prerequisites are checked or explicitly wa
|
|||||||
> 2025-11-05: WebService honours `concelier:features:noMergeEnabled` by skipping Merge DI registration and removing the `merge:reconcile` job definition (MERGE-LNM-21-002).
|
> 2025-11-05: WebService honours `concelier:features:noMergeEnabled` by skipping Merge DI registration and removing the `merge:reconcile` job definition (MERGE-LNM-21-002).
|
||||||
>
|
>
|
||||||
> 2025-11-06: Analyzer `CONCELIER0002` ships with Concelier hosts to block new references to `AdvisoryMergeService` / `AddMergeModule`. Suppressions must be paired with an explicit migration note.
|
> 2025-11-06: Analyzer `CONCELIER0002` ships with Concelier hosts to block new references to `AdvisoryMergeService` / `AddMergeModule`. Suppressions must be paired with an explicit migration note.
|
||||||
|
> 2025-11-06: Analyzer coverage validated via unit tests catching object creation, field declarations, `typeof`, and DI extension invocations; merge assemblies remain exempt for legacy cleanup helpers.
|
||||||
|
|
||||||
> **Configuration hygiene:** Document the toggle values per environment in `ops/devops/configuration/staging.md` and `ops/devops/configuration/production.md`. Air-gapped customers receive defaults through the Offline Kit release notes.
|
> **Configuration hygiene:** Document the toggle values per environment in `ops/devops/configuration/staging.md` and `ops/devops/configuration/production.md`. Air-gapped customers receive defaults through the Offline Kit release notes.
|
||||||
|
|
||||||
|
|||||||
BIN
local-nuget/Microsoft.Bcl.AsyncInterfaces.8.0.0.nupkg
Normal file
BIN
local-nuget/Microsoft.Bcl.AsyncInterfaces.8.0.0.nupkg
Normal file
Binary file not shown.
BIN
local-nuget/NETStandard.Library.2.0.3.nupkg
Normal file
BIN
local-nuget/NETStandard.Library.2.0.3.nupkg
Normal file
Binary file not shown.
BIN
local-nuget/System.Numerics.Vectors.4.6.0.nupkg
Normal file
BIN
local-nuget/System.Numerics.Vectors.4.6.0.nupkg
Normal file
Binary file not shown.
BIN
local-nuget/System.Runtime.CompilerServices.Unsafe.6.1.0.nupkg
Normal file
BIN
local-nuget/System.Runtime.CompilerServices.Unsafe.6.1.0.nupkg
Normal file
Binary file not shown.
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
"dsse": {
|
"dsse": {
|
||||||
"payloadType": "application/vnd.stellaops.report+json",
|
"payloadType": "application/vnd.stellaops.report+json",
|
||||||
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJyZWFjaGFiaWxpdHkiOiJydW50aW1lIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwic3RhdHVzIjoiQmxvY2tlZCJ9XSwiaXNzdWVzIjpbXSwic3VyZmFjZSI6eyJ0ZW5hbnQiOiJ0ZW5hbnQtYWxwaGEiLCJnZW5lcmF0ZWRBdCI6IjIwMjUtMTAtMTlUMTI6MzQ6NTYrMDA6MDAiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo0ZmVlODdkMTg2MjkxZGRmYmJjYzJjNTZjOGVkMGU4Mjg1MjBiOGY1MmUxY2RlMGUxM2JiYTA4MmYxMDkxOGQ3IiwibWFuaWZlc3RVcmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL3N1cmZhY2UvbWFuaWZlc3RzL3RlbmFudC1hbHBoYS9zaGEyNTYvNGYvZWUvNGZlZTg3ZDE4NjI5MWRkZmJiY2MyYzU2YzhlZDBlODI4NTIwYjhmNTJlMWNkZTBlMTNiYmEwODJmMTA5MThkNy5qc29uIiwibWFuaWZlc3QiOnsic2NoZW1hIjoic3RlbGxhb3BzLnN1cmZhY2UubWFuaWZlc3RAMSIsInRlbmFudCI6InRlbmFudC1hbHBoYSIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmZlZWRmYWNlIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDEyOjM0OjU2KzAwOjAwIiwiYXJ0aWZhY3RzIjpbeyJraW5kIjoiZW50cnktdHJhY2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2VudHJ5LXRyYWNlL2YwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwL2VudHJ5LXRyYWNlLmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6ZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJmb3JtYXQiOiJqc29uIiwic2l6ZUJ5dGVzIjo0MDk2fSx7ImtpbmQiOiJzYm9tLWludmVudG9yeSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20uY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHgranNvbjt2ZXJzaW9uPTEuNjt2aWV3PWludmVudG9yeSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoyNDU3NiwidmlldyI6ImludmVudG9yeSJ9LHsia2luZCI6InNib20tdXNhZ2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2ltYWdlcy9mZWVkZmFjZS9zYm9tLXVzYWdlLmNkeC5qc29uIiwiZGlnZXN0Ijoic2hhMjU2OjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIiLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuY3ljbG9uZWR4K2pzb247dmVyc2lvbj0xLjY7dmlldz11c2FnZSIsImZvcm1hdCI6ImNkeC1qc29uIiwic2l6ZUJ5dGVzIjoxNjM4NCwidmlldyI6InVzYWdlIn1dfX19",
|
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXSwic3VyZmFjZSI6eyJ0ZW5hbnQiOiJ0ZW5hbnQtYWxwaGEiLCJnZW5lcmF0ZWRBdCI6IjIwMjUtMTAtMTlUMTI6MzQ6NTYrMDA6MDAiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo0ZmVlODdkMTg2MjkxZGRmYmJjYzJjNTZjOGVkMGU4Mjg1MjBiOGY1MmUxY2RlMGUxM2JiYTA4MmYxMDkxOGQ3IiwibWFuaWZlc3RVcmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL3N1cmZhY2UvbWFuaWZlc3RzL3RlbmFudC1hbHBoYS9zaGEyNTYvNGYvZWUvNGZlZTg3ZDE4NjI5MWRkZmJiY2MyYzU2YzhlZDBlODI4NTIwYjhmNTJlMWNkZTBlMTNiYmEwODJmMTA5MThkNy5qc29uIiwibWFuaWZlc3QiOnsic2NoZW1hIjoic3RlbGxhb3BzLnN1cmZhY2UubWFuaWZlc3RAMSIsInRlbmFudCI6InRlbmFudC1hbHBoYSIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmZlZWRmYWNlIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDEyOjM0OjU2KzAwOjAwIiwiYXJ0aWZhY3RzIjpbeyJraW5kIjoiZW50cnktdHJhY2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2VudHJ5LXRyYWNlL2YwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwL2VudHJ5LXRyYWNlLmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6ZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJmb3JtYXQiOiJqc29uIiwic2l6ZUJ5dGVzIjo0MDk2fSx7ImtpbmQiOiJzYm9tLWludmVudG9yeSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20uY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHhcdTAwMkJqc29uO3ZlcnNpb249MS42O3ZpZXc9aW52ZW50b3J5IiwiZm9ybWF0IjoiY2R4LWpzb24iLCJzaXplQnl0ZXMiOjI0NTc2LCJ2aWV3IjoiaW52ZW50b3J5In0seyJraW5kIjoic2JvbS11c2FnZSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20tdXNhZ2UuY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHhcdTAwMkJqc29uO3ZlcnNpb249MS42O3ZpZXc9dXNhZ2UiLCJmb3JtYXQiOiJjZHgtanNvbiIsInNpemVCeXRlcyI6MTYzODQsInZpZXciOiJ1c2FnZSJ9XX19fQ==",
|
||||||
"signatures": [
|
"signatures": [
|
||||||
{
|
{
|
||||||
"keyId": "test-key",
|
"keyId": "test-key",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
||||||
<ProjectReference Include="../../Aoc/__Libraries/StellaOps.Aoc/StellaOps.Aoc.csproj" />
|
<ProjectReference Include="../../Aoc/__Libraries/StellaOps.Aoc/StellaOps.Aoc.csproj" />
|
||||||
<ProjectReference Include="../../Aoc/__Libraries/StellaOps.Aoc.AspNetCore/StellaOps.Aoc.AspNetCore.csproj" />
|
<ProjectReference Include="../../Aoc/__Libraries/StellaOps.Aoc.AspNetCore/StellaOps.Aoc.AspNetCore.csproj" />
|
||||||
<ProjectReference Include="../__Analyzers/StellaOps.Concelier.Analyzers/StellaOps.Concelier.Analyzers.csproj"
|
<ProjectReference Include="../__Analyzers/StellaOps.Concelier.Merge.Analyzers/StellaOps.Concelier.Merge.Analyzers.csproj"
|
||||||
OutputItemType="Analyzer"
|
OutputItemType="Analyzer"
|
||||||
ReferenceOutputAssembly="false" />
|
ReferenceOutputAssembly="false" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -2,8 +2,4 @@
|
|||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
|
|
||||||
#### New Rules
|
No analyzer rules currently scheduled for release.
|
||||||
|
|
||||||
Rule ID | Title | Notes
|
|
||||||
--------|-------|------
|
|
||||||
CONCELIER0002 | Legacy merge pipeline is disabled | Flags usage of `AddMergeModule` and `AdvisoryMergeService`.
|
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.Diagnostics;
|
|
||||||
using Microsoft.CodeAnalysis.Operations;
|
|
||||||
|
|
||||||
namespace StellaOps.Concelier.Analyzers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Analyzer that flags usages of the legacy merge service APIs.
|
|
||||||
/// </summary>
|
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
||||||
public sealed class NoMergeUsageAnalyzer : DiagnosticAnalyzer
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Diagnostic identifier for legacy merge usage violations.
|
|
||||||
/// </summary>
|
|
||||||
public const string DiagnosticId = "CONCELIER0002";
|
|
||||||
|
|
||||||
private const string Category = "Usage";
|
|
||||||
private const string MergeExtensionType = "StellaOps.Concelier.Merge.MergeServiceCollectionExtensions";
|
|
||||||
private const string MergeServiceType = "StellaOps.Concelier.Merge.Services.AdvisoryMergeService";
|
|
||||||
|
|
||||||
private static readonly LocalizableString Title = "Legacy merge pipeline is disabled";
|
|
||||||
private static readonly LocalizableString MessageFormat = "Do not reference the legacy Concelier merge pipeline (type '{0}')";
|
|
||||||
private static readonly LocalizableString Description =
|
|
||||||
"The legacy Concelier merge service is deprecated under MERGE-LNM-21-002. "
|
|
||||||
+ "Switch to observation/linkset APIs or guard calls behind the concelier:features:noMergeEnabled toggle.";
|
|
||||||
|
|
||||||
private static readonly DiagnosticDescriptor Rule = new(
|
|
||||||
DiagnosticId,
|
|
||||||
Title,
|
|
||||||
MessageFormat,
|
|
||||||
Category,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
isEnabledByDefault: true,
|
|
||||||
description: Description,
|
|
||||||
helpLinkUri: "https://stella-ops.org/docs/migration/no-merge");
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Initialize(AnalysisContext context)
|
|
||||||
{
|
|
||||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
|
||||||
context.EnableConcurrentExecution();
|
|
||||||
|
|
||||||
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
|
|
||||||
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AnalyzeInvocation(OperationAnalysisContext context)
|
|
||||||
{
|
|
||||||
if (context.Operation is not IInvocationOperation invocation)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetMethod = invocation.TargetMethod;
|
|
||||||
if (targetMethod is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!SymbolEquals(targetMethod.ContainingType, MergeExtensionType))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(targetMethod.Name, "AddMergeModule", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsAllowedAssembly(context.ContainingSymbol.ContainingAssembly))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReportDiagnostic(context, invocation.Syntax, $"{MergeExtensionType}.{targetMethod.Name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AnalyzeObjectCreation(OperationAnalysisContext context)
|
|
||||||
{
|
|
||||||
if (context.Operation is not IObjectCreationOperation creation)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var createdType = creation.Type;
|
|
||||||
if (createdType is null || !SymbolEquals(createdType, MergeServiceType))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsAllowedAssembly(context.ContainingSymbol.ContainingAssembly))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReportDiagnostic(context, creation.Syntax, MergeServiceType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool SymbolEquals(ITypeSymbol? symbol, string fullName)
|
|
||||||
{
|
|
||||||
if (symbol is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var display = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
|
||||||
if (display.StartsWith("global::", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
display = display.Substring("global::".Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Equals(display, fullName, StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsAllowedAssembly(IAssemblySymbol? assemblySymbol)
|
|
||||||
{
|
|
||||||
if (assemblySymbol is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var assemblyName = assemblySymbol.Name;
|
|
||||||
if (string.IsNullOrWhiteSpace(assemblyName))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assemblyName.StartsWith("StellaOps.Concelier.Merge", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assemblyName.EndsWith(".Analyzers", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReportDiagnostic(OperationAnalysisContext context, SyntaxNode syntax, string targetName)
|
|
||||||
{
|
|
||||||
var diagnostic = Diagnostic.Create(Rule, syntax.GetLocation(), targetName);
|
|
||||||
context.ReportDiagnostic(diagnostic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
; Shipped analyzer releases
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## Release History
|
||||||
|
|
||||||
|
### Unreleased
|
||||||
|
|
||||||
|
#### New Rules
|
||||||
|
|
||||||
|
Rule ID | Title | Notes
|
||||||
|
--------|-------|------
|
||||||
|
CONCELIER0002 | Legacy merge service usage detected | Flags references to `AdvisoryMergeService` and `AddMergeModule`.
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
using Microsoft.CodeAnalysis.Operations;
|
||||||
|
|
||||||
|
namespace StellaOps.Concelier.Merge.Analyzers;
|
||||||
|
|
||||||
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
|
public sealed class MergeUsageAnalyzer : DiagnosticAnalyzer
|
||||||
|
{
|
||||||
|
public const string DiagnosticId = "CONCELIER0002";
|
||||||
|
|
||||||
|
private const string AdvisoryMergeServiceTypeName = "StellaOps.Concelier.Merge.Services.AdvisoryMergeService";
|
||||||
|
private const string MergeExtensionsTypeName = "StellaOps.Concelier.Merge.MergeServiceCollectionExtensions";
|
||||||
|
private const string AddMergeModuleMethodName = "AddMergeModule";
|
||||||
|
|
||||||
|
private static readonly DiagnosticDescriptor Rule = new(
|
||||||
|
DiagnosticId,
|
||||||
|
title: "Legacy merge service usage detected",
|
||||||
|
messageFormat: "Advisory merge pipeline is deprecated; remove usage of '{0}' and adopt Link-Not-Merge linkset workflows (MERGE-LNM-21-002)",
|
||||||
|
category: "Usage",
|
||||||
|
defaultSeverity: DiagnosticSeverity.Warning,
|
||||||
|
isEnabledByDefault: true,
|
||||||
|
description: "Link-Not-Merge replaces the legacy AdvisoryMergeService. Set concelier:features:noMergeEnabled=true and migrate to observation/linkset APIs instead of invoking merge services directly.",
|
||||||
|
helpLinkUri: "https://stella-ops.org/docs/migration/no-merge");
|
||||||
|
|
||||||
|
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
||||||
|
|
||||||
|
public override void Initialize(AnalysisContext context)
|
||||||
|
{
|
||||||
|
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||||
|
context.EnableConcurrentExecution();
|
||||||
|
|
||||||
|
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
|
||||||
|
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
|
||||||
|
context.RegisterOperationAction(AnalyzeTypeOf, OperationKind.TypeOf);
|
||||||
|
context.RegisterSyntaxNodeAction(AnalyzeIdentifier, SyntaxKind.IdentifierName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AnalyzeInvocation(OperationAnalysisContext context)
|
||||||
|
{
|
||||||
|
if (context.Operation is not IInvocationOperation invocation)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var method = invocation.TargetMethod;
|
||||||
|
if (!IsAddMergeModule(method) && (method.ReducedFrom is null || !IsAddMergeModule(method.ReducedFrom)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAllowedAssembly(method.ContainingAssembly, context.ContainingSymbol))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportDiagnostic(context, invocation.Syntax.GetLocation(), $"{method.ContainingType.Name}.{method.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AnalyzeObjectCreation(OperationAnalysisContext context)
|
||||||
|
{
|
||||||
|
if (context.Operation is not IObjectCreationOperation creation)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creation.Type is not INamedTypeSymbol type || !IsAdvisoryMergeService(type))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAllowedAssembly(type.ContainingAssembly, context.ContainingSymbol))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportDiagnostic(context, creation.Syntax.GetLocation(), type.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AnalyzeTypeOf(OperationAnalysisContext context)
|
||||||
|
{
|
||||||
|
if (context.Operation is not ITypeOfOperation typeOfOperation)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeOfOperation.TypeOperand is not INamedTypeSymbol type || !IsAdvisoryMergeService(type))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAllowedAssembly(type.ContainingAssembly, context.ContainingSymbol))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportDiagnostic(context, typeOfOperation.Syntax.GetLocation(), type.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AnalyzeIdentifier(SyntaxNodeAnalysisContext context)
|
||||||
|
{
|
||||||
|
var identifier = (IdentifierNameSyntax)context.Node;
|
||||||
|
if (!IsRightMostIdentifier(identifier))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbolInfo = context.SemanticModel.GetSymbolInfo(identifier, context.CancellationToken);
|
||||||
|
var symbol = symbolInfo.Symbol;
|
||||||
|
if (symbol is not INamedTypeSymbol typeSymbol || !IsAdvisoryMergeService(typeSymbol))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAllowedAssembly(typeSymbol.ContainingAssembly, context.ContainingSymbol))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsPartOfSuppressedConstruct(identifier))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var diagnostic = Diagnostic.Create(Rule, identifier.GetLocation(), typeSymbol.Name);
|
||||||
|
context.ReportDiagnostic(diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPartOfSuppressedConstruct(IdentifierNameSyntax identifier)
|
||||||
|
{
|
||||||
|
foreach (var ancestor in identifier.Ancestors())
|
||||||
|
{
|
||||||
|
switch (ancestor)
|
||||||
|
{
|
||||||
|
case ObjectCreationExpressionSyntax:
|
||||||
|
case TypeOfExpressionSyntax:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRightMostIdentifier(IdentifierNameSyntax identifier)
|
||||||
|
{
|
||||||
|
if (identifier.Parent is QualifiedNameSyntax qualified)
|
||||||
|
{
|
||||||
|
return qualified.Right == identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identifier.Parent is AliasQualifiedNameSyntax aliasQualified)
|
||||||
|
{
|
||||||
|
return aliasQualified.Name == identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAddMergeModule(IMethodSymbol methodSymbol)
|
||||||
|
{
|
||||||
|
if (!string.Equals(methodSymbol.Name, AddMergeModuleMethodName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var containingType = methodSymbol.ContainingType;
|
||||||
|
if (containingType is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var display = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
|
display = TrimGlobalPrefix(display);
|
||||||
|
return string.Equals(display, MergeExtensionsTypeName, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAdvisoryMergeService(INamedTypeSymbol symbol)
|
||||||
|
{
|
||||||
|
var display = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
|
display = TrimGlobalPrefix(display);
|
||||||
|
return string.Equals(display, AdvisoryMergeServiceTypeName, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportDiagnostic(OperationAnalysisContext context, Location location, string target)
|
||||||
|
{
|
||||||
|
var diagnostic = Diagnostic.Create(Rule, location, target);
|
||||||
|
context.ReportDiagnostic(diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAllowedAssembly(IAssemblySymbol? referencedAssembly, ISymbol? containingSymbol)
|
||||||
|
{
|
||||||
|
var consumerAssembly = containingSymbol?.ContainingAssembly;
|
||||||
|
if (referencedAssembly is null || consumerAssembly is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var referencedName = referencedAssembly.Name;
|
||||||
|
if (!string.IsNullOrWhiteSpace(referencedName) &&
|
||||||
|
referencedName.StartsWith("StellaOps.Concelier.Merge", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = consumerAssembly.Name;
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.StartsWith("StellaOps.Concelier.Merge", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.EndsWith(".Analyzers", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TrimGlobalPrefix(string display)
|
||||||
|
{
|
||||||
|
if (!display.StartsWith("global::", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
|
||||||
|
return display.Substring("global::".Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
|
<AnalysisLevel>latest</AnalysisLevel>
|
||||||
|
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="README.md" Visible="false" />
|
||||||
|
<None Include="AnalyzerReleases.Shipped.md" Visible="false" />
|
||||||
|
<None Include="AnalyzerReleases.Unshipped.md" Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -29,7 +29,7 @@ public sealed class AdvisoryObservationQueryService : IAdvisoryObservationQueryS
|
|||||||
|
|
||||||
var normalizedTenant = NormalizeTenant(options.Tenant);
|
var normalizedTenant = NormalizeTenant(options.Tenant);
|
||||||
var normalizedObservationIds = NormalizeSet(options.ObservationIds, static value => value, StringComparer.Ordinal);
|
var normalizedObservationIds = NormalizeSet(options.ObservationIds, static value => value, StringComparer.Ordinal);
|
||||||
var normalizedAliases = NormalizeSet(options.Aliases, static value => value.ToLowerInvariant(), StringComparer.Ordinal);
|
var normalizedAliases = NormalizeSet(options.Aliases, static value => value, StringComparer.OrdinalIgnoreCase);
|
||||||
var normalizedPurls = NormalizeSet(options.Purls, static value => value, StringComparer.Ordinal);
|
var normalizedPurls = NormalizeSet(options.Purls, static value => value, StringComparer.Ordinal);
|
||||||
var normalizedCpes = NormalizeSet(options.Cpes, static value => value, StringComparer.Ordinal);
|
var normalizedCpes = NormalizeSet(options.Cpes, static value => value, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
> Docs alignment (2025-10-26): Linkset expectations detailed in AOC reference §4 and policy-engine architecture §2.1.
|
> Docs alignment (2025-10-26): Linkset expectations detailed in AOC reference §4 and policy-engine architecture §2.1.
|
||||||
> 2025-10-28: Advisory raw ingestion now strips client-supplied supersedes hints, logs ignored pointers, and surfaces repository-supplied supersedes identifiers; service tests cover duplicate handling and append-only semantics.
|
> 2025-10-28: Advisory raw ingestion now strips client-supplied supersedes hints, logs ignored pointers, and surfaces repository-supplied supersedes identifiers; service tests cover duplicate handling and append-only semantics.
|
||||||
> Docs alignment (2025-10-26): Deployment guide + observability guide describe supersedes metrics; ensure implementation emits `aoc_violation_total` on failure.
|
> Docs alignment (2025-10-26): Deployment guide + observability guide describe supersedes metrics; ensure implementation emits `aoc_violation_total` on failure.
|
||||||
| CONCELIER-CORE-AOC-19-004 `Remove ingestion normalization` | DOING (2025-10-28) | Concelier Core Guild | CONCELIER-CORE-AOC-19-002, POLICY-AOC-19-003 | Strip normalization/dedup/severity logic from ingestion pipelines, delegate derived computations to Policy Engine, and update exporters/tests to consume raw documents only.<br>2025-10-29 19:05Z: Audit completed for `AdvisoryRawService`/Mongo repo to confirm alias order/dedup removal persists; identified remaining normalization in observation/linkset factory that will be revised to surface raw duplicates for Policy ingestion. Change sketch + regression matrix drafted under `docs/dev/aoc-normalization-removal-notes.md` (pending commit).<br>2025-10-31 20:45Z: Added raw linkset projection to observations/storage, exposing canonical+raw views, refreshed fixtures/tests, and documented behaviour in models/doc factory.<br>2025-10-31 21:10Z: Coordinated with Policy Engine (POLICY-ENGINE-20-003) on adoption timeline; backfill + consumer readiness tracked in `docs/dev/raw-linkset-backfill-plan.md`.<br>2025-11-05 14:25Z: Resuming to document merge-dependent normalization paths and prepare implementation notes for `noMergeEnabled` gating before code changes land.<br>2025-11-05 19:20Z: Observation factory/linkset now preserve upstream ordering + duplicates; canonicalisation responsibility shifts to downstream consumers with refreshed unit coverage.<br>2025-11-06 16:10Z: Updated AOC reference/backfill docs with raw vs canonical guidance and cross-linked analyzer guardrails. |
|
| CONCELIER-CORE-AOC-19-004 `Remove ingestion normalization` | DONE (2025-11-06) | Concelier Core Guild | CONCELIER-CORE-AOC-19-002, POLICY-AOC-19-003 | Strip normalization/dedup/severity logic from ingestion pipelines, delegate derived computations to Policy Engine, and update exporters/tests to consume raw documents only.<br>2025-10-29 19:05Z: Audit completed for `AdvisoryRawService`/Mongo repo to confirm alias order/dedup removal persists; identified remaining normalization in observation/linkset factory that will be revised to surface raw duplicates for Policy ingestion. Change sketch + regression matrix drafted under `docs/dev/aoc-normalization-removal-notes.md` (pending commit).<br>2025-10-31 20:45Z: Added raw linkset projection to observations/storage, exposing canonical+raw views, refreshed fixtures/tests, and documented behaviour in models/doc factory.<br>2025-10-31 21:10Z: Coordinated with Policy Engine (POLICY-ENGINE-20-003) on adoption timeline; backfill + consumer readiness tracked in `docs/dev/raw-linkset-backfill-plan.md`.<br>2025-11-05 14:25Z: Resuming to document merge-dependent normalization paths and prepare implementation notes for `noMergeEnabled` gating before code changes land.<br>2025-11-05 19:20Z: Observation factory/linkset now preserve upstream ordering + duplicates; canonicalisation responsibility shifts to downstream consumers with refreshed unit coverage.<br>2025-11-06 16:10Z: Updated AOC reference/backfill docs with raw vs canonical guidance and cross-linked analyzer guardrails.<br>2025-11-06 23:40Z: Final pass preserves raw alias casing/whitespace end-to-end; query filters now compare case-insensitively, exporter fixtures refreshed, and docs aligned. Tests: `StellaOps.Concelier.Models/Core/Storage.Mongo.Tests` green on .NET 10 preview. |
|
||||||
> Docs alignment (2025-10-26): Architecture overview emphasises policy-only derivation; coordinate with Policy Engine guild for rollout.
|
> Docs alignment (2025-10-26): Architecture overview emphasises policy-only derivation; coordinate with Policy Engine guild for rollout.
|
||||||
> 2025-10-29: `AdvisoryRawService` now preserves upstream alias/linkset ordering (trim-only) and updated AOC documentation reflects the behaviour; follow-up to ensure policy consumers handle duplicates remains open.
|
> 2025-10-29: `AdvisoryRawService` now preserves upstream alias/linkset ordering (trim-only) and updated AOC documentation reflects the behaviour; follow-up to ensure policy consumers handle duplicates remains open.
|
||||||
| CONCELIER-CORE-AOC-19-013 `Authority tenant scope smoke coverage` | TODO | Concelier Core Guild | AUTH-AOC-19-002 | Extend Concelier smoke/e2e fixtures to configure `requiredTenants` and assert cross-tenant rejection with updated Authority tokens. | Coordinate deliverable so Authority docs (`AUTH-AOC-19-003`) can close once tests are in place. |
|
| CONCELIER-CORE-AOC-19-013 `Authority tenant scope smoke coverage` | TODO | Concelier Core Guild | AUTH-AOC-19-002 | Extend Concelier smoke/e2e fixtures to configure `requiredTenants` and assert cross-tenant rejection with updated Authority tokens. | Coordinate deliverable so Authority docs (`AUTH-AOC-19-003`) can close once tests are in place. |
|
||||||
|
|||||||
@@ -13,15 +13,21 @@ namespace StellaOps.Concelier.Merge;
|
|||||||
[Obsolete("Legacy merge module is deprecated; prefer Link-Not-Merge linkset pipelines. Track MERGE-LNM-21-002 and set concelier:features:noMergeEnabled=true to disable registration.", DiagnosticId = "CONCELIER0001", UrlFormat = "https://stella-ops.org/docs/migration/no-merge")]
|
[Obsolete("Legacy merge module is deprecated; prefer Link-Not-Merge linkset pipelines. Track MERGE-LNM-21-002 and set concelier:features:noMergeEnabled=true to disable registration.", DiagnosticId = "CONCELIER0001", UrlFormat = "https://stella-ops.org/docs/migration/no-merge")]
|
||||||
public static class MergeServiceCollectionExtensions
|
public static class MergeServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddMergeModule(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddMergeModule(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
ArgumentNullException.ThrowIfNull(configuration);
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
|
||||||
|
var noMergeEnabled = configuration.GetValue<bool?>("concelier:features:noMergeEnabled");
|
||||||
|
if (noMergeEnabled is true)
|
||||||
|
{
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
services.TryAddSingleton<CanonicalHashCalculator>();
|
services.TryAddSingleton<CanonicalHashCalculator>();
|
||||||
services.TryAddSingleton<CanonicalMerger>();
|
services.TryAddSingleton<CanonicalMerger>();
|
||||||
services.TryAddSingleton<AliasGraphResolver>();
|
services.TryAddSingleton<AliasGraphResolver>();
|
||||||
services.TryAddSingleton<AffectedPackagePrecedenceResolver>(sp =>
|
services.TryAddSingleton<AffectedPackagePrecedenceResolver>(sp =>
|
||||||
{
|
{
|
||||||
var options = configuration.GetSection("concelier:merge:precedence").Get<AdvisoryPrecedenceOptions>();
|
var options = configuration.GetSection("concelier:merge:precedence").Get<AdvisoryPrecedenceOptions>();
|
||||||
return options is null ? new AffectedPackagePrecedenceResolver() : new AffectedPackagePrecedenceResolver(options);
|
return options is null ? new AffectedPackagePrecedenceResolver() : new AffectedPackagePrecedenceResolver(options);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# TASKS
|
# TASKS
|
||||||
| Task | Owner(s) | Depends on | Notes |
|
| Task | Owner(s) | Depends on | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|Link-Not-Merge version provenance coordination|BE-Merge|CONCELIER-LNM-21-001|**DONE (2025-11-04)** – Coordinated connector rollout: updated `docs/dev/normalized-rule-recipes.md` with a per-connector status table + follow-up IDs, enabled `Normalized version rules missing` diagnostics in `AdvisoryPrecedenceMerger`, and confirmed Linkset validation metrics reflect remaining upstream gaps (ACSC/CCCS/CERTBUND/Cisco/RU-BDU awaiting structured ranges).|
|
|Link-Not-Merge version provenance coordination|BE-Merge|CONCELIER-LNM-21-001|**DONE (2025-11-04)** – Coordinated connector rollout: updated `docs/dev/normalized-rule-recipes.md` with a per-connector status table + follow-up IDs, enabled `Normalized version rules missing` diagnostics in `AdvisoryPrecedenceMerger`, and confirmed Linkset validation metrics reflect remaining upstream gaps (ACSC/CCCS/CERTBUND/Cisco/RU-BDU awaiting structured ranges).|
|
||||||
|FEEDMERGE-COORD-02-901 Connector deadline check-ins|BE-Merge|FEEDMERGE-COORD-02-900|**TODO (due 2025-10-21)** – Confirm Cccs/Cisco version-provenance updates land, capture `LinksetVersionCoverage` dashboard snapshots (expect zero missing-range warnings), and update coordination docs with the results.<br>2025-10-29: Observation metrics now surface `version_entries_total`/`missing_version_entries_total`; include screenshots for both when closing this task.|
|
|FEEDMERGE-COORD-02-901 Connector deadline check-ins|BE-Merge|FEEDMERGE-COORD-02-900|**TODO (due 2025-10-21)** – Confirm Cccs/Cisco version-provenance updates land, capture `LinksetVersionCoverage` dashboard snapshots (expect zero missing-range warnings), and update coordination docs with the results.<br>2025-10-29: Observation metrics now surface `version_entries_total`/`missing_version_entries_total`; include screenshots for both when closing this task.|
|
||||||
|FEEDMERGE-COORD-02-902 ICS-CISA version comparison support|BE-Merge, Models|FEEDMERGE-COORD-02-900|**TODO (due 2025-10-23)** – Review ICS-CISA sample advisories, validate reuse of existing comparison helpers, and pre-stage Models ticket template only if a new firmware comparator is required. Document the outcome and observation coverage logs in coordination docs + tracker files.<br>2025-10-29: `docs/dev/normalized-rule-recipes.md` (§2–§3) now covers observation entries; attach decision summary + log sample when handing off to Models.|
|
|FEEDMERGE-COORD-02-902 ICS-CISA version comparison support|BE-Merge, Models|FEEDMERGE-COORD-02-900|**TODO (due 2025-10-23)** – Review ICS-CISA sample advisories, validate reuse of existing comparison helpers, and pre-stage Models ticket template only if a new firmware comparator is required. Document the outcome and observation coverage logs in coordination docs + tracker files.<br>2025-10-29: `docs/dev/normalized-rule-recipes.md` (§2–§3) now covers observation entries; attach decision summary + log sample when handing off to Models.|
|
||||||
|FEEDMERGE-COORD-02-903 KISA firmware scheme review|BE-Merge, Models|FEEDMERGE-COORD-02-900|**TODO (due 2025-10-24)** – Pair with KISA team on proposed firmware comparison helper (`kisa.build` or variant), ensure observation mapper alignment, and open Models ticket only if a new comparator is required. Log the final helper signature and observation coverage metrics in coordination docs + tracker files.|
|
|FEEDMERGE-COORD-02-903 KISA firmware scheme review|BE-Merge, Models|FEEDMERGE-COORD-02-900|**TODO (due 2025-10-24)** – Pair with KISA team on proposed firmware comparison helper (`kisa.build` or variant), ensure observation mapper alignment, and open Models ticket only if a new comparator is required. Log the final helper signature and observation coverage metrics in coordination docs + tracker files.|
|
||||||
|
|
||||||
## Link-Not-Merge v1 Transition
|
## Link-Not-Merge v1 Transition
|
||||||
| Task | Owner(s) | Depends on | Notes |
|
| Task | Owner(s) | Depends on | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|MERGE-LNM-21-001 Migration plan authoring|BE-Merge, Architecture Guild|CONCELIER-LNM-21-101|**DONE (2025-11-03)** – Authored `docs/migration/no-merge.md` with rollout phases, backfill/validation checklists, rollback guidance, and ownership matrix for the Link-Not-Merge cutover.|
|
|MERGE-LNM-21-001 Migration plan authoring|BE-Merge, Architecture Guild|CONCELIER-LNM-21-101|**DONE (2025-11-03)** – Authored `docs/migration/no-merge.md` with rollout phases, backfill/validation checklists, rollback guidance, and ownership matrix for the Link-Not-Merge cutover.|
|
||||||
|MERGE-LNM-21-002 Merge service deprecation|BE-Merge|MERGE-LNM-21-001|**DOING (2025-11-03)** – Auditing service registrations, DI bindings, and tests consuming `AdvisoryMergeService`; drafting deprecation plan and analyzer scope prior to code removal.<br>2025-11-05 14:42Z: Implementing `concelier:features:noMergeEnabled` gate, merge job allowlist checks, `[Obsolete]` markings, and analyzer scaffolding to steer consumers toward linkset APIs.<br>2025-11-06 16:10Z: Introduced Roslyn analyzer (`CONCELIER0002`) referenced by Concelier WebService + tests, documented suppression guidance, and updated migration playbook.|
|
|MERGE-LNM-21-002 Merge service deprecation|BE-Merge|MERGE-LNM-21-001|**DOING (2025-11-03)** – Auditing service registrations, DI bindings, and tests consuming `AdvisoryMergeService`; drafting deprecation plan and analyzer scope prior to code removal.<br>2025-11-05 14:42Z: Implementing `concelier:features:noMergeEnabled` gate, merge job allowlist checks, `[Obsolete]` markings, and analyzer scaffolding to steer consumers toward linkset APIs.<br>2025-11-06 16:10Z: Introduced Roslyn analyzer (`CONCELIER0002`) referenced by Concelier WebService + tests, documented suppression guidance, and updated migration playbook.<br>2025-11-06 23:45Z: Analyzer enforcement merged; DI removal + feature-flag default change remain. Analyzer tests compile locally but restore blocked offline (`Microsoft.Bcl.AsyncInterfaces >= 8.0` absent) — capture follow-up once nuget mirror updated.|
|
||||||
> 2025-11-03: Catalogued call sites (WebService Program `AddMergeModule`, built-in job registration `merge:reconcile`, `MergeReconcileJob`) and confirmed unit tests are the only direct `MergeAsync` callers; next step is to define analyzer + replacement observability coverage.
|
> 2025-11-03: Catalogued call sites (WebService Program `AddMergeModule`, built-in job registration `merge:reconcile`, `MergeReconcileJob`) and confirmed unit tests are the only direct `MergeAsync` callers; next step is to define analyzer + replacement observability coverage.
|
||||||
|MERGE-LNM-21-003 Determinism/test updates|QA Guild, BE-Merge|MERGE-LNM-21-002|Replace merge determinism suites with observation/linkset regression tests verifying no data mutation and conflicts remain visible.|
|
|MERGE-LNM-21-003 Determinism/test updates|QA Guild, BE-Merge|MERGE-LNM-21-002|Replace merge determinism suites with observation/linkset regression tests verifying no data mutation and conflicts remain visible.|
|
||||||
|
|||||||
@@ -259,17 +259,27 @@ public sealed record AdvisoryObservationContent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record AdvisoryObservationReference
|
public sealed record AdvisoryObservationReference
|
||||||
{
|
{
|
||||||
public AdvisoryObservationReference(string type, string url)
|
public AdvisoryObservationReference(string type, string url)
|
||||||
{
|
{
|
||||||
Type = Validation.EnsureNotNullOrWhiteSpace(type, nameof(type)).ToLowerInvariant();
|
if (string.IsNullOrWhiteSpace(type))
|
||||||
Url = Validation.EnsureNotNullOrWhiteSpace(url, nameof(url));
|
{
|
||||||
}
|
throw new ArgumentException("Reference type cannot be null or whitespace.", nameof(type));
|
||||||
|
}
|
||||||
public string Type { get; }
|
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
public string Url { get; }
|
{
|
||||||
|
throw new ArgumentException("Reference url cannot be null or whitespace.", nameof(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
Type = type;
|
||||||
|
Url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Type { get; }
|
||||||
|
|
||||||
|
public string Url { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record AdvisoryObservationLinkset
|
public sealed record AdvisoryObservationLinkset
|
||||||
@@ -304,13 +314,12 @@ public sealed record AdvisoryObservationLinkset
|
|||||||
var builder = ImmutableArray.CreateBuilder<string>();
|
var builder = ImmutableArray.CreateBuilder<string>();
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
{
|
{
|
||||||
var trimmed = Validation.TrimToNull(value);
|
if (value is null)
|
||||||
if (trimmed is null)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Add(trimmed);
|
builder.Add(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.Count == 0 ? ImmutableArray<string>.Empty : builder.ToImmutable();
|
return builder.Count == 0 ? ImmutableArray<string>.Empty : builder.ToImmutable();
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MongoDB.Driver;
|
using System.Text.RegularExpressions;
|
||||||
using StellaOps.Concelier.Core.Observations;
|
using MongoDB.Bson;
|
||||||
using StellaOps.Concelier.Models.Observations;
|
using MongoDB.Driver;
|
||||||
|
using StellaOps.Concelier.Core.Observations;
|
||||||
namespace StellaOps.Concelier.Storage.Mongo.Observations;
|
using StellaOps.Concelier.Models.Observations;
|
||||||
|
|
||||||
|
namespace StellaOps.Concelier.Storage.Mongo.Observations;
|
||||||
|
|
||||||
internal sealed class AdvisoryObservationStore : IAdvisoryObservationStore
|
internal sealed class AdvisoryObservationStore : IAdvisoryObservationStore
|
||||||
{
|
{
|
||||||
@@ -48,31 +50,35 @@ internal sealed class AdvisoryObservationStore : IAdvisoryObservationStore
|
|||||||
}
|
}
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var normalizedTenant = tenant.ToLowerInvariant();
|
var normalizedTenant = tenant.ToLowerInvariant();
|
||||||
var normalizedObservationIds = NormalizeValues(observationIds, static value => value);
|
var normalizedObservationIds = NormalizeValues(observationIds, static value => value);
|
||||||
var normalizedAliases = NormalizeValues(aliases, static value => value.ToLowerInvariant());
|
var normalizedAliases = NormalizeAliasFilters(aliases);
|
||||||
var normalizedPurls = NormalizeValues(purls, static value => value);
|
var normalizedPurls = NormalizeValues(purls, static value => value);
|
||||||
var normalizedCpes = NormalizeValues(cpes, static value => value);
|
var normalizedCpes = NormalizeValues(cpes, static value => value);
|
||||||
|
|
||||||
var builder = Builders<AdvisoryObservationDocument>.Filter;
|
var builder = Builders<AdvisoryObservationDocument>.Filter;
|
||||||
var filters = new List<FilterDefinition<AdvisoryObservationDocument>>
|
var filters = new List<FilterDefinition<AdvisoryObservationDocument>>
|
||||||
{
|
{
|
||||||
builder.Eq(document => document.Tenant, normalizedTenant)
|
builder.Eq(document => document.Tenant, normalizedTenant)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (normalizedObservationIds.Length > 0)
|
if (normalizedObservationIds.Length > 0)
|
||||||
{
|
{
|
||||||
filters.Add(builder.In(document => document.Id, normalizedObservationIds));
|
filters.Add(builder.In(document => document.Id, normalizedObservationIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedAliases.Length > 0)
|
if (normalizedAliases.Length > 0)
|
||||||
{
|
{
|
||||||
filters.Add(builder.In("linkset.aliases", normalizedAliases));
|
var aliasFilters = normalizedAliases
|
||||||
}
|
.Select(alias => CreateAliasFilter(builder, alias))
|
||||||
|
.ToList();
|
||||||
if (normalizedPurls.Length > 0)
|
|
||||||
{
|
filters.Add(builder.Or(aliasFilters));
|
||||||
filters.Add(builder.In("linkset.purls", normalizedPurls));
|
}
|
||||||
|
|
||||||
|
if (normalizedPurls.Length > 0)
|
||||||
|
{
|
||||||
|
filters.Add(builder.In("linkset.purls", normalizedPurls));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedCpes.Length > 0)
|
if (normalizedCpes.Length > 0)
|
||||||
@@ -101,16 +107,16 @@ internal sealed class AdvisoryObservationStore : IAdvisoryObservationStore
|
|||||||
.Limit(limit)
|
.Limit(limit)
|
||||||
.ToListAsync(cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
return documents.Select(AdvisoryObservationDocumentFactory.ToModel).ToArray();
|
return documents.Select(AdvisoryObservationDocumentFactory.ToModel).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string[] NormalizeValues(IEnumerable<string>? values, Func<string, string> projector)
|
private static string[] NormalizeValues(IEnumerable<string>? values, Func<string, string> projector)
|
||||||
{
|
{
|
||||||
if (values is null)
|
if (values is null)
|
||||||
{
|
{
|
||||||
return Array.Empty<string>();
|
return Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
@@ -131,7 +137,51 @@ internal sealed class AdvisoryObservationStore : IAdvisoryObservationStore
|
|||||||
{
|
{
|
||||||
return Array.Empty<string>();
|
return Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return set.ToArray();
|
return set.ToArray();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static string[] NormalizeAliasFilters(IEnumerable<string>? aliases)
|
||||||
|
{
|
||||||
|
if (aliases is null)
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var list = new List<string>();
|
||||||
|
|
||||||
|
foreach (var alias in aliases)
|
||||||
|
{
|
||||||
|
if (alias is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = alias.Trim();
|
||||||
|
if (trimmed.Length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set.Add(trimmed))
|
||||||
|
{
|
||||||
|
list.Add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.Count == 0 ? Array.Empty<string>() : list.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FilterDefinition<AdvisoryObservationDocument> CreateAliasFilter(
|
||||||
|
FilterDefinitionBuilder<AdvisoryObservationDocument> builder,
|
||||||
|
string alias)
|
||||||
|
{
|
||||||
|
var escaped = Regex.Escape(alias);
|
||||||
|
var regex = new BsonRegularExpression($"^{escaped}$", "i");
|
||||||
|
|
||||||
|
return builder.Or(
|
||||||
|
builder.Regex("rawLinkset.aliases", regex),
|
||||||
|
builder.Regex("linkset.aliases", regex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,13 +42,17 @@ public sealed class AdvisoryObservationFactoryTests
|
|||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
new[] { "cpe:/a:Example:Product:1.0", "cpe:/a:example:product:1.0" },
|
new[] { "cpe:/a:Example:Product:1.0", "cpe:/a:example:product:1.0" },
|
||||||
observation.Linkset.Cpes);
|
observation.Linkset.Cpes);
|
||||||
Assert.Equal(2, observation.Linkset.References.Length);
|
Assert.Collection(
|
||||||
Assert.All(
|
|
||||||
observation.Linkset.References,
|
observation.Linkset.References,
|
||||||
reference =>
|
first =>
|
||||||
{
|
{
|
||||||
Assert.Equal("advisory", reference.Type);
|
Assert.Equal("Advisory", first.Type);
|
||||||
Assert.Equal("https://example.test/advisory", reference.Url);
|
Assert.Equal("https://example.test/advisory", first.Url);
|
||||||
|
},
|
||||||
|
second =>
|
||||||
|
{
|
||||||
|
Assert.Equal("ADVISORY", second.Type);
|
||||||
|
Assert.Equal("https://example.test/advisory", second.Url);
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ public sealed class AdvisoryObservationQueryServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
var observationIdSet = observationIds.ToImmutableHashSet(StringComparer.Ordinal);
|
var observationIdSet = observationIds.ToImmutableHashSet(StringComparer.Ordinal);
|
||||||
var aliasSet = aliases.ToImmutableHashSet(StringComparer.Ordinal);
|
var aliasSet = aliases.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
var purlSet = purls.ToImmutableHashSet(StringComparer.Ordinal);
|
var purlSet = purls.ToImmutableHashSet(StringComparer.Ordinal);
|
||||||
var cpeSet = cpes.ToImmutableHashSet(StringComparer.Ordinal);
|
var cpeSet = cpes.ToImmutableHashSet(StringComparer.Ordinal);
|
||||||
var filtered = observations
|
var filtered = observations
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.Diagnostics;
|
||||||
|
|
||||||
|
namespace StellaOps.Concelier.Merge.Analyzers.Tests;
|
||||||
|
|
||||||
|
public sealed class MergeUsageAnalyzerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ReportsDiagnostic_ForAdvisoryMergeServiceInstantiation()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
using StellaOps.Concelier.Merge.Services;
|
||||||
|
|
||||||
|
namespace Sample.App;
|
||||||
|
|
||||||
|
public sealed class Demo
|
||||||
|
{
|
||||||
|
public void Run()
|
||||||
|
{
|
||||||
|
var merge = new AdvisoryMergeService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var diagnostics = await AnalyzeAsync(source, "Sample.App");
|
||||||
|
Assert.Contains(diagnostics, d => d.Id == MergeUsageAnalyzer.DiagnosticId && d.GetMessage().Contains("AdvisoryMergeService", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReportsDiagnostic_ForAddMergeModuleInvocation()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using StellaOps.Concelier.Merge;
|
||||||
|
|
||||||
|
namespace Sample.Services;
|
||||||
|
|
||||||
|
public static class Installer
|
||||||
|
{
|
||||||
|
public static void Configure(IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddMergeModule(configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var diagnostics = await AnalyzeAsync(source, "Sample.Services");
|
||||||
|
Assert.Contains(diagnostics, d => d.Id == MergeUsageAnalyzer.DiagnosticId && d.GetMessage().Contains("AddMergeModule", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReportsDiagnostic_ForFieldDeclaration()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
using StellaOps.Concelier.Merge.Services;
|
||||||
|
|
||||||
|
namespace Sample.Library;
|
||||||
|
|
||||||
|
public sealed class Demo
|
||||||
|
{
|
||||||
|
private AdvisoryMergeService? _mergeService;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var diagnostics = await AnalyzeAsync(source, "Sample.Library");
|
||||||
|
Assert.Contains(diagnostics, d => d.Id == MergeUsageAnalyzer.DiagnosticId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DoesNotReportDiagnostic_InsideMergeAssembly()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
using StellaOps.Concelier.Merge.Services;
|
||||||
|
|
||||||
|
namespace StellaOps.Concelier.Merge.Internal;
|
||||||
|
|
||||||
|
internal static class MergeDiagnostics
|
||||||
|
{
|
||||||
|
public static AdvisoryMergeService Create() => new AdvisoryMergeService();
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var diagnostics = await AnalyzeAsync(source, "StellaOps.Concelier.Merge");
|
||||||
|
Assert.DoesNotContain(diagnostics, d => d.Id == MergeUsageAnalyzer.DiagnosticId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReportsDiagnostic_ForTypeOfUsage()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
using System;
|
||||||
|
using StellaOps.Concelier.Merge.Services;
|
||||||
|
|
||||||
|
namespace Sample.TypeOf;
|
||||||
|
|
||||||
|
public static class Demo
|
||||||
|
{
|
||||||
|
public static Type TargetType => typeof(AdvisoryMergeService);
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var diagnostics = await AnalyzeAsync(source, "Sample.TypeOf");
|
||||||
|
Assert.Contains(diagnostics, d => d.Id == MergeUsageAnalyzer.DiagnosticId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ImmutableArray<Diagnostic>> AnalyzeAsync(string source, string assemblyName)
|
||||||
|
{
|
||||||
|
var compilation = CSharpCompilation.Create(
|
||||||
|
assemblyName,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
CSharpSyntaxTree.ParseText(source),
|
||||||
|
CSharpSyntaxTree.ParseText(Stubs)
|
||||||
|
},
|
||||||
|
CreateMetadataReferences(),
|
||||||
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||||
|
|
||||||
|
var analyzer = new MergeUsageAnalyzer();
|
||||||
|
var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
|
||||||
|
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<MetadataReference> CreateMetadataReferences()
|
||||||
|
{
|
||||||
|
yield return MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location);
|
||||||
|
yield return MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string Stubs = """
|
||||||
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
{
|
||||||
|
public interface IServiceCollection { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Configuration
|
||||||
|
{
|
||||||
|
public interface IConfiguration { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace StellaOps.Concelier.Merge.Services
|
||||||
|
{
|
||||||
|
public sealed class AdvisoryMergeService { }
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace StellaOps.Concelier.Merge
|
||||||
|
{
|
||||||
|
public static class MergeServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddMergeModule(
|
||||||
|
this Microsoft.Extensions.DependencyInjection.IServiceCollection services,
|
||||||
|
Microsoft.Extensions.Configuration.IConfiguration configuration)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\\..\\__Analyzers\\StellaOps.Concelier.Merge.Analyzers\\StellaOps.Concelier.Merge.Analyzers.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -63,11 +63,14 @@ public sealed class AdvisoryObservationTests
|
|||||||
|
|
||||||
Assert.Equal("tenant-a:CVE-2025-1234:1", observation.ObservationId);
|
Assert.Equal("tenant-a:CVE-2025-1234:1", observation.ObservationId);
|
||||||
Assert.Equal("tenant-a", observation.Tenant);
|
Assert.Equal("tenant-a", observation.Tenant);
|
||||||
Assert.Equal("Vendor", observation.Source.Vendor);
|
Assert.Equal("Vendor", observation.Source.Vendor);
|
||||||
Assert.Equal(new[] { "cpe:/a:vendor:product:1" }, observation.Linkset.Cpes);
|
Assert.Equal(new[] { " Cve-2025-1234 ", "cve-2025-1234" }, observation.Linkset.Aliases.ToArray());
|
||||||
Assert.Single(observation.Linkset.References);
|
Assert.Equal(new[] { "cpe:/a:vendor:product:1" }, observation.Linkset.Cpes);
|
||||||
Assert.Equal("https://example.com/advisory", observation.Linkset.References[0].Url);
|
Assert.Equal(2, observation.Linkset.References.Length);
|
||||||
Assert.Equal(DateTimeOffset.Parse("2025-10-01T01:00:06Z"), observation.CreatedAt);
|
Assert.Equal("ADVISORY", observation.Linkset.References[0].Type);
|
||||||
Assert.Equal("emea", observation.Attributes["region"]);
|
Assert.Equal("https://example.com/advisory", observation.Linkset.References[0].Url);
|
||||||
}
|
Assert.Equal(rawLinkset.Aliases, observation.RawLinkset.Aliases);
|
||||||
}
|
Assert.Equal(DateTimeOffset.Parse("2025-10-01T01:00:06Z"), observation.CreatedAt);
|
||||||
|
Assert.Equal("emea", observation.Attributes["region"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,11 +71,12 @@ public sealed class AdvisoryObservationDocumentFactoryTests
|
|||||||
|
|
||||||
Assert.Equal("tenant-a:obs-1", observation.ObservationId);
|
Assert.Equal("tenant-a:obs-1", observation.ObservationId);
|
||||||
Assert.Equal("tenant-a", observation.Tenant);
|
Assert.Equal("tenant-a", observation.Tenant);
|
||||||
Assert.Equal("CVE-2025-1234", observation.Upstream.UpstreamId);
|
Assert.Equal("CVE-2025-1234", observation.Upstream.UpstreamId);
|
||||||
|
Assert.Equal(new[] { "CVE-2025-1234" }, observation.Linkset.Aliases.ToArray());
|
||||||
Assert.Contains("pkg:generic/foo@1.0.0", observation.Linkset.Purls);
|
Assert.Contains("pkg:generic/foo@1.0.0", observation.Linkset.Purls);
|
||||||
Assert.Equal("CSAF", observation.Content.Format);
|
Assert.Equal("CSAF", observation.Content.Format);
|
||||||
Assert.True(observation.Content.Raw?["example"]?.GetValue<bool>());
|
Assert.True(observation.Content.Raw?["example"]?.GetValue<bool>());
|
||||||
Assert.Equal("advisory", observation.Linkset.References[0].Type);
|
Assert.Equal(document.Linkset.References![0].Type, observation.Linkset.References[0].Type);
|
||||||
Assert.Equal(new[] { "CVE-2025-1234", "cve-2025-1234" }, observation.RawLinkset.Aliases);
|
Assert.Equal(new[] { "CVE-2025-1234", "cve-2025-1234" }, observation.RawLinkset.Aliases);
|
||||||
Assert.Equal("Advisory", observation.RawLinkset.References[0].Type);
|
Assert.Equal("Advisory", observation.RawLinkset.References[0].Type);
|
||||||
Assert.Equal("vendor", observation.RawLinkset.References[0].Source);
|
Assert.Equal("vendor", observation.RawLinkset.References[0].Source);
|
||||||
|
|||||||
@@ -31,22 +31,22 @@ public sealed class AdvisoryObservationStoreTests : IClassFixture<MongoIntegrati
|
|||||||
var collection = _fixture.Database.GetCollection<AdvisoryObservationDocument>(MongoStorageDefaults.Collections.AdvisoryObservations);
|
var collection = _fixture.Database.GetCollection<AdvisoryObservationDocument>(MongoStorageDefaults.Collections.AdvisoryObservations);
|
||||||
await collection.InsertManyAsync(new[]
|
await collection.InsertManyAsync(new[]
|
||||||
{
|
{
|
||||||
CreateDocument(
|
CreateDocument(
|
||||||
id: "tenant-a:nvd:alpha:1",
|
id: "tenant-a:nvd:alpha:1",
|
||||||
tenant: "tenant-a",
|
tenant: "tenant-a",
|
||||||
createdAt: new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
createdAt: new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||||
aliases: new[] { "cve-2025-0001" },
|
aliases: new[] { "CvE-2025-0001 " },
|
||||||
purls: new[] { "pkg:npm/demo@1.0.0" }),
|
purls: new[] { "pkg:npm/demo@1.0.0" }),
|
||||||
CreateDocument(
|
CreateDocument(
|
||||||
id: "tenant-a:ghsa:beta:1",
|
id: "tenant-a:ghsa:beta:1",
|
||||||
tenant: "tenant-a",
|
tenant: "tenant-a",
|
||||||
createdAt: new DateTime(2025, 1, 2, 0, 0, 0, DateTimeKind.Utc),
|
createdAt: new DateTime(2025, 1, 2, 0, 0, 0, DateTimeKind.Utc),
|
||||||
aliases: new[] { "ghsa-xyz0", "cve-2025-0001" },
|
aliases: new[] { " ghsa-xyz0", "cve-2025-0001" },
|
||||||
purls: new[] { "pkg:npm/demo@1.1.0" }),
|
purls: new[] { "pkg:npm/demo@1.1.0" }),
|
||||||
CreateDocument(
|
CreateDocument(
|
||||||
id: "tenant-b:nvd:alpha:1",
|
id: "tenant-b:nvd:alpha:1",
|
||||||
tenant: "tenant-b",
|
tenant: "tenant-b",
|
||||||
createdAt: new DateTime(2025, 1, 3, 0, 0, 0, DateTimeKind.Utc),
|
createdAt: new DateTime(2025, 1, 3, 0, 0, 0, DateTimeKind.Utc),
|
||||||
aliases: new[] { "cve-2025-0001" },
|
aliases: new[] { "cve-2025-0001" },
|
||||||
purls: new[] { "pkg:npm/demo@2.0.0" })
|
purls: new[] { "pkg:npm/demo@2.0.0" })
|
||||||
});
|
});
|
||||||
@@ -62,11 +62,15 @@ public sealed class AdvisoryObservationStoreTests : IClassFixture<MongoIntegrati
|
|||||||
limit: 5,
|
limit: 5,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
Assert.Equal(2, result.Count);
|
Assert.Equal(2, result.Count);
|
||||||
Assert.Equal("tenant-a:ghsa:beta:1", result[0].ObservationId);
|
Assert.Equal("tenant-a:ghsa:beta:1", result[0].ObservationId);
|
||||||
Assert.Equal("tenant-a:nvd:alpha:1", result[1].ObservationId);
|
Assert.Equal("tenant-a:nvd:alpha:1", result[1].ObservationId);
|
||||||
Assert.All(result, observation => Assert.Equal("tenant-a", observation.Tenant));
|
Assert.All(result, observation => Assert.Equal("tenant-a", observation.Tenant));
|
||||||
}
|
Assert.Equal("ghsa-xyz0", result[0].Linkset.Aliases[0]);
|
||||||
|
Assert.Equal("CvE-2025-0001", result[1].Linkset.Aliases[0]);
|
||||||
|
Assert.Equal(" ghsa-xyz0", result[0].RawLinkset.Aliases[0]);
|
||||||
|
Assert.Equal("CvE-2025-0001 ", result[1].RawLinkset.Aliases[0]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task FindByFiltersAsync_RespectsObservationIdsAndPurls()
|
public async Task FindByFiltersAsync_RespectsObservationIdsAndPurls()
|
||||||
@@ -166,12 +170,39 @@ public sealed class AdvisoryObservationStoreTests : IClassFixture<MongoIntegrati
|
|||||||
IEnumerable<string>? purls = null,
|
IEnumerable<string>? purls = null,
|
||||||
IEnumerable<string>? cpes = null)
|
IEnumerable<string>? cpes = null)
|
||||||
{
|
{
|
||||||
return new AdvisoryObservationDocument
|
var canonicalAliases = aliases?
|
||||||
{
|
.Where(value => value is not null)
|
||||||
Id = id,
|
.Select(value => value.Trim())
|
||||||
Tenant = tenant.ToLowerInvariant(),
|
.ToList();
|
||||||
CreatedAt = createdAt,
|
|
||||||
Source = new AdvisoryObservationSourceDocument
|
var canonicalPurls = purls?
|
||||||
|
.Where(value => value is not null)
|
||||||
|
.Select(value => value.Trim())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var canonicalCpes = cpes?
|
||||||
|
.Where(value => value is not null)
|
||||||
|
.Select(value => value.Trim())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var rawAliases = aliases?
|
||||||
|
.Where(value => value is not null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var rawPurls = purls?
|
||||||
|
.Where(value => value is not null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var rawCpes = cpes?
|
||||||
|
.Where(value => value is not null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new AdvisoryObservationDocument
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Tenant = tenant.ToLowerInvariant(),
|
||||||
|
CreatedAt = createdAt,
|
||||||
|
Source = new AdvisoryObservationSourceDocument
|
||||||
{
|
{
|
||||||
Vendor = "nvd",
|
Vendor = "nvd",
|
||||||
Stream = "feed",
|
Stream = "feed",
|
||||||
@@ -189,24 +220,31 @@ public sealed class AdvisoryObservationStoreTests : IClassFixture<MongoIntegrati
|
|||||||
Present = false
|
Present = false
|
||||||
},
|
},
|
||||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
},
|
},
|
||||||
Content = new AdvisoryObservationContentDocument
|
Content = new AdvisoryObservationContentDocument
|
||||||
{
|
{
|
||||||
Format = "csaf",
|
Format = "csaf",
|
||||||
SpecVersion = "2.0",
|
SpecVersion = "2.0",
|
||||||
Raw = BsonDocument.Parse("""{"id": "%ID%"}""".Replace("%ID%", id)),
|
Raw = BsonDocument.Parse("""{"id": "%ID%"}""".Replace("%ID%", id)),
|
||||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
},
|
},
|
||||||
Linkset = new AdvisoryObservationLinksetDocument
|
Linkset = new AdvisoryObservationLinksetDocument
|
||||||
{
|
{
|
||||||
Aliases = aliases?.Select(value => value.Trim()).ToList(),
|
Aliases = canonicalAliases,
|
||||||
Purls = purls?.Select(value => value.Trim()).ToList(),
|
Purls = canonicalPurls,
|
||||||
Cpes = cpes?.Select(value => value.Trim()).ToList(),
|
Cpes = canonicalCpes,
|
||||||
References = new List<AdvisoryObservationReferenceDocument>()
|
References = new List<AdvisoryObservationReferenceDocument>()
|
||||||
},
|
},
|
||||||
Attributes = new Dictionary<string, string>(StringComparer.Ordinal)
|
RawLinkset = new AdvisoryObservationRawLinksetDocument
|
||||||
};
|
{
|
||||||
}
|
Aliases = rawAliases,
|
||||||
|
PackageUrls = rawPurls,
|
||||||
|
Cpes = rawCpes,
|
||||||
|
References = new List<AdvisoryObservationRawReferenceDocument>()
|
||||||
|
},
|
||||||
|
Attributes = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ResetCollectionAsync()
|
private async Task ResetCollectionAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
|
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
|
||||||
<ProjectReference Include="../../StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj" />
|
<ProjectReference Include="../../StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj" />
|
||||||
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||||
<ProjectReference Include="../../__Analyzers/StellaOps.Concelier.Analyzers/StellaOps.Concelier.Analyzers.csproj"
|
<ProjectReference Include="../../__Analyzers/StellaOps.Concelier.Merge.Analyzers/StellaOps.Concelier.Merge.Analyzers.csproj"
|
||||||
OutputItemType="Analyzer"
|
OutputItemType="Analyzer"
|
||||||
ReferenceOutputAssembly="false" />
|
ReferenceOutputAssembly="false" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1183,19 +1183,19 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
|||||||
},
|
},
|
||||||
Linkset = new AdvisoryObservationLinksetDocument
|
Linkset = new AdvisoryObservationLinksetDocument
|
||||||
{
|
{
|
||||||
Aliases = aliases?.Select(value => value.Trim().ToLowerInvariant()).ToList(),
|
Aliases = aliases?.Where(value => value is not null).ToList(),
|
||||||
Purls = purls?.Select(value => value.Trim()).ToList(),
|
Purls = purls?.Where(value => value is not null).ToList(),
|
||||||
Cpes = cpes?.Select(value => value.Trim()).ToList(),
|
Cpes = cpes?.Where(value => value is not null).ToList(),
|
||||||
References = references is null
|
References = references is null
|
||||||
? new List<AdvisoryObservationReferenceDocument>()
|
? new List<AdvisoryObservationReferenceDocument>()
|
||||||
: references
|
: references
|
||||||
.Select(reference => new AdvisoryObservationReferenceDocument
|
.Select(reference => new AdvisoryObservationReferenceDocument
|
||||||
{
|
{
|
||||||
Type = reference.Type.Trim().ToLowerInvariant(),
|
Type = reference.Type,
|
||||||
Url = reference.Url.Trim()
|
Url = reference.Url
|
||||||
})
|
})
|
||||||
.ToList()
|
.ToList()
|
||||||
},
|
},
|
||||||
Attributes = new Dictionary<string, string>(StringComparer.Ordinal)
|
Attributes = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ public sealed class ScannerWebServiceOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiOptions Api { get; set; } = new();
|
public ApiOptions Api { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Console (UI) routing settings used for orchestrator link generation.
|
||||||
|
/// </summary>
|
||||||
|
public ConsoleOptions Console { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Platform event emission settings.
|
/// Platform event emission settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -266,6 +271,17 @@ public sealed class ScannerWebServiceOptions
|
|||||||
public string RuntimeSegment { get; set; } = "runtime";
|
public string RuntimeSegment { get; set; } = "runtime";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ConsoleOptions
|
||||||
|
{
|
||||||
|
public string BasePath { get; set; } = "/ui";
|
||||||
|
|
||||||
|
public string ReportsSegment { get; set; } = "reports";
|
||||||
|
|
||||||
|
public string PolicySegment { get; set; } = "policy";
|
||||||
|
|
||||||
|
public string AttestationsSegment { get; set; } = "attestations";
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class EventsOptions
|
public sealed class EventsOptions
|
||||||
{
|
{
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|||||||
@@ -16,20 +16,24 @@ namespace StellaOps.Scanner.WebService.Services;
|
|||||||
|
|
||||||
internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
||||||
{
|
{
|
||||||
private const string DefaultTenant = "default";
|
private const string DefaultTenant = "default";
|
||||||
private const string Source = "scanner.webservice";
|
private const string Source = "scanner.webservice";
|
||||||
|
|
||||||
private readonly IPlatformEventPublisher _publisher;
|
private readonly IPlatformEventPublisher _publisher;
|
||||||
private readonly TimeProvider _timeProvider;
|
private readonly TimeProvider _timeProvider;
|
||||||
private readonly ILogger<ReportEventDispatcher> _logger;
|
private readonly ILogger<ReportEventDispatcher> _logger;
|
||||||
private readonly string[] _apiBaseSegments;
|
private readonly string[] _apiBaseSegments;
|
||||||
private readonly string _reportsSegment;
|
private readonly string _reportsSegment;
|
||||||
private readonly string _policySegment;
|
private readonly string _policySegment;
|
||||||
|
private readonly string[] _consoleBaseSegments;
|
||||||
public ReportEventDispatcher(
|
private readonly string _consoleReportsSegment;
|
||||||
IPlatformEventPublisher publisher,
|
private readonly string _consolePolicySegment;
|
||||||
IOptions<ScannerWebServiceOptions> options,
|
private readonly string _consoleAttestationsSegment;
|
||||||
TimeProvider timeProvider,
|
|
||||||
|
public ReportEventDispatcher(
|
||||||
|
IPlatformEventPublisher publisher,
|
||||||
|
IOptions<ScannerWebServiceOptions> options,
|
||||||
|
TimeProvider timeProvider,
|
||||||
ILogger<ReportEventDispatcher> logger)
|
ILogger<ReportEventDispatcher> logger)
|
||||||
{
|
{
|
||||||
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
|
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
|
||||||
@@ -38,17 +42,28 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
|||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiOptions = options.Value.Api ?? new ScannerWebServiceOptions.ApiOptions();
|
var apiOptions = options.Value.Api ?? new ScannerWebServiceOptions.ApiOptions();
|
||||||
_apiBaseSegments = SplitSegments(apiOptions.BasePath);
|
_apiBaseSegments = SplitSegments(apiOptions.BasePath);
|
||||||
_reportsSegment = string.IsNullOrWhiteSpace(apiOptions.ReportsSegment)
|
_reportsSegment = string.IsNullOrWhiteSpace(apiOptions.ReportsSegment)
|
||||||
? "reports"
|
? "reports"
|
||||||
: apiOptions.ReportsSegment.Trim('/');
|
: apiOptions.ReportsSegment.Trim('/');
|
||||||
_policySegment = string.IsNullOrWhiteSpace(apiOptions.PolicySegment)
|
_policySegment = string.IsNullOrWhiteSpace(apiOptions.PolicySegment)
|
||||||
? "policy"
|
? "policy"
|
||||||
: apiOptions.PolicySegment.Trim('/');
|
: apiOptions.PolicySegment.Trim('/');
|
||||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
var consoleOptions = options.Value.Console ?? new ScannerWebServiceOptions.ConsoleOptions();
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_consoleBaseSegments = SplitSegments(consoleOptions.BasePath);
|
||||||
}
|
_consoleReportsSegment = string.IsNullOrWhiteSpace(consoleOptions.ReportsSegment)
|
||||||
|
? "reports"
|
||||||
|
: consoleOptions.ReportsSegment.Trim('/');
|
||||||
|
_consolePolicySegment = string.IsNullOrWhiteSpace(consoleOptions.PolicySegment)
|
||||||
|
? "policy"
|
||||||
|
: consoleOptions.PolicySegment.Trim('/');
|
||||||
|
_consoleAttestationsSegment = string.IsNullOrWhiteSpace(consoleOptions.AttestationsSegment)
|
||||||
|
? "attestations"
|
||||||
|
: consoleOptions.AttestationsSegment.Trim('/');
|
||||||
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task PublishAsync(
|
public async Task PublishAsync(
|
||||||
ReportRequestDto request,
|
ReportRequestDto request,
|
||||||
@@ -240,21 +255,21 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReportLinksPayload BuildLinks(HttpContext context, ReportDocumentDto document, DsseEnvelopeDto? envelope)
|
private ReportLinksPayload BuildLinks(HttpContext context, ReportDocumentDto document, DsseEnvelopeDto? envelope)
|
||||||
{
|
{
|
||||||
if (!context.Request.Host.HasValue)
|
if (!context.Request.Host.HasValue)
|
||||||
{
|
{
|
||||||
return new ReportLinksPayload();
|
return new ReportLinksPayload();
|
||||||
}
|
}
|
||||||
|
|
||||||
var reportUi = BuildAbsoluteUri(context, "ui", "reports", document.ReportId);
|
var reportUi = BuildAbsoluteUri(context, ConcatSegments(_consoleBaseSegments, _consoleReportsSegment, document.ReportId));
|
||||||
var reportApi = BuildAbsoluteUri(context, ConcatSegments(_apiBaseSegments, _reportsSegment, document.ReportId));
|
var reportApi = BuildAbsoluteUri(context, ConcatSegments(_apiBaseSegments, _reportsSegment, document.ReportId));
|
||||||
|
|
||||||
LinkTarget? policyLink = null;
|
LinkTarget? policyLink = null;
|
||||||
if (!string.IsNullOrWhiteSpace(document.Policy.RevisionId))
|
if (!string.IsNullOrWhiteSpace(document.Policy.RevisionId))
|
||||||
{
|
{
|
||||||
var policyRevision = document.Policy.RevisionId!;
|
var policyRevision = document.Policy.RevisionId!;
|
||||||
var policyUi = BuildAbsoluteUri(context, "ui", "policy", "revisions", policyRevision);
|
var policyUi = BuildAbsoluteUri(context, ConcatSegments(_consoleBaseSegments, _consolePolicySegment, "revisions", policyRevision));
|
||||||
var policyApi = BuildAbsoluteUri(context, ConcatSegments(_apiBaseSegments, _policySegment, "revisions", policyRevision));
|
var policyApi = BuildAbsoluteUri(context, ConcatSegments(_apiBaseSegments, _policySegment, "revisions", policyRevision));
|
||||||
policyLink = LinkTarget.Create(policyUi, policyApi);
|
policyLink = LinkTarget.Create(policyUi, policyApi);
|
||||||
}
|
}
|
||||||
@@ -262,7 +277,7 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
|
|||||||
LinkTarget? attestationLink = null;
|
LinkTarget? attestationLink = null;
|
||||||
if (envelope is not null)
|
if (envelope is not null)
|
||||||
{
|
{
|
||||||
var attestationUi = BuildAbsoluteUri(context, "ui", "attestations", document.ReportId);
|
var attestationUi = BuildAbsoluteUri(context, ConcatSegments(_consoleBaseSegments, _consoleAttestationsSegment, document.ReportId));
|
||||||
var attestationApi = BuildAbsoluteUri(context, ConcatSegments(_apiBaseSegments, _reportsSegment, document.ReportId, "attestation"));
|
var attestationApi = BuildAbsoluteUri(context, ConcatSegments(_apiBaseSegments, _reportsSegment, document.ReportId, "attestation"));
|
||||||
attestationLink = LinkTarget.Create(attestationUi, attestationApi);
|
attestationLink = LinkTarget.Create(attestationUi, attestationApi);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
> 2025-11-05 19:18Z: Added configurator to project wiring and unit test ensuring Surface.Env cache root is honoured.
|
> 2025-11-05 19:18Z: Added configurator to project wiring and unit test ensuring Surface.Env cache root is honoured.
|
||||||
| SCANNER-SECRETS-02 | DOING (2025-11-02) | Scanner WebService Guild, Security Guild | SURFACE-SECRETS-02 | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens).<br>2025-11-02: Export/report flows now depend on Surface.Secrets stub; integration tests in progress. | Secrets fetched through shared provider; unit/integration tests cover rotation + failure cases. |
|
| SCANNER-SECRETS-02 | DOING (2025-11-02) | Scanner WebService Guild, Security Guild | SURFACE-SECRETS-02 | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens).<br>2025-11-02: Export/report flows now depend on Surface.Secrets stub; integration tests in progress. | Secrets fetched through shared provider; unit/integration tests cover rotation + failure cases. |
|
||||||
| SCANNER-EVENTS-16-301 | BLOCKED (2025-10-26) | Scanner WebService Guild | ORCH-SVC-38-101, NOTIFY-SVC-38-001 | Emit orchestrator-compatible envelopes (`scanner.event.*`) and update integration tests to verify Notifier ingestion (no Redis queue coupling). | Tests assert envelope schema + orchestrator publish; Notifier consumer harness passes; docs updated with new event contract. Blocked by .NET 10 preview OpenAPI/Auth dependency drift preventing `dotnet test` completion. |
|
| SCANNER-EVENTS-16-301 | BLOCKED (2025-10-26) | Scanner WebService Guild | ORCH-SVC-38-101, NOTIFY-SVC-38-001 | Emit orchestrator-compatible envelopes (`scanner.event.*`) and update integration tests to verify Notifier ingestion (no Redis queue coupling). | Tests assert envelope schema + orchestrator publish; Notifier consumer harness passes; docs updated with new event contract. Blocked by .NET 10 preview OpenAPI/Auth dependency drift preventing `dotnet test` completion. |
|
||||||
| SCANNER-EVENTS-16-302 | DOING (2025-10-26) | Scanner WebService Guild | SCANNER-EVENTS-16-301 | Extend orchestrator event links (report/policy/attestation) once endpoints are finalised across gateway + console. | Links section covers UI/API targets; downstream consumers validated; docs/samples updated. |
|
| SCANNER-EVENTS-16-302 | DONE (2025-11-06) | Scanner WebService Guild | SCANNER-EVENTS-16-301 | Extend orchestrator event links (report/policy/attestation) once endpoints are finalised across gateway + console.<br>2025-11-06 22:55Z: Dispatcher now honours configurable API/console base segments, JSON samples/docs refreshed, and `ReportEventDispatcherTests` extended. Tests: `StellaOps.Scanner.WebService.Tests` build until pre-existing `SurfaceCacheOptionsConfiguratorTests` ctor signature drift (tracked separately). | Links section covers UI/API targets; downstream consumers validated; docs/samples updated. |
|
||||||
|
|
||||||
## Graph Explorer v1 (Sprint 21)
|
## Graph Explorer v1 (Sprint 21)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -16,108 +16,108 @@ using StellaOps.Policy;
|
|||||||
using StellaOps.Scanner.WebService.Contracts;
|
using StellaOps.Scanner.WebService.Contracts;
|
||||||
using StellaOps.Scanner.WebService.Options;
|
using StellaOps.Scanner.WebService.Options;
|
||||||
using StellaOps.Scanner.WebService.Services;
|
using StellaOps.Scanner.WebService.Services;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.WebService.Tests;
|
namespace StellaOps.Scanner.WebService.Tests;
|
||||||
|
|
||||||
public sealed class ReportEventDispatcherTests
|
public sealed class ReportEventDispatcherTests
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||||
{
|
{
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
};
|
};
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PublishAsync_EmitsReportReadyAndScanCompleted()
|
public async Task PublishAsync_EmitsReportReadyAndScanCompleted()
|
||||||
{
|
{
|
||||||
var publisher = new RecordingEventPublisher();
|
var publisher = new RecordingEventPublisher();
|
||||||
var dispatcher = new ReportEventDispatcher(publisher, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
|
var dispatcher = new ReportEventDispatcher(publisher, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
|
||||||
var cancellationToken = CancellationToken.None;
|
var cancellationToken = CancellationToken.None;
|
||||||
|
|
||||||
var request = new ReportRequestDto
|
var request = new ReportRequestDto
|
||||||
{
|
{
|
||||||
ImageDigest = "sha256:feedface",
|
ImageDigest = "sha256:feedface",
|
||||||
Findings = new[]
|
Findings = new[]
|
||||||
{
|
{
|
||||||
new PolicyPreviewFindingDto
|
new PolicyPreviewFindingDto
|
||||||
{
|
{
|
||||||
Id = "finding-1",
|
Id = "finding-1",
|
||||||
Severity = "Critical",
|
Severity = "Critical",
|
||||||
Repository = "acme/edge/api",
|
Repository = "acme/edge/api",
|
||||||
Cve = "CVE-2024-9999",
|
Cve = "CVE-2024-9999",
|
||||||
Tags = new[] { "reachability:runtime", "kev:CVE-2024-9999" }
|
Tags = new[] { "reachability:runtime", "kev:CVE-2024-9999" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var baseline = new PolicyVerdict("finding-1", PolicyVerdictStatus.Pass, ConfigVersion: "1.0");
|
var baseline = new PolicyVerdict("finding-1", PolicyVerdictStatus.Pass, ConfigVersion: "1.0");
|
||||||
var projected = new PolicyVerdict(
|
var projected = new PolicyVerdict(
|
||||||
"finding-1",
|
"finding-1",
|
||||||
PolicyVerdictStatus.Blocked,
|
PolicyVerdictStatus.Blocked,
|
||||||
Score: 47.5,
|
Score: 47.5,
|
||||||
ConfigVersion: "1.0",
|
ConfigVersion: "1.0",
|
||||||
SourceTrust: "NVD",
|
SourceTrust: "NVD",
|
||||||
Reachability: "runtime");
|
Reachability: "runtime");
|
||||||
|
|
||||||
var preview = new PolicyPreviewResponse(
|
var preview = new PolicyPreviewResponse(
|
||||||
Success: true,
|
Success: true,
|
||||||
PolicyDigest: "digest-123",
|
PolicyDigest: "digest-123",
|
||||||
RevisionId: "rev-42",
|
RevisionId: "rev-42",
|
||||||
Issues: ImmutableArray<PolicyIssue>.Empty,
|
Issues: ImmutableArray<PolicyIssue>.Empty,
|
||||||
Diffs: ImmutableArray.Create(new PolicyVerdictDiff(baseline, projected)),
|
Diffs: ImmutableArray.Create(new PolicyVerdictDiff(baseline, projected)),
|
||||||
ChangedCount: 1);
|
ChangedCount: 1);
|
||||||
|
|
||||||
var document = new ReportDocumentDto
|
var document = new ReportDocumentDto
|
||||||
{
|
{
|
||||||
ReportId = "report-abc",
|
ReportId = "report-abc",
|
||||||
ImageDigest = "sha256:feedface",
|
ImageDigest = "sha256:feedface",
|
||||||
GeneratedAt = DateTimeOffset.Parse("2025-10-19T12:34:56Z"),
|
GeneratedAt = DateTimeOffset.Parse("2025-10-19T12:34:56Z"),
|
||||||
Verdict = "blocked",
|
Verdict = "blocked",
|
||||||
Policy = new ReportPolicyDto
|
Policy = new ReportPolicyDto
|
||||||
{
|
{
|
||||||
RevisionId = "rev-42",
|
RevisionId = "rev-42",
|
||||||
Digest = "digest-123"
|
Digest = "digest-123"
|
||||||
},
|
},
|
||||||
Summary = new ReportSummaryDto
|
Summary = new ReportSummaryDto
|
||||||
{
|
{
|
||||||
Total = 1,
|
Total = 1,
|
||||||
Blocked = 1,
|
Blocked = 1,
|
||||||
Warned = 0,
|
Warned = 0,
|
||||||
Ignored = 0,
|
Ignored = 0,
|
||||||
Quieted = 0
|
Quieted = 0
|
||||||
},
|
},
|
||||||
Verdicts = new[]
|
Verdicts = new[]
|
||||||
{
|
{
|
||||||
new PolicyPreviewVerdictDto
|
new PolicyPreviewVerdictDto
|
||||||
{
|
{
|
||||||
FindingId = "finding-1",
|
FindingId = "finding-1",
|
||||||
Status = "Blocked",
|
Status = "Blocked",
|
||||||
Score = 47.5,
|
Score = 47.5,
|
||||||
SourceTrust = "NVD",
|
SourceTrust = "NVD",
|
||||||
Reachability = "runtime"
|
Reachability = "runtime"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var envelope = new DsseEnvelopeDto
|
var envelope = new DsseEnvelopeDto
|
||||||
{
|
{
|
||||||
PayloadType = "application/vnd.stellaops.report+json",
|
PayloadType = "application/vnd.stellaops.report+json",
|
||||||
Payload = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(document, SerializerOptions)),
|
Payload = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(document, SerializerOptions)),
|
||||||
Signatures = new[]
|
Signatures = new[]
|
||||||
{
|
{
|
||||||
new DsseSignatureDto { KeyId = "test-key", Algorithm = "hs256", Signature = "signature-value" }
|
new DsseSignatureDto { KeyId = "test-key", Algorithm = "hs256", Signature = "signature-value" }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var context = new DefaultHttpContext();
|
var context = new DefaultHttpContext();
|
||||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||||
{
|
{
|
||||||
new Claim(StellaOpsClaimTypes.Tenant, "tenant-alpha")
|
new Claim(StellaOpsClaimTypes.Tenant, "tenant-alpha")
|
||||||
}));
|
}));
|
||||||
context.Request.Scheme = "https";
|
context.Request.Scheme = "https";
|
||||||
context.Request.Host = new HostString("scanner.example");
|
context.Request.Host = new HostString("scanner.example");
|
||||||
|
|
||||||
await dispatcher.PublishAsync(request, preview, document, envelope, context, cancellationToken);
|
await dispatcher.PublishAsync(request, preview, document, envelope, context, cancellationToken);
|
||||||
|
|
||||||
Assert.Equal(2, publisher.Events.Count);
|
Assert.Equal(2, publisher.Events.Count);
|
||||||
|
|
||||||
var readyEvent = Assert.Single(publisher.Events, evt => evt.Kind == OrchestratorEventKinds.ScannerReportReady);
|
var readyEvent = Assert.Single(publisher.Events, evt => evt.Kind == OrchestratorEventKinds.ScannerReportReady);
|
||||||
@@ -165,6 +165,126 @@ public sealed class ReportEventDispatcherTests
|
|||||||
Assert.Equal("blocked", scanPayload.Report.Verdict);
|
Assert.Equal("blocked", scanPayload.Report.Verdict);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PublishAsync_HonoursConfiguredConsoleAndApiSegments()
|
||||||
|
{
|
||||||
|
var options = Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions
|
||||||
|
{
|
||||||
|
Api = new ScannerWebServiceOptions.ApiOptions
|
||||||
|
{
|
||||||
|
BasePath = "/custom-api",
|
||||||
|
ReportsSegment = "reports-view",
|
||||||
|
PolicySegment = "policy-hub"
|
||||||
|
},
|
||||||
|
Console = new ScannerWebServiceOptions.ConsoleOptions
|
||||||
|
{
|
||||||
|
BasePath = "/console",
|
||||||
|
ReportsSegment = "insights",
|
||||||
|
PolicySegment = "policy-center",
|
||||||
|
AttestationsSegment = "evidence"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var publisher = new RecordingEventPublisher();
|
||||||
|
var dispatcher = new ReportEventDispatcher(publisher, options, TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
|
||||||
|
var cancellationToken = CancellationToken.None;
|
||||||
|
|
||||||
|
var request = new ReportRequestDto
|
||||||
|
{
|
||||||
|
ImageDigest = "sha256:feedface",
|
||||||
|
Findings = new[]
|
||||||
|
{
|
||||||
|
new PolicyPreviewFindingDto
|
||||||
|
{
|
||||||
|
Id = "finding-1",
|
||||||
|
Severity = "Critical",
|
||||||
|
Repository = "acme/edge/api",
|
||||||
|
Cve = "CVE-2024-9999",
|
||||||
|
Tags = new[] { "reachability:runtime", "kev:CVE-2024-9999" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var baseline = new PolicyVerdict("finding-1", PolicyVerdictStatus.Pass, ConfigVersion: "1.0");
|
||||||
|
var projected = new PolicyVerdict(
|
||||||
|
"finding-1",
|
||||||
|
PolicyVerdictStatus.Blocked,
|
||||||
|
Score: 47.5,
|
||||||
|
ConfigVersion: "1.0",
|
||||||
|
SourceTrust: "NVD",
|
||||||
|
Reachability: "runtime");
|
||||||
|
|
||||||
|
var preview = new PolicyPreviewResponse(
|
||||||
|
Success: true,
|
||||||
|
PolicyDigest: "digest-123",
|
||||||
|
RevisionId: "rev-42",
|
||||||
|
Issues: ImmutableArray<PolicyIssue>.Empty,
|
||||||
|
Diffs: ImmutableArray.Create(new PolicyVerdictDiff(baseline, projected)),
|
||||||
|
ChangedCount: 1);
|
||||||
|
|
||||||
|
var document = new ReportDocumentDto
|
||||||
|
{
|
||||||
|
ReportId = "report-abc",
|
||||||
|
ImageDigest = "sha256:feedface",
|
||||||
|
GeneratedAt = DateTimeOffset.Parse("2025-10-19T12:34:56Z"),
|
||||||
|
Verdict = "blocked",
|
||||||
|
Policy = new ReportPolicyDto
|
||||||
|
{
|
||||||
|
RevisionId = "rev-42",
|
||||||
|
Digest = "digest-123"
|
||||||
|
},
|
||||||
|
Summary = new ReportSummaryDto
|
||||||
|
{
|
||||||
|
Total = 1,
|
||||||
|
Blocked = 1,
|
||||||
|
Warned = 0,
|
||||||
|
Ignored = 0,
|
||||||
|
Quieted = 0
|
||||||
|
},
|
||||||
|
Verdicts = new[]
|
||||||
|
{
|
||||||
|
new PolicyPreviewVerdictDto
|
||||||
|
{
|
||||||
|
FindingId = "finding-1",
|
||||||
|
Status = "Blocked",
|
||||||
|
Score = 47.5,
|
||||||
|
SourceTrust = "NVD",
|
||||||
|
Reachability = "runtime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var envelope = new DsseEnvelopeDto
|
||||||
|
{
|
||||||
|
PayloadType = "application/vnd.stellaops.report+json",
|
||||||
|
Payload = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(document, SerializerOptions)),
|
||||||
|
Signatures = new[]
|
||||||
|
{
|
||||||
|
new DsseSignatureDto { KeyId = "test-key", Algorithm = "hs256", Signature = "signature-value" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||||
|
{
|
||||||
|
new Claim(StellaOpsClaimTypes.Tenant, "tenant-alpha")
|
||||||
|
}));
|
||||||
|
context.Request.Scheme = "https";
|
||||||
|
context.Request.Host = new HostString("scanner.example");
|
||||||
|
|
||||||
|
await dispatcher.PublishAsync(request, preview, document, envelope, context, cancellationToken);
|
||||||
|
|
||||||
|
var readyEvent = Assert.Single(publisher.Events, evt => evt.Kind == OrchestratorEventKinds.ScannerReportReady);
|
||||||
|
var links = Assert.IsType<ReportReadyEventPayload>(readyEvent.Payload).Links;
|
||||||
|
|
||||||
|
Assert.Equal("https://scanner.example/console/insights/report-abc", links.Report?.Ui);
|
||||||
|
Assert.Equal("https://scanner.example/custom-api/reports-view/report-abc", links.Report?.Api);
|
||||||
|
Assert.Equal("https://scanner.example/console/policy-center/revisions/rev-42", links.Policy?.Ui);
|
||||||
|
Assert.Equal("https://scanner.example/custom-api/policy-hub/revisions/rev-42", links.Policy?.Api);
|
||||||
|
Assert.Equal("https://scanner.example/console/evidence/report-abc", links.Attestation?.Ui);
|
||||||
|
Assert.Equal("https://scanner.example/custom-api/reports-view/report-abc/attestation", links.Attestation?.Api);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class RecordingEventPublisher : IPlatformEventPublisher
|
private sealed class RecordingEventPublisher : IPlatformEventPublisher
|
||||||
{
|
{
|
||||||
public List<OrchestratorEvent> Events { get; } = new();
|
public List<OrchestratorEvent> Events { get; } = new();
|
||||||
@@ -173,6 +293,6 @@ public sealed class ReportEventDispatcherTests
|
|||||||
{
|
{
|
||||||
Events.Add(@event);
|
Events.Add(@event);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ public sealed class ReportSamplesTests
|
|||||||
Assert.NotNull(response!.Report);
|
Assert.NotNull(response!.Report);
|
||||||
Assert.NotNull(response.Dsse);
|
Assert.NotNull(response.Dsse);
|
||||||
|
|
||||||
var reportBytes = JsonSerializer.SerializeToUtf8Bytes(response.Report, SerializerOptions);
|
var reportBytes = JsonSerializer.SerializeToUtf8Bytes(response.Report, SerializerOptions);
|
||||||
var expectedPayload = Convert.ToBase64String(reportBytes);
|
var expectedPayload = Convert.ToBase64String(reportBytes);
|
||||||
Assert.Equal(expectedPayload, response.Dsse!.Payload);
|
Assert.Equal(expectedPayload, response.Dsse!.Payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Net;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -22,8 +23,8 @@ using StellaOps.Scanner.WebService.Services;
|
|||||||
|
|
||||||
namespace StellaOps.Scanner.WebService.Tests;
|
namespace StellaOps.Scanner.WebService.Tests;
|
||||||
|
|
||||||
public sealed class ScansEndpointsTests
|
public sealed class ScansEndpointsTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SubmitScanReturnsAcceptedAndStatusRetrievable()
|
public async Task SubmitScanReturnsAcceptedAndStatusRetrievable()
|
||||||
{
|
{
|
||||||
@@ -272,7 +273,7 @@ public sealed class ScansEndpointsTests
|
|||||||
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
|
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
var payload = await response.Content.ReadFromJsonAsync<EntryTraceResponse>();
|
var payload = await response.Content.ReadFromJsonAsync<EntryTraceResponse>(SerializerOptions, CancellationToken.None);
|
||||||
Assert.NotNull(payload);
|
Assert.NotNull(payload);
|
||||||
Assert.Equal(scanId, payload!.ScanId);
|
Assert.Equal(scanId, payload!.ScanId);
|
||||||
Assert.Equal("sha256:entrytrace", payload.ImageDigest);
|
Assert.Equal("sha256:entrytrace", payload.ImageDigest);
|
||||||
@@ -559,7 +560,7 @@ public sealed class ScansEndpointsTests
|
|||||||
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
|
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
var payload = await response.Content.ReadFromJsonAsync<EntryTraceResponse>();
|
var payload = await response.Content.ReadFromJsonAsync<EntryTraceResponse>(SerializerOptions, CancellationToken.None);
|
||||||
Assert.NotNull(payload);
|
Assert.NotNull(payload);
|
||||||
Assert.Equal(storedResult.ScanId, payload!.ScanId);
|
Assert.Equal(storedResult.ScanId, payload!.ScanId);
|
||||||
Assert.Equal(storedResult.ImageDigest, payload.ImageDigest);
|
Assert.Equal(storedResult.ImageDigest, payload.ImageDigest);
|
||||||
@@ -583,7 +584,10 @@ public sealed class ScansEndpointsTests
|
|||||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
Converters = { new JsonStringEnumConverter() }
|
||||||
|
};
|
||||||
|
|
||||||
private sealed record ProgressEnvelope(
|
private sealed record ProgressEnvelope(
|
||||||
string ScanId,
|
string ScanId,
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ public sealed class SurfaceCacheOptionsConfiguratorTests
|
|||||||
"surface-cache",
|
"surface-cache",
|
||||||
null,
|
null,
|
||||||
cacheRoot,
|
cacheRoot,
|
||||||
cacheQuotaMegabytes: 512,
|
512,
|
||||||
prefetchEnabled: true,
|
true,
|
||||||
featureFlags: Array.Empty<string>(),
|
Array.Empty<string>(),
|
||||||
secrets: new SurfaceSecretsConfiguration("file", "tenant-b", "/etc/secrets", null, null, allowInline: false),
|
new SurfaceSecretsConfiguration("file", "tenant-b", "/etc/secrets", null, null, false),
|
||||||
tenant: "tenant-b",
|
"tenant-b",
|
||||||
tls: new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
|
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
|
||||||
|
|
||||||
var environment = new StubSurfaceEnvironment(settings);
|
var environment = new StubSurfaceEnvironment(settings);
|
||||||
var configurator = new SurfaceCacheOptionsConfigurator(environment);
|
var configurator = new SurfaceCacheOptionsConfigurator(environment);
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ public sealed class SurfaceCacheOptionsConfiguratorTests
|
|||||||
"surface-cache",
|
"surface-cache",
|
||||||
null,
|
null,
|
||||||
cacheRoot,
|
cacheRoot,
|
||||||
cacheQuotaMegabytes: 1024,
|
1024,
|
||||||
prefetchEnabled: false,
|
false,
|
||||||
featureFlags: Array.Empty<string>(),
|
Array.Empty<string>(),
|
||||||
secrets: new SurfaceSecretsConfiguration("file", "tenant-a", "/etc/secrets", null, null, false),
|
new SurfaceSecretsConfiguration("file", "tenant-a", "/etc/secrets", null, null, false),
|
||||||
tenant: "tenant-a",
|
"tenant-a",
|
||||||
tls: new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
|
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
|
||||||
|
|
||||||
var environment = new StubSurfaceEnvironment(settings);
|
var environment = new StubSurfaceEnvironment(settings);
|
||||||
var configurator = new SurfaceCacheOptionsConfigurator(environment);
|
var configurator = new SurfaceCacheOptionsConfigurator(environment);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"kind": "scanner.event.report.ready",
|
"kind": "scanner.event.report.ready",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tenant": "tenant-alpha",
|
"tenant": "tenant-alpha",
|
||||||
"occurredAt": "2025-10-19T12:34:56Z",
|
"occurredAt": "2025-10-19T12:34:56+00:00",
|
||||||
"recordedAt": "2025-10-19T12:34:57Z",
|
"recordedAt": "2025-10-19T12:34:57+00:00",
|
||||||
"source": "scanner.webservice",
|
"source": "scanner.webservice",
|
||||||
"idempotencyKey": "scanner.event.report.ready:tenant-alpha:report-abc",
|
"idempotencyKey": "scanner.event.report.ready:tenant-alpha:report-abc",
|
||||||
"correlationId": "report-abc",
|
"correlationId": "report-abc",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"reportId": "report-abc",
|
"reportId": "report-abc",
|
||||||
"scanId": "report-abc",
|
"scanId": "report-abc",
|
||||||
"imageDigest": "sha256:feedface",
|
"imageDigest": "sha256:feedface",
|
||||||
"generatedAt": "2025-10-19T12:34:56Z",
|
"generatedAt": "2025-10-19T12:34:56+00:00",
|
||||||
"verdict": "fail",
|
"verdict": "fail",
|
||||||
"summary": {
|
"summary": {
|
||||||
"total": 1,
|
"total": 1,
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
"reportId": "report-abc",
|
"reportId": "report-abc",
|
||||||
"generatedAt": "2025-10-19T12:34:56Z",
|
"generatedAt": "2025-10-19T12:34:56+00:00",
|
||||||
"imageDigest": "sha256:feedface",
|
"imageDigest": "sha256:feedface",
|
||||||
"policy": {
|
"policy": {
|
||||||
"digest": "digest-123",
|
"digest": "digest-123",
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"kind": "scanner.event.scan.completed",
|
"kind": "scanner.event.scan.completed",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"tenant": "tenant-alpha",
|
"tenant": "tenant-alpha",
|
||||||
"occurredAt": "2025-10-19T12:34:56Z",
|
"occurredAt": "2025-10-19T12:34:56+00:00",
|
||||||
"recordedAt": "2025-10-19T12:34:57Z",
|
"recordedAt": "2025-10-19T12:34:57+00:00",
|
||||||
"source": "scanner.webservice",
|
"source": "scanner.webservice",
|
||||||
"idempotencyKey": "scanner.event.scan.completed:tenant-alpha:report-abc",
|
"idempotencyKey": "scanner.event.scan.completed:tenant-alpha:report-abc",
|
||||||
"correlationId": "report-abc",
|
"correlationId": "report-abc",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
"reportId": "report-abc",
|
"reportId": "report-abc",
|
||||||
"generatedAt": "2025-10-19T12:34:56Z",
|
"generatedAt": "2025-10-19T12:34:56+00:00",
|
||||||
"imageDigest": "sha256:feedface",
|
"imageDigest": "sha256:feedface",
|
||||||
"policy": {
|
"policy": {
|
||||||
"digest": "digest-123",
|
"digest": "digest-123",
|
||||||
|
|||||||
80
src/Scanner/samples/api/reports/report-sample.dsse.json
Normal file
80
src/Scanner/samples/api/reports/report-sample.dsse.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"report": {
|
||||||
|
"reportId": "report-abc",
|
||||||
|
"imageDigest": "sha256:feedface",
|
||||||
|
"generatedAt": "2025-10-19T12:34:56+00:00",
|
||||||
|
"verdict": "blocked",
|
||||||
|
"policy": {
|
||||||
|
"revisionId": "rev-42",
|
||||||
|
"digest": "digest-123"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"total": 1,
|
||||||
|
"blocked": 1,
|
||||||
|
"warned": 0,
|
||||||
|
"ignored": 0,
|
||||||
|
"quieted": 0
|
||||||
|
},
|
||||||
|
"verdicts": [
|
||||||
|
{
|
||||||
|
"findingId": "finding-1",
|
||||||
|
"reachability": "runtime",
|
||||||
|
"score": 47.5,
|
||||||
|
"sourceTrust": "NVD",
|
||||||
|
"status": "Blocked"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"issues": [],
|
||||||
|
"surface": {
|
||||||
|
"tenant": "tenant-alpha",
|
||||||
|
"generatedAt": "2025-10-19T12:34:56+00:00",
|
||||||
|
"manifestDigest": "sha256:4fee87d186291ddfbbcc2c56c8ed0e828520b8f52e1cde0e13bba082f10918d7",
|
||||||
|
"manifestUri": "cas://scanner-artifacts/scanner/surface/manifests/tenant-alpha/sha256/4f/ee/4fee87d186291ddfbbcc2c56c8ed0e828520b8f52e1cde0e13bba082f10918d7.json",
|
||||||
|
"manifest": {
|
||||||
|
"schema": "stellaops.surface.manifest@1",
|
||||||
|
"tenant": "tenant-alpha",
|
||||||
|
"imageDigest": "sha256:feedface",
|
||||||
|
"generatedAt": "2025-10-19T12:34:56+00:00",
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"kind": "entry-trace",
|
||||||
|
"uri": "cas://scanner-artifacts/scanner/entry-trace/f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0/entry-trace.json",
|
||||||
|
"digest": "sha256:f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
|
||||||
|
"mediaType": "application/json",
|
||||||
|
"format": "json",
|
||||||
|
"sizeBytes": 4096
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "sbom-inventory",
|
||||||
|
"uri": "cas://scanner-artifacts/scanner/images/feedface/sbom.cdx.json",
|
||||||
|
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
"mediaType": "application/vnd.cyclonedx+json;version=1.6;view=inventory",
|
||||||
|
"format": "cdx-json",
|
||||||
|
"sizeBytes": 24576,
|
||||||
|
"view": "inventory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "sbom-usage",
|
||||||
|
"uri": "cas://scanner-artifacts/scanner/images/feedface/sbom-usage.cdx.json",
|
||||||
|
"digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
|
||||||
|
"mediaType": "application/vnd.cyclonedx+json;version=1.6;view=usage",
|
||||||
|
"format": "cdx-json",
|
||||||
|
"sizeBytes": 16384,
|
||||||
|
"view": "usage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dsse": {
|
||||||
|
"payloadType": "application/vnd.stellaops.report+json",
|
||||||
|
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC1hYmMiLCJpbWFnZURpZ2VzdCI6InNoYTI1NjpmZWVkZmFjZSIsImdlbmVyYXRlZEF0IjoiMjAyNS0xMC0xOVQxMjozNDo1NiswMDowMCIsInZlcmRpY3QiOiJibG9ja2VkIiwicG9saWN5Ijp7InJldmlzaW9uSWQiOiJyZXYtNDIiLCJkaWdlc3QiOiJkaWdlc3QtMTIzIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwic2NvcmUiOjQ3LjUsInNvdXJjZVRydXN0IjoiTlZEIiwicmVhY2hhYmlsaXR5IjoicnVudGltZSJ9XSwiaXNzdWVzIjpbXSwic3VyZmFjZSI6eyJ0ZW5hbnQiOiJ0ZW5hbnQtYWxwaGEiLCJnZW5lcmF0ZWRBdCI6IjIwMjUtMTAtMTlUMTI6MzQ6NTYrMDA6MDAiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo0ZmVlODdkMTg2MjkxZGRmYmJjYzJjNTZjOGVkMGU4Mjg1MjBiOGY1MmUxY2RlMGUxM2JiYTA4MmYxMDkxOGQ3IiwibWFuaWZlc3RVcmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL3N1cmZhY2UvbWFuaWZlc3RzL3RlbmFudC1hbHBoYS9zaGEyNTYvNGYvZWUvNGZlZTg3ZDE4NjI5MWRkZmJiY2MyYzU2YzhlZDBlODI4NTIwYjhmNTJlMWNkZTBlMTNiYmEwODJmMTA5MThkNy5qc29uIiwibWFuaWZlc3QiOnsic2NoZW1hIjoic3RlbGxhb3BzLnN1cmZhY2UubWFuaWZlc3RAMSIsInRlbmFudCI6InRlbmFudC1hbHBoYSIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmZlZWRmYWNlIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDEyOjM0OjU2KzAwOjAwIiwiYXJ0aWZhY3RzIjpbeyJraW5kIjoiZW50cnktdHJhY2UiLCJ1cmkiOiJjYXM6Ly9zY2FubmVyLWFydGlmYWN0cy9zY2FubmVyL2VudHJ5LXRyYWNlL2YwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwL2VudHJ5LXRyYWNlLmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6ZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMGYwZjBmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJmb3JtYXQiOiJqc29uIiwic2l6ZUJ5dGVzIjo0MDk2fSx7ImtpbmQiOiJzYm9tLWludmVudG9yeSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20uY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHhcdTAwMkJqc29uO3ZlcnNpb249MS42O3ZpZXc9aW52ZW50b3J5IiwiZm9ybWF0IjoiY2R4LWpzb24iLCJzaXplQnl0ZXMiOjI0NTc2LCJ2aWV3IjoiaW52ZW50b3J5In0seyJraW5kIjoic2JvbS11c2FnZSIsInVyaSI6ImNhczovL3NjYW5uZXItYXJ0aWZhY3RzL3NjYW5uZXIvaW1hZ2VzL2ZlZWRmYWNlL3Nib20tdXNhZ2UuY2R4Lmpzb24iLCJkaWdlc3QiOiJzaGEyNTY6MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5jeWNsb25lZHhcdTAwMkJqc29uO3ZlcnNpb249MS42O3ZpZXc9dXNhZ2UiLCJmb3JtYXQiOiJjZHgtanNvbiIsInNpemVCeXRlcyI6MTYzODQsInZpZXciOiJ1c2FnZSJ9XX19fQ==",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"keyId": "test-key",
|
||||||
|
"algorithm": "hs256",
|
||||||
|
"signature": "signature-value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user