Add MergeUsageAnalyzer to detect legacy merge service usage
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:
master
2025-11-06 15:03:39 +02:00
parent 5a923d968c
commit 950f238a93
45 changed files with 1291 additions and 623 deletions

View File

@@ -61,6 +61,11 @@ public sealed class ScannerWebServiceOptions
/// </summary>
public ApiOptions Api { get; set; } = new();
/// <summary>
/// Console (UI) routing settings used for orchestrator link generation.
/// </summary>
public ConsoleOptions Console { get; set; } = new();
/// <summary>
/// Platform event emission settings.
/// </summary>
@@ -266,6 +271,17 @@ public sealed class ScannerWebServiceOptions
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 bool Enabled { get; set; }

View File

@@ -16,20 +16,24 @@ namespace StellaOps.Scanner.WebService.Services;
internal sealed class ReportEventDispatcher : IReportEventDispatcher
{
private const string DefaultTenant = "default";
private const string Source = "scanner.webservice";
private readonly IPlatformEventPublisher _publisher;
private readonly TimeProvider _timeProvider;
private readonly ILogger<ReportEventDispatcher> _logger;
private readonly string[] _apiBaseSegments;
private readonly string _reportsSegment;
private readonly string _policySegment;
public ReportEventDispatcher(
IPlatformEventPublisher publisher,
IOptions<ScannerWebServiceOptions> options,
TimeProvider timeProvider,
private const string DefaultTenant = "default";
private const string Source = "scanner.webservice";
private readonly IPlatformEventPublisher _publisher;
private readonly TimeProvider _timeProvider;
private readonly ILogger<ReportEventDispatcher> _logger;
private readonly string[] _apiBaseSegments;
private readonly string _reportsSegment;
private readonly string _policySegment;
private readonly string[] _consoleBaseSegments;
private readonly string _consoleReportsSegment;
private readonly string _consolePolicySegment;
private readonly string _consoleAttestationsSegment;
public ReportEventDispatcher(
IPlatformEventPublisher publisher,
IOptions<ScannerWebServiceOptions> options,
TimeProvider timeProvider,
ILogger<ReportEventDispatcher> logger)
{
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
@@ -38,17 +42,28 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
throw new ArgumentNullException(nameof(options));
}
var apiOptions = options.Value.Api ?? new ScannerWebServiceOptions.ApiOptions();
_apiBaseSegments = SplitSegments(apiOptions.BasePath);
_reportsSegment = string.IsNullOrWhiteSpace(apiOptions.ReportsSegment)
? "reports"
: apiOptions.ReportsSegment.Trim('/');
_policySegment = string.IsNullOrWhiteSpace(apiOptions.PolicySegment)
? "policy"
: apiOptions.PolicySegment.Trim('/');
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
var apiOptions = options.Value.Api ?? new ScannerWebServiceOptions.ApiOptions();
_apiBaseSegments = SplitSegments(apiOptions.BasePath);
_reportsSegment = string.IsNullOrWhiteSpace(apiOptions.ReportsSegment)
? "reports"
: apiOptions.ReportsSegment.Trim('/');
_policySegment = string.IsNullOrWhiteSpace(apiOptions.PolicySegment)
? "policy"
: apiOptions.PolicySegment.Trim('/');
var consoleOptions = options.Value.Console ?? new ScannerWebServiceOptions.ConsoleOptions();
_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(
ReportRequestDto request,
@@ -240,21 +255,21 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
};
}
private ReportLinksPayload BuildLinks(HttpContext context, ReportDocumentDto document, DsseEnvelopeDto? envelope)
{
if (!context.Request.Host.HasValue)
{
return new ReportLinksPayload();
}
var reportUi = BuildAbsoluteUri(context, "ui", "reports", document.ReportId);
private ReportLinksPayload BuildLinks(HttpContext context, ReportDocumentDto document, DsseEnvelopeDto? envelope)
{
if (!context.Request.Host.HasValue)
{
return new ReportLinksPayload();
}
var reportUi = BuildAbsoluteUri(context, ConcatSegments(_consoleBaseSegments, _consoleReportsSegment, document.ReportId));
var reportApi = BuildAbsoluteUri(context, ConcatSegments(_apiBaseSegments, _reportsSegment, document.ReportId));
LinkTarget? policyLink = null;
if (!string.IsNullOrWhiteSpace(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));
policyLink = LinkTarget.Create(policyUi, policyApi);
}
@@ -262,7 +277,7 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher
LinkTarget? attestationLink = 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"));
attestationLink = LinkTarget.Create(attestationUi, attestationApi);
}

View File

@@ -8,7 +8,7 @@
> 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-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)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -16,108 +16,108 @@ using StellaOps.Policy;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Options;
using StellaOps.Scanner.WebService.Services;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class ReportEventDispatcherTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
[Fact]
public async Task PublishAsync_EmitsReportReadyAndScanCompleted()
{
var publisher = new RecordingEventPublisher();
namespace StellaOps.Scanner.WebService.Tests;
public sealed class ReportEventDispatcherTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
[Fact]
public async Task PublishAsync_EmitsReportReadyAndScanCompleted()
{
var publisher = new RecordingEventPublisher();
var dispatcher = new ReportEventDispatcher(publisher, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), 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 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);
Assert.Equal(2, publisher.Events.Count);
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);
}
[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
{
public List<OrchestratorEvent> Events { get; } = new();
@@ -173,6 +293,6 @@ public sealed class ReportEventDispatcherTests
{
Events.Add(@event);
return Task.CompletedTask;
}
}
}
}
}
}

