finish off sprint advisories and sprints

This commit is contained in:
master
2026-01-24 00:12:43 +02:00
parent 726d70dc7f
commit c70e83719e
266 changed files with 46699 additions and 1328 deletions

View File

@@ -0,0 +1,282 @@
using FluentAssertions;
using StellaOps.Policy.Interop.Abstractions;
using StellaOps.Policy.Interop.Contracts;
using StellaOps.Policy.Interop.Evaluation;
using Xunit;
namespace StellaOps.Policy.Interop.Tests.Evaluation;
public class RemediationResolverTests
{
private readonly RemediationResolver _resolver = new();
[Theory]
[InlineData(PolicyGateTypes.CvssThreshold, RemediationCodes.CvssExceed)]
[InlineData(PolicyGateTypes.SignatureRequired, RemediationCodes.SignatureMissing)]
[InlineData(PolicyGateTypes.EvidenceFreshness, RemediationCodes.FreshnessExpired)]
[InlineData(PolicyGateTypes.SbomPresence, RemediationCodes.SbomMissing)]
[InlineData(PolicyGateTypes.MinimumConfidence, RemediationCodes.ConfidenceLow)]
[InlineData(PolicyGateTypes.UnknownsBudget, RemediationCodes.UnknownsBudgetExceeded)]
[InlineData(PolicyGateTypes.ReachabilityRequirement, RemediationCodes.ReachabilityRequired)]
public void GetDefaultForGateType_ReturnsCorrectCode(string gateType, string expectedCode)
{
var hint = _resolver.GetDefaultForGateType(gateType);
hint.Should().NotBeNull();
hint!.Code.Should().Be(expectedCode);
hint.Title.Should().NotBeNullOrWhiteSpace();
hint.Severity.Should().NotBeNullOrWhiteSpace();
hint.Actions.Should().NotBeEmpty();
}
[Fact]
public void GetDefaultForGateType_UnknownType_ReturnsNull()
{
var hint = _resolver.GetDefaultForGateType("NonExistentGate");
hint.Should().BeNull();
}
[Fact]
public void Resolve_GateWithCustomRemediation_ReturnsCustomHint()
{
var customHint = new RemediationHint
{
Code = "CUSTOM_CODE",
Title = "Custom remediation",
Severity = RemediationSeverity.Low,
Actions = [new RemediationAction { Type = RemediationActionTypes.Investigate, Description = "Custom action" }]
};
var gate = new PolicyGateDefinition
{
Id = "test-gate",
Type = PolicyGateTypes.CvssThreshold,
Remediation = customHint
};
var result = _resolver.Resolve(gate, "some failure");
result.Should().NotBeNull();
result!.Code.Should().Be("CUSTOM_CODE");
result.Title.Should().Be("Custom remediation");
}
[Fact]
public void Resolve_GateWithoutRemediation_FallsBackToDefault()
{
var gate = new PolicyGateDefinition
{
Id = "test-gate",
Type = PolicyGateTypes.CvssThreshold,
Remediation = null
};
var result = _resolver.Resolve(gate, "CVSS exceeded");
result.Should().NotBeNull();
result!.Code.Should().Be(RemediationCodes.CvssExceed);
}
[Fact]
public void Resolve_GateWithUnknownType_NoRemediation_ReturnsNull()
{
var gate = new PolicyGateDefinition
{
Id = "unknown-gate",
Type = "UnknownGateType",
Remediation = null
};
var result = _resolver.Resolve(gate, "some failure");
result.Should().BeNull();
}
[Fact]
public void Resolve_WithContext_ResolvesPlaceholders()
{
var gate = new PolicyGateDefinition
{
Id = "test-gate",
Type = PolicyGateTypes.SignatureRequired,
Remediation = null // will use default
};
var context = new RemediationContext
{
Image = "registry.example.com/app:v1.2.3",
Purl = "pkg:npm/express@4.18.0"
};
var result = _resolver.Resolve(gate, "signature missing", context);
result.Should().NotBeNull();
result!.Actions.Should().Contain(a =>
a.Command != null && a.Command.Contains("registry.example.com/app:v1.2.3"));
}
[Fact]
public void Resolve_WithContext_ResolvesAllPlaceholderTypes()
{
var hint = new RemediationHint
{
Code = "TEST",
Title = "Test",
Severity = RemediationSeverity.Medium,
Actions =
[
new RemediationAction
{
Type = RemediationActionTypes.Upgrade,
Description = "Test action",
Command = "stella fix --image {image} --purl {purl} --cve {cveId} --env {environment} --reason {reason}"
}
]
};
var gate = new PolicyGateDefinition
{
Id = "test-gate",
Type = "CustomGate",
Remediation = hint
};
var context = new RemediationContext
{
Image = "myimage:latest",
Purl = "pkg:npm/lodash@4.17.21",
CveId = "CVE-2021-23337",
Environment = "production",
Justification = "accepted risk"
};
var result = _resolver.Resolve(gate, "test", context);
result.Should().NotBeNull();
var command = result!.Actions[0].Command;
command.Should().Contain("myimage:latest");
command.Should().Contain("pkg:npm/lodash@4.17.21");
command.Should().Contain("CVE-2021-23337");
command.Should().Contain("production");
command.Should().Contain("accepted risk");
command.Should().NotContain("{");
}
[Fact]
public void Resolve_Rule_ReturnsRuleRemediation()
{
var rule = new PolicyRuleDefinition
{
Name = "require-dsse",
Action = PolicyActions.Block,
Remediation = new RemediationHint
{
Code = RemediationCodes.DsseMissing,
Title = "DSSE missing",
Severity = RemediationSeverity.Critical,
Actions = [new RemediationAction { Type = RemediationActionTypes.Sign, Description = "Sign it", Command = "stella attest attach --sign --image {image}" }]
}
};
var result = _resolver.Resolve(rule);
result.Should().NotBeNull();
result!.Code.Should().Be(RemediationCodes.DsseMissing);
}
[Fact]
public void Resolve_RuleWithoutRemediation_ReturnsNull()
{
var rule = new PolicyRuleDefinition
{
Name = "some-rule",
Action = PolicyActions.Warn,
Remediation = null
};
var result = _resolver.Resolve(rule);
result.Should().BeNull();
}
[Fact]
public void Resolve_WithNullContext_ReturnsUnresolvedTemplates()
{
var gate = new PolicyGateDefinition
{
Id = "test-gate",
Type = PolicyGateTypes.SignatureRequired,
Remediation = null
};
var result = _resolver.Resolve(gate, "test", context: null);
result.Should().NotBeNull();
// Templates should remain unresolved
result!.Actions.Should().Contain(a =>
a.Command != null && a.Command.Contains("{image}"));
}
[Fact]
public void AllDefaultHints_HaveValidSeverity()
{
var validSeverities = new[] { RemediationSeverity.Critical, RemediationSeverity.High, RemediationSeverity.Medium, RemediationSeverity.Low };
var gateTypes = new[]
{
PolicyGateTypes.CvssThreshold,
PolicyGateTypes.SignatureRequired,
PolicyGateTypes.EvidenceFreshness,
PolicyGateTypes.SbomPresence,
PolicyGateTypes.MinimumConfidence,
PolicyGateTypes.UnknownsBudget,
PolicyGateTypes.ReachabilityRequirement
};
foreach (var gateType in gateTypes)
{
var hint = _resolver.GetDefaultForGateType(gateType);
hint.Should().NotBeNull(because: $"gate type '{gateType}' should have a default hint");
hint!.Severity.Should().BeOneOf(validSeverities,
because: $"gate type '{gateType}' severity must be valid");
}
}
[Fact]
public void AllDefaultHints_HaveAtLeastOneAction()
{
var gateTypes = new[]
{
PolicyGateTypes.CvssThreshold,
PolicyGateTypes.SignatureRequired,
PolicyGateTypes.EvidenceFreshness,
PolicyGateTypes.SbomPresence,
PolicyGateTypes.MinimumConfidence,
PolicyGateTypes.UnknownsBudget,
PolicyGateTypes.ReachabilityRequirement
};
foreach (var gateType in gateTypes)
{
var hint = _resolver.GetDefaultForGateType(gateType);
hint!.Actions.Should().NotBeEmpty(
because: $"gate type '{gateType}' must have actionable remediation");
}
}
[Fact]
public void RemediationContext_ResolveTemplate_WithAdditionalValues()
{
var context = new RemediationContext
{
AdditionalValues = new Dictionary<string, string>
{
["scanId"] = "scan-12345",
["component"] = "billing-service"
}
};
var result = context.ResolveTemplate("stella scan --id {scanId} --component {component}");
result.Should().Be("stella scan --id scan-12345 --component billing-service");
}
}

