Files
git.stella-ops.org/src/VexLens/StellaOps.VexLens/Propagation/PropagationRuleEngine.cs

266 lines
9.2 KiB
C#

// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Immutable;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Proof;
namespace StellaOps.VexLens.Propagation;
/// <summary>
/// Default implementation of the propagation rule engine.
/// </summary>
public sealed class PropagationRuleEngine : IPropagationRuleEngine
{
private readonly ImmutableArray<PropagationRule> _rules;
/// <summary>
/// Creates a new PropagationRuleEngine with default rules.
/// </summary>
public PropagationRuleEngine() : this(GetDefaultRules())
{
}
/// <summary>
/// Creates a new PropagationRuleEngine with specified rules.
/// </summary>
public PropagationRuleEngine(IEnumerable<PropagationRule> rules)
{
_rules = rules.OrderBy(r => r.Priority).ToImmutableArray();
}
/// <inheritdoc />
public PropagationResult Propagate(
ComponentVerdict componentVerdict,
IDependencyGraph graph,
PropagationPolicy policy)
{
ArgumentNullException.ThrowIfNull(componentVerdict);
ArgumentNullException.ThrowIfNull(graph);
ArgumentNullException.ThrowIfNull(policy);
var ruleResults = new List<PropagationRuleResult>();
var analyzedPaths = new List<DependencyPath>();
VexStatus? inheritedStatus = null;
var overrideApplied = false;
string? overrideReason = null;
var anyTriggered = false;
// Analyze dependency paths
var paths = graph.GetPathsTo(componentVerdict.ComponentKey);
foreach (var path in paths)
{
// Skip excluded scopes
if (policy.ExcludedScopes.Contains(path.Scope))
{
continue;
}
// Skip if beyond max depth
if (path.Depth > policy.MaxTransitiveDepth)
{
continue;
}
analyzedPaths.Add(path);
}
// Evaluate rules in priority order
foreach (var rule in _rules)
{
var result = rule.Evaluate(componentVerdict, graph, policy);
ruleResults.Add(result);
if (result.Triggered)
{
anyTriggered = true;
// First triggered rule with an effect wins
if (inheritedStatus is null && !string.IsNullOrEmpty(result.Effect))
{
// Parse effect to determine inherited status
if (result.Effect.Contains("affected", StringComparison.OrdinalIgnoreCase))
{
inheritedStatus = VexStatus.Affected;
}
else if (result.Effect.Contains("not_affected", StringComparison.OrdinalIgnoreCase))
{
inheritedStatus = VexStatus.NotAffected;
}
else if (result.Effect.Contains("fixed", StringComparison.OrdinalIgnoreCase))
{
inheritedStatus = VexStatus.Fixed;
}
}
// Check for override
if (result.Effect?.Contains("override", StringComparison.OrdinalIgnoreCase) == true)
{
overrideApplied = true;
overrideReason = result.Effect;
}
}
}
return new PropagationResult(
anyTriggered,
ruleResults.ToImmutableArray(),
analyzedPaths.ToImmutableArray(),
inheritedStatus,
overrideApplied,
overrideReason);
}
/// <inheritdoc />
public IReadOnlyList<PropagationRule> GetRules() => _rules;
/// <summary>
/// Gets the default set of propagation rules.
/// </summary>
public static IEnumerable<PropagationRule> GetDefaultRules()
{
yield return new DirectDependencyAffectedRule();
yield return new TransitiveDependencyRule();
yield return new DependencyFixedRule();
yield return new DependencyNotAffectedRule();
}
}
/// <summary>
/// Rule: If direct dependency is affected, product inherits affected unless overridden.
/// </summary>
public sealed class DirectDependencyAffectedRule : PropagationRule
{
public override string RuleId => "direct-dependency-affected";
public override string Description => "If direct dependency is affected, product inherits affected unless product-level override";
public override int Priority => 10;
public override PropagationRuleResult Evaluate(
ComponentVerdict verdict,
IDependencyGraph graph,
PropagationPolicy policy)
{
if (!policy.InheritAffectedFromDirectDependency)
{
return new PropagationRuleResult(RuleId, Description, false, null, []);
}
// Check if any direct dependency is affected
var directDeps = graph.GetDirectDependencies(verdict.ComponentKey).ToList();
var affectedComponents = new List<string>();
foreach (var dep in directDeps)
{
if (dep.PathType == DependencyPathType.DirectDependency)
{
// In a real implementation, we would look up the verdict for the dependency
// For now, we track the dependency for potential impact
affectedComponents.Add(dep.To);
}
}
// This rule triggers when the component's own verdict is affected and it has direct dependencies
var triggered = verdict.Status == VexStatus.Affected && affectedComponents.Count > 0;
return new PropagationRuleResult(
RuleId,
Description,
triggered,
triggered ? "Product inherits affected status from direct dependency" : null,
affectedComponents.ToImmutableArray());
}
}
/// <summary>
/// Rule: If transitive dependency is affected, flag for review but don't auto-inherit.
/// </summary>
public sealed class TransitiveDependencyRule : PropagationRule
{
public override string RuleId => "transitive-dependency-affected";
public override string Description => "If transitive dependency is affected, flag for review but don't auto-inherit";
public override int Priority => 20;
public override PropagationRuleResult Evaluate(
ComponentVerdict verdict,
IDependencyGraph graph,
PropagationPolicy policy)
{
if (!policy.EnableTransitivePropagation)
{
return new PropagationRuleResult(RuleId, Description, false, null, []);
}
var paths = graph.GetPathsTo(verdict.ComponentKey).ToList();
var transitivePaths = paths
.Where(p => p.PathType == DependencyPathType.TransitiveDependency)
.Where(p => p.Depth <= policy.MaxTransitiveDepth)
.ToList();
var triggered = verdict.Status == VexStatus.Affected && transitivePaths.Count > 0;
var affectedComponents = transitivePaths.Select(p => p.Root).Distinct().ToImmutableArray();
return new PropagationRuleResult(
RuleId,
Description,
triggered,
triggered ? "Transitive dependency is affected - flagged for review" : null,
affectedComponents);
}
}
/// <summary>
/// Rule: If dependency was affected but is now fixed, allow product NotAffected if vulnerable code was removed.
/// </summary>
public sealed class DependencyFixedRule : PropagationRule
{
public override string RuleId => "dependency-fixed";
public override string Description => "If dependency was affected but is now fixed, allow product NotAffected if vulnerable code was removed";
public override int Priority => 30;
public override PropagationRuleResult Evaluate(
ComponentVerdict verdict,
IDependencyGraph graph,
PropagationPolicy policy)
{
// This rule triggers when a dependency is now fixed
var triggered = verdict.Status == VexStatus.Fixed;
return new PropagationRuleResult(
RuleId,
Description,
triggered,
triggered ? "Dependency is fixed - product may be not_affected with override" : null,
[]);
}
}
/// <summary>
/// Rule: If dependency is not_affected, product may inherit if dependency is leaf.
/// </summary>
public sealed class DependencyNotAffectedRule : PropagationRule
{
public override string RuleId => "dependency-not-affected";
public override string Description => "If dependency is not_affected, product may inherit if dependency is leaf";
public override int Priority => 40;
public override PropagationRuleResult Evaluate(
ComponentVerdict verdict,
IDependencyGraph graph,
PropagationPolicy policy)
{
if (!policy.InheritNotAffectedFromLeafDependency)
{
return new PropagationRuleResult(RuleId, Description, false, null, []);
}
var isLeaf = graph.IsLeaf(verdict.ComponentKey);
var triggered = verdict.Status == VexStatus.NotAffected && isLeaf;
return new PropagationRuleResult(
RuleId,
Description,
triggered,
triggered ? "Leaf dependency is not_affected - dependents may inherit" : null,
[]);
}
}