up
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring.Policies;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Tests;
|
||||
|
||||
public sealed class CvssPolicyLoaderTests
|
||||
{
|
||||
private readonly CvssPolicyLoader _loader = new();
|
||||
|
||||
[Fact]
|
||||
public void Load_ValidPolicy_ComputesDeterministicHashAndReturnsPolicy()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"policyId": "default",
|
||||
"version": "1.0.0",
|
||||
"name": "Default CVSS v4",
|
||||
"effectiveFrom": "2025-01-01T00:00:00Z",
|
||||
"severityThresholds": { "lowMin": 0.1, "mediumMin": 4.0, "highMin": 7.0, "criticalMin": 9.0 },
|
||||
"metricOverrides": [
|
||||
{ "id": "override-1", "vulnerabilityPattern": "CVE-2025-0001", "priority": 1, "scoreAdjustment": 0.3, "isActive": true }
|
||||
],
|
||||
"attestationRequirements": { "requireDsse": true, "requireRekor": false }
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _loader.Load(json);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Policy.Should().NotBeNull();
|
||||
result.Hash.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
// determinism: hash must match when reloading the same payload (even with hash field present)
|
||||
var withHash = JsonSerializer.Deserialize<JsonElement>(json);
|
||||
var roundTrip = _loader.Load(AddHash(withHash, result.Hash!));
|
||||
roundTrip.Hash.Should().Be(result.Hash);
|
||||
roundTrip.Policy!.Hash.Should().Be(result.Hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_InvalidPolicy_ReturnsValidationErrors()
|
||||
{
|
||||
// Arrange: missing required fields
|
||||
const string json = """{"name":"Missing required fields"}""";
|
||||
|
||||
// Act
|
||||
var result = _loader.Load(json);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Policy.Should().BeNull();
|
||||
result.Errors.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
private static JsonElement AddHash(JsonElement element, string hash)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(element.GetRawText());
|
||||
using var stream = new MemoryStream();
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
writer.WritePropertyName(prop.Name);
|
||||
prop.Value.WriteTo(writer);
|
||||
}
|
||||
writer.WriteString("hash", hash);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
using var finalDoc = JsonDocument.Parse(stream);
|
||||
return finalDoc.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.Policy.Scoring.Receipts;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Tests.Fakes;
|
||||
|
||||
internal sealed class InMemoryReceiptRepository : IReceiptRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, CvssScoreReceipt> _store = new();
|
||||
|
||||
public Task<CvssScoreReceipt> SaveAsync(CvssScoreReceipt receipt, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_store[receipt.ReceiptId] = receipt;
|
||||
return Task.FromResult(receipt);
|
||||
}
|
||||
|
||||
public bool Contains(string receiptId) => _store.ContainsKey(receiptId);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
using StellaOps.Policy.Scoring.Receipts;
|
||||
using StellaOps.Policy.Scoring.Tests.Fakes;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Tests;
|
||||
|
||||
public sealed class ReceiptBuilderTests
|
||||
{
|
||||
private readonly ICvssV4Engine _engine = new CvssV4Engine();
|
||||
private readonly InMemoryReceiptRepository _repository = new();
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_ComputesDeterministicHashAndStoresReceipt()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new CvssPolicy
|
||||
{
|
||||
PolicyId = "default",
|
||||
Version = "1.0.0",
|
||||
Name = "Default",
|
||||
EffectiveFrom = new DateTimeOffset(2025, 01, 01, 0, 0, 0, TimeSpan.Zero),
|
||||
Hash = "abc123",
|
||||
SeverityThresholds = new CvssSeverityThresholds()
|
||||
};
|
||||
|
||||
var request = new CreateReceiptRequest
|
||||
{
|
||||
VulnerabilityId = "CVE-2025-0001",
|
||||
TenantId = "tenant-a",
|
||||
CreatedBy = "tester",
|
||||
CreatedAt = new DateTimeOffset(2025, 11, 28, 12, 0, 0, TimeSpan.Zero),
|
||||
Policy = policy,
|
||||
BaseMetrics = new CvssBaseMetrics
|
||||
{
|
||||
AttackVector = AttackVector.Network,
|
||||
AttackComplexity = AttackComplexity.Low,
|
||||
AttackRequirements = AttackRequirements.None,
|
||||
PrivilegesRequired = PrivilegesRequired.None,
|
||||
UserInteraction = UserInteraction.None,
|
||||
VulnerableSystemConfidentiality = ImpactMetricValue.High,
|
||||
VulnerableSystemIntegrity = ImpactMetricValue.High,
|
||||
VulnerableSystemAvailability = ImpactMetricValue.High,
|
||||
SubsequentSystemConfidentiality = ImpactMetricValue.High,
|
||||
SubsequentSystemIntegrity = ImpactMetricValue.High,
|
||||
SubsequentSystemAvailability = ImpactMetricValue.High
|
||||
},
|
||||
Evidence = ImmutableList<CvssEvidenceItem>.Empty.Add(new CvssEvidenceItem
|
||||
{
|
||||
Type = "advisory",
|
||||
Uri = "sha256:deadbeef",
|
||||
Description = "Vendor advisory",
|
||||
IsAuthoritative = true
|
||||
})
|
||||
};
|
||||
|
||||
var builder = new ReceiptBuilder(_engine, _repository);
|
||||
|
||||
// Act
|
||||
var receipt1 = await builder.CreateAsync(request);
|
||||
var receipt2 = await builder.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
receipt1.ReceiptId.Should().NotBeNullOrEmpty();
|
||||
receipt1.VectorString.Should().StartWith("CVSS:4.0");
|
||||
receipt1.InputHash.Should().NotBeNullOrEmpty();
|
||||
receipt2.InputHash.Should().Be(receipt1.InputHash); // deterministic across runs with same inputs
|
||||
_repository.Contains(receipt1.ReceiptId).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_EnforcesEvidenceRequirements()
|
||||
{
|
||||
// Arrange
|
||||
var policy = new CvssPolicy
|
||||
{
|
||||
PolicyId = "strict",
|
||||
Version = "1.0.0",
|
||||
Name = "Strict Evidence",
|
||||
EffectiveFrom = DateTimeOffset.UtcNow,
|
||||
Hash = "abc123",
|
||||
EvidenceRequirements = new CvssEvidenceRequirements
|
||||
{
|
||||
MinimumCount = 2,
|
||||
RequireAuthoritative = true,
|
||||
RequiredTypes = ImmutableList.Create("advisory", "scan")
|
||||
}
|
||||
};
|
||||
|
||||
var request = new CreateReceiptRequest
|
||||
{
|
||||
VulnerabilityId = "CVE-2025-0002",
|
||||
TenantId = "tenant-b",
|
||||
CreatedBy = "tester",
|
||||
Policy = policy,
|
||||
BaseMetrics = new CvssBaseMetrics
|
||||
{
|
||||
AttackVector = AttackVector.Network,
|
||||
AttackComplexity = AttackComplexity.Low,
|
||||
AttackRequirements = AttackRequirements.None,
|
||||
PrivilegesRequired = PrivilegesRequired.None,
|
||||
UserInteraction = UserInteraction.None,
|
||||
VulnerableSystemConfidentiality = ImpactMetricValue.High,
|
||||
VulnerableSystemIntegrity = ImpactMetricValue.High,
|
||||
VulnerableSystemAvailability = ImpactMetricValue.High,
|
||||
SubsequentSystemConfidentiality = ImpactMetricValue.High,
|
||||
SubsequentSystemIntegrity = ImpactMetricValue.High,
|
||||
SubsequentSystemAvailability = ImpactMetricValue.High
|
||||
},
|
||||
Evidence = ImmutableList<CvssEvidenceItem>.Empty.Add(new CvssEvidenceItem
|
||||
{
|
||||
Type = "advisory",
|
||||
Uri = "sha256:123",
|
||||
IsAuthoritative = false
|
||||
})
|
||||
};
|
||||
|
||||
var builder = new ReceiptBuilder(_engine, _repository);
|
||||
|
||||
// Act
|
||||
var act = async () => await builder.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Evidence*");
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.6.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
|
||||
<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">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
Reference in New Issue
Block a user