diff --git a/src/Policy/__Libraries/StellaOps.Policy/Secrets/ISecretEvidenceProvider.cs b/src/Policy/__Libraries/StellaOps.Policy/Secrets/ISecretEvidenceProvider.cs
new file mode 100644
index 000000000..60a0dde62
--- /dev/null
+++ b/src/Policy/__Libraries/StellaOps.Policy/Secrets/ISecretEvidenceProvider.cs
@@ -0,0 +1,113 @@
+// -----------------------------------------------------------------------------
+// ISecretEvidenceProvider.cs
+// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration)
+// Task: PSD-001 - Define ISecretEvidenceProvider interface
+// -----------------------------------------------------------------------------
+
+using System.Collections.Immutable;
+
+namespace StellaOps.Policy.Secrets;
+
+///
+/// Provides secret leak evidence for policy evaluation.
+/// This interface abstracts the source of secret findings, allowing the policy
+/// engine to evaluate secret-related rules without direct dependency on Scanner.
+///
+public interface ISecretEvidenceProvider
+{
+ ///
+ /// Gets all secret findings for the current evaluation context.
+ ///
+ /// A read-only list of secret leak findings.
+ IReadOnlyList GetFindings();
+
+ ///
+ /// Gets the active rule bundle metadata, if available.
+ ///
+ /// Bundle metadata, or null if no bundle is loaded.
+ SecretBundleMetadata? GetBundleMetadata();
+
+ ///
+ /// Checks if masking was successfully applied to all findings.
+ ///
+ /// True if all findings have been properly masked.
+ bool IsMaskingApplied();
+}
+
+///
+/// A single secret leak finding for policy evaluation.
+/// This is a policy-side representation of secret detection results.
+///
+public sealed record SecretFinding
+{
+ ///
+ /// The rule ID that produced this finding (e.g., "stellaops.secrets.aws-access-key").
+ ///
+ public required string RuleId { get; init; }
+
+ ///
+ /// The rule version.
+ ///
+ public required string RuleVersion { get; init; }
+
+ ///
+ /// The severity of the finding: "low", "medium", "high", or "critical".
+ ///
+ public required string Severity { get; init; }
+
+ ///
+ /// The confidence level: "low", "medium", or "high".
+ ///
+ public required string Confidence { get; init; }
+
+ ///
+ /// The file path where the secret was found (relative to scan root).
+ ///
+ public required string FilePath { get; init; }
+
+ ///
+ /// The 1-based line number.
+ ///
+ public required int LineNumber { get; init; }
+
+ ///
+ /// The masked payload (e.g., "AKIA****B7"). Never contains the actual secret.
+ ///
+ public required string Mask { get; init; }
+
+ ///
+ /// The bundle ID that contained the rule.
+ ///
+ public required string BundleId { get; init; }
+
+ ///
+ /// The bundle version.
+ ///
+ public required string BundleVersion { get; init; }
+
+ ///
+ /// When this finding was detected.
+ ///
+ public required DateTimeOffset DetectedAt { get; init; }
+
+ ///
+ /// Whether masking was successfully applied to this finding.
+ ///
+ public bool MaskApplied { get; init; } = true;
+
+ ///
+ /// Additional metadata for the finding.
+ ///
+ public ImmutableDictionary Metadata { get; init; } =
+ ImmutableDictionary.Empty;
+}
+
+///
+/// Metadata about the active secret detection rule bundle.
+///
+public sealed record SecretBundleMetadata(
+ string BundleId,
+ string Version,
+ DateTimeOffset SignedAt,
+ int RuleCount,
+ string? SignerKeyId = null);
diff --git a/src/Policy/__Libraries/StellaOps.Policy/Secrets/SecretEvidenceContext.cs b/src/Policy/__Libraries/StellaOps.Policy/Secrets/SecretEvidenceContext.cs
new file mode 100644
index 000000000..036910fa9
--- /dev/null
+++ b/src/Policy/__Libraries/StellaOps.Policy/Secrets/SecretEvidenceContext.cs
@@ -0,0 +1,285 @@
+// -----------------------------------------------------------------------------
+// SecretEvidenceContext.cs
+// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration)
+// Task: PSD-002 - Implement SecretEvidenceContext binding
+// -----------------------------------------------------------------------------
+
+using System.Collections.Immutable;
+
+namespace StellaOps.Policy.Secrets;
+
+///
+/// Provides access to secret leak evidence for policy evaluation.
+/// This context wraps an ISecretEvidenceProvider and provides convenient
+/// accessors for policy rules to query secret findings.
+///
+public sealed class SecretEvidenceContext
+{
+ private readonly ISecretEvidenceProvider _provider;
+ private IReadOnlyList? _cachedFindings;
+
+ ///
+ /// Creates a new secret evidence context.
+ ///
+ /// The evidence provider.
+ public SecretEvidenceContext(ISecretEvidenceProvider provider)
+ {
+ _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ }
+
+ ///
+ /// Gets all secret findings.
+ ///
+ public IReadOnlyList Findings =>
+ _cachedFindings ??= _provider.GetFindings();
+
+ ///
+ /// Gets the active bundle metadata.
+ ///
+ public SecretBundleMetadata? Bundle => _provider.GetBundleMetadata();
+
+ ///
+ /// Gets whether masking was successfully applied to all findings.
+ ///
+ public bool MaskingApplied => _provider.IsMaskingApplied();
+
+ ///
+ /// Checks if any finding exists.
+ ///
+ public bool HasAnyFinding => Findings.Count > 0;
+
+ ///
+ /// Gets the count of all findings.
+ ///
+ public int FindingCount => Findings.Count;
+
+ ///
+ /// Checks if any finding matches the specified severity.
+ ///
+ /// The severity to match ("low", "medium", "high", "critical").
+ /// True if any finding matches.
+ public bool HasFindingWithSeverity(string severity)
+ {
+ return Findings.Any(f =>
+ f.Severity.Equals(severity, StringComparison.OrdinalIgnoreCase));
+ }
+
+ ///
+ /// Checks if any finding matches the specified confidence.
+ ///
+ /// The confidence to match ("low", "medium", "high").
+ /// True if any finding matches.
+ public bool HasFindingWithConfidence(string confidence)
+ {
+ return Findings.Any(f =>
+ f.Confidence.Equals(confidence, StringComparison.OrdinalIgnoreCase));
+ }
+
+ ///
+ /// Checks if any finding matches the specified rule ID pattern.
+ ///
+ /// The rule ID or pattern (supports * suffix glob).
+ /// True if any finding matches.
+ public bool HasFindingWithRuleId(string ruleIdPattern)
+ {
+ return Findings.Any(f => MatchesRuleId(f.RuleId, ruleIdPattern));
+ }
+
+ ///
+ /// Checks if any finding matches all specified criteria.
+ ///
+ /// Optional rule ID pattern.
+ /// Optional severity filter.
+ /// Optional confidence filter.
+ /// True if any finding matches all specified criteria.
+ public bool HasFinding(
+ string? ruleIdPattern = null,
+ string? severity = null,
+ string? confidence = null)
+ {
+ return Findings.Any(f =>
+ MatchesRuleId(f.RuleId, ruleIdPattern) &&
+ MatchesSeverity(f.Severity, severity) &&
+ MatchesConfidence(f.Confidence, confidence));
+ }
+
+ ///
+ /// Gets the count of findings matching the optional rule ID pattern.
+ ///
+ /// Optional rule ID pattern.
+ /// The count of matching findings.
+ public int GetMatchCount(string? ruleIdPattern = null)
+ {
+ if (string.IsNullOrEmpty(ruleIdPattern))
+ {
+ return Findings.Count;
+ }
+
+ return Findings.Count(f => MatchesRuleId(f.RuleId, ruleIdPattern));
+ }
+
+ ///
+ /// Checks if the bundle version meets or exceeds the required version.
+ ///
+ /// The minimum required version (YYYY.MM format).
+ /// True if the bundle version meets the requirement.
+ public bool BundleVersionMeetsRequirement(string requiredVersion)
+ {
+ var bundle = Bundle;
+ if (bundle is null)
+ {
+ return false;
+ }
+
+ return string.Compare(bundle.Version, requiredVersion, StringComparison.Ordinal) >= 0;
+ }
+
+ ///
+ /// Checks if all findings are in paths matching the allowlist patterns.
+ ///
+ /// Glob patterns for allowed paths.
+ /// True if all findings are in allowed paths, or no findings exist.
+ public bool AllFindingsInAllowlist(IReadOnlyList patterns)
+ {
+ if (Findings.Count == 0)
+ {
+ return true;
+ }
+
+ return Findings.All(f => patterns.Any(p => MatchesGlob(f.FilePath, p)));
+ }
+
+ private static bool MatchesRuleId(string ruleId, string? pattern)
+ {
+ if (string.IsNullOrEmpty(pattern))
+ {
+ return true;
+ }
+
+ if (pattern.EndsWith('*'))
+ {
+ return ruleId.StartsWith(pattern[..^1], StringComparison.Ordinal);
+ }
+
+ return ruleId.Equals(pattern, StringComparison.Ordinal);
+ }
+
+ private static bool MatchesSeverity(string severity, string? filter)
+ {
+ if (string.IsNullOrEmpty(filter))
+ {
+ return true;
+ }
+
+ return severity.Equals(filter, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool MatchesConfidence(string confidence, string? filter)
+ {
+ if (string.IsNullOrEmpty(filter))
+ {
+ return true;
+ }
+
+ return confidence.Equals(filter, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool MatchesGlob(string path, string pattern)
+ {
+ // Simple glob matching supporting ** and *
+ // Normalize path separators
+ var normalizedPath = path.Replace('\\', '/');
+ var normalizedPattern = pattern.Replace('\\', '/');
+
+ return MatchesGlobRecursive(normalizedPath, normalizedPattern);
+ }
+
+ private static bool MatchesGlobRecursive(string path, string pattern)
+ {
+ if (pattern == "**")
+ {
+ return true;
+ }
+
+ if (pattern.StartsWith("**/"))
+ {
+ var suffix = pattern[3..];
+ // Match from any position
+ var segments = path.Split('/');
+ for (var i = 0; i < segments.Length; i++)
+ {
+ var remaining = string.Join('/', segments.Skip(i));
+ if (MatchesGlobRecursive(remaining, suffix))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ if (pattern.Contains("/**/"))
+ {
+ var parts = pattern.Split("/**/", 2);
+ if (!MatchesGlobSegment(path.Split('/').FirstOrDefault() ?? "", parts[0]))
+ {
+ return false;
+ }
+ return MatchesGlobRecursive(path, "**/" + parts[1]);
+ }
+
+ // Simple segment matching with * wildcard
+ return MatchesGlobSegment(path, pattern);
+ }
+
+ private static bool MatchesGlobSegment(string path, string pattern)
+ {
+ if (!pattern.Contains('*'))
+ {
+ return path.Equals(pattern, StringComparison.OrdinalIgnoreCase);
+ }
+
+ // Convert glob pattern to regex-like matching
+ var patternIndex = 0;
+ var pathIndex = 0;
+
+ while (patternIndex < pattern.Length && pathIndex < path.Length)
+ {
+ if (pattern[patternIndex] == '*')
+ {
+ patternIndex++;
+ if (patternIndex >= pattern.Length)
+ {
+ return true; // Trailing * matches everything
+ }
+
+ // Find next non-* character to match
+ while (pathIndex < path.Length)
+ {
+ if (MatchesGlobSegment(path[pathIndex..], pattern[patternIndex..]))
+ {
+ return true;
+ }
+ pathIndex++;
+ }
+ return false;
+ }
+
+ if (!char.Equals(char.ToLowerInvariant(pattern[patternIndex]),
+ char.ToLowerInvariant(path[pathIndex])))
+ {
+ return false;
+ }
+
+ patternIndex++;
+ pathIndex++;
+ }
+
+ // Handle remaining pattern (should be all *)
+ while (patternIndex < pattern.Length && pattern[patternIndex] == '*')
+ {
+ patternIndex++;
+ }
+
+ return patternIndex >= pattern.Length && pathIndex >= path.Length;
+ }
+}