old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user