View File

@@ -28,8 +28,8 @@ public sealed class ReportSamplesTests
Assert.NotNull(response!.Report);
Assert.NotNull(response.Dsse);
var reportBytes = JsonSerializer.SerializeToUtf8Bytes(response.Report, SerializerOptions);
var expectedPayload = Convert.ToBase64String(reportBytes);
Assert.Equal(expectedPayload, response.Dsse!.Payload);
}
var reportBytes = JsonSerializer.SerializeToUtf8Bytes(response.Report, SerializerOptions);
var expectedPayload = Convert.ToBase64String(reportBytes);
Assert.Equal(expectedPayload, response.Dsse!.Payload);
}
}

View File

@@ -6,6 +6,7 @@ using System.Net;
using System.Net.Http.Json;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Http;
@@ -22,8 +23,8 @@ using StellaOps.Scanner.WebService.Services;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class ScansEndpointsTests
{
public sealed class ScansEndpointsTests
{
[Fact]
public async Task SubmitScanReturnsAcceptedAndStatusRetrievable()
{
@@ -272,7 +273,7 @@ public sealed class ScansEndpointsTests
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
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.Equal(scanId, payload!.ScanId);
Assert.Equal("sha256:entrytrace", payload.ImageDigest);
@@ -559,7 +560,7 @@ public sealed class ScansEndpointsTests
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
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.Equal(storedResult.ScanId, payload!.ScanId);
Assert.Equal(storedResult.ImageDigest, payload.ImageDigest);
@@ -583,7 +584,10 @@ public sealed class ScansEndpointsTests
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(
string ScanId,

View File

@@ -19,12 +19,12 @@ public sealed class SurfaceCacheOptionsConfiguratorTests
"surface-cache",
null,
cacheRoot,
cacheQuotaMegabytes: 512,
prefetchEnabled: true,
featureFlags: Array.Empty<string>(),
secrets: new SurfaceSecretsConfiguration("file", "tenant-b", "/etc/secrets", null, null, allowInline: false),
tenant: "tenant-b",
tls: new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
512,
true,
Array.Empty<string>(),
new SurfaceSecretsConfiguration("file", "tenant-b", "/etc/secrets", null, null, false),
"tenant-b",
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
var environment = new StubSurfaceEnvironment(settings);
var configurator = new SurfaceCacheOptionsConfigurator(environment);

View File

@@ -19,12 +19,12 @@ public sealed class SurfaceCacheOptionsConfiguratorTests
"surface-cache",
null,
cacheRoot,
cacheQuotaMegabytes: 1024,
prefetchEnabled: false,
featureFlags: Array.Empty<string>(),
secrets: new SurfaceSecretsConfiguration("file", "tenant-a", "/etc/secrets", null, null, false),
tenant: "tenant-a",
tls: new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
1024,
false,
Array.Empty<string>(),
new SurfaceSecretsConfiguration("file", "tenant-a", "/etc/secrets", null, null, false),
"tenant-a",
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
var environment = new StubSurfaceEnvironment(settings);
var configurator = new SurfaceCacheOptionsConfigurator(environment);

View File

@@ -3,8 +3,8 @@
"kind": "scanner.event.report.ready",
"version": 1,
"tenant": "tenant-alpha",
"occurredAt": "2025-10-19T12:34:56Z",
"recordedAt": "2025-10-19T12:34:57Z",
"occurredAt": "2025-10-19T12:34:56+00:00",
"recordedAt": "2025-10-19T12:34:57+00:00",
"source": "scanner.webservice",
"idempotencyKey": "scanner.event.report.ready:tenant-alpha:report-abc",
"correlationId": "report-abc",
@@ -25,7 +25,7 @@
"reportId": "report-abc",
"scanId": "report-abc",
"imageDigest": "sha256:feedface",
"generatedAt": "2025-10-19T12:34:56Z",
"generatedAt": "2025-10-19T12:34:56+00:00",
"verdict": "fail",
"summary": {
"total": 1,
@@ -72,7 +72,7 @@
},
"report": {
"reportId": "report-abc",
"generatedAt": "2025-10-19T12:34:56Z",
"generatedAt": "2025-10-19T12:34:56+00:00",
"imageDigest": "sha256:feedface",
"policy": {
"digest": "digest-123",

View File

@@ -3,8 +3,8 @@
"kind": "scanner.event.scan.completed",
"version": 1,
"tenant": "tenant-alpha",
"occurredAt": "2025-10-19T12:34:56Z",
"recordedAt": "2025-10-19T12:34:57Z",
"occurredAt": "2025-10-19T12:34:56+00:00",
"recordedAt": "2025-10-19T12:34:57+00:00",
"source": "scanner.webservice",
"idempotencyKey": "scanner.event.scan.completed:tenant-alpha:report-abc",
"correlationId": "report-abc",
@@ -78,7 +78,7 @@
},
"report": {
"reportId": "report-abc",
"generatedAt": "2025-10-19T12:34:56Z",
"generatedAt": "2025-10-19T12:34:56+00:00",
"imageDigest": "sha256:feedface",
"policy": {
"digest": "digest-123",

View 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"
}
]
}
}