Files
git.stella-ops.org/src/Policy/__Tests/StellaOps.Policy.Tests/PolicyPreviewServiceTests.cs

190 lines
7.9 KiB
C#

using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Policy.Tests;
public sealed class PolicyPreviewServiceTests
{
private readonly ITestOutputHelper _output;
public PolicyPreviewServiceTests(ITestOutputHelper output)
{
_output = output ?? throw new ArgumentNullException(nameof(output));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PreviewAsync_ComputesDiffs_ForBlockingRule()
{
const string yaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var timeProvider = new FakeTimeProvider();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, null), CancellationToken.None);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
var findings = ImmutableArray.Create(
PolicyFinding.Create("finding-1", PolicySeverity.Critical, environment: "prod", source: "NVD"),
PolicyFinding.Create("finding-2", PolicySeverity.Low));
var baseline = ImmutableArray.Create(
new PolicyVerdict("finding-1", PolicyVerdictStatus.Pass),
new PolicyVerdict("finding-2", PolicyVerdictStatus.Pass));
var response = await service.PreviewAsync(new PolicyPreviewRequest(
"sha256:abc",
findings,
baseline),
CancellationToken.None);
Assert.True(response.Success);
Assert.Equal(1, response.ChangedCount);
var diff1 = Assert.Single(response.Diffs.Where(diff => diff.Projected.FindingId == "finding-1"));
Assert.Equal(PolicyVerdictStatus.Pass, diff1.Baseline.Status);
Assert.Equal(PolicyVerdictStatus.Blocked, diff1.Projected.Status);
Assert.Equal("Block Critical", diff1.Projected.RuleName);
Assert.True(diff1.Projected.Score > 0);
Assert.Equal(PolicyScoringConfig.Default.Version, diff1.Projected.ConfigVersion);
Assert.Equal(PolicyVerdictStatus.Pass, response.Diffs.First(diff => diff.Projected.FindingId == "finding-2").Projected.Status);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PreviewAsync_UsesProposedPolicy_WhenProvided()
{
const string yaml = """
version: "1.0"
rules:
- name: Ignore Dev
environments: [dev]
action:
type: ignore
justification: dev waiver
""";
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
var findings = ImmutableArray.Create(
PolicyFinding.Create("finding-1", PolicySeverity.Medium, environment: "dev"));
var baseline = ImmutableArray.Create(new PolicyVerdict("finding-1", PolicyVerdictStatus.Blocked));
var response = await service.PreviewAsync(new PolicyPreviewRequest(
"sha256:def",
findings,
baseline,
SnapshotOverride: null,
ProposedPolicy: new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "dev override")),
CancellationToken.None);
Assert.True(response.Success);
var diff = Assert.Single(response.Diffs);
Assert.Equal(PolicyVerdictStatus.Blocked, diff.Baseline.Status);
Assert.Equal(PolicyVerdictStatus.Ignored, diff.Projected.Status);
Assert.Equal("Ignore Dev", diff.Projected.RuleName);
Assert.True(diff.Projected.Score >= 0);
Assert.Equal(1, response.ChangedCount);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PreviewAsync_ReturnsIssues_WhenPolicyInvalid()
{
var snapshotRepo = new InMemoryPolicySnapshotRepository();
var auditRepo = new InMemoryPolicyAuditRepository();
var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
const string invalid = "version: 1.0";
var request = new PolicyPreviewRequest(
"sha256:ghi",
ImmutableArray<PolicyFinding>.Empty,
ImmutableArray<PolicyVerdict>.Empty,
SnapshotOverride: null,
ProposedPolicy: new PolicySnapshotContent(invalid, PolicyDocumentFormat.Yaml, null, null, null));
var response = await service.PreviewAsync(request, CancellationToken.None);
Assert.False(response.Success);
Assert.NotEmpty(response.Issues);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PreviewAsync_QuietWithoutVexDowngradesToWarn()
{
const string yaml = """
version: "1.0"
rules:
- name: Quiet Without VEX
severity: [Low]
quiet: true
action:
type: ignore
""";
var binding = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml);
if (!binding.Success)
{
foreach (var issue in binding.Issues)
{
_output.WriteLine($"{issue.Severity} {issue.Code} {issue.Path} :: {issue.Message}");
}
var parseMethod = typeof(PolicyBinder).GetMethod("ParseToNode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
var node = (System.Text.Json.Nodes.JsonNode?)parseMethod?.Invoke(null, new object[] { yaml, PolicyDocumentFormat.Yaml });
_output.WriteLine(node?.ToJsonString() ?? "<null>");
}
Assert.True(binding.Success);
Assert.Empty(binding.Issues);
Assert.False(binding.Document.Rules[0].Metadata.ContainsKey("quiet"));
Assert.True(binding.Document.Rules[0].Action.Quiet);
var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "quiet test"), CancellationToken.None);
var snapshot = await store.GetLatestAsync();
Assert.NotNull(snapshot);
Assert.True(snapshot!.Document.Rules[0].Action.Quiet);
Assert.Null(snapshot.Document.Rules[0].Action.RequireVex);
Assert.Equal(PolicyActionType.Ignore, snapshot.Document.Rules[0].Action.Type);
var manualVerdict = PolicyEvaluation.EvaluateFinding(snapshot.Document, snapshot.ScoringConfig, PolicyFinding.Create("finding-quiet", PolicySeverity.Low), out _);
Assert.Equal(PolicyVerdictStatus.Warned, manualVerdict.Status);
var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);
var findings = ImmutableArray.Create(PolicyFinding.Create("finding-quiet", PolicySeverity.Low));
var baseline = ImmutableArray<PolicyVerdict>.Empty;
var response = await service.PreviewAsync(new PolicyPreviewRequest(
"sha256:quiet",
findings,
baseline),
CancellationToken.None);
Assert.True(response.Success);
var verdict = Assert.Single(response.Diffs).Projected;
Assert.Equal(PolicyVerdictStatus.Warned, verdict.Status);
Assert.Contains("requireVex", verdict.Notes, System.StringComparison.OrdinalIgnoreCase);
Assert.True(verdict.Score >= 0);
}
}