View File

@@ -0,0 +1,99 @@
using System.Text.Json;
using FluentAssertions;
using StellaOps.Policy.Interop.Contracts;
using StellaOps.Policy.Interop.Export;
using Xunit;
namespace StellaOps.Policy.Interop.Tests.Export;
public class JsonPolicyExporterTests
{
private readonly JsonPolicyExporter _exporter = new();
private static PolicyPackDocument LoadGoldenFixture()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
var json = File.ReadAllText(fixturePath);
return JsonSerializer.Deserialize<PolicyPackDocument>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
}
[Fact]
public async Task ExportToJson_ProducesValidDocument()
{
var doc = LoadGoldenFixture();
var request = new PolicyExportRequest { Format = PolicyFormats.Json };
var result = await _exporter.ExportToJsonAsync(doc, request);
result.Should().NotBeNull();
result.ApiVersion.Should().Be(PolicyPackDocument.ApiVersionV2);
result.Metadata.Digest.Should().StartWith("sha256:");
}
[Fact]
public async Task ExportToJson_IsDeterministic()
{
var doc = LoadGoldenFixture();
var request = new PolicyExportRequest { Format = PolicyFormats.Json };
var result1 = await _exporter.ExportToJsonAsync(doc, request);
var result2 = await _exporter.ExportToJsonAsync(doc, request);
result1.Metadata.Digest.Should().Be(result2.Metadata.Digest);
}
[Fact]
public async Task ExportToJson_WithEnvironment_MergesConfig()
{
var doc = LoadGoldenFixture();
var request = new PolicyExportRequest { Format = PolicyFormats.Json, Environment = "staging" };
var result = await _exporter.ExportToJsonAsync(doc, request);
// Environment-specific config should be merged into base config
var cvssGate = result.Spec.Gates.First(g => g.Id == "cvss-threshold");
cvssGate.Environments.Should().BeNull(because: "environments are merged for single-env export");
}
[Fact]
public async Task ExportToJson_WithoutRemediation_StripsHints()
{
var doc = LoadGoldenFixture();
var request = new PolicyExportRequest { Format = PolicyFormats.Json, IncludeRemediation = false };
var result = await _exporter.ExportToJsonAsync(doc, request);
result.Spec.Gates.Should().AllSatisfy(g => g.Remediation.Should().BeNull());
result.Spec.Rules.Should().AllSatisfy(r => r.Remediation.Should().BeNull());
}
[Fact]
public void SerializeCanonical_ProducesDeterministicOutput()
{
var doc = LoadGoldenFixture();
var bytes1 = JsonPolicyExporter.SerializeCanonical(doc);
var bytes2 = JsonPolicyExporter.SerializeCanonical(doc);
bytes1.Should().BeEquivalentTo(bytes2);
}
[Fact]
public async Task RoundTrip_ExportImport_ProducesEquivalent()
{
var doc = LoadGoldenFixture();
var request = new PolicyExportRequest { Format = PolicyFormats.Json };
var exported = await _exporter.ExportToJsonAsync(doc, request);
var json = JsonPolicyExporter.SerializeToString(exported);
// Re-import
var reimported = JsonSerializer.Deserialize<PolicyPackDocument>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
reimported.Should().NotBeNull();
reimported!.Spec.Gates.Should().HaveCount(doc.Spec.Gates.Count);
reimported.Spec.Rules.Should().HaveCount(doc.Spec.Rules.Count);
}
}

View File

