feat(secrets): Implement secret leak policies and signal binding

- Added `spl-secret-block@1.json` to block deployments with critical or high severity secret findings.
- Introduced `spl-secret-warn@1.json` to warn on secret findings without blocking deployments.
- Created `SecretSignalBinder.cs` to bind secret evidence to policy evaluation signals.
- Developed unit tests for `SecretEvidenceContext` and `SecretSignalBinder` to ensure correct functionality.
- Enhanced `SecretSignalContextExtensions` to integrate secret evidence into signal contexts.
This commit is contained in:
StellaOps Bot
2026-01-04 15:44:49 +02:00
parent 1f33143bd1
commit f7d27c6fda
44 changed files with 2406 additions and 1107 deletions

View File

@@ -0,0 +1,259 @@
// -----------------------------------------------------------------------------
// SecretEvidenceContextTests.cs
// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration)
// Task: PSD-011 - Add unit and integration tests
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using FluentAssertions;
using Moq;
using StellaOps.Policy.Secrets;
using Xunit;
namespace StellaOps.Policy.Tests.Secrets;
[Trait("Category", "Unit")]
public sealed class SecretEvidenceContextTests
{
[Fact]
public void Constructor_WithNullProvider_ThrowsArgumentNullException()
{
var action = () => new SecretEvidenceContext(null!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void HasAnyFinding_NoFindings_ReturnsFalse()
{
var provider = CreateMockProvider([]);
var context = new SecretEvidenceContext(provider);
context.HasAnyFinding.Should().BeFalse();
}
[Fact]
public void HasAnyFinding_WithFindings_ReturnsTrue()
{
var finding = CreateFinding("stellaops.secrets.test", "high");
var provider = CreateMockProvider([finding]);
var context = new SecretEvidenceContext(provider);
context.HasAnyFinding.Should().BeTrue();
}
[Fact]
public void FindingCount_ReturnsCorrectCount()
{
var findings = new[]
{
CreateFinding("stellaops.secrets.test1", "high"),
CreateFinding("stellaops.secrets.test2", "medium"),
CreateFinding("stellaops.secrets.test3", "low"),
};
var provider = CreateMockProvider(findings);
var context = new SecretEvidenceContext(provider);
context.FindingCount.Should().Be(3);
}
[Fact]
public void HasFindingWithSeverity_MatchingSeverity_ReturnsTrue()
{
var finding = CreateFinding("stellaops.secrets.test", "critical");
var provider = CreateMockProvider([finding]);
var context = new SecretEvidenceContext(provider);
context.HasFindingWithSeverity("critical").Should().BeTrue();
context.HasFindingWithSeverity("CRITICAL").Should().BeTrue(); // Case insensitive
}
[Fact]
public void HasFindingWithSeverity_NoMatchingSeverity_ReturnsFalse()
{
var finding = CreateFinding("stellaops.secrets.test", "low");
var provider = CreateMockProvider([finding]);
var context = new SecretEvidenceContext(provider);
context.HasFindingWithSeverity("critical").Should().BeFalse();
}
[Fact]
public void HasFindingWithConfidence_MatchingConfidence_ReturnsTrue()
{
var finding = CreateFinding("stellaops.secrets.test", "high", "high");
var provider = CreateMockProvider([finding]);
var context = new SecretEvidenceContext(provider);
context.HasFindingWithConfidence("high").Should().BeTrue();
}
[Fact]
public void HasFindingWithRuleId_ExactMatch_ReturnsTrue()
{
var finding = CreateFinding("stellaops.secrets.aws-access-key", "high");
var provider = CreateMockProvider([finding]);
var context = new SecretEvidenceContext(provider);
context.HasFindingWithRuleId("stellaops.secrets.aws-access-key").Should().BeTrue();
}
[Fact]
public void HasFindingWithRuleId_PatternMatch_ReturnsTrue()
{
var finding = CreateFinding("stellaops.secrets.aws-access-key", "high");
var provider = CreateMockProvider([finding]);
var context = new SecretEvidenceContext(provider);
context.HasFindingWithRuleId("stellaops.secrets.aws-*").Should().BeTrue();
}
[Fact]
public void HasFindingWithRuleId_NoMatch_ReturnsFalse()
{
var finding = CreateFinding("stellaops.secrets.github-token", "high");
var provider = CreateMockProvider([finding]);
var context = new SecretEvidenceContext(provider);
context.HasFindingWithRuleId("stellaops.secrets.aws-*").Should().BeFalse();
}
[Fact]
public void GetMatchCount_WithPattern_ReturnsCorrectCount()
{
var findings = new[]
{
CreateFinding("stellaops.secrets.aws-access-key", "high"),
CreateFinding("stellaops.secrets.aws-secret-key", "high"),
CreateFinding("stellaops.secrets.github-token", "medium"),
};
var provider = CreateMockProvider(findings);
var context = new SecretEvidenceContext(provider);
context.GetMatchCount("stellaops.secrets.aws-*").Should().Be(2);
context.GetMatchCount("stellaops.secrets.github-*").Should().Be(1);
}
[Fact]
public void BundleVersionMeetsRequirement_ValidVersion_ReturnsTrue()
{
var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 100);
var provider = CreateMockProvider([], bundle);
var context = new SecretEvidenceContext(provider);
context.BundleVersionMeetsRequirement("2025.01").Should().BeTrue();
context.BundleVersionMeetsRequirement("2025.06").Should().BeTrue();
}
[Fact]
public void BundleVersionMeetsRequirement_OlderVersion_ReturnsFalse()
{
var bundle = new SecretBundleMetadata("bundle-1", "2024.12", DateTimeOffset.UtcNow, 100);
var provider = CreateMockProvider([], bundle);
var context = new SecretEvidenceContext(provider);
context.BundleVersionMeetsRequirement("2025.01").Should().BeFalse();
}
[Fact]
public void BundleVersionMeetsRequirement_NoBundle_ReturnsFalse()
{
var provider = CreateMockProvider([]);
var context = new SecretEvidenceContext(provider);
context.BundleVersionMeetsRequirement("2025.01").Should().BeFalse();
}
[Fact]
public void MaskingApplied_ReturnsProviderValue()
{
var mock = new Mock<ISecretEvidenceProvider>();
mock.Setup(p => p.GetFindings()).Returns([]);
mock.Setup(p => p.IsMaskingApplied()).Returns(true);
var context = new SecretEvidenceContext(mock.Object);
context.MaskingApplied.Should().BeTrue();
}
[Fact]
public void AllFindingsInAllowlist_NoFindings_ReturnsTrue()
{
var provider = CreateMockProvider([]);
var context = new SecretEvidenceContext(provider);
context.AllFindingsInAllowlist(["**/test/**"]).Should().BeTrue();
}
[Fact]
public void AllFindingsInAllowlist_AllMatch_ReturnsTrue()
{
var findings = new[]
{
CreateFinding("rule1", "high", "high", "test/data/secrets.txt"),
CreateFinding("rule2", "high", "high", "test/fixtures/keys.txt"),
};
var provider = CreateMockProvider(findings);
var context = new SecretEvidenceContext(provider);
context.AllFindingsInAllowlist(["test/**"]).Should().BeTrue();
}
[Fact]
public void AllFindingsInAllowlist_SomeNotMatch_ReturnsFalse()
{
var findings = new[]
{
CreateFinding("rule1", "high", "high", "test/data/secrets.txt"),
CreateFinding("rule2", "high", "high", "src/app/config.json"),
};
var provider = CreateMockProvider(findings);
var context = new SecretEvidenceContext(provider);
context.AllFindingsInAllowlist(["test/**"]).Should().BeFalse();
}
[Fact]
public void AllFindingsInAllowlist_DoubleStarPattern_MatchesNestedPaths()
{
var findings = new[]
{
CreateFinding("rule1", "high", "high", "a/b/c/d/test.txt"),
};
var provider = CreateMockProvider(findings);
var context = new SecretEvidenceContext(provider);
context.AllFindingsInAllowlist(["**/test.txt"]).Should().BeTrue();
}
private static ISecretEvidenceProvider CreateMockProvider(
SecretFinding[] findings,
SecretBundleMetadata? bundle = null,
bool maskingApplied = true)
{
var mock = new Mock<ISecretEvidenceProvider>();
mock.Setup(p => p.GetFindings()).Returns(findings);
mock.Setup(p => p.GetBundleMetadata()).Returns(bundle);
mock.Setup(p => p.IsMaskingApplied()).Returns(maskingApplied);
return mock.Object;
}
private static SecretFinding CreateFinding(
string ruleId,
string severity,
string confidence = "high",
string filePath = "test/file.txt")
{
return new SecretFinding
{
RuleId = ruleId,
RuleVersion = "1.0.0",
Severity = severity,
Confidence = confidence,
FilePath = filePath,
LineNumber = 10,
Mask = "***REDACTED***",
BundleId = "bundle-1",
BundleVersion = "2025.01",
DetectedAt = DateTimeOffset.UtcNow,
};
}
}

View File

@@ -0,0 +1,264 @@
// -----------------------------------------------------------------------------
// SecretSignalBinderTests.cs
// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration)
// Task: PSD-011 - Add unit and integration tests
// -----------------------------------------------------------------------------
using FluentAssertions;
using Moq;
using StellaOps.Policy.Secrets;
using Xunit;
namespace StellaOps.Policy.Tests.Secrets;
[Trait("Category", "Unit")]
public sealed class SecretSignalBinderTests
{
[Fact]
public void BindToSignals_WithNullContext_ThrowsArgumentNullException()
{
var action = () => SecretSignalBinder.BindToSignals(null!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void BindToSignals_NoFindings_ReturnsExpectedSignals()
{
var provider = CreateMockProvider([]);
var context = new SecretEvidenceContext(provider);
var signals = SecretSignalBinder.BindToSignals(context);
signals["secret.has_finding"].Should().Be(false);
signals["secret.count"].Should().Be(0);
signals["secret.severity.critical"].Should().Be(false);
signals["secret.severity.high"].Should().Be(false);
signals["secret.severity.medium"].Should().Be(false);
signals["secret.severity.low"].Should().Be(false);
}
[Fact]
public void BindToSignals_WithCriticalFinding_SetsCriticalSignal()
{
var finding = CreateFinding("rule1", "critical");
var provider = CreateMockProvider([finding]);
var context = new SecretEvidenceContext(provider);
var signals = SecretSignalBinder.BindToSignals(context);
signals["secret.has_finding"].Should().Be(true);
signals["secret.count"].Should().Be(1);
signals["secret.severity.critical"].Should().Be(true);
}
[Fact]
public void BindToSignals_WithMultipleSeverities_SetsAllMatchingSignals()
{
var findings = new[]
{
CreateFinding("rule1", "high"),
CreateFinding("rule2", "medium"),
CreateFinding("rule3", "high"),
};
var provider = CreateMockProvider(findings);
var context = new SecretEvidenceContext(provider);
var signals = SecretSignalBinder.BindToSignals(context);
signals["secret.has_finding"].Should().Be(true);
signals["secret.count"].Should().Be(3);
signals["secret.severity.critical"].Should().Be(false);
signals["secret.severity.high"].Should().Be(true);
signals["secret.severity.medium"].Should().Be(true);
signals["secret.severity.low"].Should().Be(false);
}
[Fact]
public void BindToSignals_WithBundle_SetsBundleSignals()
{
var bundle = new SecretBundleMetadata(
"stellaops-bundle-2025",
"2025.06",
DateTimeOffset.UtcNow,
150,
"key-001");
var provider = CreateMockProvider([], bundle);
var context = new SecretEvidenceContext(provider);
var signals = SecretSignalBinder.BindToSignals(context);
signals["secret.bundle.version"].Should().Be("2025.06");
signals["secret.bundle.id"].Should().Be("stellaops-bundle-2025");
signals["secret.bundle.rule_count"].Should().Be(150);
signals["secret.bundle.signer_key_id"].Should().Be("key-001");
}
[Fact]
public void BindToSignals_NoBundle_SetsBundleSignalsToDefaults()
{
var provider = CreateMockProvider([]);
var context = new SecretEvidenceContext(provider);
var signals = SecretSignalBinder.BindToSignals(context);
signals["secret.bundle.version"].Should().BeNull();
signals["secret.bundle.id"].Should().BeNull();
signals["secret.bundle.rule_count"].Should().Be(0);
signals["secret.bundle.signer_key_id"].Should().BeNull();
}
[Fact]
public void BindToSignals_WithConfidenceLevels_SetsConfidenceSignals()
{
var findings = new[]
{
CreateFinding("rule1", "high", "high"),
CreateFinding("rule2", "high", "medium"),
};
var provider = CreateMockProvider(findings);
var context = new SecretEvidenceContext(provider);
var signals = SecretSignalBinder.BindToSignals(context);
signals["secret.confidence.high"].Should().Be(true);
signals["secret.confidence.medium"].Should().Be(true);
signals["secret.confidence.low"].Should().Be(false);
}
[Fact]
public void BindToSignals_WithMasking_SetsMaskSignal()
{
var provider = CreateMockProvider([], maskingApplied: true);
var context = new SecretEvidenceContext(provider);
var signals = SecretSignalBinder.BindToSignals(context);
signals["secret.mask.applied"].Should().Be(true);
}
[Fact]
public void BindToSignals_SetsRuleSpecificCounts()
{
var findings = new[]
{
CreateFinding("stellaops.secrets.aws-access-key", "high"),
CreateFinding("stellaops.secrets.aws-secret-key", "high"),
CreateFinding("stellaops.secrets.github-token", "medium"),
CreateFinding("stellaops.secrets.private-key-rsa", "critical"),
};
var provider = CreateMockProvider(findings);
var context = new SecretEvidenceContext(provider);
var signals = SecretSignalBinder.BindToSignals(context);
signals["secret.aws.count"].Should().Be(2);
signals["secret.github.count"].Should().Be(1);
signals["secret.private_key.count"].Should().Be(1);
}
[Fact]
public void BindToNestedObject_ReturnsHierarchicalStructure()
{
var finding = CreateFinding("rule1", "critical", "high");
var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 100);
var provider = CreateMockProvider([finding], bundle);
var context = new SecretEvidenceContext(provider);
var nested = SecretSignalBinder.BindToNestedObject(context);
nested["has_finding"].Should().Be(true);
nested["count"].Should().Be(1);
var severity = nested["severity"] as IDictionary<string, object?>;
severity.Should().NotBeNull();
severity!["critical"].Should().Be(true);
var bundleDict = nested["bundle"] as IDictionary<string, object?>;
bundleDict.Should().NotBeNull();
bundleDict!["version"].Should().Be("2025.06");
}
[Fact]
public void CheckBundleVersion_ValidRequirement_ReturnsTrue()
{
var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 100);
var provider = CreateMockProvider([], bundle);
var context = new SecretEvidenceContext(provider);
SecretSignalBinder.CheckBundleVersion(context, "2025.01").Should().BeTrue();
}
[Fact]
public void CheckBundleVersion_InvalidRequirement_ReturnsFalse()
{
var bundle = new SecretBundleMetadata("bundle-1", "2024.12", DateTimeOffset.UtcNow, 100);
var provider = CreateMockProvider([], bundle);
var context = new SecretEvidenceContext(provider);
SecretSignalBinder.CheckBundleVersion(context, "2025.01").Should().BeFalse();
}
[Fact]
public void CreateFindingSummary_NoFindings_ReturnsNoFindingsMessage()
{
var provider = CreateMockProvider([]);
var context = new SecretEvidenceContext(provider);
var summary = SecretSignalBinder.CreateFindingSummary(context);
summary.Should().Be("No secret findings detected.");
}
[Fact]
public void CreateFindingSummary_WithFindings_ReturnsFormattedSummary()
{
var findings = new[]
{
CreateFinding("rule1", "critical"),
CreateFinding("rule2", "high"),
CreateFinding("rule3", "high"),
CreateFinding("rule4", "medium"),
};
var provider = CreateMockProvider(findings);
var context = new SecretEvidenceContext(provider);
var summary = SecretSignalBinder.CreateFindingSummary(context);
summary.Should().Contain("4 secret finding(s)");
summary.Should().Contain("1 critical");
summary.Should().Contain("2 high");
summary.Should().Contain("1 medium");
}
private static ISecretEvidenceProvider CreateMockProvider(
SecretFinding[] findings,
SecretBundleMetadata? bundle = null,
bool maskingApplied = true)
{
var mock = new Mock<ISecretEvidenceProvider>();
mock.Setup(p => p.GetFindings()).Returns(findings);
mock.Setup(p => p.GetBundleMetadata()).Returns(bundle);
mock.Setup(p => p.IsMaskingApplied()).Returns(maskingApplied);
return mock.Object;
}
private static SecretFinding CreateFinding(
string ruleId,
string severity,
string confidence = "high")
{
return new SecretFinding
{
RuleId = ruleId,
RuleVersion = "1.0.0",
Severity = severity,
Confidence = confidence,
FilePath = "test/file.txt",
LineNumber = 10,
Mask = "***REDACTED***",
BundleId = "bundle-1",
BundleVersion = "2025.01",
DetectedAt = DateTimeOffset.UtcNow,
};
}
}

View File

@@ -0,0 +1,182 @@
// -----------------------------------------------------------------------------
// SecretSignalContextExtensionsTests.cs
// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration)
// Task: PSD-011 - Add unit and integration tests
// -----------------------------------------------------------------------------
using FluentAssertions;
using Moq;
using StellaOps.Policy.Secrets;
using Xunit;
namespace StellaOps.PolicyDsl.Tests;
[Trait("Category", "Unit")]
public sealed class SecretSignalContextExtensionsTests
{
[Fact]
public void WithSecretEvidence_OnSignalContext_AddsAllSignals()
{
var finding = CreateFinding("stellaops.secrets.aws-key", "critical", "high");
var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 100);
var provider = CreateMockProvider([finding], bundle, true);
var evidenceContext = new SecretEvidenceContext(provider);
var signalContext = new SignalContext();
signalContext.WithSecretEvidence(evidenceContext);
// Check flat signals
signalContext.GetSignal<bool>("secret.has_finding").Should().BeTrue();
signalContext.GetSignal<int>("secret.count").Should().Be(1);
signalContext.GetSignal<bool>("secret.severity.critical").Should().BeTrue();
signalContext.GetSignal<bool>("secret.mask.applied").Should().BeTrue();
signalContext.GetSignal<string>("secret.bundle.version").Should().Be("2025.06");
// Check nested object
var secretObj = signalContext.GetSignal("secret") as IDictionary<string, object?>;
secretObj.Should().NotBeNull();
secretObj!["has_finding"].Should().Be(true);
}
[Fact]
public void WithSecretEvidence_OnSignalContextBuilder_AddsAllSignals()
{
var finding = CreateFinding("stellaops.secrets.github-token", "high", "medium");
var provider = CreateMockProvider([finding]);
var evidenceContext = new SecretEvidenceContext(provider);
var context = SignalContext.Builder()
.WithSecretEvidence(evidenceContext)
.Build();
context.GetSignal<bool>("secret.has_finding").Should().BeTrue();
context.GetSignal<bool>("secret.severity.high").Should().BeTrue();
context.GetSignal<bool>("secret.confidence.medium").Should().BeTrue();
}
[Fact]
public void WithSecretEvidence_FromProvider_AddsSignals()
{
var finding = CreateFinding("stellaops.secrets.private-key-rsa", "critical");
var provider = CreateMockProvider([finding]);
var context = SignalContext.Builder()
.WithSecretEvidence(provider)
.Build();
context.GetSignal<bool>("secret.has_finding").Should().BeTrue();
context.GetSignal<int>("secret.private_key.count").Should().Be(1);
}
[Fact]
public void CreateBuilderWithSecrets_ReturnsConfiguredBuilder()
{
var provider = CreateMockProvider([]);
var evidenceContext = new SecretEvidenceContext(provider);
var builder = SecretSignalContextExtensions.CreateBuilderWithSecrets(evidenceContext);
var context = builder.Build();
context.GetSignal<bool>("secret.has_finding").Should().BeFalse();
context.GetSignal<int>("secret.count").Should().Be(0);
}
[Fact]
public void CreateContextWithSecrets_ReturnsFullyConfiguredContext()
{
var findings = new[]
{
CreateFinding("rule1", "high"),
CreateFinding("rule2", "medium"),
};
var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 50);
var provider = CreateMockProvider(findings, bundle);
var evidenceContext = new SecretEvidenceContext(provider);
var context = SecretSignalContextExtensions.CreateContextWithSecrets(evidenceContext);
context.GetSignal<bool>("secret.has_finding").Should().BeTrue();
context.GetSignal<int>("secret.count").Should().Be(2);
context.GetSignal<string>("secret.bundle.id").Should().Be("bundle-1");
}
[Fact]
public void WithSecretEvidence_NullContext_ThrowsArgumentNullException()
{
SignalContext context = null!;
var provider = CreateMockProvider([]);
var evidenceContext = new SecretEvidenceContext(provider);
var action = () => context.WithSecretEvidence(evidenceContext);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void WithSecretEvidence_NullEvidenceContext_ThrowsArgumentNullException()
{
var context = new SignalContext();
SecretEvidenceContext evidenceContext = null!;
var action = () => context.WithSecretEvidence(evidenceContext);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void SignalContext_CanCombineSecretSignalsWithOtherSignals()
{
var finding = CreateFinding("rule1", "high");
var provider = CreateMockProvider([finding]);
var evidenceContext = new SecretEvidenceContext(provider);
var context = SignalContext.Builder()
.WithFlag("custom.flag", true)
.WithNumber("custom.score", 0.85m)
.WithSecretEvidence(evidenceContext)
.WithFinding("high", 0.9m, "CVE-2025-1234")
.Build();
// Custom signals preserved
context.GetSignal<bool>("custom.flag").Should().BeTrue();
context.GetSignal<decimal>("custom.score").Should().Be(0.85m);
// Secret signals added
context.GetSignal<bool>("secret.has_finding").Should().BeTrue();
// Other builder methods work
var finding2 = context.GetSignal("finding") as IDictionary<string, object?>;
finding2.Should().NotBeNull();
finding2!["severity"].Should().Be("high");
}
private static ISecretEvidenceProvider CreateMockProvider(
SecretFinding[] findings,
SecretBundleMetadata? bundle = null,
bool maskingApplied = true)
{
var mock = new Mock<ISecretEvidenceProvider>();
mock.Setup(p => p.GetFindings()).Returns(findings);
mock.Setup(p => p.GetBundleMetadata()).Returns(bundle);
mock.Setup(p => p.IsMaskingApplied()).Returns(maskingApplied);
return mock.Object;
}
private static SecretFinding CreateFinding(
string ruleId,
string severity,
string confidence = "high")
{
return new SecretFinding
{
RuleId = ruleId,
RuleVersion = "1.0.0",
Severity = severity,
Confidence = confidence,
FilePath = "test/file.txt",
LineNumber = 10,
Mask = "***REDACTED***",
BundleId = "bundle-1",
BundleVersion = "2025.01",
DetectedAt = DateTimeOffset.UtcNow,
};
}
}

View File

@@ -11,9 +11,10 @@
<!-- Disable Concelier test infra to avoid duplicate package references -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="FsCheck" />
<PackageReference Include="FsCheck.Xunit.v3" />
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>