old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions

This commit is contained in:
master
2026-01-15 18:37:59 +02:00
parent c631bacee2
commit 88a85cdd92
208 changed files with 32271 additions and 2287 deletions

View File

@@ -0,0 +1,347 @@
// -----------------------------------------------------------------------------
// CvssThresholdGateTests.cs
// Sprint: SPRINT_20260112_017_POLICY_cvss_threshold_gate
// Tasks: CVSS-GATE-008, CVSS-GATE-009
// Description: Unit tests for CVSS threshold gate.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Gates;
using StellaOps.Policy.TrustLattice;
using Xunit;
namespace StellaOps.Policy.Tests.Gates;
[Trait("Category", "Unit")]
public sealed class CvssThresholdGateTests
{
private static MergeResult CreateMergeResult() => new()
{
Status = VexStatus.Affected,
Confidence = 0.8,
HasConflicts = false,
AllClaims = ImmutableArray<ScoredClaim>.Empty,
WinningClaim = new ScoredClaim
{
SourceId = "test",
Status = VexStatus.Affected,
OriginalScore = 0.8,
AdjustedScore = 0.8,
ScopeSpecificity = 1,
Accepted = true,
Reason = "test"
},
Conflicts = ImmutableArray<ConflictRecord>.Empty
};
private static PolicyGateContext CreateContext(
string environment = "production",
string? cveId = null,
Dictionary<string, string>? metadata = null) => new()
{
Environment = environment,
CveId = cveId,
Metadata = metadata
};
[Fact]
public async Task EvaluateAsync_Disabled_ReturnsPass()
{
var options = new CvssThresholdGateOptions { Enabled = false };
var gate = new CvssThresholdGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
Assert.Equal("disabled", result.Reason);
}
[Fact]
public async Task EvaluateAsync_CveOnDenylist_ReturnsFail()
{
var options = new CvssThresholdGateOptions
{
Denylist = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "CVE-2024-12345" }
};
var gate = new CvssThresholdGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-12345"));
Assert.False(result.Passed);
Assert.Equal("denylist", result.Reason);
}
[Fact]
public async Task EvaluateAsync_CveOnAllowlist_ReturnsPass()
{
var options = new CvssThresholdGateOptions
{
Allowlist = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "CVE-2024-99999" }
};
var gate = new CvssThresholdGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-99999"));
Assert.True(result.Passed);
Assert.Equal("allowlist", result.Reason);
}
[Fact]
public async Task EvaluateAsync_DenylistTakesPrecedenceOverAllowlist()
{
var options = new CvssThresholdGateOptions
{
Allowlist = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "CVE-2024-12345" },
Denylist = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "CVE-2024-12345" }
};
var gate = new CvssThresholdGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-12345"));
Assert.False(result.Passed);
Assert.Equal("denylist", result.Reason);
}
[Fact]
public async Task EvaluateAsync_NoCvssScore_FailOnMissingFalse_ReturnsPass()
{
var options = new CvssThresholdGateOptions { FailOnMissingCvss = false };
var gate = new CvssThresholdGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-00001"));
Assert.True(result.Passed);
Assert.Equal("no_cvss_available", result.Reason);
}
[Fact]
public async Task EvaluateAsync_NoCvssScore_FailOnMissingTrue_ReturnsFail()
{
var options = new CvssThresholdGateOptions { FailOnMissingCvss = true };
var gate = new CvssThresholdGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-00001"));
Assert.False(result.Passed);
Assert.Equal("missing_cvss", result.Reason);
}
[Theory]
[InlineData(6.9, true)] // Below threshold
[InlineData(7.0, false)] // At threshold (fails - must be strictly below)
[InlineData(7.1, false)] // Above threshold
[InlineData(9.9, false)] // Well above threshold
public async Task EvaluateAsync_V31Score_DefaultThreshold_ReturnsExpected(double score, bool expectedPass)
{
var options = new CvssThresholdGateOptions
{
DefaultThreshold = 7.0,
CvssVersionPreference = "v3.1"
};
var lookup = (string? _) => new CvssScoreInfo { CvssV31BaseScore = score };
var gate = new CvssThresholdGate(options, lookup);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-00001"));
Assert.Equal(expectedPass, result.Passed);
}
[Theory]
[InlineData(7.9, true)] // Below staging threshold
[InlineData(8.0, false)] // At staging threshold
[InlineData(8.5, false)] // Above staging threshold
public async Task EvaluateAsync_StagingEnvironment_UsesStagingThreshold(double score, bool expectedPass)
{
var options = new CvssThresholdGateOptions
{
Thresholds = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
{
["production"] = 7.0,
["staging"] = 8.0,
["development"] = 9.0
},
CvssVersionPreference = "v3.1"
};
var lookup = (string? _) => new CvssScoreInfo { CvssV31BaseScore = score };
var gate = new CvssThresholdGate(options, lookup);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "staging", cveId: "CVE-2024-00001"));
Assert.Equal(expectedPass, result.Passed);
}
[Theory]
[InlineData(8.9, true)] // Below development threshold
[InlineData(9.0, false)] // At development threshold
[InlineData(9.5, false)] // Above development threshold
public async Task EvaluateAsync_DevelopmentEnvironment_UsesDevelopmentThreshold(double score, bool expectedPass)
{
var options = new CvssThresholdGateOptions
{
Thresholds = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
{
["production"] = 7.0,
["staging"] = 8.0,
["development"] = 9.0
},
CvssVersionPreference = "v3.1"
};
var lookup = (string? _) => new CvssScoreInfo { CvssV31BaseScore = score };
var gate = new CvssThresholdGate(options, lookup);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "development", cveId: "CVE-2024-00001"));
Assert.Equal(expectedPass, result.Passed);
}
[Fact]
public async Task EvaluateAsync_UnknownEnvironment_UsesDefaultThreshold()
{
var options = new CvssThresholdGateOptions
{
DefaultThreshold = 5.0,
Thresholds = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
{
["production"] = 7.0
},
CvssVersionPreference = "v3.1"
};
var lookup = (string? _) => new CvssScoreInfo { CvssV31BaseScore = 5.5 };
var gate = new CvssThresholdGate(options, lookup);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "qa", cveId: "CVE-2024-00001"));
Assert.False(result.Passed);
Assert.Equal("cvss_exceeds_threshold", result.Reason);
}
[Fact]
public async Task EvaluateAsync_V40Score_UsesV40WhenPreferred()
{
var options = new CvssThresholdGateOptions
{
DefaultThreshold = 7.0,
CvssVersionPreference = "v4.0"
};
var lookup = (string? _) => new CvssScoreInfo
{
CvssV31BaseScore = 8.0, // Would fail
CvssV40BaseScore = 6.0 // Would pass
};
var gate = new CvssThresholdGate(options, lookup);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-00001"));
Assert.True(result.Passed);
Assert.Equal("v4.0", result.Details["cvss_version"]);
}
[Fact]
public async Task EvaluateAsync_HighestPreference_UsesHigherScore()
{
var options = new CvssThresholdGateOptions
{
DefaultThreshold = 7.5,
CvssVersionPreference = "highest"
};
var lookup = (string? _) => new CvssScoreInfo
{
CvssV31BaseScore = 7.0, // Would pass alone
CvssV40BaseScore = 8.0 // Would fail, and is higher
};
var gate = new CvssThresholdGate(options, lookup);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-00001"));
Assert.False(result.Passed);
Assert.Equal(8.0, (double)result.Details["cvss_score"]);
}
[Fact]
public async Task EvaluateAsync_RequireAllVersionsPass_BothMustPass()
{
var options = new CvssThresholdGateOptions
{
DefaultThreshold = 7.5,
CvssVersionPreference = "highest",
RequireAllVersionsPass = true
};
var lookup = (string? _) => new CvssScoreInfo
{
CvssV31BaseScore = 7.0, // Would pass
CvssV40BaseScore = 8.0 // Would fail
};
var gate = new CvssThresholdGate(options, lookup);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-00001"));
Assert.False(result.Passed);
}
[Fact]
public async Task EvaluateAsync_RequireAllVersionsPass_BothPass()
{
var options = new CvssThresholdGateOptions
{
DefaultThreshold = 8.5,
CvssVersionPreference = "highest",
RequireAllVersionsPass = true
};
var lookup = (string? _) => new CvssScoreInfo
{
CvssV31BaseScore = 7.0,
CvssV40BaseScore = 8.0
};
var gate = new CvssThresholdGate(options, lookup);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-00001"));
Assert.True(result.Passed);
}
[Fact]
public async Task EvaluateAsync_MetadataFallback_ExtractsFromContext()
{
var options = new CvssThresholdGateOptions
{
DefaultThreshold = 7.0,
CvssVersionPreference = "v3.1"
};
var metadata = new Dictionary<string, string>
{
["cvss_v31_score"] = "6.5"
};
var gate = new CvssThresholdGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-00001", metadata: metadata));
Assert.True(result.Passed);
Assert.Equal(6.5, (double)result.Details["cvss_score"]);
}
[Fact]
public async Task EvaluateAsync_CaseInsensitiveCveMatch()
{
var options = new CvssThresholdGateOptions
{
Allowlist = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "cve-2024-12345" }
};
var gate = new CvssThresholdGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(cveId: "CVE-2024-12345"));
Assert.True(result.Passed);
Assert.Equal("allowlist", result.Reason);
}
[Fact]
public async Task EvaluateAsync_IncludesAllDetailsInResult()
{
var options = new CvssThresholdGateOptions
{
DefaultThreshold = 7.0,
CvssVersionPreference = "v3.1"
};
var lookup = (string? _) => new CvssScoreInfo
{
CvssV31BaseScore = 8.5,
CvssV40BaseScore = 7.2
};
var gate = new CvssThresholdGate(options, lookup);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "production", cveId: "CVE-2024-00001"));
Assert.False(result.Passed);
Assert.Equal(7.0, (double)result.Details["threshold"]);
Assert.Equal("production", result.Details["environment"]);
Assert.Equal("v3.1", result.Details["cvss_version"]);
Assert.Equal(8.5, (double)result.Details["cvss_score"]);
Assert.Equal(8.5, (double)result.Details["cvss_v31_score"]);
Assert.Equal(7.2, (double)result.Details["cvss_v40_score"]);
Assert.Equal("CVE-2024-00001", result.Details["cve_id"]);
}
}

