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

@@ -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;
}
}
}
}
}
}