@@ -0,0 +1,251 @@
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": {
"name": "production-baseline",
"version": "1.0.0",
"description": "Production release gate policy with evidence-based verification.",
"createdAt": "2026-01-23T00:00:00Z",
"exportedFrom": {
"engine": "stella-policy-engine",
"engineVersion": "10.0.0",
"exportedAt": "2026-01-23T00:00:00Z"
}
},
"spec": {
"settings": {
"defaultAction": "block",
"unknownsThreshold": 0.6,
"stopOnFirstFailure": true,
"deterministicMode": true
},
"gates": [
{
"id": "cvss-threshold",
"type": "CvssThresholdGate",
"enabled": true,
"config": {
"threshold": 7.0,
"cvssVersion": "highest",
"failOnMissing": false
},
"environments": {
"production": { "threshold": 7.0 },
"staging": { "threshold": 8.0 },
"development": { "threshold": 9.0 }
},
"remediation": {
"code": "CVSS_EXCEED",
"title": "CVSS score exceeds threshold",
"description": "One or more vulnerabilities exceed the configured CVSS severity threshold for this environment.",
"actions": [
{
"type": "upgrade",
"description": "Upgrade the affected package to a patched version.",
"command": "stella advisory patch --purl {purl}"
},
{
"type": "vex",
"description": "Provide a VEX not_affected statement if the vulnerability is unreachable.",
"command": "stella vex emit --status not_affected --purl {purl} --justification {reason}"
},
{
"type": "override",
"description": "Request a policy override with documented justification.",
"command": "stella gate evaluate --allow-override --justification '{reason}'"
}
],
"references": [
{ "title": "CVSS v3.1 Specification", "url": "https://www.first.org/cvss/v3.1/specification-document" }
],
"severity": "high"
}
},
{
"id": "signature-required",
"type": "SignatureRequiredGate",
"enabled": true,
"config": {
"requireDsse": true,
"requireRekor": true,
"acceptedAlgorithms": ["ES256", "RS256", "EdDSA"]
},
"remediation": {
"code": "SIG_MISS",
"title": "Required signature missing",
"description": "The artifact is missing a required DSSE signature or Rekor transparency log entry.",
"actions": [
{
"type": "sign",
"description": "Sign the attestation with DSSE and attach to the artifact.",
"command": "stella attest attach --sign --image {image}"
},
{
"type": "anchor",
"description": "Anchor the attestation in the Rekor transparency log.",
"command": "stella attest attach --rekor --image {image}"
}
],
"severity": "critical"
}
},
{
"id": "evidence-freshness",
"type": "EvidenceFreshnessGate",
"enabled": true,
"config": {
"maxAgeHours": 24,
"requireTst": false
},
"environments": {
"production": { "maxAgeHours": 24, "requireTst": true },
"staging": { "maxAgeHours": 72 }
},
"remediation": {
"code": "FRESH_EXPIRED",
"title": "Evidence freshness expired",
"description": "The attestation evidence exceeds the maximum age threshold for this environment.",
"actions": [
{
"type": "generate",
"description": "Re-generate attestation with current timestamp.",
"command": "stella attest build --image {image}"
},
{
"type": "sign",
"description": "Request an RFC-3161 timestamp for freshness proof.",
"command": "stella attest attach --tst --image {image}"
}
],
"references": [
{ "title": "RFC 3161 - TSA Protocol", "url": "https://datatracker.ietf.org/doc/html/rfc3161" }
],
"severity": "high"
}
},
{
"id": "sbom-presence",
"type": "SbomPresenceGate",
"enabled": true,
"config": {
"requireCanonicalDigest": true,
"acceptedFormats": ["cyclonedx-1.5", "cyclonedx-1.6", "spdx-2.3"]
},
"remediation": {
"code": "SBOM_MISS",
"title": "SBOM missing or invalid",
"description": "A canonical SBOM with verified digest is required for release verification.",
"actions": [
{
"type": "generate",
"description": "Generate an SBOM and include its digest in the attestation.",
"command": "stella sbom generate --format cyclonedx --output sbom.cdx.json"
}
],
"severity": "high"
}
},
{
"id": "minimum-confidence",
"type": "MinimumConfidenceGate",
"enabled": true,
"config": {
"threshold": 0.75
},
"environments": {
"production": { "threshold": 0.75 },
"staging": { "threshold": 0.60 },
"development": { "threshold": 0.40 }
},
"remediation": {
"code": "CONF_LOW",
"title": "Confidence score below threshold",
"description": "The reachability confidence score is below the minimum required for this environment.",
"actions": [
{
"type": "investigate",
"description": "Provide additional reachability evidence to increase confidence.",
"command": "stella scan reachability --purl {purl} --deep"
}
],
"severity": "medium"
}
}
],
"rules": [
{
"name": "require-dsse-signature",
"action": "block",
"priority": 10,
"match": { "dsse.verified": false },
"remediation": {
"code": "DSSE_MISS",
"title": "DSSE signature missing or invalid",
"actions": [
{
"type": "sign",
"description": "Sign attestation with DSSE.",
"command": "stella attest attach --sign --image {image}"
}
],
"severity": "critical"
}
},
{
"name": "require-rekor-proof",
"action": "block",
"priority": 20,
"match": { "rekor.verified": false },
"remediation": {
"code": "REKOR_MISS",
"title": "Rekor v2 inclusion proof missing or invalid",
"actions": [
{
"type": "anchor",
"description": "Anchor attestation in Rekor transparency log.",
"command": "stella attest attach --rekor --image {image}"
}
],
"severity": "critical"
}
},
{
"name": "require-sbom-digest",
"action": "block",
"priority": 30,
"match": { "sbom.canonicalDigest": null },
"remediation": {
"code": "SBOM_MISS",
"title": "Canonical SBOM digest missing",
"actions": [
{
"type": "generate",
"description": "Generate SBOM and include canonical digest in attestation.",
"command": "stella sbom generate --format cyclonedx --output sbom.cdx.json"
}
],
"severity": "high"
}
},
{
"name": "require-freshness-tst",
"action": "warn",
"priority": 40,
"match": { "freshness.tstVerified": false },
"remediation": {
"code": "TST_MISS",
"title": "RFC-3161 timestamp missing",
"description": "Timestamp verification is recommended for freshness assurance.",
"actions": [
{
"type": "sign",
"description": "Request a TSA timestamp.",
"command": "stella attest attach --tst --image {image}"
}
],
"severity": "medium"
}
}
]
}
}

View File

