using System.Collections.Immutable; using StellaOps.Concelier.SbomIntegration.Models; namespace StellaOps.Scanner.Reachability.Dependencies; /// /// Static graph traversal reachability for dependency graphs. /// public sealed class StaticReachabilityAnalyzer { public ReachabilityReport Analyze( DependencyGraph graph, ImmutableArray entryPoints, ReachabilityPolicy? policy = null) { ArgumentNullException.ThrowIfNull(graph); var resolvedPolicy = policy ?? new ReachabilityPolicy(); var results = new Dictionary(StringComparer.Ordinal); var findings = new List(); if (entryPoints.IsDefaultOrEmpty) { foreach (var node in graph.Nodes) { results[node] = ReachabilityStatus.Unknown; findings.Add(new ReachabilityFinding { ComponentRef = node, Status = ReachabilityStatus.Unknown, Reason = "no-entrypoints" }); } return ReachabilityReportBuilder.Build(graph, results, findings); } var reachable = new HashSet(StringComparer.Ordinal); var potential = new HashSet(StringComparer.Ordinal); var predecessor = new Dictionary(StringComparer.Ordinal); var pathKind = new Dictionary(StringComparer.Ordinal); var queue = new Queue<(string Node, PathKind Kind)>(); foreach (var entry in entryPoints) { if (string.IsNullOrWhiteSpace(entry)) { continue; } var normalized = entry.Trim(); if (reachable.Add(normalized)) { predecessor[normalized] = null; pathKind[normalized] = PathKind.Required; queue.Enqueue((normalized, PathKind.Required)); } } while (queue.Count > 0) { var (node, kind) = queue.Dequeue(); if (!graph.Edges.TryGetValue(node, out var edges)) { continue; } foreach (var edge in edges) { if (!IsScopeIncluded(edge.Scope, resolvedPolicy.ScopeHandling)) { continue; } var nextKind = Combine(kind, edge.Scope, resolvedPolicy.ScopeHandling); var target = edge.To; if (nextKind == PathKind.Required) { if (reachable.Add(target)) { predecessor[target] = node; pathKind[target] = PathKind.Required; queue.Enqueue((target, PathKind.Required)); } else if (pathKind.TryGetValue(target, out var existing) && existing == PathKind.Optional) { predecessor[target] = node; pathKind[target] = PathKind.Required; queue.Enqueue((target, PathKind.Required)); } } else { if (reachable.Contains(target)) { continue; } if (potential.Add(target)) { predecessor[target] = node; pathKind[target] = PathKind.Optional; queue.Enqueue((target, PathKind.Optional)); } } } } foreach (var node in graph.Nodes) { if (reachable.Contains(node)) { results[node] = ReachabilityStatus.Reachable; findings.Add(new ReachabilityFinding { ComponentRef = node, Status = ReachabilityStatus.Reachable, Path = BuildPath(node, predecessor) }); continue; } if (potential.Contains(node)) { results[node] = ReachabilityStatus.PotentiallyReachable; findings.Add(new ReachabilityFinding { ComponentRef = node, Status = ReachabilityStatus.PotentiallyReachable, Path = BuildPath(node, predecessor), Reason = "optional-dependency" }); continue; } results[node] = ReachabilityStatus.Unreachable; findings.Add(new ReachabilityFinding { ComponentRef = node, Status = ReachabilityStatus.Unreachable, Reason = "no-path" }); } return ReachabilityReportBuilder.Build(graph, results, findings); } private static ImmutableArray BuildPath( string target, IReadOnlyDictionary predecessor) { var path = new List(); var current = target; var seen = new HashSet(StringComparer.Ordinal); while (true) { if (!seen.Add(current)) { break; } path.Add(current); if (!predecessor.TryGetValue(current, out var previous) || string.IsNullOrWhiteSpace(previous)) { break; } current = previous; } path.Reverse(); return path.ToImmutableArray(); } private static bool IsScopeIncluded(DependencyScope scope, ReachabilityScopePolicy policy) { return scope switch { DependencyScope.Runtime => policy.IncludeRuntime, DependencyScope.Development => policy.IncludeDevelopment, DependencyScope.Test => policy.IncludeTest, DependencyScope.Optional => policy.IncludeOptional != OptionalDependencyHandling.Exclude, _ => true }; } private static PathKind Combine( PathKind current, DependencyScope scope, ReachabilityScopePolicy policy) { if (scope != DependencyScope.Optional) { return current; } return policy.IncludeOptional switch { OptionalDependencyHandling.Reachable => current, OptionalDependencyHandling.AsPotentiallyReachable => PathKind.Optional, _ => current }; } private enum PathKind { Required, Optional } }