Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 4602ccc3a3
1444 changed files with 109919 additions and 8058 deletions

View File

@@ -2,10 +2,16 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy;
using StellaOps.PolicyDsl;
using StellaOps.Policy.Engine.Evaluation;
using StellaOps.Policy.Engine.Services;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Unknowns.Configuration;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.Policy.Unknowns.Services;
using Xunit;
using Xunit.Sdk;
@@ -331,6 +337,35 @@ policy "Baseline Production Policy" syntax "stella-dsl@1" {
Assert.Contains(result.Warnings, warning => warning.Contains("Git-sourced", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Evaluate_UnknownBudgetExceeded_BlocksEvaluation()
{
var document = CompileBaseline();
var budgetService = CreateBudgetService();
var evaluator = new PolicyEvaluator(budgetService: budgetService);
var context = new PolicyEvaluationContext(
new PolicyEvaluationSeverity("High"),
new PolicyEvaluationEnvironment(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["name"] = "prod"
}.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)),
new PolicyEvaluationAdvisory("GHSA", ImmutableDictionary<string, string>.Empty),
PolicyEvaluationVexEvidence.Empty,
PolicyEvaluationSbom.Empty,
PolicyEvaluationExceptions.Empty,
ImmutableArray.Create(CreateUnknown(UnknownReasonCode.Reachability)),
ImmutableArray<ExceptionObject>.Empty,
PolicyEvaluationReachability.Unknown,
PolicyEvaluationEntropy.Unknown);
var result = evaluator.Evaluate(new PolicyEvaluationRequest(document, context));
Assert.Equal("blocked", result.Status);
Assert.Equal(PolicyFailureReason.UnknownBudgetExceeded, result.FailureReason);
Assert.NotNull(result.UnknownBudgetStatus);
}
private PolicyIrDocument CompileBaseline()
{
var compilation = compiler.Compile(BaselinePolicy);
@@ -354,10 +389,69 @@ policy "Baseline Production Policy" syntax "stella-dsl@1" {
PolicyEvaluationVexEvidence.Empty,
PolicyEvaluationSbom.Empty,
exceptions ?? PolicyEvaluationExceptions.Empty,
ImmutableArray<Unknown>.Empty,
ImmutableArray<ExceptionObject>.Empty,
PolicyEvaluationReachability.Unknown,
PolicyEvaluationEntropy.Unknown);
}
private static UnknownBudgetService CreateBudgetService()
{
var options = new UnknownBudgetOptions
{
Budgets = new Dictionary<string, UnknownBudget>(StringComparer.OrdinalIgnoreCase)
{
["prod"] = new UnknownBudget
{
Environment = "prod",
TotalLimit = 0,
Action = BudgetAction.Block
}
}
};
return new UnknownBudgetService(
new TestOptionsMonitor<UnknownBudgetOptions>(options),
NullLogger<UnknownBudgetService>.Instance);
}
private static Unknown CreateUnknown(UnknownReasonCode reasonCode)
{
var timestamp = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
return new Unknown
{
Id = Guid.NewGuid(),
TenantId = Guid.NewGuid(),
PackageId = "pkg:npm/lodash",
PackageVersion = "4.17.21",
Band = UnknownBand.Hot,
Score = 80m,
UncertaintyFactor = 0.5m,
ExploitPressure = 0.7m,
ReasonCode = reasonCode,
FirstSeenAt = timestamp,
LastEvaluatedAt = timestamp,
CreatedAt = timestamp,
UpdatedAt = timestamp
};
}
private sealed class TestOptionsMonitor<T>(T current) : IOptionsMonitor<T>
{
private readonly T _current = current;
public T CurrentValue => _current;
public T Get(string? name) => _current;
public IDisposable OnChange(Action<T, string?> listener) => NoopDisposable.Instance;
}
private sealed class NoopDisposable : IDisposable
{
public static readonly NoopDisposable Instance = new();
public void Dispose() { }
}
private static string Describe(ImmutableArray<PolicyIssue> issues) =>
string.Join(" | ", issues.Select(issue => $"{issue.Severity}:{issue.Code}:{issue.Message}"));

View File

@@ -51,6 +51,7 @@ public sealed class PolicyRuntimeEvaluationServiceTests
Assert.Equal("pack-1", response.PackId);
Assert.Equal(1, response.Version);
Assert.NotNull(response.PolicyDigest);
Assert.NotNull(response.Confidence);
Assert.False(response.Cached);
}

View File

@@ -0,0 +1,142 @@
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Exceptions.Services;
using Xunit;
namespace StellaOps.Policy.Exceptions.Tests;
public sealed class EvidenceRequirementValidatorTests
{
[Fact]
public async Task ValidateForApprovalAsync_NoHooks_ReturnsValid()
{
var validator = CreateValidator(new StubHookRegistry([]));
var exception = CreateException();
var result = await validator.ValidateForApprovalAsync(exception);
result.IsValid.Should().BeTrue();
result.MissingEvidence.Should().BeEmpty();
}
[Fact]
public async Task ValidateForApprovalAsync_MissingEvidence_ReturnsInvalid()
{
var hooks = ImmutableArray.Create(new EvidenceHook
{
HookId = "hook-1",
Type = EvidenceType.FeatureFlagDisabled,
Description = "Feature flag disabled",
IsMandatory = true
});
var validator = CreateValidator(new StubHookRegistry(hooks));
var exception = CreateException();
var result = await validator.ValidateForApprovalAsync(exception);
result.IsValid.Should().BeFalse();
result.MissingEvidence.Should().HaveCount(1);
}
[Fact]
public async Task ValidateForApprovalAsync_TrustScoreTooLow_ReturnsInvalid()
{
var hooks = ImmutableArray.Create(new EvidenceHook
{
HookId = "hook-1",
Type = EvidenceType.BackportMerged,
Description = "Backport merged",
IsMandatory = true,
MinTrustScore = 0.8m
});
var validator = CreateValidator(
new StubHookRegistry(hooks),
trustScore: 0.5m);
var exception = CreateException(new EvidenceRequirements
{
Hooks = hooks,
SubmittedEvidence = ImmutableArray.Create(new SubmittedEvidence
{
EvidenceId = "e-1",
HookId = "hook-1",
Type = EvidenceType.BackportMerged,
Reference = "ref",
SubmittedAt = DateTimeOffset.UtcNow,
SubmittedBy = "tester",
ValidationStatus = EvidenceValidationStatus.Valid
})
});
var result = await validator.ValidateForApprovalAsync(exception);
result.IsValid.Should().BeFalse();
result.InvalidEvidence.Should().HaveCount(1);
}
private static EvidenceRequirementValidator CreateValidator(
IEvidenceHookRegistry registry,
decimal trustScore = 1.0m,
bool schemaValid = true,
bool signatureValid = true)
{
return new EvidenceRequirementValidator(
registry,
new StubAttestationVerifier(signatureValid),
new StubTrustScoreService(trustScore),
new StubSchemaValidator(schemaValid),
NullLogger<EvidenceRequirementValidator>.Instance);
}
private static ExceptionObject CreateException(EvidenceRequirements? requirements = null)
{
return new ExceptionObject
{
ExceptionId = "EXC-TEST",
Version = 1,
Status = ExceptionStatus.Active,
Type = ExceptionType.Vulnerability,
Scope = new ExceptionScope { VulnerabilityId = "CVE-2024-0001" },
OwnerId = "owner",
RequesterId = "requester",
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
ReasonCode = ExceptionReason.AcceptedRisk,
Rationale = "This rationale is long enough to satisfy the minimum character requirement.",
EvidenceRequirements = requirements
};
}
private sealed class StubHookRegistry(ImmutableArray<EvidenceHook> hooks) : IEvidenceHookRegistry
{
public Task<ImmutableArray<EvidenceHook>> GetRequiredHooksAsync(
ExceptionType exceptionType,
ExceptionScope scope,
CancellationToken ct = default) => Task.FromResult(hooks);
}
private sealed class StubAttestationVerifier(bool isValid) : IAttestationVerifier
{
public Task<EvidenceVerificationResult> VerifyAsync(string dsseEnvelope, CancellationToken ct = default) =>
Task.FromResult(new EvidenceVerificationResult(isValid, isValid ? null : "invalid"));
}
private sealed class StubTrustScoreService(decimal score) : ITrustScoreService
{
public Task<decimal> GetScoreAsync(string reference, CancellationToken ct = default) => Task.FromResult(score);
}
private sealed class StubSchemaValidator(bool isValid) : IEvidenceSchemaValidator
{
public Task<EvidenceSchemaValidationResult> ValidateAsync(
string schemaId,
string? content,
CancellationToken ct = default) =>
Task.FromResult(new EvidenceSchemaValidationResult(isValid, isValid ? null : "schema invalid"));
}
}

