update evidence bundle to include new evidence types and implement ProofSpine integration
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
namespace StellaOps.Policy.Suppression;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
/// <summary>
|
||||
/// Provider for checking policy suppression overrides (waivers).
|
||||
/// </summary>
|
||||
public interface ISuppressionOverrideProvider
|
||||
{
|
||||
bool HasActiveOverride(FindingKey findingKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple in-memory override provider for tests and local runs.
|
||||
/// </summary>
|
||||
public sealed class InMemorySuppressionOverrideProvider : ISuppressionOverrideProvider
|
||||
{
|
||||
private readonly ImmutableHashSet<FindingKey> _overrides;
|
||||
|
||||
public InMemorySuppressionOverrideProvider(IEnumerable<FindingKey>? overrides = null)
|
||||
{
|
||||
_overrides = overrides?.ToImmutableHashSet() ?? ImmutableHashSet<FindingKey>.Empty;
|
||||
}
|
||||
|
||||
public bool HasActiveOverride(FindingKey findingKey) => _overrides.Contains(findingKey);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
namespace StellaOps.Policy.Suppression;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates whether a finding should be suppressed based on the 4-condition rule.
|
||||
/// All conditions must be met for suppression:
|
||||
/// 1. reachable == false
|
||||
/// 2. vex_status == NOT_AFFECTED
|
||||
/// 3. kev == false
|
||||
/// 4. No policy override active
|
||||
/// </summary>
|
||||
public sealed class SuppressionRuleEvaluator
|
||||
{
|
||||
private readonly ISuppressionOverrideProvider _overrideProvider;
|
||||
|
||||
public SuppressionRuleEvaluator(ISuppressionOverrideProvider overrideProvider)
|
||||
{
|
||||
_overrideProvider = overrideProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates suppression for a single finding.
|
||||
/// </summary>
|
||||
public SuppressionResult Evaluate(SuppressionInput input)
|
||||
{
|
||||
var conditions = new List<SuppressionConditionResult>
|
||||
{
|
||||
EvaluateReachableCondition(input),
|
||||
EvaluateVexCondition(input),
|
||||
EvaluateKevCondition(input),
|
||||
EvaluateOverrideCondition(input),
|
||||
};
|
||||
|
||||
var shouldSuppress = conditions.All(c => c.Passed);
|
||||
|
||||
return new SuppressionResult(
|
||||
FindingKey: input.FindingKey,
|
||||
Suppressed: shouldSuppress,
|
||||
Conditions: conditions.ToImmutableArray(),
|
||||
Reason: shouldSuppress
|
||||
? "All 4 suppression conditions met"
|
||||
: $"Condition failed: {conditions.First(c => !c.Passed).ConditionName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates suppression for multiple findings (batch).
|
||||
/// </summary>
|
||||
public ImmutableArray<SuppressionResult> EvaluateBatch(IEnumerable<SuppressionInput> inputs)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inputs);
|
||||
return inputs.Select(Evaluate).ToImmutableArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates patch churn suppression: version changes with no material risk change.
|
||||
/// </summary>
|
||||
public SuppressionResult EvaluatePatchChurn(PatchChurnInput input)
|
||||
{
|
||||
var conditions = new List<SuppressionConditionResult>
|
||||
{
|
||||
new(
|
||||
ConditionName: "version_changed",
|
||||
Passed: input.VersionChanged,
|
||||
Reason: input.VersionChanged ? "Version changed" : "Version unchanged"),
|
||||
new(
|
||||
ConditionName: "not_in_affected_range",
|
||||
Passed: !input.WasInAffectedRange && !input.IsInAffectedRange,
|
||||
Reason: $"Was: {input.WasInAffectedRange}, Now: {input.IsInAffectedRange}"),
|
||||
new(
|
||||
ConditionName: "no_kev",
|
||||
Passed: !input.Kev,
|
||||
Reason: input.Kev ? "KEV flagged" : "Not KEV"),
|
||||
new(
|
||||
ConditionName: "no_policy_flip",
|
||||
Passed: !input.PolicyFlipped,
|
||||
Reason: input.PolicyFlipped ? "Policy changed" : "Policy unchanged"),
|
||||
};
|
||||
|
||||
var shouldSuppress = conditions.All(c => c.Passed);
|
||||
|
||||
return new SuppressionResult(
|
||||
FindingKey: input.FindingKey,
|
||||
Suppressed: shouldSuppress,
|
||||
Conditions: conditions.ToImmutableArray(),
|
||||
Reason: shouldSuppress ? "Patch churn - no material change" : "Material change detected");
|
||||
}
|
||||
|
||||
private SuppressionConditionResult EvaluateReachableCondition(SuppressionInput input)
|
||||
{
|
||||
var passed = input.Reachable == false;
|
||||
return new SuppressionConditionResult(
|
||||
ConditionName: "unreachable",
|
||||
Passed: passed,
|
||||
Reason: input.Reachable switch
|
||||
{
|
||||
null => "Reachability unknown",
|
||||
true => "Code is reachable",
|
||||
false => "Code is unreachable"
|
||||
});
|
||||
}
|
||||
|
||||
private static SuppressionConditionResult EvaluateVexCondition(SuppressionInput input)
|
||||
{
|
||||
var passed = input.VexStatus == VexStatus.NotAffected;
|
||||
return new SuppressionConditionResult(
|
||||
ConditionName: "vex_not_affected",
|
||||
Passed: passed,
|
||||
Reason: $"VEX status: {input.VexStatus}");
|
||||
}
|
||||
|
||||
private static SuppressionConditionResult EvaluateKevCondition(SuppressionInput input)
|
||||
{
|
||||
var passed = !input.Kev;
|
||||
return new SuppressionConditionResult(
|
||||
ConditionName: "not_kev",
|
||||
Passed: passed,
|
||||
Reason: input.Kev ? "Known Exploited Vulnerability" : "Not in KEV catalog");
|
||||
}
|
||||
|
||||
private SuppressionConditionResult EvaluateOverrideCondition(SuppressionInput input)
|
||||
{
|
||||
var hasOverride = _overrideProvider.HasActiveOverride(input.FindingKey);
|
||||
return new SuppressionConditionResult(
|
||||
ConditionName: "no_override",
|
||||
Passed: !hasOverride,
|
||||
Reason: hasOverride ? "Policy override active" : "No policy override");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input for suppression evaluation.
|
||||
/// </summary>
|
||||
public sealed record SuppressionInput(
|
||||
FindingKey FindingKey,
|
||||
bool? Reachable,
|
||||
VexStatus VexStatus,
|
||||
bool Kev);
|
||||
|
||||
/// <summary>
|
||||
/// Input for patch churn suppression evaluation.
|
||||
/// </summary>
|
||||
public sealed record PatchChurnInput(
|
||||
FindingKey FindingKey,
|
||||
bool VersionChanged,
|
||||
bool WasInAffectedRange,
|
||||
bool IsInAffectedRange,
|
||||
bool Kev,
|
||||
bool PolicyFlipped);
|
||||
|
||||
/// <summary>
|
||||
/// Result of suppression evaluation.
|
||||
/// </summary>
|
||||
public sealed record SuppressionResult(
|
||||
FindingKey FindingKey,
|
||||
bool Suppressed,
|
||||
ImmutableArray<SuppressionConditionResult> Conditions,
|
||||
string Reason);
|
||||
|
||||
/// <summary>
|
||||
/// Result of a single suppression condition.
|
||||
/// </summary>
|
||||
public sealed record SuppressionConditionResult(
|
||||
string ConditionName,
|
||||
bool Passed,
|
||||
string Reason);
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier for a vulnerability finding.
|
||||
/// </summary>
|
||||
public sealed record FindingKey(
|
||||
string ComponentPurl,
|
||||
string ComponentVersion,
|
||||
string CveId)
|
||||
{
|
||||
public override string ToString() => $"{ComponentPurl}@{ComponentVersion}:{CveId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX status values.
|
||||
/// </summary>
|
||||
public enum VexStatus
|
||||
{
|
||||
Unknown,
|
||||
Affected,
|
||||
NotAffected,
|
||||
Fixed,
|
||||
UnderInvestigation
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
using StellaOps.Policy.Suppression;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Suppression;
|
||||
|
||||
public sealed class SuppressionRuleEvaluatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Evaluate_Suppresses_WhenAllConditionsPass()
|
||||
{
|
||||
var key = CreateFindingKey();
|
||||
var evaluator = new SuppressionRuleEvaluator(new InMemorySuppressionOverrideProvider());
|
||||
|
||||
var result = evaluator.Evaluate(new SuppressionInput(
|
||||
FindingKey: key,
|
||||
Reachable: false,
|
||||
VexStatus: VexStatus.NotAffected,
|
||||
Kev: false));
|
||||
|
||||
Assert.True(result.Suppressed);
|
||||
Assert.All(result.Conditions, condition => Assert.True(condition.Passed, condition.ConditionName));
|
||||
Assert.Equal("All 4 suppression conditions met", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_DoesNotSuppress_WhenReachableIsTrue()
|
||||
{
|
||||
var key = CreateFindingKey();
|
||||
var evaluator = new SuppressionRuleEvaluator(new InMemorySuppressionOverrideProvider());
|
||||
|
||||
var result = evaluator.Evaluate(new SuppressionInput(
|
||||
FindingKey: key,
|
||||
Reachable: true,
|
||||
VexStatus: VexStatus.NotAffected,
|
||||
Kev: false));
|
||||
|
||||
Assert.False(result.Suppressed);
|
||||
Assert.Contains("unreachable", result.Reason, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_DoesNotSuppress_WhenReachableIsUnknown()
|
||||
{
|
||||
var key = CreateFindingKey();
|
||||
var evaluator = new SuppressionRuleEvaluator(new InMemorySuppressionOverrideProvider());
|
||||
|
||||
var result = evaluator.Evaluate(new SuppressionInput(
|
||||
FindingKey: key,
|
||||
Reachable: null,
|
||||
VexStatus: VexStatus.NotAffected,
|
||||
Kev: false));
|
||||
|
||||
Assert.False(result.Suppressed);
|
||||
Assert.Contains("unreachable", result.Reason, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_DoesNotSuppress_WhenVexIsNotNotAffected()
|
||||
{
|
||||
var key = CreateFindingKey();
|
||||
var evaluator = new SuppressionRuleEvaluator(new InMemorySuppressionOverrideProvider());
|
||||
|
||||
var result = evaluator.Evaluate(new SuppressionInput(
|
||||
FindingKey: key,
|
||||
Reachable: false,
|
||||
VexStatus: VexStatus.Affected,
|
||||
Kev: false));
|
||||
|
||||
Assert.False(result.Suppressed);
|
||||
Assert.Contains("vex_not_affected", result.Reason, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_DoesNotSuppress_WhenKev()
|
||||
{
|
||||
var key = CreateFindingKey();
|
||||
var evaluator = new SuppressionRuleEvaluator(new InMemorySuppressionOverrideProvider());
|
||||
|
||||
var result = evaluator.Evaluate(new SuppressionInput(
|
||||
FindingKey: key,
|
||||
Reachable: false,
|
||||
VexStatus: VexStatus.NotAffected,
|
||||
Kev: true));
|
||||
|
||||
Assert.False(result.Suppressed);
|
||||
Assert.Contains("not_kev", result.Reason, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_DoesNotSuppress_WhenOverrideActive()
|
||||
{
|
||||
var key = CreateFindingKey();
|
||||
var evaluator = new SuppressionRuleEvaluator(new InMemorySuppressionOverrideProvider(new[] { key }));
|
||||
|
||||
var result = evaluator.Evaluate(new SuppressionInput(
|
||||
FindingKey: key,
|
||||
Reachable: false,
|
||||
VexStatus: VexStatus.NotAffected,
|
||||
Kev: false));
|
||||
|
||||
Assert.False(result.Suppressed);
|
||||
Assert.Contains("no_override", result.Reason, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePatchChurn_Suppresses_WhenVersionChangesButNoMaterialChange()
|
||||
{
|
||||
var key = CreateFindingKey();
|
||||
var evaluator = new SuppressionRuleEvaluator(new InMemorySuppressionOverrideProvider());
|
||||
|
||||
var result = evaluator.EvaluatePatchChurn(new PatchChurnInput(
|
||||
FindingKey: key,
|
||||
VersionChanged: true,
|
||||
WasInAffectedRange: false,
|
||||
IsInAffectedRange: false,
|
||||
Kev: false,
|
||||
PolicyFlipped: false));
|
||||
|
||||
Assert.True(result.Suppressed);
|
||||
Assert.Equal("Patch churn - no material change", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePatchChurn_DoesNotSuppress_WhenInAffectedRange()
|
||||
{
|
||||
var key = CreateFindingKey();
|
||||
var evaluator = new SuppressionRuleEvaluator(new InMemorySuppressionOverrideProvider());
|
||||
|
||||
var result = evaluator.EvaluatePatchChurn(new PatchChurnInput(
|
||||
FindingKey: key,
|
||||
VersionChanged: true,
|
||||
WasInAffectedRange: false,
|
||||
IsInAffectedRange: true,
|
||||
Kev: false,
|
||||
PolicyFlipped: false));
|
||||
|
||||
Assert.False(result.Suppressed);
|
||||
}
|
||||
|
||||
private static FindingKey CreateFindingKey() => new(
|
||||
ComponentPurl: "pkg:nuget/Example.Component@1.0.0",
|
||||
ComponentVersion: "1.0.0",
|
||||
CveId: "CVE-2025-0001");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user