266 lines
9.2 KiB
C#
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,
|
|
[]);
|
|
}
|
|
}
|