214 lines
6.7 KiB
C#
214 lines
6.7 KiB
C#
using System.Collections.Immutable;
|
|
using StellaOps.Concelier.SbomIntegration.Models;
|
|
|
|
namespace StellaOps.Scanner.Reachability.Dependencies;
|
|
|
|
/// <summary>
|
|
/// Static graph traversal reachability for dependency graphs.
|
|
/// </summary>
|
|
public sealed class StaticReachabilityAnalyzer
|
|
{
|
|
public ReachabilityReport Analyze(
|
|
DependencyGraph graph,
|
|
ImmutableArray<string> entryPoints,
|
|
ReachabilityPolicy? policy = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(graph);
|
|
|
|
var resolvedPolicy = policy ?? new ReachabilityPolicy();
|
|
var results = new Dictionary<string, ReachabilityStatus>(StringComparer.Ordinal);
|
|
var findings = new List<ReachabilityFinding>();
|
|
|
|
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<string>(StringComparer.Ordinal);
|
|
var potential = new HashSet<string>(StringComparer.Ordinal);
|
|
var predecessor = new Dictionary<string, string?>(StringComparer.Ordinal);
|
|
var pathKind = new Dictionary<string, PathKind>(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<string> BuildPath(
|
|
string target,
|
|
IReadOnlyDictionary<string, string?> predecessor)
|
|
{
|
|
var path = new List<string>();
|
|
var current = target;
|
|
var seen = new HashSet<string>(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
|
|
}
|
|
}
|