finish off sprint advisories and sprints

This commit is contained in:
master
2026-01-24 00:12:43 +02:00
parent 726d70dc7f
commit c70e83719e
266 changed files with 46699 additions and 1328 deletions

View File

@@ -0,0 +1,367 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_039_Scanner_runtime_linkage_verification
// Task: RLV-009 - Platform API: Function Map Endpoints
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using StellaOps.Scanner.Reachability.FunctionMap;
using StellaOps.Scanner.Reachability.FunctionMap.Verification;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class FunctionMapEndpointsTests
{
private readonly IFunctionMapService _service;
private readonly PlatformRequestContext _context = new("test-tenant", "test-actor", null);
public FunctionMapEndpointsTests()
{
var verifier = new ClaimVerifier(NullLogger<ClaimVerifier>.Instance);
_service = new FunctionMapService(verifier);
}
#region Create
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_ReturnsDetailWithId()
{
var request = new CreateFunctionMapRequest
{
SbomRef = "oci://registry/app@sha256:abc123",
ServiceName = "myservice",
HotFunctions = ["SSL_read", "SSL_write"]
};
var result = await _service.CreateAsync(_context, request);
Assert.NotNull(result.Value);
Assert.StartsWith("fmap-", result.Value.Id);
Assert.Equal("myservice", result.Value.ServiceName);
Assert.Equal("oci://registry/app@sha256:abc123", result.Value.SbomRef);
Assert.StartsWith("sha256:", result.Value.PredicateDigest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_WithOptions_SetsThresholds()
{
var request = new CreateFunctionMapRequest
{
SbomRef = "oci://registry/app@sha256:abc123",
ServiceName = "myservice",
Options = new FunctionMapOptionsDto
{
MinObservationRate = 0.90,
WindowSeconds = 3600,
FailOnUnexpected = true
}
};
var result = await _service.CreateAsync(_context, request);
Assert.NotNull(result.Value.Coverage);
Assert.Equal(0.90, result.Value.Coverage!.MinObservationRate);
Assert.Equal(3600, result.Value.Coverage.WindowSeconds);
Assert.True(result.Value.Coverage.FailOnUnexpected);
}
#endregion
#region List
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_Empty_ReturnsEmptyList()
{
var svc = CreateFreshService();
var result = await svc.ListAsync(_context);
Assert.Empty(result.Value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_AfterCreate_ReturnsCreatedMap()
{
var svc = CreateFreshService();
await svc.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "svc1"
});
var result = await svc.ListAsync(_context);
Assert.Single(result.Value);
Assert.Equal("svc1", result.Value[0].ServiceName);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_MultiTenant_IsolatesByTenant()
{
var svc = CreateFreshService();
var tenantA = new PlatformRequestContext("tenant-a", "actor", null);
var tenantB = new PlatformRequestContext("tenant-b", "actor", null);
await svc.CreateAsync(tenantA, new CreateFunctionMapRequest
{
SbomRef = "oci://a",
ServiceName = "svc-a"
});
await svc.CreateAsync(tenantB, new CreateFunctionMapRequest
{
SbomRef = "oci://b",
ServiceName = "svc-b"
});
var resultA = await svc.ListAsync(tenantA);
var resultB = await svc.ListAsync(tenantB);
Assert.Single(resultA.Value);
Assert.Equal("svc-a", resultA.Value[0].ServiceName);
Assert.Single(resultB.Value);
Assert.Equal("svc-b", resultB.Value[0].ServiceName);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_WithPagination_RespectsLimitAndOffset()
{
var svc = CreateFreshService();
for (int i = 0; i < 5; i++)
{
await svc.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = $"oci://test{i}",
ServiceName = $"svc{i}"
});
}
var page1 = await svc.ListAsync(_context, limit: 2, offset: 0);
var page2 = await svc.ListAsync(_context, limit: 2, offset: 2);
Assert.Equal(2, page1.Value.Count);
Assert.Equal(2, page2.Value.Count);
}
#endregion
#region GetById
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetById_Existing_ReturnsDetail()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "myservice"
});
var result = await _service.GetByIdAsync(_context, created.Value.Id);
Assert.NotNull(result.Value);
Assert.Equal(created.Value.Id, result.Value!.Id);
Assert.Equal("myservice", result.Value.ServiceName);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetById_NonExistent_ReturnsNull()
{
var result = await _service.GetByIdAsync(_context, "fmap-nonexistent");
Assert.Null(result.Value);
}
#endregion
#region Delete
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Delete_Existing_ReturnsTrue()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "todelete"
});
var result = await _service.DeleteAsync(_context, created.Value.Id);
Assert.True(result.Value);
var getResult = await _service.GetByIdAsync(_context, created.Value.Id);
Assert.Null(getResult.Value);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Delete_NonExistent_ReturnsFalse()
{
var result = await _service.DeleteAsync(_context, "fmap-nonexistent");
Assert.False(result.Value);
}
#endregion
#region Verify
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_EmptyObservations_ReturnsNotVerified()
{
// Empty function map with no expected paths: ClaimVerifier returns rate=0.0 which
// is below the default threshold (0.95), so verification fails.
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "empty-map"
});
var verifyRequest = new VerifyFunctionMapRequest
{
Observations = []
};
var result = await _service.VerifyAsync(_context, created.Value.Id, verifyRequest);
Assert.False(result.Value.Verified);
Assert.Equal(0, result.Value.PathCount);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_NonExistentMap_ReturnsNotVerified()
{
var verifyRequest = new VerifyFunctionMapRequest
{
Observations = []
};
var result = await _service.VerifyAsync(_context, "fmap-nonexistent", verifyRequest);
Assert.False(result.Value.Verified);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_UpdatesLastVerifiedTimestamp()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "verify-ts"
});
Assert.Null(created.Value.LastVerifiedAt);
await _service.VerifyAsync(_context, created.Value.Id, new VerifyFunctionMapRequest());
var updated = await _service.GetByIdAsync(_context, created.Value.Id);
Assert.NotNull(updated.Value!.LastVerifiedAt);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Verify_WithOptions_PassesOverrides()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "verify-opts",
Options = new FunctionMapOptionsDto { MinObservationRate = 0.99 }
});
var verifyRequest = new VerifyFunctionMapRequest
{
Options = new VerifyOptionsDto
{
MinObservationRateOverride = 0.50
},
Observations = []
};
var result = await _service.VerifyAsync(_context, created.Value.Id, verifyRequest);
// With 0 expected paths and 0 observations, rate=0.0 which is below even the
// overridden 0.50 threshold. Verify the override target is applied correctly.
Assert.Equal(0.50, result.Value.TargetRate);
Assert.Equal(0.0, result.Value.ObservationRate);
}
#endregion
#region Coverage
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetCoverage_EmptyMap_ReturnsZeroCoverage()
{
var created = await _service.CreateAsync(_context, new CreateFunctionMapRequest
{
SbomRef = "oci://test",
ServiceName = "cov-empty"
});
var result = await _service.GetCoverageAsync(_context, created.Value.Id);
Assert.Equal(0, result.Value.TotalPaths);
Assert.Equal(0, result.Value.ObservedPaths);
Assert.Equal(0, result.Value.TotalExpectedCalls);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetCoverage_NonExistentMap_ReturnsZero()
{
var result = await _service.GetCoverageAsync(_context, "fmap-nonexistent");
Assert.Equal(0, result.Value.TotalPaths);
}
#endregion
#region Determinism
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_PredicateDigest_IsDeterministic()
{
var svc1 = CreateFreshService();
var svc2 = CreateFreshService();
var request = new CreateFunctionMapRequest
{
SbomRef = "oci://registry/app@sha256:deterministic",
ServiceName = "determ-svc",
Options = new FunctionMapOptionsDto
{
MinObservationRate = 0.95,
WindowSeconds = 1800
}
};
var result1 = await svc1.CreateAsync(_context, request);
var result2 = await svc2.CreateAsync(_context, request);
// Same inputs should produce same predicate digest
Assert.Equal(result1.Value.PredicateDigest, result2.Value.PredicateDigest);
}
#endregion
private static FunctionMapService CreateFreshService()
{
var verifier = new ClaimVerifier(NullLogger<ClaimVerifier>.Instance);
return new FunctionMapService(verifier);
}
}