View File

@@ -0,0 +1,71 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Policy.Exceptions.Models;
using Xunit;
namespace StellaOps.Policy.Exceptions.Tests;
public sealed class EvidenceRequirementsTests
{
[Fact]
public void EvidenceRequirements_ShouldBeSatisfied_WhenAllMandatoryHooksValid()
{
var hooks = ImmutableArray.Create(
new EvidenceHook
{
HookId = "hook-1",
Type = EvidenceType.FeatureFlagDisabled,
Description = "Feature flag disabled",
IsMandatory = true
},
new EvidenceHook
{
HookId = "hook-2",
Type = EvidenceType.BackportMerged,
Description = "Backport merged",
IsMandatory = false
});
var submitted = ImmutableArray.Create(new SubmittedEvidence
{
EvidenceId = "e-1",
HookId = "hook-1",
Type = EvidenceType.FeatureFlagDisabled,
Reference = "attestation:feature-flag",
SubmittedAt = DateTimeOffset.UtcNow,
SubmittedBy = "tester",
ValidationStatus = EvidenceValidationStatus.Valid
});
var requirements = new EvidenceRequirements
{
Hooks = hooks,
SubmittedEvidence = submitted
};
requirements.IsSatisfied.Should().BeTrue();
requirements.MissingEvidence.Should().BeEmpty();
}
[Fact]
public void EvidenceRequirements_ShouldReportMissing_WhenMandatoryHookMissing()
{
var hooks = ImmutableArray.Create(new EvidenceHook
{
HookId = "hook-1",
Type = EvidenceType.CompensatingControl,
Description = "Compensating control",
IsMandatory = true
});
var requirements = new EvidenceRequirements
{
Hooks = hooks,
SubmittedEvidence = []
};
requirements.IsSatisfied.Should().BeFalse();
requirements.MissingEvidence.Should().HaveCount(1);
requirements.MissingEvidence[0].HookId.Should().Be("hook-1");
}
}

View File

@@ -210,6 +210,40 @@ public sealed class ExceptionObjectTests
exception.EvidenceRefs.Should().Contain("sha256:evidence1hash");
}
[Fact]
public void ExceptionObject_IsBlockedByRecheck_WhenBlockTriggered_ShouldBeTrue()
{
// Arrange
var exception = CreateException(recheckResult: new RecheckEvaluationResult
{
IsTriggered = true,
TriggeredConditions = [],
RecommendedAction = RecheckAction.Block,
EvaluatedAt = DateTimeOffset.UtcNow
});
// Act & Assert
exception.IsBlockedByRecheck.Should().BeTrue();
exception.RequiresReapproval.Should().BeFalse();
}
[Fact]
public void ExceptionObject_RequiresReapproval_WhenReapprovalTriggered_ShouldBeTrue()
{
// Arrange
var exception = CreateException(recheckResult: new RecheckEvaluationResult
{
IsTriggered = true,
TriggeredConditions = [],
RecommendedAction = RecheckAction.RequireReapproval,
EvaluatedAt = DateTimeOffset.UtcNow
});
// Act & Assert
exception.RequiresReapproval.Should().BeTrue();
exception.IsBlockedByRecheck.Should().BeFalse();
}
[Fact]
public void ExceptionObject_WithMetadata_ShouldStoreKeyValuePairs()
{
@@ -265,7 +299,8 @@ public sealed class ExceptionObjectTests
DateTimeOffset? expiresAt = null,
ImmutableArray<string>? approverIds = null,
ImmutableArray<string>? evidenceRefs = null,
ImmutableDictionary<string, string>? metadata = null)
ImmutableDictionary<string, string>? metadata = null,
RecheckEvaluationResult? recheckResult = null)
{
return new ExceptionObject
{
@@ -287,7 +322,9 @@ public sealed class ExceptionObjectTests
Rationale = "This is a test rationale that meets the minimum character requirement of 50 characters.",
EvidenceRefs = evidenceRefs ?? [],
CompensatingControls = [],
Metadata = metadata ?? ImmutableDictionary<string, string>.Empty
Metadata = metadata ?? ImmutableDictionary<string, string>.Empty,
LastRecheckResult = recheckResult,
LastRecheckAt = recheckResult?.EvaluatedAt
};
}

View File

@@ -0,0 +1,190 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Exceptions.Services;
using Xunit;
namespace StellaOps.Policy.Exceptions.Tests;
public sealed class RecheckEvaluationServiceTests
{
[Fact]
public async Task EvaluateAsync_NoPolicy_ReturnsNoTrigger()
{
var service = new RecheckEvaluationService();
var exception = CreateException(recheckPolicy: null);
var context = new RecheckEvaluationContext
{
ArtifactDigest = "sha256:abc",
Environment = "prod",
EvaluatedAt = DateTimeOffset.UtcNow
};
var result = await service.EvaluateAsync(exception, context);
result.IsTriggered.Should().BeFalse();
result.RecommendedAction.Should().BeNull();
}
[Fact]
public async Task EvaluateAsync_EpssAbove_Triggers()
{
var service = new RecheckEvaluationService();
var policy = new RecheckPolicy
{
PolicyId = "policy-1",
Name = "EPSS gate",
DefaultAction = RecheckAction.Warn,
CreatedAt = DateTimeOffset.UtcNow,
Conditions = ImmutableArray.Create(new RecheckCondition
{
Type = RecheckConditionType.EPSSAbove,
Threshold = 0.5m,
Action = RecheckAction.RequireReapproval
})
};
var exception = CreateException(recheckPolicy: policy);
var context = new RecheckEvaluationContext
{
ArtifactDigest = "sha256:abc",
Environment = "prod",
EvaluatedAt = DateTimeOffset.UtcNow,
EpssScore = 0.9m
};
var result = await service.EvaluateAsync(exception, context);
result.IsTriggered.Should().BeTrue();
result.TriggeredConditions.Should().HaveCount(1);
result.RecommendedAction.Should().Be(RecheckAction.RequireReapproval);
}
[Fact]
public async Task EvaluateAsync_EnvironmentScope_FiltersConditions()
{
var service = new RecheckEvaluationService();
var policy = new RecheckPolicy
{
PolicyId = "policy-1",
Name = "Env gate",
DefaultAction = RecheckAction.Warn,
CreatedAt = DateTimeOffset.UtcNow,
Conditions = ImmutableArray.Create(new RecheckCondition
{
Type = RecheckConditionType.KEVFlagged,
Action = RecheckAction.Block,
EnvironmentScope = ["prod"]
})
};
var exception = CreateException(recheckPolicy: policy);
var context = new RecheckEvaluationContext
{
ArtifactDigest = "sha256:abc",
Environment = "dev",
EvaluatedAt = DateTimeOffset.UtcNow,
KevFlagged = true
};
var result = await service.EvaluateAsync(exception, context);
result.IsTriggered.Should().BeFalse();
}
[Fact]
public async Task EvaluateAsync_ActionPriority_PicksBlock()
{
var service = new RecheckEvaluationService();
var policy = new RecheckPolicy
{
PolicyId = "policy-1",
Name = "Priority gate",
DefaultAction = RecheckAction.Warn,
CreatedAt = DateTimeOffset.UtcNow,
Conditions = ImmutableArray.Create(
new RecheckCondition
{
Type = RecheckConditionType.ExpiryWithin,
Threshold = 10,
Action = RecheckAction.Warn
},
new RecheckCondition
{
Type = RecheckConditionType.KEVFlagged,
Action = RecheckAction.Block
})
};
var exception = CreateException(
expiresAt: DateTimeOffset.UtcNow.AddDays(1),
recheckPolicy: policy);
var context = new RecheckEvaluationContext
{
ArtifactDigest = "sha256:abc",
Environment = "prod",
EvaluatedAt = DateTimeOffset.UtcNow,
KevFlagged = true
};
var result = await service.EvaluateAsync(exception, context);
result.IsTriggered.Should().BeTrue();
result.RecommendedAction.Should().Be(RecheckAction.Block);
}
[Fact]
public async Task EvaluateAsync_ExpiryWithin_UsesThreshold()
{
var service = new RecheckEvaluationService();
var policy = new RecheckPolicy
{
PolicyId = "policy-1",
Name = "Expiry gate",
DefaultAction = RecheckAction.Warn,
CreatedAt = DateTimeOffset.UtcNow,
Conditions = ImmutableArray.Create(new RecheckCondition
{
Type = RecheckConditionType.ExpiryWithin,
Threshold = 5,
Action = RecheckAction.Warn
})
};
var exception = CreateException(
expiresAt: DateTimeOffset.UtcNow.AddDays(3),
recheckPolicy: policy);
var context = new RecheckEvaluationContext
{
ArtifactDigest = "sha256:abc",
Environment = "prod",
EvaluatedAt = DateTimeOffset.UtcNow
};
var result = await service.EvaluateAsync(exception, context);
result.IsTriggered.Should().BeTrue();
}
private static ExceptionObject CreateException(
DateTimeOffset? expiresAt = null,
RecheckPolicy? recheckPolicy = null)
{
return new ExceptionObject
{
ExceptionId = "EXC-TEST",
Version = 1,
Status = ExceptionStatus.Active,
Type = ExceptionType.Vulnerability,
Scope = new ExceptionScope { VulnerabilityId = "CVE-2024-0001" },
OwnerId = "owner",
RequesterId = "requester",
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow,
ExpiresAt = expiresAt ?? DateTimeOffset.UtcNow.AddDays(30),
ReasonCode = ExceptionReason.AcceptedRisk,
Rationale = "This rationale is long enough to satisfy the minimum character requirement.",
RecheckPolicy = recheckPolicy
};
}
}

