// 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; /// /// Default implementation of the propagation rule engine. /// public sealed class PropagationRuleEngine : IPropagationRuleEngine { private readonly ImmutableArray _rules; /// /// Creates a new PropagationRuleEngine with default rules. /// public PropagationRuleEngine() : this(GetDefaultRules()) { } /// /// Creates a new PropagationRuleEngine with specified rules. /// public PropagationRuleEngine(IEnumerable rules) { _rules = rules.OrderBy(r => r.Priority).ToImmutableArray(); } /// public PropagationResult Propagate( ComponentVerdict componentVerdict, IDependencyGraph graph, PropagationPolicy policy) { ArgumentNullException.ThrowIfNull(componentVerdict); ArgumentNullException.ThrowIfNull(graph); ArgumentNullException.ThrowIfNull(policy); var ruleResults = new List(); var analyzedPaths = new List(); 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); } /// public IReadOnlyList GetRules() => _rules; /// /// Gets the default set of propagation rules. /// public static IEnumerable GetDefaultRules() { yield return new DirectDependencyAffectedRule(); yield return new TransitiveDependencyRule(); yield return new DependencyFixedRule(); yield return new DependencyNotAffectedRule(); } } /// /// Rule: If direct dependency is affected, product inherits affected unless overridden. /// 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(); 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()); } } /// /// Rule: If transitive dependency is affected, flag for review but don't auto-inherit. /// 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); } } /// /// Rule: If dependency was affected but is now fixed, allow product NotAffected if vulnerable code was removed. /// 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, []); } } /// /// Rule: If dependency is not_affected, product may inherit if dependency is leaf. /// 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, []); } }