@@ -0,0 +1,122 @@
package stella.release
import rego.v1
default allow := false
# Gate: cvss-threshold (CvssThresholdGate)
deny contains msg if {
input.cvss.score >= 7.0
msg := "CVSS score exceeds threshold"
}
# Gate: signature-required (SignatureRequiredGate)
deny contains msg if {
not input.dsse.verified
msg := "Required signature missing"
}
deny contains msg if {
not input.rekor.verified
msg := "Required signature missing"
}
# Gate: evidence-freshness (EvidenceFreshnessGate)
deny contains msg if {
input.freshness.maxAgeHours <= 24
not input.freshness.tstVerified
msg := "Evidence freshness expired"
}
# Gate: sbom-presence (SbomPresenceGate)
deny contains msg if {
not input.sbom.canonicalDigest
msg := "SBOM missing or invalid"
}
# Gate: minimum-confidence (MinimumConfidenceGate)
deny contains msg if {
input.confidence < 0.75
msg := "Confidence score below threshold"
}
# Rule: require-dsse-signature
deny contains msg if {
not input.dsse.verified
msg := "DSSE signature missing or invalid"
}
# Rule: require-rekor-proof
deny contains msg if {
not input.rekor.verified
msg := "Rekor v2 inclusion proof missing or invalid"
}
# Rule: require-sbom-digest
deny contains msg if {
not input.sbom.canonicalDigest
msg := "Canonical SBOM digest missing"
}
# Rule: require-freshness-tst
deny contains msg if {
not input.freshness.tstVerified
msg := "RFC-3161 timestamp missing"
}
allow if { count(deny) == 0 }
# Remediation hints (structured output)
remediation contains hint if {
some msg in deny
msg == "CVSS score exceeds threshold"
hint := {"code": "CVSS_EXCEED", "fix": "Run: stella advisory patch --purl {purl}", "severity": "high"}
}
remediation contains hint if {
some msg in deny
msg == "Required signature missing"
hint := {"code": "SIG_MISS", "fix": "Run: stella attest attach --sign --image {image}", "severity": "critical"}
}
remediation contains hint if {
some msg in deny
msg == "Evidence freshness expired"
hint := {"code": "FRESH_EXPIRED", "fix": "Run: stella attest build --image {image}", "severity": "high"}
}
remediation contains hint if {
some msg in deny
msg == "SBOM missing or invalid"
hint := {"code": "SBOM_MISS", "fix": "Run: stella sbom generate --format cyclonedx --output sbom.cdx.json", "severity": "high"}
}
remediation contains hint if {
some msg in deny
msg == "Confidence score below threshold"
hint := {"code": "CONF_LOW", "fix": "Run: stella scan reachability --purl {purl} --deep", "severity": "medium"}
}
remediation contains hint if {
some msg in deny
msg == "DSSE signature missing or invalid"
hint := {"code": "DSSE_MISS", "fix": "Run: stella attest attach --sign --image {image}", "severity": "critical"}
}
remediation contains hint if {
some msg in deny
msg == "Rekor v2 inclusion proof missing or invalid"
hint := {"code": "REKOR_MISS", "fix": "Run: stella attest attach --rekor --image {image}", "severity": "critical"}
}
remediation contains hint if {
some msg in deny
msg == "Canonical SBOM digest missing"
hint := {"code": "SBOM_MISS", "fix": "Run: stella sbom generate --format cyclonedx --output sbom.cdx.json", "severity": "high"}
}
remediation contains hint if {
some msg in deny
msg == "RFC-3161 timestamp missing"
hint := {"code": "TST_MISS", "fix": "Run: stella attest attach --tst --image {image}", "severity": "medium"}
}

View File

@@ -0,0 +1,110 @@
using FluentAssertions;
using StellaOps.Policy.Interop.Contracts;
using StellaOps.Policy.Interop.Import;
using Xunit;
namespace StellaOps.Policy.Interop.Tests.Import;
public class FormatDetectorTests
{
[Fact]
public void Detect_JsonWithApiVersion_ReturnsJson()
{
var content = """{ "apiVersion": "policy.stellaops.io/v2", "kind": "PolicyPack" }""";
FormatDetector.Detect(content).Should().Be(PolicyFormats.Json);
}
[Fact]
public void Detect_JsonWithKind_ReturnsJson()
{
var content = """{ "kind": "PolicyPack", "metadata": {} }""";
FormatDetector.Detect(content).Should().Be(PolicyFormats.Json);
}
[Fact]
public void Detect_GenericJson_ReturnsJson()
{
var content = """{ "foo": "bar" }""";
FormatDetector.Detect(content).Should().Be(PolicyFormats.Json);
}
[Fact]
public void Detect_RegoWithPackage_ReturnsRego()
{
var content = "package stella.release\n\ndefault allow := false\n";
FormatDetector.Detect(content).Should().Be(PolicyFormats.Rego);
}
[Fact]
public void Detect_RegoWithComment_ThenPackage_ReturnsRego()
{
var content = "# Policy file\npackage stella.release\n\ndefault allow := false\n";
FormatDetector.Detect(content).Should().Be(PolicyFormats.Rego);
}
[Fact]
public void Detect_RegoWithDenyContains_ReturnsRego()
{
var content = "deny contains msg if {\n not input.dsse.verified\n}\n";
FormatDetector.Detect(content).Should().Be(PolicyFormats.Rego);
}
[Fact]
public void Detect_EmptyContent_ReturnsNull()
{
FormatDetector.Detect("").Should().BeNull();
FormatDetector.Detect(" ").Should().BeNull();
}
[Fact]
public void Detect_UnrecognizableContent_ReturnsNull()
{
FormatDetector.Detect("hello world").Should().BeNull();
}
[Fact]
public void DetectFromExtension_JsonFile_ReturnsJson()
{
FormatDetector.DetectFromExtension("policy.json").Should().Be(PolicyFormats.Json);
FormatDetector.DetectFromExtension("/path/to/my-policy.json").Should().Be(PolicyFormats.Json);
}
[Fact]
public void DetectFromExtension_RegoFile_ReturnsRego()
{
FormatDetector.DetectFromExtension("policy.rego").Should().Be(PolicyFormats.Rego);
FormatDetector.DetectFromExtension("/path/to/release.rego").Should().Be(PolicyFormats.Rego);
}
[Fact]
public void DetectFromExtension_UnknownExtension_ReturnsNull()
{
FormatDetector.DetectFromExtension("policy.yaml").Should().BeNull();
FormatDetector.DetectFromExtension("policy.txt").Should().BeNull();
}
[Fact]
public void Detect_WithFilePath_ExtensionTakesPriority()
{
// Content looks like Rego but extension is .json
var content = "package stella.release\ndefault allow := false\n";
FormatDetector.Detect("policy.json", content).Should().Be(PolicyFormats.Json);
}
[Fact]
public void Detect_WithFilePath_FallsBackToContent()
{
var content = """{ "apiVersion": "policy.stellaops.io/v2" }""";
FormatDetector.Detect("policy.unknown", content).Should().Be(PolicyFormats.Json);
}
[Theory]
[InlineData(" { \"apiVersion\": \"policy.stellaops.io/v2\" }", PolicyFormats.Json)]
[InlineData("\n\n{ \"kind\": \"PolicyPack\" }", PolicyFormats.Json)]
[InlineData(" package stella.release\n", PolicyFormats.Rego)]
[InlineData("\n# comment\npackage foo\n", PolicyFormats.Rego)]
public void Detect_WithLeadingWhitespace_DetectsCorrectly(string content, string expected)
{
FormatDetector.Detect(content).Should().Be(expected);
}
}

