Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user