up
This commit is contained in:
@@ -34,16 +34,20 @@ public sealed class PolicyEvaluationTests
|
||||
source: "community",
|
||||
tags: ImmutableArray.Create("reachability:indirect"));
|
||||
|
||||
var verdict = PolicyEvaluation.EvaluateFinding(document, config, finding);
|
||||
var verdict = PolicyEvaluation.EvaluateFinding(document, config, finding, out var explanation);
|
||||
|
||||
Assert.Equal(PolicyVerdictStatus.Blocked, verdict.Status);
|
||||
Assert.Equal(19.5, verdict.Score, 3);
|
||||
|
||||
var inputs = verdict.GetInputs();
|
||||
Assert.Equal(50, inputs["severityWeight"]);
|
||||
Assert.Equal(0.65, inputs["trustWeight"], 3);
|
||||
Assert.Equal(0.6, inputs["reachabilityWeight"], 3);
|
||||
Assert.Equal(19.5, inputs["baseScore"], 3);
|
||||
Assert.Equal(PolicyVerdictStatus.Blocked, verdict.Status);
|
||||
Assert.Equal(19.5, verdict.Score, 3);
|
||||
|
||||
var inputs = verdict.GetInputs();
|
||||
Assert.Equal(50, inputs["severityWeight"]);
|
||||
Assert.Equal(0.65, inputs["trustWeight"], 3);
|
||||
Assert.Equal(0.6, inputs["reachabilityWeight"], 3);
|
||||
Assert.Equal(19.5, inputs["baseScore"], 3);
|
||||
|
||||
Assert.NotNull(explanation);
|
||||
Assert.Equal(PolicyVerdictStatus.Blocked, explanation!.Decision);
|
||||
Assert.Equal("BlockMedium", explanation.RuleName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -79,17 +83,20 @@ public sealed class PolicyEvaluationTests
|
||||
PolicySeverity.Critical,
|
||||
tags: ImmutableArray.Create("reachability:entrypoint"));
|
||||
|
||||
var verdict = PolicyEvaluation.EvaluateFinding(document, config, finding);
|
||||
var verdict = PolicyEvaluation.EvaluateFinding(document, config, finding, out var explanation);
|
||||
|
||||
Assert.Equal(PolicyVerdictStatus.Ignored, verdict.Status);
|
||||
Assert.True(verdict.Quiet);
|
||||
Assert.Equal("QuietIgnore", verdict.QuietedBy);
|
||||
Assert.Equal(10, verdict.Score, 3);
|
||||
|
||||
var inputs = verdict.GetInputs();
|
||||
Assert.Equal(90, inputs["baseScore"], 3);
|
||||
Assert.Equal(config.IgnorePenalty, inputs["ignorePenalty"]);
|
||||
Assert.Equal(config.QuietPenalty, inputs["quietPenalty"]);
|
||||
Assert.Equal(PolicyVerdictStatus.Ignored, verdict.Status);
|
||||
Assert.True(verdict.Quiet);
|
||||
Assert.Equal("QuietIgnore", verdict.QuietedBy);
|
||||
Assert.Equal(10, verdict.Score, 3);
|
||||
|
||||
var inputs = verdict.GetInputs();
|
||||
Assert.Equal(90, inputs["baseScore"], 3);
|
||||
Assert.Equal(config.IgnorePenalty, inputs["ignorePenalty"]);
|
||||
Assert.Equal(config.QuietPenalty, inputs["quietPenalty"]);
|
||||
|
||||
Assert.NotNull(explanation);
|
||||
Assert.Equal(PolicyVerdictStatus.Ignored, explanation!.Decision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -121,16 +128,19 @@ public sealed class PolicyEvaluationTests
|
||||
PolicySeverity.Unknown,
|
||||
tags: ImmutableArray.Create("reachability:unknown", "unknown-age-days:5"));
|
||||
|
||||
var verdict = PolicyEvaluation.EvaluateFinding(document, config, finding);
|
||||
var verdict = PolicyEvaluation.EvaluateFinding(document, config, finding, out var explanation);
|
||||
|
||||
Assert.Equal(PolicyVerdictStatus.Blocked, verdict.Status);
|
||||
Assert.Equal(30, verdict.Score, 3); // 60 * 1 * 0.5
|
||||
Assert.Equal(0.55, verdict.UnknownConfidence ?? 0, 3);
|
||||
Assert.Equal("medium", verdict.ConfidenceBand);
|
||||
Assert.Equal(5, verdict.UnknownAgeDays ?? 0, 3);
|
||||
|
||||
var inputs = verdict.GetInputs();
|
||||
Assert.Equal(0.55, inputs["unknownConfidence"], 3);
|
||||
Assert.Equal(5, inputs["unknownAgeDays"], 3);
|
||||
}
|
||||
}
|
||||
Assert.Equal(PolicyVerdictStatus.Blocked, verdict.Status);
|
||||
Assert.Equal(30, verdict.Score, 3); // 60 * 1 * 0.5
|
||||
Assert.Equal(0.55, verdict.UnknownConfidence ?? 0, 3);
|
||||
Assert.Equal("medium", verdict.ConfidenceBand);
|
||||
Assert.Equal(5, verdict.UnknownAgeDays ?? 0, 3);
|
||||
|
||||
var inputs = verdict.GetInputs();
|
||||
Assert.Equal(0.55, inputs["unknownConfidence"], 3);
|
||||
Assert.Equal(5, inputs["unknownAgeDays"], 3);
|
||||
|
||||
Assert.NotNull(explanation);
|
||||
Assert.Equal(PolicyVerdictStatus.Blocked, explanation!.Decision);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ rules:
|
||||
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));
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests;
|
||||
|
||||
public class PolicyValidationCliTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunAsync_EmitsCanonicalDigest_OnValidPolicy()
|
||||
{
|
||||
var tmp = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tmp, """
|
||||
{
|
||||
"apiVersion": "spl.stellaops/v1",
|
||||
"kind": "Policy",
|
||||
"metadata": { "name": "demo" },
|
||||
"spec": {
|
||||
"defaultEffect": "deny",
|
||||
"statements": [
|
||||
{ "id": "ALLOW", "effect": "allow", "match": { "resource": "*", "actions": ["read"] } }
|
||||
]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var options = new PolicyValidationCliOptions
|
||||
{
|
||||
Inputs = new[] { tmp },
|
||||
OutputJson = false,
|
||||
Strict = false,
|
||||
};
|
||||
|
||||
using var output = new StringWriter();
|
||||
using var error = new StringWriter();
|
||||
var cli = new PolicyValidationCli(output, error);
|
||||
|
||||
var exit = await cli.RunAsync(options);
|
||||
|
||||
exit.Should().Be(0);
|
||||
var text = output.ToString();
|
||||
text.Should().Contain("OK");
|
||||
text.Should().Contain("canonical.spl.digest:");
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using StellaOps.Policy;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests;
|
||||
|
||||
public class SplCanonicalizerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Canonicalize_SortsStatementsActionsAndConditions()
|
||||
{
|
||||
const string input = """
|
||||
{
|
||||
"kind": "Policy",
|
||||
"apiVersion": "spl.stellaops/v1",
|
||||
"spec": {
|
||||
"statements": [
|
||||
{
|
||||
"effect": "deny",
|
||||
"id": "B-2",
|
||||
"match": {
|
||||
"resource": "/accounts/*",
|
||||
"actions": ["delete", "read"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "desc",
|
||||
"effect": "allow",
|
||||
"id": "A-1",
|
||||
"match": {
|
||||
"actions": ["write", "read"],
|
||||
"resource": "/accounts/*",
|
||||
"conditions": [
|
||||
{"operator": "gte", "value": 2, "field": "tier"},
|
||||
{"field": "env", "value": "prod", "operator": "eq"}
|
||||
]
|
||||
},
|
||||
"audit": {"severity": "warn", "message": "audit msg"}
|
||||
}
|
||||
],
|
||||
"defaultEffect": "deny"
|
||||
},
|
||||
"metadata": {
|
||||
"labels": {"env": "prod"},
|
||||
"annotations": {"a": "1"},
|
||||
"name": "demo"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var canonical = SplCanonicalizer.CanonicalizeToString(input);
|
||||
|
||||
const string expected = "{\"apiVersion\":\"spl.stellaops/v1\",\"kind\":\"Policy\",\"metadata\":{\"annotations\":{\"a\":\"1\"},\"labels\":{\"env\":\"prod\"},\"name\":\"demo\"},\"spec\":{\"defaultEffect\":\"deny\",\"statements\":[{\"audit\":{\"message\":\"audit msg\",\"severity\":\"warn\"},\"description\":\"desc\",\"effect\":\"allow\",\"id\":\"A-1\",\"match\":{\"actions\":[\"read\",\"write\"],\"conditions\":[{\"field\":\"env\",\"operator\":\"eq\",\"value\":\"prod\"},{\"field\":\"tier\",\"operator\":\"gte\",\"value\":2}],\"resource\":\"/accounts/*\"}},{\"effect\":\"deny\",\"id\":\"B-2\",\"match\":{\"actions\":[\"delete\",\"read\"],\"resource\":\"/accounts/*\"}}]}}}";
|
||||
|
||||
Assert.Equal(expected, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_IgnoresOrderingNoise()
|
||||
{
|
||||
const string versionA = """
|
||||
{"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"spec":{"defaultEffect":"deny","statements":[{"id":"B","effect":"deny","match":{"resource":"/r","actions":["write","read"]}},{"id":"A","effect":"allow","match":{"resource":"/r","actions":["read"],"conditions":[{"field":"env","operator":"eq","value":"prod"}]}}]}}
|
||||
""";
|
||||
|
||||
const string versionB = """
|
||||
{"spec":{"statements":[{"match":{"actions":["read"],"resource":"/r","conditions":[{"value":"prod","operator":"eq","field":"env"}]},"effect":"allow","id":"A"},{"match":{"actions":["read","write"],"resource":"/r"},"effect":"deny","id":"B"}],"defaultEffect":"deny"},"kind":"Policy","metadata":{"name":"demo"},"apiVersion":"spl.stellaops/v1"}
|
||||
""";
|
||||
|
||||
var hashA = SplCanonicalizer.ComputeDigest(versionA);
|
||||
var hashB = SplCanonicalizer.ComputeDigest(versionB);
|
||||
|
||||
Assert.Equal(hashA, hashB);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeDigest_DetectsContentChange()
|
||||
{
|
||||
const string baseDoc = """
|
||||
{"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"spec":{"statements":[{"id":"A","effect":"allow","match":{"resource":"/r","actions":["read"]}}]}}
|
||||
""";
|
||||
|
||||
const string changedDoc = """
|
||||
{"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"spec":{"statements":[{"id":"A","effect":"allow","match":{"resource":"/r","actions":["read","write"]}}]}}
|
||||
""";
|
||||
|
||||
var original = SplCanonicalizer.ComputeDigest(baseDoc);
|
||||
var changed = SplCanonicalizer.ComputeDigest(changedDoc);
|
||||
|
||||
Assert.NotEqual(original, changed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Policy;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests;
|
||||
|
||||
public class SplLayeringEngineTests
|
||||
{
|
||||
[Fact]
|
||||
public void Merge_ReplacesStatementsById_AndKeepsBaseOnes()
|
||||
{
|
||||
const string baseDoc = """
|
||||
{"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"spec":{"defaultEffect":"deny","statements":[{"id":"A","effect":"allow","match":{"resource":"/r","actions":["read"]}}, {"id":"B","effect":"deny","match":{"resource":"/r","actions":["write"]}}]}}
|
||||
""";
|
||||
|
||||
const string overlay = """
|
||||
{"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"spec":{"statements":[{"id":"A","effect":"deny","match":{"resource":"/r","actions":["read","write"]}}, {"id":"C","effect":"allow","match":{"resource":"/r","actions":["read"]}}]}}
|
||||
""";
|
||||
|
||||
var merged = SplLayeringEngine.Merge(baseDoc, overlay);
|
||||
|
||||
const string expected = "{\"apiVersion\":\"spl.stellaops/v1\",\"kind\":\"Policy\",\"metadata\":{\"name\":\"demo\"},\"spec\":{\"defaultEffect\":\"deny\",\"statements\":[{\"effect\":\"deny\",\"id\":\"A\",\"match\":{\"actions\":[\"read\",\"write\"],\"resource\":\"/r\"}},{\"effect\":\"deny\",\"id\":\"B\",\"match\":{\"actions\":[\"write\"],\"resource\":\"/r\"}},{\"effect\":\"allow\",\"id\":\"C\",\"match\":{\"actions\":[\"read\"],\"resource\":\"/r\"}}]}}";
|
||||
|
||||
Assert.Equal(expected, merged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_MergesMetadataAndDefaultEffect()
|
||||
{
|
||||
const string baseDoc = """
|
||||
{"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo","labels":{"env":"dev"}},"spec":{"defaultEffect":"deny","statements":[{"id":"A","effect":"allow","match":{"resource":"/r","actions":["read"]}}]}}
|
||||
""";
|
||||
|
||||
const string overlay = """
|
||||
{"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"labels":{"env":"prod","tier":"gold"}},"spec":{"defaultEffect":"allow","statements":[{"id":"B","effect":"deny","match":{"resource":"/r","actions":["write"]}}]}}
|
||||
""";
|
||||
|
||||
var merged = SplLayeringEngine.Merge(baseDoc, overlay);
|
||||
|
||||
const string expected = "{\"apiVersion\":\"spl.stellaops/v1\",\"kind\":\"Policy\",\"metadata\":{\"labels\":{\"env\":\"prod\",\"tier\":\"gold\"},\"name\":\"demo\"},\"spec\":{\"defaultEffect\":\"allow\",\"statements\":[{\"effect\":\"allow\",\"id\":\"A\",\"match\":{\"actions\":[\"read\"],\"resource\":\"/r\"}},{\"effect\":\"deny\",\"id\":\"B\",\"match\":{\"actions\":[\"write\"],\"resource\":\"/r\"}}]}}";
|
||||
|
||||
Assert.Equal(expected, merged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_PreservesUnknownTopLevelAndSpecFields()
|
||||
{
|
||||
const string baseDoc = """
|
||||
{"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"extras":{"foo":1},"spec":{"defaultEffect":"deny","statements":[{"id":"A","effect":"allow","match":{"resource":"/r","actions":["read"]}}],"extensions":{"bar":true}}}
|
||||
""";
|
||||
|
||||
const string overlay = """
|
||||
{"apiVersion":"spl.stellaops/v1","kind":"Policy","metadata":{"name":"demo"},"spec":{"statements":[{"id":"B","effect":"deny","match":{"resource":"/r","actions":["write"]}}]}}
|
||||
""";
|
||||
|
||||
var merged = SplLayeringEngine.Merge(baseDoc, overlay);
|
||||
|
||||
using var doc = JsonDocument.Parse(merged);
|
||||
var root = doc.RootElement;
|
||||
|
||||
Assert.True(root.TryGetProperty("extras", out var extras) && extras.TryGetProperty("foo", out var foo) && foo.GetInt32() == 1);
|
||||
Assert.True(root.GetProperty("spec").TryGetProperty("extensions", out var extensions) && extensions.TryGetProperty("bar", out var bar) && bar.GetBoolean());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests;
|
||||
|
||||
public class SplMigrationToolTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToSplPolicyJson_ConvertsRulesAndMetadata()
|
||||
{
|
||||
var rule = PolicyRule.Create(
|
||||
name: "Block CVE",
|
||||
action: new PolicyAction(PolicyActionType.Block, null, null, null, false),
|
||||
severities: ImmutableArray.Create(PolicySeverity.Critical),
|
||||
environments: ImmutableArray<string>.Empty,
|
||||
sources: ImmutableArray<string>.Empty,
|
||||
vendors: ImmutableArray<string>.Empty,
|
||||
licenses: ImmutableArray<string>.Empty,
|
||||
tags: ImmutableArray<string>.Empty,
|
||||
match: PolicyRuleMatchCriteria.Create(
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray.Create("/app"),
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray<string>.Empty),
|
||||
expires: null,
|
||||
justification: "block it",
|
||||
identifier: "RULE-1");
|
||||
|
||||
var document = new PolicyDocument(
|
||||
PolicySchema.CurrentVersion,
|
||||
ImmutableArray.Create(rule),
|
||||
ImmutableDictionary<string, string>.Empty.Add("name", "demo"),
|
||||
PolicyExceptionConfiguration.Empty);
|
||||
|
||||
var spl = SplMigrationTool.ToSplPolicyJson(document);
|
||||
|
||||
const string expected = "{\"apiVersion\":\"spl.stellaops/v1\",\"kind\":\"Policy\",\"metadata\":{\"labels\":{\"name\":\"demo\"},\"name\":\"demo\"},\"spec\":{\"defaultEffect\":\"deny\",\"statements\":[{\"effect\":\"deny\",\"id\":\"RULE-1\",\"match\":{\"actions\":[\"access\"],\"resource\":\"/app\"}}]}}";
|
||||
|
||||
Assert.Equal(expected, spl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToSplPolicyJson_UsesOverlaySafeIdsAndAudits()
|
||||
{
|
||||
var rule = PolicyRule.Create(
|
||||
name: "Warn entrypoint",
|
||||
action: new PolicyAction(PolicyActionType.Warn, null, null, null, true),
|
||||
severities: ImmutableArray.Create(PolicySeverity.Low),
|
||||
environments: ImmutableArray<string>.Empty,
|
||||
sources: ImmutableArray<string>.Empty,
|
||||
vendors: ImmutableArray<string>.Empty,
|
||||
licenses: ImmutableArray<string>.Empty,
|
||||
tags: ImmutableArray<string>.Empty,
|
||||
match: PolicyRuleMatchCriteria.Empty,
|
||||
expires: null,
|
||||
justification: "soft warning");
|
||||
|
||||
var document = new PolicyDocument(
|
||||
PolicySchema.CurrentVersion,
|
||||
ImmutableArray.Create(rule),
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
PolicyExceptionConfiguration.Empty);
|
||||
|
||||
var spl = SplMigrationTool.ToSplPolicyJson(document);
|
||||
|
||||
const string expectedId = "warn-entrypoint";
|
||||
Assert.Contains(expectedId, spl);
|
||||
Assert.Contains("\"audit\":{\"message\":\"soft warning\",\"severity\":\"warn\"}", spl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Policy;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests;
|
||||
|
||||
public class SplSchemaResourceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Schema_IncludesReachabilityAndExploitability()
|
||||
{
|
||||
var schema = SplSchemaResource.GetSchema();
|
||||
using var doc = JsonDocument.Parse(schema);
|
||||
var match = doc.RootElement
|
||||
.GetProperty("properties")
|
||||
.GetProperty("spec")
|
||||
.GetProperty("properties")
|
||||
.GetProperty("statements")
|
||||
.GetProperty("items")
|
||||
.GetProperty("properties")
|
||||
.GetProperty("match")
|
||||
.GetProperty("properties");
|
||||
|
||||
Assert.True(match.TryGetProperty("reachability", out var reachability));
|
||||
Assert.Equal(JsonValueKind.Object, reachability.ValueKind);
|
||||
Assert.True(match.TryGetProperty("exploitability", out var exploitability));
|
||||
Assert.Equal(JsonValueKind.Object, exploitability.ValueKind);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user