View File

@@ -0,0 +1,166 @@
using FluentAssertions;
using StellaOps.Policy.Interop.Contracts;
using StellaOps.Policy.Interop.Import;
using Xunit;
namespace StellaOps.Policy.Interop.Tests.Import;
public class JsonPolicyImporterTests
{
private readonly JsonPolicyImporter _importer = new();
[Fact]
public async Task Import_GoldenFixture_Succeeds()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
var content = await File.ReadAllTextAsync(fixturePath);
var result = await _importer.ImportFromStringAsync(content, new PolicyImportOptions());
result.Success.Should().BeTrue();
result.Document.Should().NotBeNull();
result.DetectedFormat.Should().Be(PolicyFormats.Json);
result.GateCount.Should().Be(5);
result.RuleCount.Should().Be(4);
result.Diagnostics.Should().NotContain(d => d.Severity == PolicyDiagnostic.Severities.Error);
}
[Fact]
public async Task Import_InvalidJson_ReturnsParseError()
{
var result = await _importer.ImportFromStringAsync("{ invalid json }", new PolicyImportOptions());
result.Success.Should().BeFalse();
result.Diagnostics.Should().Contain(d => d.Code == "JSON_PARSE_ERROR");
}
[Fact]
public async Task Import_UnknownApiVersion_ReturnsError()
{
var json = """
{
"apiVersion": "policy.stellaops.io/v99",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": { "settings": { "defaultAction": "block" }, "gates": [], "rules": [] }
}
""";
var result = await _importer.ImportFromStringAsync(json, new PolicyImportOptions());
result.Success.Should().BeFalse();
result.Diagnostics.Should().Contain(d => d.Code == "VERSION_UNKNOWN");
}
[Fact]
public async Task Import_V1ApiVersion_ReturnsWarning()
{
var json = """
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": { "settings": { "defaultAction": "block" }, "gates": [], "rules": [] }
}
""";
var result = await _importer.ImportFromStringAsync(json, new PolicyImportOptions());
result.Success.Should().BeTrue();
result.Diagnostics.Should().Contain(d => d.Code == "VERSION_V1");
}
[Fact]
public async Task Import_DuplicateGateIds_ReturnsError()
{
var json = """
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": {
"settings": { "defaultAction": "block" },
"gates": [
{ "id": "dup-gate", "type": "SomeGate" },
{ "id": "dup-gate", "type": "AnotherGate" }
],
"rules": []
}
}
""";
var result = await _importer.ImportFromStringAsync(json, new PolicyImportOptions());
result.Success.Should().BeFalse();
result.Diagnostics.Should().Contain(d => d.Code == "GATE_ID_DUPLICATE");
}
[Fact]
public async Task Import_DuplicateRuleNames_ReturnsError()
{
var json = """
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": {
"settings": { "defaultAction": "block" },
"gates": [],
"rules": [
{ "name": "dup-rule", "action": "block" },
{ "name": "dup-rule", "action": "warn" }
]
}
}
""";
var result = await _importer.ImportFromStringAsync(json, new PolicyImportOptions());
result.Success.Should().BeFalse();
result.Diagnostics.Should().Contain(d => d.Code == "RULE_NAME_DUPLICATE");
}
[Fact]
public async Task Import_EmptyContent_ReturnsError()
{
var result = await _importer.ImportFromStringAsync("", new PolicyImportOptions());
result.Success.Should().BeFalse();
}
[Fact]
public async Task Import_RegoContent_ReturnsRegoError()
{
var rego = "package stella.release\ndefault allow := false\n";
var result = await _importer.ImportFromStringAsync(rego, new PolicyImportOptions());
result.Success.Should().BeFalse();
result.DetectedFormat.Should().Be(PolicyFormats.Rego);
result.Diagnostics.Should().Contain(d => d.Code == "REGO_USE_IMPORTER");
}
[Fact]
public async Task Import_ValidateOnly_DoesNotPersist()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
var content = await File.ReadAllTextAsync(fixturePath);
var result = await _importer.ImportFromStringAsync(content,
new PolicyImportOptions { ValidateOnly = true });
result.Success.Should().BeTrue();
result.Document.Should().NotBeNull(); // Document returned even in validate-only
}
[Fact]
public async Task Import_Stream_WorksLikeString()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
await using var stream = File.OpenRead(fixturePath);
var result = await _importer.ImportAsync(stream, new PolicyImportOptions());
result.Success.Should().BeTrue();
result.GateCount.Should().Be(5);
}
}

View File

