consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -376,9 +376,9 @@ public sealed class AnalyticsIngestionEdgeCaseTests
|
||||
[Fact]
|
||||
public void ResolveArtifactVersion_HandlesDigestInTag()
|
||||
{
|
||||
var envelope = new OrchestratorEventEnvelope
|
||||
var envelope = new JobEngineEventEnvelope
|
||||
{
|
||||
Scope = new OrchestratorEventScope
|
||||
Scope = new JobEngineEventScope
|
||||
{
|
||||
Image = "registry.example.com/repo@sha256:abc123"
|
||||
}
|
||||
@@ -393,9 +393,9 @@ public sealed class AnalyticsIngestionEdgeCaseTests
|
||||
[Fact]
|
||||
public void ResolveArtifactVersion_HandlesPortInRegistry()
|
||||
{
|
||||
var envelope = new OrchestratorEventEnvelope
|
||||
var envelope = new JobEngineEventEnvelope
|
||||
{
|
||||
Scope = new OrchestratorEventScope
|
||||
Scope = new JobEngineEventScope
|
||||
{
|
||||
Image = "registry.example.com:5000/repo:v1.2.3"
|
||||
}
|
||||
@@ -409,9 +409,9 @@ public sealed class AnalyticsIngestionEdgeCaseTests
|
||||
[Fact]
|
||||
public void ResolveArtifactVersion_ReturnsNullForPortOnly()
|
||||
{
|
||||
var envelope = new OrchestratorEventEnvelope
|
||||
var envelope = new JobEngineEventEnvelope
|
||||
{
|
||||
Scope = new OrchestratorEventScope
|
||||
Scope = new JobEngineEventScope
|
||||
{
|
||||
Image = "registry.example.com:5000/repo"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ public sealed class AnalyticsIngestionHelpersTests
|
||||
[Fact]
|
||||
public void ResolveArtifactVersion_ParsesImageTag()
|
||||
{
|
||||
var envelope = new OrchestratorEventEnvelope
|
||||
var envelope = new JobEngineEventEnvelope
|
||||
{
|
||||
Scope = new OrchestratorEventScope
|
||||
Scope = new JobEngineEventScope
|
||||
{
|
||||
Image = "registry.example.com/repo:1.2.3"
|
||||
}
|
||||
@@ -53,9 +53,9 @@ public sealed class AnalyticsIngestionHelpersTests
|
||||
[Fact]
|
||||
public void ResolveArtifactVersion_ReturnsNullWhenMissingTag()
|
||||
{
|
||||
var envelope = new OrchestratorEventEnvelope
|
||||
var envelope = new JobEngineEventEnvelope
|
||||
{
|
||||
Scope = new OrchestratorEventScope
|
||||
Scope = new JobEngineEventScope
|
||||
{
|
||||
Image = "registry.example.com/repo"
|
||||
}
|
||||
@@ -89,26 +89,26 @@ public sealed class AnalyticsIngestionHelpersTests
|
||||
[Fact]
|
||||
public void ResolveArtifactName_PrefersRepoThenImageThenComponent()
|
||||
{
|
||||
var withRepo = new OrchestratorEventEnvelope
|
||||
var withRepo = new JobEngineEventEnvelope
|
||||
{
|
||||
Scope = new OrchestratorEventScope
|
||||
Scope = new JobEngineEventScope
|
||||
{
|
||||
Repo = "github.com/stellaops/core",
|
||||
Image = "registry.example.com/stellaops/core:1.2.3",
|
||||
Component = "stellaops-core"
|
||||
}
|
||||
};
|
||||
var withImage = new OrchestratorEventEnvelope
|
||||
var withImage = new JobEngineEventEnvelope
|
||||
{
|
||||
Scope = new OrchestratorEventScope
|
||||
Scope = new JobEngineEventScope
|
||||
{
|
||||
Image = "registry.example.com/stellaops/console:2.0.0",
|
||||
Component = "stellaops-console"
|
||||
}
|
||||
};
|
||||
var withComponent = new OrchestratorEventEnvelope
|
||||
var withComponent = new JobEngineEventEnvelope
|
||||
{
|
||||
Scope = new OrchestratorEventScope
|
||||
Scope = new JobEngineEventScope
|
||||
{
|
||||
Component = "stellaops-agent"
|
||||
}
|
||||
@@ -117,7 +117,7 @@ public sealed class AnalyticsIngestionHelpersTests
|
||||
Assert.Equal("github.com/stellaops/core", AnalyticsIngestionService.ResolveArtifactName(withRepo));
|
||||
Assert.Equal("registry.example.com/stellaops/console:2.0.0", AnalyticsIngestionService.ResolveArtifactName(withImage));
|
||||
Assert.Equal("stellaops-agent", AnalyticsIngestionService.ResolveArtifactName(withComponent));
|
||||
Assert.Equal("unknown", AnalyticsIngestionService.ResolveArtifactName(new OrchestratorEventEnvelope()));
|
||||
Assert.Equal("unknown", AnalyticsIngestionService.ResolveArtifactName(new JobEngineEventEnvelope()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -19,8 +19,8 @@ public sealed class ScannerPlatformEventsBehaviorTests
|
||||
[Fact]
|
||||
public void IsSupportedScannerEventKind_RecognizesReportReadyAndScanCompleted()
|
||||
{
|
||||
Assert.True(AnalyticsIngestionService.IsSupportedScannerEventKind(OrchestratorEventKinds.ScannerReportReady));
|
||||
Assert.True(AnalyticsIngestionService.IsSupportedScannerEventKind(OrchestratorEventKinds.ScannerScanCompleted));
|
||||
Assert.True(AnalyticsIngestionService.IsSupportedScannerEventKind(JobEngineEventKinds.ScannerReportReady));
|
||||
Assert.True(AnalyticsIngestionService.IsSupportedScannerEventKind(JobEngineEventKinds.ScannerScanCompleted));
|
||||
Assert.False(AnalyticsIngestionService.IsSupportedScannerEventKind("scanner.unknown"));
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed class ScannerPlatformEventsBehaviorTests
|
||||
|
||||
var result = AnalyticsIngestionService.TryDeserializeScannerPayload(
|
||||
dsseElement,
|
||||
OrchestratorEventKinds.ScannerReportReady,
|
||||
JobEngineEventKinds.ScannerReportReady,
|
||||
SerializerOptions,
|
||||
out var parsed);
|
||||
|
||||
@@ -80,7 +80,7 @@ public sealed class ScannerPlatformEventsBehaviorTests
|
||||
|
||||
var result = AnalyticsIngestionService.TryDeserializeScannerPayload(
|
||||
completedElement,
|
||||
OrchestratorEventKinds.ScannerScanCompleted,
|
||||
JobEngineEventKinds.ScannerScanCompleted,
|
||||
SerializerOptions,
|
||||
out var parsed);
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ using StellaOps.Signals.EvidenceWeightedScore;
|
||||
using StellaOps.Signals.UnifiedScore;
|
||||
using StellaOps.Signals.UnifiedScore.Replay;
|
||||
using StellaOps.TestKit;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Tests;
|
||||
@@ -179,6 +181,26 @@ public sealed class ScoreEndpointsTests
|
||||
Assert.True(result.Value.ComputedAt > DateTimeOffset.UtcNow.AddMinutes(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WithPartialSignals_ReturnsUnknownsAndProofReference()
|
||||
{
|
||||
var request = new ScoreEvaluateRequest
|
||||
{
|
||||
Signals = new SignalInputs
|
||||
{
|
||||
Reachability = 0.8
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _service.EvaluateAsync(_context, request);
|
||||
|
||||
Assert.NotNull(result.Value.Unknowns);
|
||||
Assert.Contains("runtime", result.Value.Unknowns!);
|
||||
Assert.Contains("backport", result.Value.Unknowns!);
|
||||
Assert.NotNull(result.Value.ProofRef);
|
||||
Assert.StartsWith("proof://score/", result.Value.ProofRef, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_DifferentTenants_ProduceDifferentScoreIds()
|
||||
{
|
||||
@@ -238,6 +260,49 @@ public sealed class ScoreEndpointsTests
|
||||
Assert.Null(result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExplanationAsync_NonExistent_ReturnsNull()
|
||||
{
|
||||
var result = await _service.GetExplanationAsync(_context, "sha256:missing");
|
||||
|
||||
Assert.Null(result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExplanationAsync_WithPersistedDigest_ReturnsCanonicalContract()
|
||||
{
|
||||
var record = new ScoreHistoryRecord
|
||||
{
|
||||
Id = "score_123",
|
||||
TenantId = "test-tenant",
|
||||
ProjectId = "proj-a",
|
||||
CveId = "CVE-2026-0001",
|
||||
Purl = "pkg:generic/demo@1.0.0",
|
||||
Score = 0.62m,
|
||||
Band = "Investigate",
|
||||
WeightsVersion = "v-test",
|
||||
SignalSnapshot = "{\"vex\":{\"isPresent\":true},\"epss\":{\"isPresent\":true},\"reachability\":{\"isPresent\":false},\"runtime\":{\"isPresent\":true},\"backport\":{\"isPresent\":false},\"sbom\":{\"isPresent\":true}}",
|
||||
ReplayDigest = "sha256:abc123",
|
||||
CreatedAt = DateTimeOffset.Parse("2026-02-26T12:00:00Z")
|
||||
};
|
||||
|
||||
_scoreHistoryStore
|
||||
.GetByReplayDigestAsync("sha256:abc123", "test-tenant", Arg.Any<CancellationToken>())
|
||||
.Returns(record);
|
||||
|
||||
var result = await _service.GetExplanationAsync(_context, "SHA256:ABC123");
|
||||
|
||||
Assert.NotNull(result.Value);
|
||||
Assert.Equal("score.explain.v1", result.Value.ContractVersion);
|
||||
Assert.Equal("sha256:abc123", result.Value.Digest);
|
||||
Assert.Equal("score_123", result.Value.ScoreId);
|
||||
Assert.Equal(62, result.Value.FinalScore);
|
||||
Assert.Equal("sha256:abc123", result.Value.DeterministicInputHash);
|
||||
Assert.Equal("/api/v1/score/score_123/replay", result.Value.ReplayLink);
|
||||
Assert.Equal(6, result.Value.Factors.Count);
|
||||
Assert.Equal(2, result.Value.Sources.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TSF-005: ListWeightManifestsAsync
|
||||
@@ -348,7 +413,7 @@ public sealed class ScoreEndpointsTests
|
||||
{
|
||||
var request = new ScoreVerifyRequest
|
||||
{
|
||||
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
|
||||
SignedReplayLogDsse = CreateReplayEnvelope(),
|
||||
OriginalInputs = new ScoreVerifyInputs
|
||||
{
|
||||
Signals = new SignalInputs
|
||||
@@ -378,7 +443,7 @@ public sealed class ScoreEndpointsTests
|
||||
{
|
||||
var request = new ScoreVerifyRequest
|
||||
{
|
||||
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
|
||||
SignedReplayLogDsse = CreateReplayEnvelope(),
|
||||
OriginalInputs = null
|
||||
};
|
||||
|
||||
@@ -393,7 +458,7 @@ public sealed class ScoreEndpointsTests
|
||||
{
|
||||
var request = new ScoreVerifyRequest
|
||||
{
|
||||
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
|
||||
SignedReplayLogDsse = CreateReplayEnvelope(),
|
||||
OriginalInputs = new ScoreVerifyInputs
|
||||
{
|
||||
Signals = new SignalInputs { Reachability = 0.5 }
|
||||
@@ -411,7 +476,7 @@ public sealed class ScoreEndpointsTests
|
||||
{
|
||||
var request = new ScoreVerifyRequest
|
||||
{
|
||||
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
|
||||
SignedReplayLogDsse = CreateReplayEnvelope(),
|
||||
OriginalInputs = new ScoreVerifyInputs
|
||||
{
|
||||
Signals = new SignalInputs { Reachability = 0.5 },
|
||||
@@ -425,6 +490,48 @@ public sealed class ScoreEndpointsTests
|
||||
Assert.True(result.Value.Verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyReplayAsync_WithMismatchedReplayPayload_ReturnsDeterministicDifferences()
|
||||
{
|
||||
var request = new ScoreVerifyRequest
|
||||
{
|
||||
SignedReplayLogDsse = CreateReplayEnvelope(finalScore: 99, ewsDigest: "sha256:does-not-match"),
|
||||
OriginalInputs = new ScoreVerifyInputs
|
||||
{
|
||||
Signals = new SignalInputs { Reachability = 0.5, Runtime = 0.5 }
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _service.VerifyReplayAsync(_context, request);
|
||||
|
||||
Assert.False(result.Value.Verified);
|
||||
Assert.False(result.Value.ScoreMatches);
|
||||
Assert.False(result.Value.DigestMatches);
|
||||
Assert.NotNull(result.Value.Differences);
|
||||
Assert.Contains(result.Value.Differences!, diff => diff.Field == "final_score");
|
||||
Assert.Contains(result.Value.Differences!, diff => diff.Field == "ews_digest");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyReplayAsync_WithMalformedEnvelope_ReturnsDeterministicEnvelopeError()
|
||||
{
|
||||
var request = new ScoreVerifyRequest
|
||||
{
|
||||
SignedReplayLogDsse = "not-base64",
|
||||
OriginalInputs = new ScoreVerifyInputs
|
||||
{
|
||||
Signals = new SignalInputs { Reachability = 0.5, Runtime = 0.5 }
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _service.VerifyReplayAsync(_context, request);
|
||||
|
||||
Assert.False(result.Value.Verified);
|
||||
Assert.NotNull(result.Value.Differences);
|
||||
var envelopeDifference = Assert.Single(result.Value.Differences!, diff => diff.Field == "signed_replay_log_dsse");
|
||||
Assert.Equal("valid_dsse_envelope", envelopeDifference.Expected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TSF-011: GetReplayAsync
|
||||
@@ -605,5 +712,28 @@ public sealed class ScoreEndpointsTests
|
||||
.Returns(result);
|
||||
}
|
||||
|
||||
private static string CreateReplayEnvelope(int? finalScore = null, string? ewsDigest = null)
|
||||
{
|
||||
var payload = new Dictionary<string, object?>();
|
||||
if (finalScore.HasValue)
|
||||
{
|
||||
payload["final_score"] = finalScore.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ewsDigest))
|
||||
{
|
||||
payload["ews_digest"] = ewsDigest;
|
||||
}
|
||||
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload));
|
||||
var envelope = new Dictionary<string, object?>
|
||||
{
|
||||
["payloadType"] = "application/vnd.stellaops.score-replay+json",
|
||||
["payload"] = Convert.ToBase64String(payloadBytes)
|
||||
};
|
||||
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(envelope)));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user