View File

@@ -0,0 +1,413 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_041_Policy_interop_import_export_rego
// Task: TASK-07 - Platform API Endpoints
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class PolicyInteropEndpointsTests
{
private readonly IPolicyInteropService _service = new PolicyInteropService();
private readonly PlatformRequestContext _context = new("test-tenant", "test-actor", null);
private const string GoldenPolicyJson = """
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": { "name": "test-policy", "version": "1.0.0" },
"spec": {
"settings": { "defaultAction": "block" },
"gates": [
{
"id": "cvss-threshold",
"type": "CvssThresholdGate",
"enabled": true,
"config": { "threshold": 7.0 }
},
{
"id": "signature-required",
"type": "SignatureRequiredGate",
"enabled": true
}
]
}
}
""";
private const string SampleRego = """
package stella.release
import rego.v1
default allow := false
deny contains msg if {
input.cvss.score >= 7.0
msg := "CVSS too high"
}
deny contains msg if {
not input.dsse.verified
msg := "DSSE missing"
}
allow if { count(deny) == 0 }
""";
#region Export
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_ToJson_ReturnsContent()
{
var request = new PolicyExportApiRequest
{
PolicyContent = GoldenPolicyJson,
Format = "json"
};
var result = await _service.ExportAsync(_context, request);
Assert.True(result.Success);
Assert.Equal("json", result.Format);
Assert.NotNull(result.Content);
Assert.Contains("policy.stellaops.io/v2", result.Content);
Assert.NotNull(result.Digest);
Assert.StartsWith("sha256:", result.Digest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_ToRego_ReturnsRegoSource()
{
var request = new PolicyExportApiRequest
{
PolicyContent = GoldenPolicyJson,
Format = "rego"
};
var result = await _service.ExportAsync(_context, request);
Assert.True(result.Success);
Assert.Equal("rego", result.Format);
Assert.NotNull(result.Content);
Assert.Contains("package stella.release", result.Content);
Assert.Contains("deny contains msg if", result.Content);
Assert.Contains("input.cvss.score >= 7.0", result.Content);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_EmptyContent_ReturnsFalse()
{
var request = new PolicyExportApiRequest
{
PolicyContent = "",
Format = "json"
};
var result = await _service.ExportAsync(_context, request);
Assert.False(result.Success);
Assert.NotNull(result.Diagnostics);
Assert.Contains(result.Diagnostics, d => d.Code == "EMPTY_INPUT");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_InvalidJson_ReturnsFalse()
{
var request = new PolicyExportApiRequest
{
PolicyContent = "not json",
Format = "json"
};
var result = await _service.ExportAsync(_context, request);
Assert.False(result.Success);
Assert.NotNull(result.Diagnostics);
Assert.Contains(result.Diagnostics, d => d.Code == "PARSE_ERROR");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Export_ToRego_WithEnvironment_UsesEnvThresholds()
{
var policyWithEnvs = """
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": {
"settings": { "defaultAction": "block" },
"gates": [{
"id": "cvss",
"type": "CvssThresholdGate",
"enabled": true,
"config": { "threshold": 7.0 },
"environments": {
"staging": { "threshold": 8.0 }
}
}]
}
}
""";
var request = new PolicyExportApiRequest
{
PolicyContent = policyWithEnvs,
Format = "rego",
Environment = "staging"
};
var result = await _service.ExportAsync(_context, request);
Assert.True(result.Success);
Assert.Contains("input.cvss.score >= 8.0", result.Content);
}
#endregion
#region Import
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Import_JsonContent_ReturnsSuccess()
{
var request = new PolicyImportApiRequest
{
Content = GoldenPolicyJson,
Format = "json"
};
var result = await _service.ImportAsync(_context, request);
Assert.True(result.Success);
Assert.Equal("json", result.SourceFormat);
Assert.Equal(2, result.GatesImported);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Import_RegoContent_MapsToNativeGates()
{
var request = new PolicyImportApiRequest
{
Content = SampleRego,
Format = "rego"
};
var result = await _service.ImportAsync(_context, request);
Assert.True(result.Success);
Assert.Equal("rego", result.SourceFormat);
Assert.True(result.NativeMapped > 0);
Assert.NotNull(result.Mappings);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Import_EmptyContent_ReturnsFalse()
{
var request = new PolicyImportApiRequest { Content = "" };
var result = await _service.ImportAsync(_context, request);
Assert.False(result.Success);
Assert.NotNull(result.Diagnostics);
Assert.Contains(result.Diagnostics, d => d.Code == "EMPTY_INPUT");
}
#endregion
#region Validate
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Validate_ValidJson_ReturnsValid()
{
var request = new PolicyValidateApiRequest
{
Content = GoldenPolicyJson,
Format = "json"
};
var result = await _service.ValidateAsync(_context, request);
Assert.True(result.Valid);
Assert.Equal("json", result.DetectedFormat);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Validate_ValidRego_ReturnsValid()
{
var request = new PolicyValidateApiRequest
{
Content = SampleRego,
Format = "rego"
};
var result = await _service.ValidateAsync(_context, request);
Assert.True(result.Valid);
Assert.Equal("rego", result.DetectedFormat);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Validate_InvalidJson_ReturnsInvalid()
{
var request = new PolicyValidateApiRequest
{
Content = "not valid json",
Format = "json"
};
var result = await _service.ValidateAsync(_context, request);
Assert.False(result.Valid);
Assert.NotNull(result.Errors);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Validate_Strict_WrongVersion_ReturnsInvalid()
{
var v1Policy = """
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": { "settings": {}, "gates": [] }
}
""";
var request = new PolicyValidateApiRequest
{
Content = v1Policy,
Format = "json",
Strict = true
};
var result = await _service.ValidateAsync(_context, request);
Assert.False(result.Valid);
Assert.NotNull(result.Errors);
Assert.Contains(result.Errors, e => e.Code == "VERSION_MISMATCH");
}
#endregion
#region Evaluate
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_AllGatesPass_ReturnsAllow()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = GoldenPolicyJson,
Input = new PolicyEvaluationInputDto
{
CvssScore = 5.0,
DsseVerified = true,
RekorVerified = true
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Equal("allow", result.Decision);
Assert.NotNull(result.Gates);
Assert.All(result.Gates, g => Assert.True(g.Passed));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_CvssExceeds_ReturnsBlock()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = GoldenPolicyJson,
Input = new PolicyEvaluationInputDto
{
CvssScore = 9.0,
DsseVerified = true,
RekorVerified = true
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Equal("block", result.Decision);
Assert.Contains(result.Gates!, g => !g.Passed && g.GateType == "CvssThresholdGate");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_SignatureMissing_ReturnsBlock()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = GoldenPolicyJson,
Input = new PolicyEvaluationInputDto
{
CvssScore = 5.0,
DsseVerified = false,
RekorVerified = true
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Equal("block", result.Decision);
Assert.Contains(result.Gates!, g => !g.Passed && g.GateType == "SignatureRequiredGate");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_EmptyPolicy_ReturnsBlock()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = "",
Input = new PolicyEvaluationInputDto { CvssScore = 5.0 }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Equal("block", result.Decision);
Assert.NotNull(result.Remediation);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Evaluate_RegoPolicy_ImportsThenEvaluates()
{
var request = new PolicyEvaluateApiRequest
{
PolicyContent = SampleRego,
Format = "rego",
Input = new PolicyEvaluationInputDto
{
CvssScore = 5.0,
DsseVerified = true,
RekorVerified = true
}
};
var result = await _service.EvaluateAsync(_context, request);
// After importing the Rego, the CVSS gate with threshold 7.0 should pass for score 5.0
Assert.Equal("allow", result.Decision);
}
#endregion
}

View File

@@ -0,0 +1,606 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_037_Signals_unified_trust_score_algebra
// Task: TSF-005 - Platform API Endpoints (Score Evaluate)
// Task: TSF-011 - Score Replay & Verification Endpoint
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using StellaOps.Signals.EvidenceWeightedScore;
using StellaOps.Signals.UnifiedScore;
using StellaOps.Signals.UnifiedScore.Replay;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
/// <summary>
/// Integration tests for score evaluation endpoints via <see cref="ScoreEvaluationService"/>.
/// Covers TSF-005 (score evaluate endpoints) and TSF-011 (replay/verify endpoints).
/// </summary>
[Trait("Category", TestCategories.Unit)]
public sealed class ScoreEndpointsTests
{
private readonly IUnifiedScoreService _unifiedScoreService;
private readonly IWeightManifestLoader _manifestLoader;
private readonly IReplayLogBuilder _replayLogBuilder;
private readonly IReplayVerifier _replayVerifier;
private readonly ScoreEvaluationService _service;
private readonly PlatformRequestContext _context = new("test-tenant", "test-actor", null);
public ScoreEndpointsTests()
{
_unifiedScoreService = Substitute.For<IUnifiedScoreService>();
_manifestLoader = Substitute.For<IWeightManifestLoader>();
_replayLogBuilder = Substitute.For<IReplayLogBuilder>();
_replayVerifier = Substitute.For<IReplayVerifier>();
// Default manifest setup
var defaultManifest = WeightManifest.FromEvidenceWeights(EvidenceWeights.Default, "v-test");
_manifestLoader
.ListVersionsAsync(Arg.Any<CancellationToken>())
.Returns(new List<string> { "v-test" });
_manifestLoader
.LoadAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(defaultManifest);
_manifestLoader
.LoadLatestAsync(Arg.Any<CancellationToken>())
.Returns(defaultManifest);
_manifestLoader
.GetEffectiveAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
.Returns(defaultManifest);
// Default unified score result
SetupDefaultScoreResult();
_service = new ScoreEvaluationService(
_unifiedScoreService,
_manifestLoader,
_replayLogBuilder,
_replayVerifier,
NullLogger<ScoreEvaluationService>.Instance);
}
#region TSF-005: EvaluateAsync
[Fact]
public async Task EvaluateAsync_WithSignals_ReturnsScoreResponse()
{
var request = new ScoreEvaluateRequest
{
CveId = "CVE-2024-1234",
Signals = new SignalInputs
{
Reachability = 0.8,
Runtime = 0.7,
Backport = 0.5,
Exploit = 0.3,
Source = 0.6,
Mitigation = 0.1
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value);
Assert.StartsWith("score_", result.Value.ScoreId);
Assert.InRange(result.Value.ScoreValue, 0, 100);
Assert.NotEmpty(result.Value.Bucket);
Assert.NotEmpty(result.Value.EwsDigest);
Assert.False(result.Cached);
}
[Fact]
public async Task EvaluateAsync_WithBreakdownOption_ReturnsBreakdown()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs
{
Reachability = 0.9,
Runtime = 0.8
},
Options = new ScoreEvaluateOptions { IncludeBreakdown = true }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value.Breakdown);
Assert.NotEmpty(result.Value.Breakdown);
Assert.All(result.Value.Breakdown, b =>
{
Assert.NotEmpty(b.Dimension);
Assert.NotEmpty(b.Symbol);
});
}
[Fact]
public async Task EvaluateAsync_WithoutBreakdownOption_ExcludesBreakdown()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5 },
Options = new ScoreEvaluateOptions { IncludeBreakdown = false }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.Null(result.Value.Breakdown);
}
[Fact]
public async Task EvaluateAsync_ReturnsGuardrails()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs
{
Reachability = 0.5,
Runtime = 0.5
}
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value.Guardrails);
}
[Fact]
public async Task EvaluateAsync_ReturnsWeightManifestReference()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5 }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value.WeightManifest);
Assert.NotEmpty(result.Value.WeightManifest.Version);
Assert.NotEmpty(result.Value.WeightManifest.ContentHash);
}
[Fact]
public async Task EvaluateAsync_ReturnsComputedAt()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5 }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.True(result.Value.ComputedAt <= DateTimeOffset.UtcNow);
Assert.True(result.Value.ComputedAt > DateTimeOffset.UtcNow.AddMinutes(-1));
}
[Fact]
public async Task EvaluateAsync_DifferentTenants_ProduceDifferentScoreIds()
{
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5, Runtime = 0.5 }
};
var tenantA = new PlatformRequestContext("tenant-a", "actor", null);
var tenantB = new PlatformRequestContext("tenant-b", "actor", null);
var resultA = await _service.EvaluateAsync(tenantA, request);
var resultB = await _service.EvaluateAsync(tenantB, request);
Assert.NotEqual(resultA.Value.ScoreId, resultB.Value.ScoreId);
}
[Fact]
public async Task EvaluateAsync_WithDeltaOption_IncludesDelta()
{
SetupScoreResultWithDelta();
var request = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.5 },
Options = new ScoreEvaluateOptions { IncludeDelta = true }
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value.DeltaIfPresent);
}
[Fact]
public async Task EvaluateAsync_WithNullSignals_UsesDefaults()
{
var request = new ScoreEvaluateRequest
{
CveId = "CVE-2024-0001"
};
var result = await _service.EvaluateAsync(_context, request);
Assert.NotNull(result.Value);
Assert.InRange(result.Value.ScoreValue, 0, 100);
}
#endregion
#region TSF-005: GetByIdAsync
[Fact]
public async Task GetByIdAsync_NonExistent_ReturnsNull()
{
var result = await _service.GetByIdAsync(_context, "score_nonexistent");
Assert.Null(result.Value);
}
#endregion
#region TSF-005: ListWeightManifestsAsync
[Fact]
public async Task ListWeightManifestsAsync_ReturnsSummaries()
{
var result = await _service.ListWeightManifestsAsync(_context);
Assert.NotNull(result.Value);
Assert.Single(result.Value);
Assert.Equal("v-test", result.Value[0].Version);
Assert.Equal("production", result.Value[0].Profile);
}
[Fact]
public async Task ListWeightManifestsAsync_Empty_ReturnsEmptyList()
{
_manifestLoader
.ListVersionsAsync(Arg.Any<CancellationToken>())
.Returns(new List<string>());
var result = await _service.ListWeightManifestsAsync(_context);
Assert.Empty(result.Value);
}
#endregion
#region TSF-005: GetWeightManifestAsync
[Fact]
public async Task GetWeightManifestAsync_Existing_ReturnsDetail()
{
var result = await _service.GetWeightManifestAsync(_context, "v-test");
Assert.NotNull(result.Value);
Assert.Equal("v-test", result.Value.Version);
Assert.NotNull(result.Value.Weights);
Assert.NotNull(result.Value.Weights.Legacy);
}
[Fact]
public async Task GetWeightManifestAsync_NonExistent_ReturnsNull()
{
_manifestLoader
.LoadAsync("v-nonexistent", Arg.Any<CancellationToken>())
.Returns((WeightManifest?)null);
var result = await _service.GetWeightManifestAsync(_context, "v-nonexistent");
Assert.Null(result.Value);
}
[Fact]
public async Task GetWeightManifestAsync_ReturnsLegacyWeights()
{
var result = await _service.GetWeightManifestAsync(_context, "v-test");
Assert.NotNull(result.Value?.Weights.Legacy);
Assert.True(result.Value.Weights.Legacy.Rch > 0);
Assert.True(result.Value.Weights.Legacy.Rts > 0);
}
[Fact]
public async Task GetWeightManifestAsync_ReturnsAdvisoryWeights()
{
var result = await _service.GetWeightManifestAsync(_context, "v-test");
Assert.NotNull(result.Value?.Weights.Advisory);
Assert.True(result.Value.Weights.Advisory.Cvss > 0);
Assert.True(result.Value.Weights.Advisory.Epss > 0);
}
#endregion
#region TSF-005: GetEffectiveWeightManifestAsync
[Fact]
public async Task GetEffectiveWeightManifestAsync_ReturnsManifest()
{
var result = await _service.GetEffectiveWeightManifestAsync(
_context, DateTimeOffset.UtcNow);
Assert.NotNull(result.Value);
Assert.Equal("v-test", result.Value.Version);
}
[Fact]
public async Task GetEffectiveWeightManifestAsync_NoManifest_ReturnsNull()
{
_manifestLoader
.GetEffectiveAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
.Returns((WeightManifest?)null);
var result = await _service.GetEffectiveWeightManifestAsync(
_context, DateTimeOffset.UtcNow);
Assert.Null(result.Value);
}
#endregion
#region TSF-011: VerifyReplayAsync
[Fact]
public async Task VerifyReplayAsync_WithInputs_ReturnsVerificationResult()
{
var request = new ScoreVerifyRequest
{
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
OriginalInputs = new ScoreVerifyInputs
{
Signals = new SignalInputs
{
Reachability = 0.8,
Runtime = 0.7,
Backport = 0.5,
Exploit = 0.3,
Source = 0.6,
Mitigation = 0.1
}
}
};
var result = await _service.VerifyReplayAsync(_context, request);
Assert.NotNull(result.Value);
Assert.True(result.Value.Verified);
Assert.True(result.Value.ScoreMatches);
Assert.True(result.Value.DigestMatches);
Assert.InRange(result.Value.ReplayedScore, 0, 100);
Assert.Equal(result.Value.ReplayedScore, result.Value.OriginalScore);
}
[Fact]
public async Task VerifyReplayAsync_WithNullInputs_UsesDefaultSignals()
{
var request = new ScoreVerifyRequest
{
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
OriginalInputs = null
};
var result = await _service.VerifyReplayAsync(_context, request);
Assert.NotNull(result.Value);
Assert.InRange(result.Value.ReplayedScore, 0, 100);
}
[Fact]
public async Task VerifyReplayAsync_ReturnsVerifiedAt()
{
var request = new ScoreVerifyRequest
{
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
OriginalInputs = new ScoreVerifyInputs
{
Signals = new SignalInputs { Reachability = 0.5 }
}
};
var result = await _service.VerifyReplayAsync(_context, request);
Assert.True(result.Value.VerifiedAt <= DateTimeOffset.UtcNow);
Assert.True(result.Value.VerifiedAt > DateTimeOffset.UtcNow.AddMinutes(-1));
}
[Fact]
public async Task VerifyReplayAsync_WithWeightVersion_UsesSpecifiedVersion()
{
var request = new ScoreVerifyRequest
{
SignedReplayLogDsse = "eyJwYXlsb2FkIjoiZXlKMFpYTjBJam9pYUdWc2JHOGlmUT09In0=",
OriginalInputs = new ScoreVerifyInputs
{
Signals = new SignalInputs { Reachability = 0.5 },
WeightManifestVersion = "v-test"
}
};
var result = await _service.VerifyReplayAsync(_context, request);
Assert.NotNull(result.Value);
Assert.True(result.Value.Verified);
}
#endregion
#region TSF-011: GetReplayAsync
[Fact]
public async Task GetReplayAsync_NonExistent_ReturnsNull()
{
var result = await _service.GetReplayAsync(_context, "score_nonexistent");
Assert.Null(result.Value);
}
#endregion
#region TSF-011: Deterministic Score Computation
[Fact]
public async Task EvaluateAsync_SameInputs_ProducesSameEwsDigest()
{
var request = new ScoreEvaluateRequest
{
CveId = "CVE-2024-1234",
Signals = new SignalInputs
{
Reachability = 0.8,
Runtime = 0.7,
Backport = 0.5,
Exploit = 0.3,
Source = 0.6,
Mitigation = 0.1
}
};
var result1 = await _service.EvaluateAsync(_context, request);
var result2 = await _service.EvaluateAsync(_context, request);
Assert.Equal(result1.Value.EwsDigest, result2.Value.EwsDigest);
}
[Fact]
public async Task EvaluateAsync_DifferentInputs_ProducesDifferentEwsDigest()
{
var request1 = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.8, Runtime = 0.7 }
};
var request2 = new ScoreEvaluateRequest
{
Signals = new SignalInputs { Reachability = 0.2, Runtime = 0.1 }
};
// Setup different results for different inputs
SetupScoreResultWithDigest("digest-high");
var result1 = await _service.EvaluateAsync(_context, request1);
SetupScoreResultWithDigest("digest-low");
var result2 = await _service.EvaluateAsync(_context, request2);
Assert.NotEqual(result1.Value.EwsDigest, result2.Value.EwsDigest);
}
#endregion
#region Helpers
private void SetupDefaultScoreResult()
{
var defaultResult = new UnifiedScoreResult
{
Score = 62,
Bucket = ScoreBucket.Investigate,
UnknownsFraction = 0.3,
UnknownsBand = Signals.UnifiedScore.UnknownsBand.Adequate,
Breakdown =
[
new DimensionContribution { Dimension = "Reachability", Symbol = "Rch", InputValue = 0.5, Weight = 0.30, Contribution = 15.0 },
new DimensionContribution { Dimension = "Runtime", Symbol = "Rts", InputValue = 0.5, Weight = 0.25, Contribution = 12.5 },
new DimensionContribution { Dimension = "Backport", Symbol = "Bkp", InputValue = 0.5, Weight = 0.15, Contribution = 7.5 },
new DimensionContribution { Dimension = "Exploit", Symbol = "Xpl", InputValue = 0.5, Weight = 0.15, Contribution = 7.5 },
new DimensionContribution { Dimension = "Source", Symbol = "Src", InputValue = 0.5, Weight = 0.10, Contribution = 5.0 },
new DimensionContribution { Dimension = "Mitigation", Symbol = "Mit", InputValue = 0.0, Weight = 0.10, Contribution = 0.0 }
],
Guardrails = new AppliedGuardrails
{
SpeculativeCap = false,
NotAffectedCap = false,
RuntimeFloor = false,
OriginalScore = 62,
AdjustedScore = 62
},
WeightManifestRef = new WeightManifestRef
{
Version = "v-test",
ContentHash = "sha256:abc123"
},
EwsDigest = "sha256:deterministic-digest-test",
DeterminizationFingerprint = "fp:test-fingerprint",
ComputedAt = DateTimeOffset.UtcNow
};
_unifiedScoreService
.ComputeAsync(Arg.Any<UnifiedScoreRequest>(), Arg.Any<CancellationToken>())
.Returns(defaultResult);
}
private void SetupScoreResultWithDelta()
{
var resultWithDelta = new UnifiedScoreResult
{
Score = 45,
Bucket = ScoreBucket.ScheduleNext,
Breakdown =
[
new DimensionContribution { Dimension = "Reachability", Symbol = "Rch", InputValue = 0.5, Weight = 0.30, Contribution = 15.0 }
],
Guardrails = new AppliedGuardrails
{
SpeculativeCap = false,
NotAffectedCap = false,
RuntimeFloor = false,
OriginalScore = 45,
AdjustedScore = 45
},
DeltaIfPresent =
[
new SignalDelta
{
Signal = "Runtime",
MinImpact = -10.0,
MaxImpact = 15.0,
Weight = 0.25,
Description = "Runtime signal could shift score by -10 to +15"
}
],
WeightManifestRef = new WeightManifestRef
{
Version = "v-test",
ContentHash = "sha256:abc123"
},
EwsDigest = "sha256:delta-digest",
ComputedAt = DateTimeOffset.UtcNow
};
_unifiedScoreService
.ComputeAsync(Arg.Any<UnifiedScoreRequest>(), Arg.Any<CancellationToken>())
.Returns(resultWithDelta);
}
private void SetupScoreResultWithDigest(string digest)
{
var result = new UnifiedScoreResult
{
Score = 50,
Bucket = ScoreBucket.Investigate,
Breakdown =
[
new DimensionContribution { Dimension = "Reachability", Symbol = "Rch", InputValue = 0.5, Weight = 0.30, Contribution = 15.0 }
],
Guardrails = new AppliedGuardrails
{
SpeculativeCap = false,
NotAffectedCap = false,
RuntimeFloor = false,
OriginalScore = 50,
AdjustedScore = 50
},
WeightManifestRef = new WeightManifestRef
{
Version = "v-test",
ContentHash = "sha256:abc123"
},
EwsDigest = digest,
ComputedAt = DateTimeOffset.UtcNow
};
_unifiedScoreService
.ComputeAsync(Arg.Any<UnifiedScoreRequest>(), Arg.Any<CancellationToken>())
.Returns(result);
}
#endregion
}

View File

@@ -8,8 +8,14 @@
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NSubstitute" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Platform.WebService\StellaOps.Platform.WebService.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
<ProjectReference Include="..\..\..\Scanner\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
<ProjectReference Include="..\..\..\Signals\StellaOps.Signals\StellaOps.Signals.csproj" />
</ItemGroup>
</Project>