@@ -0,0 +1,285 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260122_041_Policy_interop_import_export_rego
// Task: TASK-05 - Rego Import & Embedded OPA Evaluator
using FluentAssertions;
using StellaOps.Policy.Interop.Contracts;
using StellaOps.Policy.Interop.Import;
using Xunit;
namespace StellaOps.Policy.Interop.Tests.Import;
public class RegoPolicyImporterTests
{
private readonly RegoPolicyImporter _importer = new();
private const string SampleRegoWithAllGates = """
package stella.release
import rego.v1
default allow := false
deny contains msg if {
input.cvss.score >= 7.0
msg := "CVSS score exceeds threshold"
}
deny contains msg if {
not input.dsse.verified
msg := "DSSE signature missing"
}
deny contains msg if {
not input.rekor.verified
msg := "Rekor proof missing"
}
deny contains msg if {
not input.sbom.canonicalDigest
msg := "SBOM digest missing"
}
deny contains msg if {
input.confidence < 0.75
msg := "Confidence too low"
}
deny contains msg if {
not input.freshness.tstVerified
msg := "Evidence freshness expired"
}
deny contains msg if {
not input.reachability.status
msg := "Reachability proof required"
}
deny contains msg if {
input.unknownsRatio > 0.6
msg := "Unknowns budget exceeded"
}
allow if { count(deny) == 0 }
""";
[Fact]
public async Task Import_ValidRego_ReturnsSuccess()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Success.Should().BeTrue();
result.Document.Should().NotBeNull();
result.DetectedFormat.Should().Be(PolicyFormats.Rego);
}
[Fact]
public async Task Import_MapsCvssGateToNative()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Document!.Spec!.Gates.Should().Contain(g => g.Type == PolicyGateTypes.CvssThreshold);
var cvssGate = result.Document.Spec.Gates.First(g => g.Type == PolicyGateTypes.CvssThreshold);
cvssGate.Config.Should().ContainKey("threshold");
}
[Fact]
public async Task Import_MapsSignatureGateToNative()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Document!.Spec!.Gates.Should().Contain(g => g.Type == PolicyGateTypes.SignatureRequired);
}
[Fact]
public async Task Import_MapsSbomGateToNative()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Document!.Spec!.Gates.Should().Contain(g => g.Type == PolicyGateTypes.SbomPresence);
}
[Fact]
public async Task Import_MapsConfidenceGateToNative()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Document!.Spec!.Gates.Should().Contain(g => g.Type == PolicyGateTypes.MinimumConfidence);
var confGate = result.Document.Spec.Gates.First(g => g.Type == PolicyGateTypes.MinimumConfidence);
confGate.Config.Should().ContainKey("threshold");
}
[Fact]
public async Task Import_MapsFreshnessGateToNative()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Document!.Spec!.Gates.Should().Contain(g => g.Type == PolicyGateTypes.EvidenceFreshness);
}
[Fact]
public async Task Import_MapsReachabilityGateToNative()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Document!.Spec!.Gates.Should().Contain(g => g.Type == PolicyGateTypes.ReachabilityRequirement);
}
[Fact]
public async Task Import_MapsUnknownsGateToNative()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Document!.Spec!.Gates.Should().Contain(g => g.Type == PolicyGateTypes.UnknownsBudget);
}
[Fact]
public async Task Import_AllGatesMappedNatively()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Mapping.Should().NotBeNull();
result.Mapping!.NativeMapped.Should().NotBeEmpty();
result.Mapping.OpaEvaluated.Should().BeEmpty();
result.Diagnostics.Should().Contain(d => d.Code == "NATIVE_MAPPED");
}
[Fact]
public async Task Import_UnknownPattern_CreatesCustomRule()
{
var regoWithCustom = """
package stella.release
import rego.v1
default allow := false
deny contains msg if {
input.custom.field == "dangerous"
msg := "Custom check failed"
}
allow if { count(deny) == 0 }
""";
var result = await _importer.ImportFromStringAsync(regoWithCustom, new PolicyImportOptions());
result.Success.Should().BeTrue();
result.Document!.Spec!.Rules.Should().NotBeEmpty();
result.Mapping!.OpaEvaluated.Should().NotBeEmpty();
result.Diagnostics.Should().Contain(d => d.Code == "UNMAPPED_RULE");
}
[Fact]
public async Task Import_WithEnvironment_CapturesEnvironment()
{
var regoWithEnv = """
package stella.release
import rego.v1
default allow := false
deny contains msg if {
input.environment == "production"
input.cvss.score >= 7.0
msg := "CVSS exceeds production threshold"
}
allow if { count(deny) == 0 }
""";
var result = await _importer.ImportFromStringAsync(regoWithEnv, new PolicyImportOptions());
result.Success.Should().BeTrue();
var cvssGate = result.Document!.Spec!.Gates.First(g => g.Type == PolicyGateTypes.CvssThreshold);
cvssGate.Environments.Should().ContainKey("production");
}
[Fact]
public async Task Import_JsonContent_RejectsWithFormatMismatch()
{
var jsonContent = """{"apiVersion": "policy.stellaops.io/v2"}""";
var result = await _importer.ImportFromStringAsync(jsonContent, new PolicyImportOptions());
result.Success.Should().BeFalse();
result.Diagnostics.Should().Contain(d => d.Code == "FORMAT_MISMATCH");
}
[Fact]
public async Task Import_ExtractsPackageName()
{
var regoWithCustomPkg = """
package myorg.custom.policy
import rego.v1
default allow := false
deny contains msg if {
not input.dsse.verified
msg := "unsigned"
}
allow if { count(deny) == 0 }
""";
var result = await _importer.ImportFromStringAsync(regoWithCustomPkg, new PolicyImportOptions());
result.Success.Should().BeTrue();
result.Document!.Metadata!.Name.Should().Be("myorg-custom-policy");
result.Document.Metadata.Description.Should().Contain("myorg.custom.policy");
}
[Fact]
public async Task Import_WithRemediation_AttachesToGates()
{
var regoWithRemediation = """
package stella.release
import rego.v1
default allow := false
deny contains msg if {
not input.dsse.verified
msg := "DSSE signature missing"
}
allow if { count(deny) == 0 }
remediation contains hint if {
some msg in deny
msg == "DSSE signature missing"
hint := {"code": "SIG_MISS", "fix": "Run: stella attest attach --sign", "severity": "critical"}
}
""";
var result = await _importer.ImportFromStringAsync(regoWithRemediation, new PolicyImportOptions());
result.Success.Should().BeTrue();
var sigGate = result.Document!.Spec!.Gates.First(g => g.Type == PolicyGateTypes.SignatureRequired);
sigGate.Remediation.Should().NotBeNull();
sigGate.Remediation!.Code.Should().Be("SIG_MISS");
sigGate.Remediation.Severity.Should().Be("critical");
}
[Fact]
public async Task Import_SetsApiVersionAndKind()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Document!.ApiVersion.Should().Be(PolicyPackDocument.ApiVersionV2);
result.Document.Kind.Should().Be(PolicyPackDocument.KindPolicyPack);
}
[Fact]
public async Task Import_SetsDefaultActionToBlock()
{
var result = await _importer.ImportFromStringAsync(SampleRegoWithAllGates, new PolicyImportOptions());
result.Document!.Spec!.Settings!.DefaultAction.Should().Be(PolicyActions.Block);
}
[Fact]
public async Task Import_EmptyStream_ReturnsFailure()
{
using var stream = new MemoryStream(Array.Empty<byte>());
var result = await _importer.ImportAsync(stream, new PolicyImportOptions());
result.Success.Should().BeFalse();
result.Diagnostics.Should().Contain(d => d.Severity == "error");
}
}

View File