View File

@@ -50,6 +50,45 @@ public sealed class PostgresExceptionObjectRepositoryTests : IAsyncLifetime
created.Version.Should().Be(1);
}
[Fact]
public async Task CreateAsync_PersistsRecheckTrackingFields()
{
// Arrange
var lastResult = new RecheckEvaluationResult
{
IsTriggered = true,
TriggeredConditions = ImmutableArray.Create(
new TriggeredCondition(
RecheckConditionType.EPSSAbove,
"EPSS above threshold",
CurrentValue: 0.7m,
ThresholdValue: 0.5m,
Action: RecheckAction.Block)),
RecommendedAction = RecheckAction.Block,
EvaluatedAt = DateTimeOffset.UtcNow
};
var exception = CreateVulnerabilityException("CVE-2024-12345") with
{
RecheckPolicyId = "policy-critical",
LastRecheckResult = lastResult,
LastRecheckAt = DateTimeOffset.UtcNow
};
// Act
await _repository.CreateAsync(exception, "creator@example.com");
var fetched = await _repository.GetByIdAsync(exception.ExceptionId);
// Assert
fetched.Should().NotBeNull();
fetched!.RecheckPolicyId.Should().Be("policy-critical");
fetched.LastRecheckResult.Should().NotBeNull();
fetched.LastRecheckResult!.RecommendedAction.Should().Be(RecheckAction.Block);
fetched.LastRecheckResult!.TriggeredConditions.Should().ContainSingle(
c => c.Type == RecheckConditionType.EPSSAbove);
fetched.LastRecheckAt.Should().BeCloseTo(exception.LastRecheckAt!.Value, TimeSpan.FromSeconds(1));
}
[Fact]
public async Task CreateAsync_RecordsCreatedEvent()
{

View File

@@ -0,0 +1,46 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using StellaOps.Policy.Storage.Postgres;
using Xunit;
namespace StellaOps.Policy.Storage.Postgres.Tests;
[Collection(PolicyPostgresCollection.Name)]
public sealed class RecheckEvidenceMigrationTests : IAsyncLifetime
{
private readonly PolicyPostgresFixture _fixture;
private readonly PolicyDataSource _dataSource;
public RecheckEvidenceMigrationTests(PolicyPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
_dataSource = new PolicyDataSource(Options.Create(options), NullLogger<PolicyDataSource>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task Migration_CreatesRecheckAndEvidenceTables()
{
await using var connection = await _dataSource.OpenConnectionAsync("default", "reader", CancellationToken.None);
await AssertTableExistsAsync(connection, "policy.recheck_policies");
await AssertTableExistsAsync(connection, "policy.evidence_hooks");
await AssertTableExistsAsync(connection, "policy.submitted_evidence");
}
private static async Task AssertTableExistsAsync(NpgsqlConnection connection, string tableName)
{
await using var command = new NpgsqlCommand("SELECT to_regclass(@name)", connection);
command.Parameters.AddWithValue("name", tableName);
var result = await command.ExecuteScalarAsync();
result.Should().NotBeNull($"{tableName} should exist after migrations");
}
}

View File

@@ -28,6 +28,7 @@
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Policy.Storage.Postgres\StellaOps.Policy.Storage.Postgres.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Policy.Exceptions\StellaOps.Policy.Exceptions.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Policy.Unknowns\StellaOps.Policy.Unknowns.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
<ProjectReference Include="..\..\StellaOps.Policy.Scoring\StellaOps.Policy.Scoring.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,120 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Storage.Postgres;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.Policy.Unknowns.Repositories;
using Xunit;
namespace StellaOps.Policy.Storage.Postgres.Tests;
[Collection(PolicyPostgresCollection.Name)]
public sealed class UnknownsRepositoryTests : IAsyncLifetime
{
private readonly PolicyPostgresFixture _fixture;
private readonly PolicyDataSource _dataSource;
private readonly Guid _tenantId = Guid.NewGuid();
public UnknownsRepositoryTests(PolicyPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
_dataSource = new PolicyDataSource(Options.Create(options), NullLogger<PolicyDataSource>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public async Task DisposeAsync() => await _dataSource.DisposeAsync();
[Fact]
public async Task CreateAndGetById_RoundTripsReasonCodeAndEvidence()
{
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId.ToString());
var repository = new UnknownsRepository(connection);
var now = new DateTimeOffset(2025, 1, 2, 3, 4, 5, TimeSpan.Zero);
var unknown = CreateUnknown(
reasonCode: UnknownReasonCode.Reachability,
remediationHint: "Run reachability analysis",
evidenceRefs: new List<EvidenceRef>
{
new("reachability", "proofs/unknowns/unk-123/evidence.json", "sha256:abc123")
},
assumptions: new List<string> { "assume-dynamic-imports" },
timestamp: now);
var created = await repository.CreateAsync(unknown);
var fetched = await repository.GetByIdAsync(_tenantId, created.Id);
fetched.Should().NotBeNull();
fetched!.ReasonCode.Should().Be(UnknownReasonCode.Reachability);
fetched.RemediationHint.Should().Be("Run reachability analysis");
fetched.EvidenceRefs.Should().ContainSingle();
fetched.EvidenceRefs[0].Type.Should().Be("reachability");
fetched.EvidenceRefs[0].Uri.Should().Contain("evidence.json");
fetched.Assumptions.Should().ContainSingle("assume-dynamic-imports");
}
[Fact]
public async Task UpdateAsync_PersistsReasonCodeAndAssumptions()
{
await using var connection = await _dataSource.OpenConnectionAsync(_tenantId.ToString());
var repository = new UnknownsRepository(connection);
var now = new DateTimeOffset(2025, 2, 3, 4, 5, 6, TimeSpan.Zero);
var unknown = CreateUnknown(
reasonCode: UnknownReasonCode.Identity,
remediationHint: null,
evidenceRefs: Array.Empty<EvidenceRef>(),
assumptions: Array.Empty<string>(),
timestamp: now);
var created = await repository.CreateAsync(unknown);
var updated = created with
{
ReasonCode = UnknownReasonCode.VexConflict,
RemediationHint = "Publish authoritative VEX",
EvidenceRefs = new List<EvidenceRef>
{
new("vex", "proofs/unknowns/unk-123/vex.json", "sha256:def456")
},
Assumptions = new List<string> { "assume-vex-defaults" }
};
var result = await repository.UpdateAsync(updated);
var fetched = await repository.GetByIdAsync(_tenantId, created.Id);
result.Should().BeTrue();
fetched.Should().NotBeNull();
fetched!.ReasonCode.Should().Be(UnknownReasonCode.VexConflict);
fetched.RemediationHint.Should().Be("Publish authoritative VEX");
fetched.Assumptions.Should().ContainSingle("assume-vex-defaults");
}
private Unknown CreateUnknown(
UnknownReasonCode reasonCode,
string? remediationHint,
IReadOnlyList<EvidenceRef> evidenceRefs,
IReadOnlyList<string> assumptions,
DateTimeOffset timestamp) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
PackageId = "pkg:npm/lodash",
PackageVersion = "4.17.21",
Band = UnknownBand.Hot,
Score = 90.5m,
UncertaintyFactor = 0.75m,
ExploitPressure = 0.9m,
ReasonCode = reasonCode,
RemediationHint = remediationHint,
EvidenceRefs = evidenceRefs,
Assumptions = assumptions,
FirstSeenAt = timestamp,
LastEvaluatedAt = timestamp,
CreatedAt = timestamp,
UpdatedAt = timestamp
};
}

View File

@@ -0,0 +1,165 @@
using FluentAssertions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Confidence.Configuration;
using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Confidence.Services;
using Xunit;
namespace StellaOps.Policy.Tests.Confidence;
public sealed class ConfidenceCalculatorTests
{
private static readonly DateTimeOffset FixedTimestamp = new(2025, 12, 22, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void Calculate_AllHighFactors_ReturnsVeryHighConfidence()
{
var calculator = CreateCalculator();
var input = CreateInput(
reachability: ReachabilityState.ConfirmedUnreachable,
runtime: RuntimePosture.Supports,
vex: VexStatus.NotAffected,
provenance: ProvenanceLevel.SlsaLevel3,
policyStrength: 1.0m);
var result = calculator.Calculate(input);
result.Tier.Should().Be(ConfidenceTier.VeryHigh);
result.Value.Should().BeGreaterThanOrEqualTo(0.9m);
}
[Fact]
public void Calculate_AllLowFactors_ReturnsLowConfidence()
{
var calculator = CreateCalculator();
var input = CreateInput(
reachability: ReachabilityState.Unknown,
runtime: RuntimePosture.Contradicts,
vex: VexStatus.UnderInvestigation,
provenance: ProvenanceLevel.Unsigned,
policyStrength: 0.3m);
var result = calculator.Calculate(input);
result.Tier.Should().Be(ConfidenceTier.Low);
}
[Fact]
public void Calculate_MissingEvidence_UsesFallbackValues()
{
var calculator = CreateCalculator();
var input = new ConfidenceInput();
var result = calculator.Calculate(input);
result.Value.Should().BeApproximately(0.47m, 0.05m);
result.Factors.Should().AllSatisfy(f => f.Reason.Should().Contain("No"));
}
[Fact]
public void Calculate_GeneratesImprovements_ForLowFactors()
{
var calculator = CreateCalculator();
var input = CreateInput(reachability: ReachabilityState.Unknown);
var result = calculator.Calculate(input);
result.Improvements.Should().Contain(i => i.Factor == ConfidenceFactorType.Reachability);
}
[Fact]
public void Calculate_WeightsSumToOne()
{
var options = new ConfidenceWeightOptions();
options.Validate().Should().BeTrue();
}
[Fact]
public void Calculate_FactorContributions_SumToValue()
{
var calculator = CreateCalculator();
var input = CreateInput();
var result = calculator.Calculate(input);
var sumOfContributions = result.Factors.Sum(f => f.Contribution);
result.Value.Should().BeApproximately(sumOfContributions, 0.001m);
}
private static ConfidenceInput CreateInput(
ReachabilityState reachability = ReachabilityState.StaticUnreachable,
RuntimePosture runtime = RuntimePosture.Supports,
VexStatus vex = VexStatus.NotAffected,
ProvenanceLevel provenance = ProvenanceLevel.Signed,
decimal policyStrength = 0.8m)
{
return new ConfidenceInput
{
Reachability = new ReachabilityEvidence
{
State = reachability,
AnalysisConfidence = 1.0m,
GraphDigests = ["sha256:reachability"]
},
Runtime = new RuntimeEvidence
{
Posture = runtime,
ObservationCount = 3,
LastObserved = FixedTimestamp,
SessionDigests = ["sha256:runtime"]
},
Vex = new VexEvidence
{
Statements =
[
new VexStatement
{
Status = vex,
Issuer = "NVD",
TrustScore = 0.95m,
Timestamp = FixedTimestamp,
StatementDigest = "sha256:vex"
}
]
},
Provenance = new ProvenanceEvidence
{
Level = provenance,
SbomCompleteness = 0.95m,
AttestationDigests = ["sha256:attestation"]
},
Policy = new PolicyEvidence
{
RuleName = "rule-1",
MatchStrength = policyStrength,
EvaluationDigest = "sha256:policy"
},
EvaluationTimestamp = FixedTimestamp
};
}
private static ConfidenceCalculator CreateCalculator()
{
return new ConfidenceCalculator(new StaticOptionsMonitor<ConfidenceWeightOptions>(new ConfidenceWeightOptions()));
}
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
{
private readonly T _value;
public StaticOptionsMonitor(T value) => _value = value;
public T CurrentValue => _value;
public T Get(string? name) => _value;
public IDisposable OnChange(Action<T, string> listener) => NullDisposable.Instance;
private sealed class NullDisposable : IDisposable
{
public static readonly NullDisposable Instance = new();
public void Dispose() { }
}
}
}

View File

@@ -0,0 +1,208 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Freshness;
using Xunit;
namespace StellaOps.Policy.Tests.Freshness;
public sealed class EvidenceTtlEnforcerTests
{
private readonly EvidenceTtlEnforcer _enforcer;
private readonly EvidenceTtlOptions _options;
public EvidenceTtlEnforcerTests()
{
_options = new EvidenceTtlOptions();
_enforcer = new EvidenceTtlEnforcer(
Options.Create(_options),
NullLogger<EvidenceTtlEnforcer>.Instance);
}
[Fact]
public void CheckFreshness_AllFresh_ReturnsFresh()
{
var now = DateTimeOffset.UtcNow;
var bundle = new EvidenceBundle
{
Reachability = new ReachabilityEvidence { ComputedAt = now.AddHours(-1) },
VexStatus = new VexEvidence { Timestamp = now.AddHours(-1) },
Provenance = new ProvenanceEvidence { BuildTime = now.AddDays(-1) }
};
var result = _enforcer.CheckFreshness(bundle, now);
Assert.Equal(FreshnessStatus.Fresh, result.OverallStatus);
Assert.True(result.IsAcceptable);
Assert.False(result.HasWarnings);
}
[Fact]
public void CheckFreshness_ReachabilityNearExpiry_ReturnsWarning()
{
var now = DateTimeOffset.UtcNow;
var bundle = new EvidenceBundle
{
// 7 day TTL, 20% warning threshold = warn after 5.6 days
// At 6 days old, should be in warning state
Reachability = new ReachabilityEvidence { ComputedAt = now.AddDays(-6) }
};
var result = _enforcer.CheckFreshness(bundle, now);
Assert.Equal(FreshnessStatus.Warning, result.OverallStatus);
Assert.True(result.IsAcceptable);
Assert.True(result.HasWarnings);
var reachabilityCheck = result.Checks.First(c => c.Type == EvidenceType.Reachability);
Assert.Equal(FreshnessStatus.Warning, reachabilityCheck.Status);
}
[Fact]
public void CheckFreshness_BoundaryExpired_ReturnsStale()
{
var now = DateTimeOffset.UtcNow;
var bundle = new EvidenceBundle
{
// 72 hour TTL, so 5 days is definitely expired
Boundary = new BoundaryEvidence { ObservedAt = now.AddDays(-5) }
};
var result = _enforcer.CheckFreshness(bundle, now);
Assert.Equal(FreshnessStatus.Stale, result.OverallStatus);
Assert.False(result.IsAcceptable);
var boundaryCheck = result.Checks.First(c => c.Type == EvidenceType.Boundary);
Assert.Equal(FreshnessStatus.Stale, boundaryCheck.Status);
Assert.True(boundaryCheck.Remaining == TimeSpan.Zero);
}
[Theory]
[InlineData(EvidenceType.Sbom, 30)]
[InlineData(EvidenceType.Boundary, 3)]
[InlineData(EvidenceType.Reachability, 7)]
[InlineData(EvidenceType.Vex, 14)]
[InlineData(EvidenceType.PolicyDecision, 1)]
[InlineData(EvidenceType.HumanApproval, 30)]
[InlineData(EvidenceType.CallStack, 7)]
public void GetTtl_ReturnsConfiguredValue(EvidenceType type, int expectedDays)
{
var ttl = _enforcer.GetTtl(type);
Assert.Equal(expectedDays, ttl.TotalDays, precision: 1);
}
[Fact]
public void CheckFreshness_MixedStates_ReturnsStaleOverall()
{
var now = DateTimeOffset.UtcNow;
var bundle = new EvidenceBundle
{
Reachability = new ReachabilityEvidence { ComputedAt = now.AddHours(-1) }, // Fresh
Boundary = new BoundaryEvidence { ObservedAt = now.AddDays(-5) }, // Stale (72h TTL)
VexStatus = new VexEvidence { Timestamp = now.AddDays(-2) } // Fresh
};
var result = _enforcer.CheckFreshness(bundle, now);
Assert.Equal(FreshnessStatus.Stale, result.OverallStatus);
Assert.False(result.IsAcceptable);
Assert.Equal(3, result.Checks.Count);
var freshChecks = result.Checks.Where(c => c.Status == FreshnessStatus.Fresh).ToList();
var staleChecks = result.Checks.Where(c => c.Status == FreshnessStatus.Stale).ToList();
Assert.Equal(2, freshChecks.Count);
Assert.Single(staleChecks);
Assert.Equal(EvidenceType.Boundary, staleChecks[0].Type);
}
[Fact]
public void ComputeExpiration_CalculatesCorrectly()
{
var createdAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
var expiresAt = _enforcer.ComputeExpiration(EvidenceType.Boundary, createdAt);
// Boundary TTL is 72 hours = 3 days
Assert.Equal(createdAt.AddHours(72), expiresAt);
}
[Fact]
public void CheckFreshness_EmptyBundle_ReturnsEmptyChecks()
{
var now = DateTimeOffset.UtcNow;
var bundle = new EvidenceBundle();
var result = _enforcer.CheckFreshness(bundle, now);
Assert.Equal(FreshnessStatus.Fresh, result.OverallStatus);
Assert.Empty(result.Checks);
}
[Fact]
public void CheckFreshness_CustomOptions_UsesCustomTtl()
{
var customOptions = new EvidenceTtlOptions
{
BoundaryTtl = TimeSpan.FromDays(1), // Custom: 1 day instead of default 3 days
WarningThresholdPercent = 0.5 // Custom: 50% instead of default 20%
};
var customEnforcer = new EvidenceTtlEnforcer(
Options.Create(customOptions),
NullLogger<EvidenceTtlEnforcer>.Instance);
var now = DateTimeOffset.UtcNow;
var bundle = new EvidenceBundle
{
// 1 day TTL with 50% warning threshold = warn after 12 hours
// At 16 hours old, should be in warning state
Boundary = new BoundaryEvidence { ObservedAt = now.AddHours(-16) }
};
var result = customEnforcer.CheckFreshness(bundle, now);
Assert.Equal(FreshnessStatus.Warning, result.OverallStatus);
}
[Fact]
public void CheckType_GeneratesCorrectMessage()
{
var now = DateTimeOffset.UtcNow;
var bundle = new EvidenceBundle
{
Reachability = new ReachabilityEvidence { ComputedAt = now.AddDays(-8) } // Expired (7 day TTL)
};
var result = _enforcer.CheckFreshness(bundle, now);
var check = result.Checks.First();
Assert.Equal(EvidenceType.Reachability, check.Type);
Assert.Contains("expired", check.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("ago", check.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CheckFreshness_RecommendedAction_BasedOnConfiguration()
{
var blockOptions = new EvidenceTtlOptions
{
StaleAction = StaleEvidenceAction.Block
};
var blockEnforcer = new EvidenceTtlEnforcer(
Options.Create(blockOptions),
NullLogger<EvidenceTtlEnforcer>.Instance);
var now = DateTimeOffset.UtcNow;
var bundle = new EvidenceBundle
{
Boundary = new BoundaryEvidence { ObservedAt = now.AddDays(-5) } // Stale
};
var result = blockEnforcer.CheckFreshness(bundle, now);
Assert.Equal(StaleEvidenceAction.Block, result.RecommendedAction);
}
}

View File

@@ -0,0 +1,98 @@
using FluentAssertions;
using StellaOps.Policy.TrustLattice;
using VexStatus = StellaOps.Policy.Confidence.Models.VexStatus;
using Xunit;
namespace StellaOps.Policy.Tests.TrustLattice;
public sealed class ClaimScoreMergerTests
{
[Fact]
public void Merge_SelectsHighestScore()
{
var claims = new List<(VexClaim, ClaimScoreResult)>
{
(new VexClaim
{
SourceId = "source-a",
Status = VexStatus.NotAffected,
ScopeSpecificity = 2,
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
}, new ClaimScoreResult { Score = 0.7, BaseTrust = 0.7, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
(new VexClaim
{
SourceId = "source-b",
Status = VexStatus.NotAffected,
ScopeSpecificity = 3,
IssuedAt = DateTimeOffset.Parse("2025-01-02T00:00:00Z"),
}, new ClaimScoreResult { Score = 0.9, BaseTrust = 0.9, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
};
var merger = new ClaimScoreMerger();
var result = merger.Merge(claims, new MergePolicy());
result.Status.Should().Be(VexStatus.NotAffected);
result.WinningClaim.SourceId.Should().Be("source-b");
result.Confidence.Should().Be(0.9);
}
[Fact]
public void Merge_AppliesConflictPenalty()
{
var claims = new List<(VexClaim, ClaimScoreResult)>
{
(new VexClaim
{
SourceId = "source-a",
Status = VexStatus.NotAffected,
ScopeSpecificity = 2,
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
}, new ClaimScoreResult { Score = 0.8, BaseTrust = 0.8, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
(new VexClaim
{
SourceId = "source-b",
Status = VexStatus.Affected,
ScopeSpecificity = 1,
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
}, new ClaimScoreResult { Score = 0.7, BaseTrust = 0.7, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
};
var merger = new ClaimScoreMerger();
var result = merger.Merge(claims, new MergePolicy { ConflictPenalty = 0.25 });
result.HasConflicts.Should().BeTrue();
result.RequiresReplayProof.Should().BeTrue();
result.Conflicts.Should().HaveCount(1);
result.AllClaims.Should().Contain(c => c.SourceId == "source-b" && c.AdjustedScore == 0.525);
}
[Fact]
public void Merge_IsDeterministic()
{
var claims = new List<(VexClaim, ClaimScoreResult)>
{
(new VexClaim
{
SourceId = "source-a",
Status = VexStatus.Fixed,
ScopeSpecificity = 1,
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
}, new ClaimScoreResult { Score = 0.6, BaseTrust = 0.6, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
(new VexClaim
{
SourceId = "source-b",
Status = VexStatus.Fixed,
ScopeSpecificity = 1,
IssuedAt = DateTimeOffset.Parse("2025-01-01T00:00:00Z"),
}, new ClaimScoreResult { Score = 0.6, BaseTrust = 0.6, StrengthMultiplier = 1, FreshnessMultiplier = 1 }),
};
var merger = new ClaimScoreMerger();
var expected = merger.Merge(claims, new MergePolicy());
for (var i = 0; i < 1000; i++)
{
merger.Merge(claims, new MergePolicy()).WinningClaim.SourceId.Should().Be(expected.WinningClaim.SourceId);
}
}
}

View File

@@ -0,0 +1,98 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Policy.Gates;
using StellaOps.Policy.TrustLattice;
using VexStatus = StellaOps.Policy.Confidence.Models.VexStatus;
using Xunit;
namespace StellaOps.Policy.Tests.TrustLattice;
public sealed class PolicyGateRegistryTests
{
[Fact]
public async Task Registry_StopsOnFirstFailure()
{
var registry = new PolicyGateRegistry(new StubServiceProvider(), new PolicyGateRegistryOptions { StopOnFirstFailure = true });
registry.Register<FailingGate>("fail");
registry.Register<PassingGate>("pass");
var mergeResult = CreateMergeResult();
var context = new PolicyGateContext();
var evaluation = await registry.EvaluateAsync(mergeResult, context);
evaluation.Results.Should().HaveCount(1);
evaluation.Results[0].GateName.Should().Be("fail");
evaluation.AllPassed.Should().BeFalse();
}
[Fact]
public async Task Registry_CollectsAllWhenConfigured()
{
var registry = new PolicyGateRegistry(new StubServiceProvider(), new PolicyGateRegistryOptions { StopOnFirstFailure = false });
registry.Register<FailingGate>("fail");
registry.Register<PassingGate>("pass");
var mergeResult = CreateMergeResult();
var context = new PolicyGateContext();
var evaluation = await registry.EvaluateAsync(mergeResult, context);
evaluation.Results.Should().HaveCount(2);
evaluation.Results.Select(r => r.GateName).Should().ContainInOrder("fail", "pass");
}
private static MergeResult CreateMergeResult()
{
var winner = new ScoredClaim
{
SourceId = "source",
Status = VexStatus.NotAffected,
OriginalScore = 0.9,
AdjustedScore = 0.9,
ScopeSpecificity = 1,
Accepted = true,
Reason = "winner",
};
return new MergeResult
{
Status = VexStatus.NotAffected,
Confidence = 0.9,
HasConflicts = false,
RequiresReplayProof = false,
WinningClaim = winner,
AllClaims = ImmutableArray.Create(winner),
Conflicts = ImmutableArray<ConflictRecord>.Empty,
};
}
private sealed class StubServiceProvider : IServiceProvider
{
public object? GetService(Type serviceType) => null;
}
private sealed class FailingGate : IPolicyGate
{
public Task<GateResult> EvaluateAsync(MergeResult mergeResult, PolicyGateContext context, CancellationToken ct = default)
=> Task.FromResult(new GateResult
{
GateName = nameof(FailingGate),
Passed = false,
Reason = "fail",
Details = ImmutableDictionary<string, object>.Empty,
});
}
private sealed class PassingGate : IPolicyGate
{
public Task<GateResult> EvaluateAsync(MergeResult mergeResult, PolicyGateContext context, CancellationToken ct = default)
=> Task.FromResult(new GateResult
{
GateName = nameof(PassingGate),
Passed = true,
Reason = null,
Details = ImmutableDictionary<string, object>.Empty,
});
}
}

View File

@@ -0,0 +1,133 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Policy.Gates;
using StellaOps.Policy.TrustLattice;
using VexStatus = StellaOps.Policy.Confidence.Models.VexStatus;
using Xunit;
namespace StellaOps.Policy.Tests.TrustLattice;
public sealed class PolicyGatesTests
{
[Fact]
public async Task MinimumConfidenceGate_FailsBelowThreshold()
{
var gate = new MinimumConfidenceGate();
var mergeResult = CreateMergeResult(VexStatus.NotAffected, 0.7);
var context = new PolicyGateContext { Environment = "production" };
var result = await gate.EvaluateAsync(mergeResult, context);
result.Passed.Should().BeFalse();
result.Reason.Should().Be("confidence_below_threshold");
}
[Fact]
public async Task UnknownsBudgetGate_FailsWhenBudgetExceeded()
{
var gate = new UnknownsBudgetGate(new UnknownsBudgetGateOptions { MaxUnknownCount = 1, MaxCumulativeUncertainty = 0.5 });
var mergeResult = CreateMergeResult(VexStatus.NotAffected, 0.9);
var context = new PolicyGateContext
{
UnknownCount = 2,
UnknownClaimScores = new[] { 0.4, 0.3 }
};
var result = await gate.EvaluateAsync(mergeResult, context);
result.Passed.Should().BeFalse();
result.Reason.Should().Be("unknowns_budget_exceeded");
}
[Fact]
public async Task SourceQuotaGate_FailsWithoutCorroboration()
{
var gate = new SourceQuotaGate(new SourceQuotaGateOptions { MaxInfluencePercent = 60, CorroborationDelta = 0.10 });
var mergeResult = new MergeResult
{
Status = VexStatus.NotAffected,
Confidence = 0.8,
HasConflicts = false,
RequiresReplayProof = false,
WinningClaim = new ScoredClaim
{
SourceId = "source-a",
Status = VexStatus.NotAffected,
OriginalScore = 0.9,
AdjustedScore = 0.9,
ScopeSpecificity = 1,
Accepted = true,
Reason = "winner",
},
AllClaims = ImmutableArray.Create(
new ScoredClaim
{
SourceId = "source-a",
Status = VexStatus.NotAffected,
OriginalScore = 0.9,
AdjustedScore = 0.9,
ScopeSpecificity = 1,
Accepted = true,
Reason = "winner",
},
new ScoredClaim
{
SourceId = "source-b",
Status = VexStatus.NotAffected,
OriginalScore = 0.1,
AdjustedScore = 0.1,
ScopeSpecificity = 1,
Accepted = false,
Reason = "initial",
}),
Conflicts = ImmutableArray<ConflictRecord>.Empty,
};
var result = await gate.EvaluateAsync(mergeResult, new PolicyGateContext());
result.Passed.Should().BeFalse();
result.Reason.Should().Be("source_quota_exceeded");
}
[Fact]
public async Task ReachabilityRequirementGate_FailsWithoutProof()
{
var gate = new ReachabilityRequirementGate();
var mergeResult = CreateMergeResult(VexStatus.NotAffected, 0.9);
var context = new PolicyGateContext
{
Severity = "CRITICAL",
HasReachabilityProof = false,
};
var result = await gate.EvaluateAsync(mergeResult, context);
result.Passed.Should().BeFalse();
result.Reason.Should().Be("reachability_proof_missing");
}
private static MergeResult CreateMergeResult(VexStatus status, double confidence)
{
var winner = new ScoredClaim
{
SourceId = "source-a",
Status = status,
OriginalScore = confidence,
AdjustedScore = confidence,
ScopeSpecificity = 1,
Accepted = true,
Reason = "winner",
};
return new MergeResult
{
Status = status,
Confidence = confidence,
HasConflicts = false,
RequiresReplayProof = false,
WinningClaim = winner,
AllClaims = ImmutableArray.Create(winner),
Conflicts = ImmutableArray<ConflictRecord>.Empty,
};
}
}

View File

@@ -0,0 +1,221 @@
using System.Collections.Immutable;
using System.Linq;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Unknowns.Configuration;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.Policy.Unknowns.Services;
using Xunit;
namespace StellaOps.Policy.Unknowns.Tests.Services;
public sealed class UnknownBudgetServiceTests
{
[Fact]
public void GetBudgetForEnvironment_KnownEnv_ReturnsBudget()
{
var service = CreateService(new UnknownBudget
{
Environment = "prod",
TotalLimit = 3
});
var budget = service.GetBudgetForEnvironment("prod");
budget.TotalLimit.Should().Be(3);
budget.Environment.Should().Be("prod");
}
[Fact]
public void CheckBudget_WithinLimit_ReturnsSuccess()
{
var service = CreateService(new UnknownBudget
{
Environment = "prod",
TotalLimit = 3,
Action = BudgetAction.Block
});
var result = service.CheckBudget("prod", CreateUnknowns(count: 2));
result.IsWithinBudget.Should().BeTrue();
}
[Fact]
public void CheckBudget_ExceedsTotal_ReturnsViolation()
{
var service = CreateService(new UnknownBudget
{
Environment = "prod",
TotalLimit = 3,
Action = BudgetAction.Block
});
var result = service.CheckBudget("prod", CreateUnknowns(count: 5));
result.IsWithinBudget.Should().BeFalse();
result.RecommendedAction.Should().Be(BudgetAction.Block);
result.TotalUnknowns.Should().Be(5);
}
[Fact]
public void CheckBudget_ExceedsReasonLimit_ReturnsSpecificViolation()
{
var service = CreateService(new UnknownBudget
{
Environment = "prod",
TotalLimit = 5,
ReasonLimits = new Dictionary<UnknownReasonCode, int>
{
[UnknownReasonCode.Reachability] = 0
},
Action = BudgetAction.Block
});
var unknowns = CreateUnknowns(reachability: 2, identity: 1);
var result = service.CheckBudget("prod", unknowns);
result.Violations.Should().ContainKey(UnknownReasonCode.Reachability);
result.Violations[UnknownReasonCode.Reachability].Count.Should().Be(2);
}
[Fact]
public void CheckBudgetWithEscalation_ExceptionCovers_AllowsOperation()
{
var service = CreateService(new UnknownBudget
{
Environment = "prod",
TotalLimit = 1,
ReasonLimits = new Dictionary<UnknownReasonCode, int>
{
[UnknownReasonCode.Reachability] = 0
},
Action = BudgetAction.WarnUnlessException
});
var unknowns = CreateUnknowns(reachability: 1);
var exceptions = new[] { CreateException(UnknownReasonCode.Reachability) };
var result = service.CheckBudgetWithEscalation("prod", unknowns, exceptions);
result.IsWithinBudget.Should().BeTrue();
result.Message.Should().Contain("covered by approved exceptions");
}
[Fact]
public void ShouldBlock_BlockAction_ReturnsTrue()
{
var service = CreateService(new UnknownBudget { Environment = "prod" });
var result = new BudgetCheckResult
{
IsWithinBudget = false,
RecommendedAction = BudgetAction.Block,
TotalUnknowns = 4
};
service.ShouldBlock(result).Should().BeTrue();
}
private static UnknownBudgetService CreateService(UnknownBudget prodBudget)
{
var options = new UnknownBudgetOptions
{
Budgets = new Dictionary<string, UnknownBudget>(StringComparer.OrdinalIgnoreCase)
{
["prod"] = prodBudget,
["default"] = new UnknownBudget
{
Environment = "default",
TotalLimit = 5,
Action = BudgetAction.Warn
}
}
};
return new UnknownBudgetService(
new TestOptionsMonitor<UnknownBudgetOptions>(options),
NullLogger<UnknownBudgetService>.Instance);
}
private static IReadOnlyList<Unknown> CreateUnknowns(
int count = 0,
int reachability = 0,
int identity = 0)
{
var results = new List<Unknown>();
results.AddRange(Enumerable.Range(0, reachability).Select(_ => CreateUnknown(UnknownReasonCode.Reachability)));
results.AddRange(Enumerable.Range(0, identity).Select(_ => CreateUnknown(UnknownReasonCode.Identity)));
var remaining = Math.Max(0, count - results.Count);
results.AddRange(Enumerable.Range(0, remaining).Select(_ => CreateUnknown(UnknownReasonCode.FeedGap)));
return results;
}
private static Unknown CreateUnknown(UnknownReasonCode reasonCode)
{
var timestamp = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
return new Unknown
{
Id = Guid.NewGuid(),
TenantId = Guid.NewGuid(),
PackageId = "pkg:npm/lodash",
PackageVersion = "4.17.21",
Band = UnknownBand.Hot,
Score = 80m,
UncertaintyFactor = 0.5m,
ExploitPressure = 0.7m,
ReasonCode = reasonCode,
FirstSeenAt = timestamp,
LastEvaluatedAt = timestamp,
CreatedAt = timestamp,
UpdatedAt = timestamp
};
}
private static ExceptionObject CreateException(UnknownReasonCode reasonCode)
{
var now = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
return new ExceptionObject
{
ExceptionId = "EXC-UNKNOWN-001",
Version = 1,
Status = ExceptionStatus.Approved,
Type = ExceptionType.Unknown,
Scope = new ExceptionScope
{
Environments = ImmutableArray.Create("prod")
},
OwnerId = "owner",
RequesterId = "requester",
CreatedAt = now,
UpdatedAt = now,
ExpiresAt = now.AddDays(30),
ReasonCode = ExceptionReason.AcceptedRisk,
Rationale = "Approved exception for unknown budget coverage",
Metadata = ImmutableDictionary<string, string>.Empty
.Add("unknown_reason_codes", reasonCode.ToString())
};
}
private sealed class TestOptionsMonitor<T>(T current) : IOptionsMonitor<T>
{
private readonly T _current = current;
public T CurrentValue => _current;
public T Get(string? name) => _current;
public IDisposable OnChange(Action<T, string?> listener) => NoopDisposable.Instance;
}
private sealed class NoopDisposable : IDisposable
{
public static readonly NoopDisposable Instance = new();
public void Dispose() { }
}
}

View File

@@ -1,4 +1,5 @@
using FluentAssertions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.Policy.Unknowns.Services;
@@ -10,6 +11,7 @@ namespace StellaOps.Policy.Unknowns.Tests.Services;
public class UnknownRankerTests
{
private readonly UnknownRanker _ranker = new();
private static readonly DateTimeOffset DefaultAsOf = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
#region Determinism Tests
@@ -17,7 +19,7 @@ public class UnknownRankerTests
public void Rank_SameInput_ReturnsSameResult()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: true,
@@ -38,7 +40,7 @@ public class UnknownRankerTests
public void Rank_MultipleExecutions_ProducesIdenticalScores()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: false,
HasConflictingSources: false,
@@ -67,7 +69,7 @@ public class UnknownRankerTests
public void ComputeUncertainty_MissingVex_Adds040()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: false, // Missing VEX = +0.40
HasReachabilityData: true,
HasConflictingSources: false,
@@ -87,7 +89,7 @@ public class UnknownRankerTests
public void ComputeUncertainty_MissingReachability_Adds030()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: false, // Missing reachability = +0.30
HasConflictingSources: false,
@@ -107,7 +109,7 @@ public class UnknownRankerTests
public void ComputeUncertainty_ConflictingSources_Adds020()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: true, // Conflicts = +0.20
@@ -127,7 +129,7 @@ public class UnknownRankerTests
public void ComputeUncertainty_StaleAdvisory_Adds010()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -147,7 +149,7 @@ public class UnknownRankerTests
public void ComputeUncertainty_AllFactors_SumsTo100()
{
// Arrange - All uncertainty factors active (0.40 + 0.30 + 0.20 + 0.10 = 1.00)
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: true,
@@ -167,7 +169,7 @@ public class UnknownRankerTests
public void ComputeUncertainty_NoFactors_ReturnsZero()
{
// Arrange - All uncertainty factors inactive
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -191,7 +193,7 @@ public class UnknownRankerTests
public void ComputeExploitPressure_InKev_Adds050()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -211,7 +213,7 @@ public class UnknownRankerTests
public void ComputeExploitPressure_HighEpss_Adds030()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -231,7 +233,7 @@ public class UnknownRankerTests
public void ComputeExploitPressure_MediumEpss_Adds015()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -251,7 +253,7 @@ public class UnknownRankerTests
public void ComputeExploitPressure_CriticalCvss_Adds005()
{
// Arrange
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -271,7 +273,7 @@ public class UnknownRankerTests
public void ComputeExploitPressure_AllFactors_SumsCorrectly()
{
// Arrange - KEV (0.50) + high EPSS (0.30) + critical CVSS (0.05) = 0.85
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -291,7 +293,7 @@ public class UnknownRankerTests
public void ComputeExploitPressure_EpssThresholds_AreMutuallyExclusive()
{
// Arrange - High EPSS should NOT also add medium EPSS bonus
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -318,7 +320,7 @@ public class UnknownRankerTests
// Uncertainty: 0.40 (missing VEX)
// Pressure: 0.50 (KEV)
// Expected: (0.40 × 50) + (0.50 × 50) = 20 + 25 = 45
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: false,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -341,7 +343,7 @@ public class UnknownRankerTests
// Uncertainty: 1.00 (all factors)
// Pressure: 0.85 (KEV + high EPSS + critical CVSS, capped at 1.00)
// Expected: (1.00 × 50) + (0.85 × 50) = 50 + 42.5 = 92.50
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: true,
@@ -361,7 +363,7 @@ public class UnknownRankerTests
public void Rank_MinimumScore_IsZero()
{
// Arrange - No uncertainty, no pressure
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -379,6 +381,198 @@ public class UnknownRankerTests
#endregion
#region Reason Code Tests
[Fact]
public void Rank_AnalyzerUnsupported_AssignsAnalyzerLimit()
{
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0,
IsAnalyzerSupported: false);
var result = _ranker.Rank(input);
result.ReasonCode.Should().Be(UnknownReasonCode.AnalyzerLimit);
}
[Fact]
public void Rank_MissingReachability_AssignsReachability()
{
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: false,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0);
var result = _ranker.Rank(input);
result.ReasonCode.Should().Be(UnknownReasonCode.Reachability);
}
[Fact]
public void Rank_MissingDigest_AssignsIdentity()
{
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0,
HasPackageDigest: false);
var result = _ranker.Rank(input);
result.ReasonCode.Should().Be(UnknownReasonCode.Identity);
}
#endregion
#region Decay Factor Tests
[Fact]
public void ComputeDecay_NullLastEvaluated_Returns100Percent()
{
var input = CreateInputWithAge(lastEvaluatedAt: null);
var result = _ranker.Rank(input);
result.DecayFactor.Should().Be(1.00m);
}
[Theory]
[InlineData(0, 1.00)]
[InlineData(7, 1.00)]
[InlineData(8, 0.90)]
[InlineData(30, 0.90)]
[InlineData(31, 0.75)]
[InlineData(90, 0.75)]
[InlineData(91, 0.60)]
[InlineData(180, 0.60)]
[InlineData(181, 0.40)]
[InlineData(365, 0.40)]
[InlineData(366, 0.20)]
[InlineData(1000, 0.20)]
public void ComputeDecay_AgeBuckets_ReturnsCorrectMultiplier(int ageDays, decimal expected)
{
var input = CreateInputWithAge(ageDays: ageDays);
var result = _ranker.Rank(input);
result.DecayFactor.Should().Be(expected);
}
[Fact]
public void Rank_WithDecay_AppliesMultiplierToScore()
{
var input = CreateHighScoreInput(ageDays: 100);
var result = _ranker.Rank(input);
result.Score.Should().Be(30.00m);
result.DecayFactor.Should().Be(0.60m);
}
[Fact]
public void Rank_DecayDisabled_ReturnsFullScore()
{
var options = new UnknownRankerOptions { EnableDecay = false };
var ranker = new UnknownRanker(Options.Create(options));
var input = CreateHighScoreInput(ageDays: 100);
var result = ranker.Rank(input);
result.DecayFactor.Should().Be(1.0m);
result.Score.Should().Be(50.00m);
}
[Fact]
public void Rank_Decay_Determinism_SameInputSameOutput()
{
var input = CreateInputWithAge(ageDays: 45);
var results = Enumerable.Range(0, 100)
.Select(_ => _ranker.Rank(input))
.ToList();
results.Should().AllBeEquivalentTo(results[0]);
}
#endregion
#region Containment Reduction Tests
[Fact]
public void ComputeContainmentReduction_NullInputs_ReturnsZero()
{
var input = CreateInputWithContainment(blastRadius: null, containment: null);
var result = _ranker.Rank(input);
result.ContainmentReduction.Should().Be(0m);
}
[Fact]
public void ComputeContainmentReduction_IsolatedPackage_Returns15Percent()
{
var blast = new BlastRadius { Dependents = 0, NetFacing = true };
var input = CreateInputWithContainment(blastRadius: blast);
var result = _ranker.Rank(input);
result.ContainmentReduction.Should().Be(0.15m);
}
[Fact]
public void ComputeContainmentReduction_AllContainmentFactors_CapsAt40Percent()
{
var blast = new BlastRadius { Dependents = 0, NetFacing = false, Privilege = "none" };
var contain = new ContainmentSignals { Seccomp = "enforced", FileSystem = "ro", NetworkPolicy = "isolated" };
var input = CreateInputWithContainment(blastRadius: blast, containment: contain);
var result = _ranker.Rank(input);
result.ContainmentReduction.Should().Be(0.40m);
}
[Fact]
public void Rank_WithContainment_AppliesReductionToScore()
{
var blast = new BlastRadius { Dependents = 0 };
var input = CreateHighScoreInputWithContainment(blast);
var result = _ranker.Rank(input);
result.Score.Should().Be(48.00m);
result.ContainmentReduction.Should().Be(0.20m);
}
[Fact]
public void Rank_ContainmentDisabled_NoReduction()
{
var options = new UnknownRankerOptions { EnableContainmentReduction = false };
var ranker = new UnknownRanker(Options.Create(options));
var blast = new BlastRadius { Dependents = 0 };
var input = CreateHighScoreInputWithContainment(blast);
var result = ranker.Rank(input);
result.ContainmentReduction.Should().Be(0m);
result.Score.Should().Be(60.00m);
}
#endregion
#region Band Assignment Tests
[Theory]
@@ -404,7 +598,7 @@ public class UnknownRankerTests
public void Rank_ScoreAbove75_AssignsHotBand()
{
// Arrange - Score = (1.00 × 50) + (0.50 × 50) = 75.00
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: true,
@@ -426,7 +620,7 @@ public class UnknownRankerTests
{
// Arrange - Score = (0.70 × 50) + (0.50 × 50) = 35 + 25 = 60
// Uncertainty: 0.70 (missing VEX + missing reachability)
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: false,
@@ -447,7 +641,7 @@ public class UnknownRankerTests
public void Rank_ScoreBetween25And50_AssignsColdBand()
{
// Arrange - Score = (0.40 × 50) + (0.15 × 50) = 20 + 7.5 = 27.5
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: false,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -468,7 +662,7 @@ public class UnknownRankerTests
public void Rank_ScoreBelow25_AssignsResolvedBand()
{
// Arrange - Score = (0.10 × 50) + (0.05 × 50) = 5 + 2.5 = 7.5
var input = new UnknownRankInput(
var input = CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
@@ -486,4 +680,113 @@ public class UnknownRankerTests
}
#endregion
private static UnknownRankInput CreateInput(
bool HasVexStatement,
bool HasReachabilityData,
bool HasConflictingSources,
bool IsStaleAdvisory,
bool IsInKev,
decimal EpssScore,
decimal CvssScore,
DateTimeOffset? FirstSeenAt = null,
DateTimeOffset? LastEvaluatedAt = null,
DateTimeOffset? AsOfDateTime = null,
BlastRadius? BlastRadius = null,
ContainmentSignals? Containment = null,
bool HasPackageDigest = true,
bool HasProvenanceAttestation = true,
bool HasVexConflicts = false,
bool HasFeedCoverage = true,
bool HasConfigVisibility = true,
bool IsAnalyzerSupported = true)
{
var asOf = AsOfDateTime ?? DefaultAsOf;
return new UnknownRankInput(
HasVexStatement,
HasReachabilityData,
HasConflictingSources,
IsStaleAdvisory,
IsInKev,
EpssScore,
CvssScore,
FirstSeenAt,
LastEvaluatedAt,
asOf,
BlastRadius,
Containment,
HasPackageDigest,
HasProvenanceAttestation,
HasVexConflicts,
HasFeedCoverage,
HasConfigVisibility,
IsAnalyzerSupported);
}
private static UnknownRankInput CreateInputWithAge(
int? ageDays = null,
DateTimeOffset? lastEvaluatedAt = null,
DateTimeOffset? asOfDateTime = null)
{
var asOf = asOfDateTime ?? DefaultAsOf;
var evaluatedAt = lastEvaluatedAt ?? (ageDays.HasValue ? asOf.AddDays(-ageDays.Value) : null);
return CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0,
LastEvaluatedAt: evaluatedAt,
AsOfDateTime: asOf);
}
private static UnknownRankInput CreateHighScoreInput(int ageDays)
{
var asOf = DefaultAsOf;
return CreateInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: true,
IsStaleAdvisory: true,
IsInKev: false,
EpssScore: 0,
CvssScore: 0,
LastEvaluatedAt: asOf.AddDays(-ageDays),
AsOfDateTime: asOf);
}
private static UnknownRankInput CreateInputWithContainment(
BlastRadius? blastRadius = null,
ContainmentSignals? containment = null)
{
return CreateInput(
HasVexStatement: true,
HasReachabilityData: true,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: false,
EpssScore: 0,
CvssScore: 0,
BlastRadius: blastRadius,
Containment: containment);
}
private static UnknownRankInput CreateHighScoreInputWithContainment(BlastRadius blastRadius)
{
return CreateInput(
HasVexStatement: false,
HasReachabilityData: false,
HasConflictingSources: false,
IsStaleAdvisory: false,
IsInKev: true,
EpssScore: 0,
CvssScore: 0,
BlastRadius: blastRadius);
}
}

View File

@@ -21,6 +21,7 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj" />
</ItemGroup>
</Project>