View File

@@ -0,0 +1,384 @@
// -----------------------------------------------------------------------------
// SbomPresenceGateTests.cs
// Sprint: SPRINT_20260112_017_POLICY_sbom_presence_gate
// Tasks: SBOM-GATE-009
// Description: Unit tests for SBOM presence gate.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Gates;
using StellaOps.Policy.TrustLattice;
using Xunit;
namespace StellaOps.Policy.Tests.Gates;
[Trait("Category", "Unit")]
public sealed class SbomPresenceGateTests
{
private static MergeResult CreateMergeResult() => new()
{
Status = VexStatus.Affected,
Confidence = 0.8,
HasConflicts = false,
AllClaims = ImmutableArray<ScoredClaim>.Empty,
WinningClaim = new ScoredClaim
{
SourceId = "test",
Status = VexStatus.Affected,
OriginalScore = 0.8,
AdjustedScore = 0.8,
ScopeSpecificity = 1,
Accepted = true,
Reason = "test"
},
Conflicts = ImmutableArray<ConflictRecord>.Empty
};
private static PolicyGateContext CreateContext(
string environment = "production",
Dictionary<string, string>? metadata = null) => new()
{
Environment = environment,
Metadata = metadata
};
[Fact]
public async Task EvaluateAsync_Disabled_ReturnsPass()
{
var options = new SbomPresenceGateOptions { Enabled = false };
var gate = new SbomPresenceGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
Assert.Equal("disabled", result.Reason);
}
[Fact]
public async Task EvaluateAsync_OptionalEnforcement_ReturnsPass()
{
var options = new SbomPresenceGateOptions
{
Enforcement = new Dictionary<string, SbomEnforcementLevel>(StringComparer.OrdinalIgnoreCase)
{
["development"] = SbomEnforcementLevel.Optional
}
};
var gate = new SbomPresenceGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "development"));
Assert.True(result.Passed);
Assert.Equal("optional_enforcement", result.Reason);
}
[Fact]
public async Task EvaluateAsync_MissingSbom_RequiredEnforcement_ReturnsFail()
{
var options = new SbomPresenceGateOptions();
var gate = new SbomPresenceGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
Assert.Equal("sbom_missing", result.Reason);
}
[Fact]
public async Task EvaluateAsync_MissingSbom_RecommendedEnforcement_ReturnsPassWithWarning()
{
var options = new SbomPresenceGateOptions
{
Enforcement = new Dictionary<string, SbomEnforcementLevel>(StringComparer.OrdinalIgnoreCase)
{
["staging"] = SbomEnforcementLevel.Recommended
}
};
var gate = new SbomPresenceGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "staging"));
Assert.True(result.Passed);
Assert.Equal("sbom_missing_recommended", result.Reason);
Assert.Contains("warning", result.Details.Keys);
}
[Fact]
public async Task EvaluateAsync_ValidSbom_ReturnsPass()
{
var options = new SbomPresenceGateOptions();
var sbomInfo = new SbomInfo
{
Present = true,
Format = "spdx-2.3",
ComponentCount = 10,
HasPrimaryComponent = true,
SchemaValid = true
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
Assert.Equal("sbom_valid", result.Reason);
}
[Theory]
[InlineData("spdx-2.2")]
[InlineData("spdx-2.3")]
[InlineData("spdx-3.0.1")]
[InlineData("cyclonedx-1.4")]
[InlineData("cyclonedx-1.5")]
[InlineData("cyclonedx-1.6")]
[InlineData("cyclonedx-1.7")]
public async Task EvaluateAsync_AcceptedFormats_ReturnsPass(string format)
{
var options = new SbomPresenceGateOptions();
var sbomInfo = new SbomInfo
{
Present = true,
Format = format,
ComponentCount = 5,
HasPrimaryComponent = true
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
}
[Theory]
[InlineData("unknown-1.0")]
[InlineData("custom-format")]
[InlineData("spdx-1.0")]
public async Task EvaluateAsync_InvalidFormat_ReturnsFail(string format)
{
var options = new SbomPresenceGateOptions();
var sbomInfo = new SbomInfo
{
Present = true,
Format = format,
ComponentCount = 5,
HasPrimaryComponent = true
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
Assert.Equal("invalid_format", result.Reason);
}
[Fact]
public async Task EvaluateAsync_InsufficientComponents_ReturnsFail()
{
var options = new SbomPresenceGateOptions { MinimumComponents = 5 };
var sbomInfo = new SbomInfo
{
Present = true,
Format = "spdx-2.3",
ComponentCount = 3,
HasPrimaryComponent = true
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
Assert.Equal("insufficient_components", result.Reason);
Assert.Equal(5, (int)result.Details["minimum_components"]);
Assert.Equal(3, (int)result.Details["component_count"]);
}
[Fact]
public async Task EvaluateAsync_SchemaValidationFailed_ReturnsFail()
{
var options = new SbomPresenceGateOptions { SchemaValidation = true };
var sbomInfo = new SbomInfo
{
Present = true,
Format = "spdx-2.3",
ComponentCount = 5,
HasPrimaryComponent = true,
SchemaValid = false,
SchemaErrors = new[] { "Missing required field 'name'", "Invalid date format" }
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
Assert.Equal("schema_validation_failed", result.Reason);
Assert.Contains("schema_errors", result.Details.Keys);
}
[Fact]
public async Task EvaluateAsync_SignatureRequired_MissingSignature_ReturnsFail()
{
var options = new SbomPresenceGateOptions { RequireSignature = true };
var sbomInfo = new SbomInfo
{
Present = true,
Format = "spdx-2.3",
ComponentCount = 5,
HasPrimaryComponent = true,
HasSignature = false
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
Assert.Equal("signature_missing", result.Reason);
}
[Fact]
public async Task EvaluateAsync_SignatureRequired_InvalidSignature_ReturnsFail()
{
var options = new SbomPresenceGateOptions { RequireSignature = true };
var sbomInfo = new SbomInfo
{
Present = true,
Format = "spdx-2.3",
ComponentCount = 5,
HasPrimaryComponent = true,
HasSignature = true,
SignatureValid = false
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
Assert.Equal("signature_invalid", result.Reason);
}
[Fact]
public async Task EvaluateAsync_SignatureRequired_ValidSignature_ReturnsPass()
{
var options = new SbomPresenceGateOptions { RequireSignature = true };
var sbomInfo = new SbomInfo
{
Present = true,
Format = "spdx-2.3",
ComponentCount = 5,
HasPrimaryComponent = true,
HasSignature = true,
SignatureValid = true
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
}
[Fact]
public async Task EvaluateAsync_PrimaryComponentRequired_Missing_ReturnsFail()
{
var options = new SbomPresenceGateOptions { RequirePrimaryComponent = true };
var sbomInfo = new SbomInfo
{
Present = true,
Format = "spdx-2.3",
ComponentCount = 5,
HasPrimaryComponent = false
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
Assert.Equal("primary_component_missing", result.Reason);
}
[Theory]
[InlineData("production", SbomEnforcementLevel.Required)]
[InlineData("staging", SbomEnforcementLevel.Required)]
[InlineData("development", SbomEnforcementLevel.Optional)]
public async Task EvaluateAsync_EnvironmentEnforcement_UsesCorrectLevel(string environment, SbomEnforcementLevel expectedLevel)
{
var options = new SbomPresenceGateOptions
{
Enforcement = new Dictionary<string, SbomEnforcementLevel>(StringComparer.OrdinalIgnoreCase)
{
["production"] = SbomEnforcementLevel.Required,
["staging"] = SbomEnforcementLevel.Required,
["development"] = SbomEnforcementLevel.Optional
}
};
var gate = new SbomPresenceGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: environment));
Assert.Equal(expectedLevel.ToString(), result.Details["enforcement"]);
}
[Fact]
public async Task EvaluateAsync_UnknownEnvironment_UsesDefaultEnforcement()
{
var options = new SbomPresenceGateOptions
{
DefaultEnforcement = SbomEnforcementLevel.Recommended,
Enforcement = new Dictionary<string, SbomEnforcementLevel>(StringComparer.OrdinalIgnoreCase)
{
["production"] = SbomEnforcementLevel.Required
}
};
var gate = new SbomPresenceGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "qa"));
Assert.Equal(SbomEnforcementLevel.Recommended.ToString(), result.Details["enforcement"]);
}
[Fact]
public async Task EvaluateAsync_MetadataFallback_ParsesSbomInfo()
{
var options = new SbomPresenceGateOptions();
var metadata = new Dictionary<string, string>
{
["sbom_present"] = "true",
["sbom_format"] = "cyclonedx-1.6",
["sbom_component_count"] = "25",
["sbom_has_primary_component"] = "true"
};
var gate = new SbomPresenceGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(metadata: metadata));
Assert.True(result.Passed);
Assert.Equal("cyclonedx-1.6", result.Details["format"]);
Assert.Equal(25, (int)result.Details["component_count"]);
}
[Theory]
[InlineData("SPDX-2.3", "spdx-2.3")]
[InlineData("CycloneDX-1.6", "cyclonedx-1.6")]
[InlineData("spdx 2.3", "spdx-2.3")]
[InlineData("cdx-1.5", "cyclonedx-1.5")]
public async Task EvaluateAsync_FormatNormalization_HandlesVariations(string inputFormat, string normalizedExpected)
{
var options = new SbomPresenceGateOptions();
var sbomInfo = new SbomInfo
{
Present = true,
Format = inputFormat,
ComponentCount = 5,
HasPrimaryComponent = true
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
// If format was accepted, it was normalized correctly
Assert.True(result.Passed, $"Format '{inputFormat}' should normalize to '{normalizedExpected}' and be accepted");
}
[Fact]
public async Task EvaluateAsync_IncludesOptionalMetadata()
{
var options = new SbomPresenceGateOptions();
var createdAt = new DateTimeOffset(2026, 1, 15, 10, 30, 0, TimeSpan.Zero);
var sbomInfo = new SbomInfo
{
Present = true,
Format = "spdx-2.3",
ComponentCount = 10,
HasPrimaryComponent = true,
DocumentUri = "urn:sbom:example:12345",
CreatedAt = createdAt
};
var gate = new SbomPresenceGate(options, _ => sbomInfo);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
Assert.Equal("urn:sbom:example:12345", result.Details["document_uri"]);
Assert.Contains("2026-01-15", (string)result.Details["created_at"]);
}
}

View File

@@ -0,0 +1,450 @@
// -----------------------------------------------------------------------------
// SignatureRequiredGateTests.cs
// Sprint: SPRINT_20260112_017_POLICY_signature_required_gate
// Tasks: SIG-GATE-009
// Description: Unit tests for signature required gate.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Gates;
using StellaOps.Policy.TrustLattice;
using Xunit;
namespace StellaOps.Policy.Tests.Gates;
[Trait("Category", "Unit")]
public sealed class SignatureRequiredGateTests
{
private static MergeResult CreateMergeResult() => new()
{
Status = VexStatus.Affected,
Confidence = 0.8,
HasConflicts = false,
AllClaims = ImmutableArray<ScoredClaim>.Empty,
WinningClaim = new ScoredClaim
{
SourceId = "test",
Status = VexStatus.Affected,
OriginalScore = 0.8,
AdjustedScore = 0.8,
ScopeSpecificity = 1,
Accepted = true,
Reason = "test"
},
Conflicts = ImmutableArray<ConflictRecord>.Empty
};
private static PolicyGateContext CreateContext(string environment = "production") => new()
{
Environment = environment
};
[Fact]
public async Task EvaluateAsync_Disabled_ReturnsPass()
{
var options = new SignatureRequiredGateOptions { Enabled = false };
var gate = new SignatureRequiredGate(options);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
Assert.Equal("disabled", result.Reason);
}
[Fact]
public async Task EvaluateAsync_MissingSignature_ReturnsFail()
{
var options = new SignatureRequiredGateOptions();
var signatures = new List<SignatureInfo>(); // No signatures
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
Assert.Equal("signature_validation_failed", result.Reason);
}
[Fact]
public async Task EvaluateAsync_AllValidSignatures_ReturnsPass()
{
var options = new SignatureRequiredGateOptions();
var signatures = new List<SignatureInfo>
{
new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = true },
new() { EvidenceType = "vex", HasSignature = true, SignatureValid = true },
new() { EvidenceType = "attestation", HasSignature = true, SignatureValid = true }
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
Assert.Equal("signatures_verified", result.Reason);
}
[Fact]
public async Task EvaluateAsync_InvalidSignature_ReturnsFail()
{
var options = new SignatureRequiredGateOptions();
var signatures = new List<SignatureInfo>
{
new() { EvidenceType = "sbom", HasSignature = true, SignatureValid = false, VerificationErrors = new[] { "Invalid hash" } },
new() { EvidenceType = "vex", HasSignature = true, SignatureValid = true },
new() { EvidenceType = "attestation", HasSignature = true, SignatureValid = true }
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
Assert.Contains("failures", result.Details.Keys);
}
[Fact]
public async Task EvaluateAsync_NotRequiredType_PassesWithoutSignature()
{
var options = new SignatureRequiredGateOptions
{
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig { Required = false },
["vex"] = new EvidenceSignatureConfig { Required = true },
["attestation"] = new EvidenceSignatureConfig { Required = true }
}
};
var signatures = new List<SignatureInfo>
{
// No SBOM signature - but it's not required
new() { EvidenceType = "vex", HasSignature = true, SignatureValid = true },
new() { EvidenceType = "attestation", HasSignature = true, SignatureValid = true }
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
}
[Theory]
[InlineData("build@company.com", new[] { "build@company.com" }, true)]
[InlineData("release@company.com", new[] { "*@company.com" }, true)]
[InlineData("external@other.com", new[] { "*@company.com" }, false)]
[InlineData("build@company.com", new[] { "other@company.com" }, false)]
public async Task EvaluateAsync_IssuerValidation_EnforcesConstraints(
string signerIdentity,
string[] trustedIssuers,
bool expectedPass)
{
var options = new SignatureRequiredGateOptions
{
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig
{
Required = true,
TrustedIssuers = new HashSet<string>(trustedIssuers, StringComparer.OrdinalIgnoreCase)
},
["vex"] = new EvidenceSignatureConfig { Required = false },
["attestation"] = new EvidenceSignatureConfig { Required = false }
}
};
var signatures = new List<SignatureInfo>
{
new()
{
EvidenceType = "sbom",
HasSignature = true,
SignatureValid = true,
SignerIdentity = signerIdentity
}
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.Equal(expectedPass, result.Passed);
}
[Theory]
[InlineData("ES256", true)]
[InlineData("RS256", true)]
[InlineData("EdDSA", true)]
[InlineData("UNKNOWN", false)]
public async Task EvaluateAsync_AlgorithmValidation_EnforcesAccepted(string algorithm, bool expectedPass)
{
var options = new SignatureRequiredGateOptions
{
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig { Required = true },
["vex"] = new EvidenceSignatureConfig { Required = false },
["attestation"] = new EvidenceSignatureConfig { Required = false }
}
};
var signatures = new List<SignatureInfo>
{
new()
{
EvidenceType = "sbom",
HasSignature = true,
SignatureValid = true,
Algorithm = algorithm
}
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.Equal(expectedPass, result.Passed);
}
[Fact]
public async Task EvaluateAsync_KeyIdValidation_EnforcesConstraints()
{
var options = new SignatureRequiredGateOptions
{
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig
{
Required = true,
TrustedKeyIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "key-001", "key-002" }
},
["vex"] = new EvidenceSignatureConfig { Required = false },
["attestation"] = new EvidenceSignatureConfig { Required = false }
}
};
var signatures = new List<SignatureInfo>
{
new()
{
EvidenceType = "sbom",
HasSignature = true,
SignatureValid = true,
KeyId = "key-999",
IsKeyless = false
}
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
}
[Fact]
public async Task EvaluateAsync_KeylessSignature_ValidWithTransparencyLog()
{
var options = new SignatureRequiredGateOptions
{
EnableKeylessVerification = true,
RequireTransparencyLogInclusion = true,
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig { Required = true },
["vex"] = new EvidenceSignatureConfig { Required = false },
["attestation"] = new EvidenceSignatureConfig { Required = false }
}
};
var signatures = new List<SignatureInfo>
{
new()
{
EvidenceType = "sbom",
HasSignature = true,
SignatureValid = true,
IsKeyless = true,
HasTransparencyLogInclusion = true,
CertificateChainValid = true
}
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
}
[Fact]
public async Task EvaluateAsync_KeylessSignature_FailsWithoutTransparencyLog()
{
var options = new SignatureRequiredGateOptions
{
EnableKeylessVerification = true,
RequireTransparencyLogInclusion = true,
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig { Required = true },
["vex"] = new EvidenceSignatureConfig { Required = false },
["attestation"] = new EvidenceSignatureConfig { Required = false }
}
};
var signatures = new List<SignatureInfo>
{
new()
{
EvidenceType = "sbom",
HasSignature = true,
SignatureValid = true,
IsKeyless = true,
HasTransparencyLogInclusion = false
}
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
}
[Fact]
public async Task EvaluateAsync_KeylessDisabled_FailsKeylessSignature()
{
var options = new SignatureRequiredGateOptions
{
EnableKeylessVerification = false,
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig { Required = true },
["vex"] = new EvidenceSignatureConfig { Required = false },
["attestation"] = new EvidenceSignatureConfig { Required = false }
}
};
var signatures = new List<SignatureInfo>
{
new()
{
EvidenceType = "sbom",
HasSignature = true,
SignatureValid = true,
IsKeyless = true
}
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
}
[Fact]
public async Task EvaluateAsync_EnvironmentOverride_SkipsTypes()
{
var options = new SignatureRequiredGateOptions
{
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig { Required = true },
["vex"] = new EvidenceSignatureConfig { Required = true },
["attestation"] = new EvidenceSignatureConfig { Required = true }
},
Environments = new Dictionary<string, EnvironmentSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["development"] = new EnvironmentSignatureConfig
{
SkipEvidenceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "sbom", "vex" }
}
}
};
var signatures = new List<SignatureInfo>
{
// Only attestation signature in development
new() { EvidenceType = "attestation", HasSignature = true, SignatureValid = true }
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "development"));
Assert.True(result.Passed);
}
[Fact]
public async Task EvaluateAsync_EnvironmentOverride_AddsIssuers()
{
var options = new SignatureRequiredGateOptions
{
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig
{
Required = true,
TrustedIssuers = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "prod@company.com" }
},
["vex"] = new EvidenceSignatureConfig { Required = false },
["attestation"] = new EvidenceSignatureConfig { Required = false }
},
Environments = new Dictionary<string, EnvironmentSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["staging"] = new EnvironmentSignatureConfig
{
AdditionalIssuers = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "staging@company.com" }
}
}
};
var signatures = new List<SignatureInfo>
{
new()
{
EvidenceType = "sbom",
HasSignature = true,
SignatureValid = true,
SignerIdentity = "staging@company.com"
}
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext(environment: "staging"));
Assert.True(result.Passed);
}
[Fact]
public async Task EvaluateAsync_InvalidCertificateChain_Fails()
{
var options = new SignatureRequiredGateOptions
{
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig { Required = true },
["vex"] = new EvidenceSignatureConfig { Required = false },
["attestation"] = new EvidenceSignatureConfig { Required = false }
}
};
var signatures = new List<SignatureInfo>
{
new()
{
EvidenceType = "sbom",
HasSignature = true,
SignatureValid = true,
IsKeyless = true,
HasTransparencyLogInclusion = true,
CertificateChainValid = false
}
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.False(result.Passed);
}
[Fact]
public async Task EvaluateAsync_WildcardIssuerMatch_MatchesSubdomains()
{
var options = new SignatureRequiredGateOptions
{
EvidenceTypes = new Dictionary<string, EvidenceSignatureConfig>(StringComparer.OrdinalIgnoreCase)
{
["sbom"] = new EvidenceSignatureConfig
{
Required = true,
TrustedIssuers = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "*@*.company.com" }
},
["vex"] = new EvidenceSignatureConfig { Required = false },
["attestation"] = new EvidenceSignatureConfig { Required = false }
}
};
var signatures = new List<SignatureInfo>
{
new()
{
EvidenceType = "sbom",
HasSignature = true,
SignatureValid = true,
SignerIdentity = "build@ci.company.com"
}
};
var gate = new SignatureRequiredGate(options, _ => signatures);
var result = await gate.EvaluateAsync(CreateMergeResult(), CreateContext());
Assert.True(result.Passed);
}
}