@@ -0,0 +1,272 @@
using System.Text.Json;
using FluentAssertions;
using StellaOps.Policy.Interop.Abstractions;
using StellaOps.Policy.Interop.Contracts;
using StellaOps.Policy.Interop.Rego;
using Xunit;
namespace StellaOps.Policy.Interop.Tests.Rego;
public class RegoCodeGeneratorTests
{
private readonly RegoCodeGenerator _generator = new();
private static PolicyPackDocument LoadGoldenFixture()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
var json = File.ReadAllText(fixturePath);
return JsonSerializer.Deserialize<PolicyPackDocument>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
}
[Fact]
public void Generate_ProducesValidRegoHeader()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.Success.Should().BeTrue();
result.RegoSource.Should().StartWith("package stella.release");
result.RegoSource.Should().Contain("import rego.v1");
result.RegoSource.Should().Contain("default allow := false");
result.PackageName.Should().Be("stella.release");
}
[Fact]
public void Generate_ContainsDenyRules()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.RegoSource.Should().Contain("deny contains msg if {");
}
[Fact]
public void Generate_ContainsAllowRule()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.RegoSource.Should().Contain("allow if { count(deny) == 0 }");
}
[Fact]
public void Generate_CvssGate_ProducesScoreComparison()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.RegoSource.Should().Contain("input.cvss.score >= 7.0");
}
[Fact]
public void Generate_SignatureGate_ProducesDsseAndRekorChecks()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.RegoSource.Should().Contain("not input.dsse.verified");
result.RegoSource.Should().Contain("not input.rekor.verified");
}
[Fact]
public void Generate_SbomGate_ProducesDigestCheck()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.RegoSource.Should().Contain("not input.sbom.canonicalDigest");
}
[Fact]
public void Generate_ConfidenceGate_ProducesThresholdCheck()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.RegoSource.Should().Contain("input.confidence < 0.75");
}
[Fact]
public void Generate_WithRemediation_ProducesRemediationRules()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions { IncludeRemediation = true });
result.RegoSource.Should().Contain("remediation contains hint if {");
result.RegoSource.Should().Contain("\"code\":");
result.RegoSource.Should().Contain("\"fix\":");
result.RegoSource.Should().Contain("\"severity\":");
}
[Fact]
public void Generate_WithoutRemediation_OmitsRemediationRules()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions { IncludeRemediation = false });
result.RegoSource.Should().NotContain("remediation contains hint if {");
}
[Fact]
public void Generate_WithEnvironment_UsesEnvironmentThresholds()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions { Environment = "staging" });
// Staging CVSS threshold is 8.0
result.RegoSource.Should().Contain("input.cvss.score >= 8.0");
result.RegoSource.Should().Contain("input.environment == \"staging\"");
}
[Fact]
public void Generate_CustomPackageName_UsesIt()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions { PackageName = "myorg.policy" });
result.RegoSource.Should().StartWith("package myorg.policy");
result.PackageName.Should().Be("myorg.policy");
}
[Fact]
public void Generate_WithComments_IncludesGateComments()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions { IncludeComments = true });
result.RegoSource.Should().Contain("# Gate: cvss-threshold (CvssThresholdGate)");
result.RegoSource.Should().Contain("# Rule: require-dsse-signature");
}
[Fact]
public void Generate_WithoutComments_OmitsComments()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions { IncludeComments = false });
result.RegoSource.Should().NotContain("# Gate:");
result.RegoSource.Should().NotContain("# Rule:");
}
[Fact]
public void Generate_ProducesDigest()
{
var doc = LoadGoldenFixture();
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.Digest.Should().NotBeNull();
result.Digest.Should().StartWith("sha256:");
}
[Fact]
public void Generate_IsDeterministic()
{
var doc = LoadGoldenFixture();
var options = new RegoGenerationOptions();
var result1 = _generator.Generate(doc, options);
var result2 = _generator.Generate(doc, options);
result1.Digest.Should().Be(result2.Digest);
result1.RegoSource.Should().Be(result2.RegoSource);
}
[Fact]
public void Generate_DisabledGate_IsSkipped()
{
var doc = new PolicyPackDocument
{
ApiVersion = PolicyPackDocument.ApiVersionV2,
Kind = PolicyPackDocument.KindPolicyPack,
Metadata = new PolicyPackMetadata { Name = "test", Version = "1.0.0" },
Spec = new PolicyPackSpec
{
Settings = new PolicyPackSettings { DefaultAction = PolicyActions.Block },
Gates =
[
new PolicyGateDefinition { Id = "disabled-gate", Type = PolicyGateTypes.CvssThreshold, Enabled = false }
]
}
};
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.RegoSource.Should().NotContain("input.cvss.score");
}
[Fact]
public void Generate_UnknownGateType_ProducesWarning()
{
var doc = new PolicyPackDocument
{
ApiVersion = PolicyPackDocument.ApiVersionV2,
Kind = PolicyPackDocument.KindPolicyPack,
Metadata = new PolicyPackMetadata { Name = "test", Version = "1.0.0" },
Spec = new PolicyPackSpec
{
Settings = new PolicyPackSettings { DefaultAction = PolicyActions.Block },
Gates =
[
new PolicyGateDefinition { Id = "unknown-gate", Type = "CustomUnknownGate", Enabled = true }
]
}
};
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.Warnings.Should().Contain(w => w.Contains("CustomUnknownGate"));
}
[Fact]
public void Generate_AllowRule_IsSkipped()
{
var doc = new PolicyPackDocument
{
ApiVersion = PolicyPackDocument.ApiVersionV2,
Kind = PolicyPackDocument.KindPolicyPack,
Metadata = new PolicyPackMetadata { Name = "test", Version = "1.0.0" },
Spec = new PolicyPackSpec
{
Settings = new PolicyPackSettings { DefaultAction = PolicyActions.Block },
Rules =
[
new PolicyRuleDefinition { Name = "allow-rule", Action = PolicyActions.Allow }
]
}
};
var result = _generator.Generate(doc, new RegoGenerationOptions());
// Allow rules don't generate deny rules
result.RegoSource.Should().NotContain("allow-rule");
}
[Fact]
public void Generate_RuleWithNullMatch_ProducesNotCheck()
{
var doc = new PolicyPackDocument
{
ApiVersion = PolicyPackDocument.ApiVersionV2,
Kind = PolicyPackDocument.KindPolicyPack,
Metadata = new PolicyPackMetadata { Name = "test", Version = "1.0.0" },
Spec = new PolicyPackSpec
{
Settings = new PolicyPackSettings { DefaultAction = PolicyActions.Block },
Rules =
[
new PolicyRuleDefinition
{
Name = "null-check",
Action = PolicyActions.Block,
Match = new Dictionary<string, object?> { ["sbom.canonicalDigest"] = null }
}
]
}
};
var result = _generator.Generate(doc, new RegoGenerationOptions());
result.RegoSource.Should().Contain("not input.sbom.canonicalDigest");
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="JsonSchema.Net" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Policy.Interop\StellaOps.Policy.Interop.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,234 @@
using System.Text.Json;
using FluentAssertions;
using Json.Schema;
using StellaOps.Policy.Interop.Contracts;
using Xunit;
namespace StellaOps.Policy.Interop.Tests.Validation;
/// <summary>
/// Validates the PolicyPack v2 JSON Schema against golden fixtures.
/// </summary>
public class PolicySchemaValidatorTests
{
private static readonly JsonSchema Schema = LoadSchema();
private static JsonSchema LoadSchema()
{
var schemaPath = Path.Combine(
AppContext.BaseDirectory, "..", "..", "..", "..", "..",
"__Libraries", "StellaOps.Policy.Interop", "Schemas", "policy-pack-v2.schema.json");
if (!File.Exists(schemaPath))
{
// Fallback: try embedded resource path
schemaPath = Path.Combine(AppContext.BaseDirectory, "Schemas", "policy-pack-v2.schema.json");
}
var schemaJson = File.ReadAllText(schemaPath);
return JsonSchema.FromText(schemaJson);
}
[Fact]
public void GoldenFixture_ShouldValidateAgainstSchema()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
var fixtureJson = File.ReadAllText(fixturePath);
using var document = JsonDocument.Parse(fixtureJson);
var result = Schema.Evaluate(document.RootElement, new EvaluationOptions
{
OutputFormat = OutputFormat.List
});
result.IsValid.Should().BeTrue(
because: "the golden fixture must validate against the PolicyPack v2 schema. " +
$"Errors: {FormatErrors(result)}");
}
[Fact]
public void GoldenFixture_ShouldDeserializeToDocument()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
var fixtureJson = File.ReadAllText(fixturePath);
var document = JsonSerializer.Deserialize<PolicyPackDocument>(fixtureJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
document.Should().NotBeNull();
document!.ApiVersion.Should().Be(PolicyPackDocument.ApiVersionV2);
document.Kind.Should().Be(PolicyPackDocument.KindPolicyPack);
document.Metadata.Name.Should().Be("production-baseline");
document.Metadata.Version.Should().Be("1.0.0");
document.Spec.Settings.DefaultAction.Should().Be(PolicyActions.Block);
document.Spec.Gates.Should().HaveCount(5);
document.Spec.Rules.Should().HaveCount(4);
}
[Fact]
public void GoldenFixture_AllGates_ShouldHaveRemediation()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
var fixtureJson = File.ReadAllText(fixturePath);
var document = JsonSerializer.Deserialize<PolicyPackDocument>(fixtureJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
foreach (var gate in document!.Spec.Gates)
{
gate.Remediation.Should().NotBeNull(
because: $"gate '{gate.Id}' must have a remediation hint defined");
gate.Remediation!.Code.Should().NotBeNullOrWhiteSpace();
gate.Remediation.Severity.Should().BeOneOf(
RemediationSeverity.Critical,
RemediationSeverity.High,
RemediationSeverity.Medium,
RemediationSeverity.Low);
gate.Remediation.Actions.Should().NotBeEmpty(
because: $"gate '{gate.Id}' remediation must have at least one action");
}
}
[Fact]
public void GoldenFixture_AllRules_ShouldHaveRemediation()
{
var fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-policy-pack-v2.json");
var fixtureJson = File.ReadAllText(fixturePath);
var document = JsonSerializer.Deserialize<PolicyPackDocument>(fixtureJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
foreach (var rule in document!.Spec.Rules)
{
rule.Remediation.Should().NotBeNull(
because: $"rule '{rule.Name}' must have a remediation hint defined");
rule.Remediation!.Code.Should().NotBeNullOrWhiteSpace();
rule.Remediation.Actions.Should().NotBeEmpty(
because: $"rule '{rule.Name}' remediation must have at least one action");
}
}
[Fact]
public void InvalidDocument_MissingApiVersion_ShouldFailValidation()
{
using var json = JsonDocument.Parse("""
{
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": { "settings": { "defaultAction": "block" } }
}
""");
var result = Schema.Evaluate(json.RootElement, new EvaluationOptions
{
OutputFormat = OutputFormat.List
});
result.IsValid.Should().BeFalse();
}
[Fact]
public void InvalidDocument_WrongApiVersion_ShouldFailValidation()
{
using var json = JsonDocument.Parse("""
{
"apiVersion": "policy.stellaops.io/v1",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": { "settings": { "defaultAction": "block" } }
}
""");
var result = Schema.Evaluate(json.RootElement, new EvaluationOptions
{
OutputFormat = OutputFormat.List
});
result.IsValid.Should().BeFalse();
}
[Fact]
public void InvalidDocument_BadGateId_ShouldFailValidation()
{
using var json = JsonDocument.Parse("""
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": {
"settings": { "defaultAction": "block" },
"gates": [{ "id": "INVALID_ID!", "type": "SomeGate" }]
}
}
""");
var result = Schema.Evaluate(json.RootElement, new EvaluationOptions
{
OutputFormat = OutputFormat.List
});
result.IsValid.Should().BeFalse();
}
[Fact]
public void InvalidDocument_BadRemediationCode_ShouldFailValidation()
{
using var json = JsonDocument.Parse("""
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": { "name": "test", "version": "1.0.0" },
"spec": {
"settings": { "defaultAction": "block" },
"gates": [{
"id": "test-gate",
"type": "SomeGate",
"remediation": {
"code": "invalid-lowercase",
"title": "Test",
"severity": "high"
}
}]
}
}
""");
var result = Schema.Evaluate(json.RootElement, new EvaluationOptions
{
OutputFormat = OutputFormat.List
});
result.IsValid.Should().BeFalse();
}
[Fact]
public void ValidMinimalDocument_ShouldPassValidation()
{
using var json = JsonDocument.Parse("""
{
"apiVersion": "policy.stellaops.io/v2",
"kind": "PolicyPack",
"metadata": { "name": "minimal", "version": "1.0.0" },
"spec": { "settings": { "defaultAction": "allow" } }
}
""");
var result = Schema.Evaluate(json.RootElement, new EvaluationOptions
{
OutputFormat = OutputFormat.List
});
result.IsValid.Should().BeTrue(
because: $"a minimal valid document should pass. Errors: {FormatErrors(result)}");
}
private static string FormatErrors(EvaluationResults result)
{
if (result.IsValid) return "none";
var details = result.Details?
.Where(d => !d.IsValid && d.Errors != null)
.SelectMany(d => d.Errors!.Select(e => $"{d.InstanceLocation}: {e.Value}"))
.ToList();
return details != null ? string.Join("; ", details) : "unknown";
}
}