190 lines
7.9 KiB
C#
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);
|
|
}
|
|
}
|