View File

@@ -0,0 +1,268 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) 2025 StellaOps
// Sprint: SPRINT_20260112_004_BE_policy_determinization_attested_rules (DET-ATT-004)
// Task: Unit tests for VexProofGate anchor-aware mode
using System.Collections.Immutable;
using StellaOps.Policy.Gates;
using StellaOps.Policy.TrustLattice;
using VexStatus = StellaOps.Policy.Confidence.Models.VexStatus;
using Xunit;
namespace StellaOps.Policy.Tests.Gates;
public class VexProofGateTests
{
private static readonly DateTimeOffset FixedTime = new(2026, 1, 14, 12, 0, 0, TimeSpan.Zero);
private static MergeResult CreateMergeResult(VexStatus status) =>
new()
{
Status = status,
Confidence = 0.9,
HasConflicts = false,
AllClaims = ImmutableArray<ScoredClaim>.Empty,
WinningClaim = new ScoredClaim
{
SourceId = "test",
Status = status,
OriginalScore = 0.9,
AdjustedScore = 0.9,
ScopeSpecificity = 1,
Accepted = true,
Reason = "Test claim"
},
Conflicts = ImmutableArray<ConflictRecord>.Empty
};
[Fact]
public async Task EvaluateAsync_WhenDisabled_ReturnsPass()
{
// Arrange
var options = new VexProofGateOptions { Enabled = false };
var gate = new VexProofGate(options);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
var context = new PolicyGateContext { Environment = "production" };
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
Assert.True(result.Passed);
Assert.Equal("disabled", result.Reason);
}
[Fact]
public async Task EvaluateAsync_WhenAnchorAwareModeEnabled_RequiresAnchoring()
{
// Arrange
var options = new VexProofGateOptions
{
Enabled = true,
RequireProofForNotAffected = true,
AnchorAwareMode = true,
RequireVexAnchoring = true
};
var gate = new VexProofGate(options);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
var context = new PolicyGateContext
{
Environment = "production",
Metadata = new Dictionary<string, string>
{
["vex_proof_id"] = "proof-123",
["vex_proof_confidence_tier"] = "high",
["vex_proof_anchored"] = "false" // Not anchored
}
};
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
Assert.False(result.Passed);
Assert.Equal("vex_not_anchored", result.Reason);
}
[Fact]
public async Task EvaluateAsync_WhenAnchorAwareModeEnabled_PassesWithAnchoring()
{
// Arrange
var options = new VexProofGateOptions
{
Enabled = true,
RequireProofForNotAffected = true,
AnchorAwareMode = true,
RequireVexAnchoring = true,
RequireRekorVerification = false
};
var gate = new VexProofGate(options);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
var context = new PolicyGateContext
{
Environment = "production",
Metadata = new Dictionary<string, string>
{
["vex_proof_id"] = "proof-123",
["vex_proof_confidence_tier"] = "high",
["vex_proof_anchored"] = "true",
["vex_proof_envelope_digest"] = "sha256:abc123"
}
};
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
Assert.True(result.Passed);
Assert.Equal("proof_valid", result.Reason);
}
[Fact]
public async Task EvaluateAsync_WhenRekorRequired_FailsWithoutRekor()
{
// Arrange
var options = new VexProofGateOptions
{
Enabled = true,
RequireProofForNotAffected = true,
AnchorAwareMode = true,
RequireVexAnchoring = true,
RequireRekorVerification = true
};
var gate = new VexProofGate(options);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
var context = new PolicyGateContext
{
Environment = "production",
Metadata = new Dictionary<string, string>
{
["vex_proof_id"] = "proof-123",
["vex_proof_confidence_tier"] = "high",
["vex_proof_anchored"] = "true",
["vex_proof_rekor_verified"] = "false"
}
};
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
Assert.False(result.Passed);
Assert.Equal("rekor_verification_missing", result.Reason);
}
[Fact]
public async Task EvaluateAsync_WhenRekorRequired_PassesWithRekor()
{
// Arrange
var options = new VexProofGateOptions
{
Enabled = true,
RequireProofForNotAffected = true,
AnchorAwareMode = true,
RequireVexAnchoring = true,
RequireRekorVerification = true
};
var gate = new VexProofGate(options);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
var context = new PolicyGateContext
{
Environment = "production",
Metadata = new Dictionary<string, string>
{
["vex_proof_id"] = "proof-123",
["vex_proof_confidence_tier"] = "high",
["vex_proof_anchored"] = "true",
["vex_proof_envelope_digest"] = "sha256:abc123",
["vex_proof_rekor_verified"] = "true",
["vex_proof_rekor_log_index"] = "12345678"
}
};
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
Assert.True(result.Passed);
Assert.Equal("proof_valid", result.Reason);
Assert.True(result.Details.ContainsKey("rekorLogIndex"));
}
[Fact]
public async Task EvaluateAsync_StrictAnchorAware_EnforcesAllRequirements()
{
// Arrange
var options = VexProofGateOptions.StrictAnchorAware;
var gate = new VexProofGate(options);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
var context = new PolicyGateContext
{
Environment = "production",
Metadata = new Dictionary<string, string>
{
["vex_proof_id"] = "proof-123",
["vex_proof_confidence_tier"] = "high",
["vex_proof_all_signed"] = "true",
["vex_proof_anchored"] = "true",
["vex_proof_envelope_digest"] = "sha256:abc123",
["vex_proof_rekor_verified"] = "true",
["vex_proof_rekor_log_index"] = "12345678"
}
};
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
Assert.True(result.Passed);
Assert.Equal("proof_valid", result.Reason);
}
[Fact]
public async Task EvaluateAsync_StrictAnchorAware_FailsWithoutSignedStatements()
{
// Arrange
var options = VexProofGateOptions.StrictAnchorAware;
var gate = new VexProofGate(options);
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
var context = new PolicyGateContext
{
Environment = "production",
Metadata = new Dictionary<string, string>
{
["vex_proof_id"] = "proof-123",
["vex_proof_confidence_tier"] = "high",
["vex_proof_all_signed"] = "false", // Not signed
["vex_proof_anchored"] = "true",
["vex_proof_rekor_verified"] = "true"
}
};
// Act
var result = await gate.EvaluateAsync(mergeResult, context);
// Assert
Assert.False(result.Passed);
Assert.Equal("unsigned_statements", result.Reason);
}
[Fact]
public void StrictAnchorAware_HasExpectedDefaults()
{
// Act
var options = VexProofGateOptions.StrictAnchorAware;
// Assert
Assert.True(options.Enabled);
Assert.Equal("high", options.MinimumConfidenceTier);
Assert.True(options.RequireProofForNotAffected);
Assert.True(options.RequireProofForFixed);
Assert.True(options.RequireSignedStatements);
Assert.True(options.AnchorAwareMode);
Assert.True(options.RequireVexAnchoring);
Assert.True(options.RequireRekorVerification);
Assert.Equal(0, options.MaxAllowedConflicts);
Assert.Equal(72, options.MaxProofAgeHours);
}
}