415 lines
12 KiB
C#
415 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace StellaOps.Scanner.Reachability.Ordering;
|
|
|
|
public sealed class DeterministicGraphOrderer : IGraphOrderer
|
|
{
|
|
public IReadOnlyList<string> OrderNodes(
|
|
RichGraph graph,
|
|
GraphOrderingStrategy strategy = GraphOrderingStrategy.TopologicalLexicographic)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(graph);
|
|
|
|
return strategy switch
|
|
{
|
|
GraphOrderingStrategy.TopologicalLexicographic => TopologicalLexicographicOrder(graph),
|
|
GraphOrderingStrategy.BreadthFirstLexicographic => BreadthFirstLexicographicOrder(graph),
|
|
GraphOrderingStrategy.DepthFirstLexicographic => DepthFirstLexicographicOrder(graph),
|
|
GraphOrderingStrategy.Lexicographic => LexicographicOrder(graph),
|
|
_ => TopologicalLexicographicOrder(graph)
|
|
};
|
|
}
|
|
|
|
public IReadOnlyList<RichGraphEdge> OrderEdges(
|
|
RichGraph graph,
|
|
IReadOnlyList<string> nodeOrder)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(graph);
|
|
ArgumentNullException.ThrowIfNull(nodeOrder);
|
|
|
|
var index = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
for (var i = 0; i < nodeOrder.Count; i++)
|
|
{
|
|
index[nodeOrder[i]] = i;
|
|
}
|
|
|
|
return graph.Edges
|
|
.Where(e => index.ContainsKey(e.From) && index.ContainsKey(e.To))
|
|
.OrderBy(e => index[e.From])
|
|
.ThenBy(e => index[e.To])
|
|
.ThenBy(e => e.Kind, StringComparer.Ordinal)
|
|
.ThenBy(e => e.Purl, StringComparer.Ordinal)
|
|
.ThenBy(e => e.SymbolDigest, StringComparer.Ordinal)
|
|
.ToList();
|
|
}
|
|
|
|
public CanonicalGraph Canonicalize(
|
|
RichGraph graph,
|
|
GraphOrderingStrategy strategy = GraphOrderingStrategy.TopologicalLexicographic)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(graph);
|
|
|
|
var nodeById = graph.Nodes
|
|
.Where(n => !string.IsNullOrWhiteSpace(n.Id))
|
|
.GroupBy(n => n.Id, StringComparer.Ordinal)
|
|
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
|
|
|
var nodeOrder = OrderNodes(graph, strategy);
|
|
var edges = OrderEdges(graph, nodeOrder);
|
|
|
|
var index = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
for (var i = 0; i < nodeOrder.Count; i++)
|
|
{
|
|
index[nodeOrder[i]] = i;
|
|
}
|
|
|
|
var nodes = nodeOrder
|
|
.Select(id =>
|
|
{
|
|
nodeById.TryGetValue(id, out var node);
|
|
return new CanonicalNode
|
|
{
|
|
Index = index[id],
|
|
Id = id,
|
|
NodeType = node?.Kind ?? "unknown",
|
|
Label = node?.Display
|
|
};
|
|
})
|
|
.ToList();
|
|
|
|
var canonicalEdges = edges
|
|
.Select(e => new CanonicalEdge
|
|
{
|
|
SourceIndex = index[e.From],
|
|
TargetIndex = index[e.To],
|
|
EdgeType = e.Kind
|
|
})
|
|
.ToList();
|
|
|
|
var anchors = FindAnchorNodes(graph, nodeOrder);
|
|
|
|
return new CanonicalGraph
|
|
{
|
|
Strategy = strategy,
|
|
Nodes = nodes,
|
|
Edges = canonicalEdges,
|
|
ContentHash = ComputeCanonicalHash(nodes, canonicalEdges),
|
|
AnchorNodes = anchors
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyList<string> TopologicalLexicographicOrder(RichGraph graph)
|
|
{
|
|
var nodes = graph.Nodes.Select(n => n.Id).Where(id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).ToList();
|
|
nodes.Sort(StringComparer.Ordinal);
|
|
|
|
var adjacency = nodes.ToDictionary(n => n, _ => new List<string>(), StringComparer.Ordinal);
|
|
var indegree = nodes.ToDictionary(n => n, _ => 0, StringComparer.Ordinal);
|
|
|
|
foreach (var edge in graph.Edges)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(edge.From) || string.IsNullOrWhiteSpace(edge.To))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!adjacency.TryGetValue(edge.From, out var neighbors) || !indegree.ContainsKey(edge.To))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
neighbors.Add(edge.To);
|
|
indegree[edge.To] = indegree[edge.To] + 1;
|
|
}
|
|
|
|
foreach (var list in adjacency.Values)
|
|
{
|
|
list.Sort(StringComparer.Ordinal);
|
|
}
|
|
|
|
var ready = new SortedSet<string>(indegree.Where(kv => kv.Value == 0).Select(kv => kv.Key), StringComparer.Ordinal);
|
|
var result = new List<string>(nodes.Count);
|
|
|
|
while (ready.Count > 0)
|
|
{
|
|
var next = ready.Min!;
|
|
ready.Remove(next);
|
|
result.Add(next);
|
|
|
|
foreach (var neighbor in adjacency[next])
|
|
{
|
|
indegree[neighbor] = indegree[neighbor] - 1;
|
|
if (indegree[neighbor] == 0)
|
|
{
|
|
ready.Add(neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result.Count == nodes.Count)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
var seen = new HashSet<string>(result, StringComparer.Ordinal);
|
|
var remainder = nodes
|
|
.Where(n => !seen.Contains(n))
|
|
.OrderBy(n => n, StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
result.AddRange(remainder);
|
|
return result;
|
|
}
|
|
|
|
private static IReadOnlyList<string> BreadthFirstLexicographicOrder(RichGraph graph)
|
|
{
|
|
var ordered = new List<string>();
|
|
var visited = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
var adjacency = BuildAdjacency(graph);
|
|
var entries = FindEntryPoints(graph).OrderBy(e => e, StringComparer.Ordinal).ToList();
|
|
var queue = new Queue<string>(entries);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var current = queue.Dequeue();
|
|
if (!visited.Add(current))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ordered.Add(current);
|
|
|
|
if (!adjacency.TryGetValue(current, out var neighbors))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var neighbor in neighbors)
|
|
{
|
|
if (!visited.Contains(neighbor))
|
|
{
|
|
queue.Enqueue(neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Append disconnected nodes deterministically
|
|
foreach (var nodeId in graph.Nodes.Select(n => n.Id).Distinct(StringComparer.Ordinal).OrderBy(n => n, StringComparer.Ordinal))
|
|
{
|
|
if (visited.Add(nodeId))
|
|
{
|
|
ordered.Add(nodeId);
|
|
}
|
|
}
|
|
|
|
return ordered;
|
|
}
|
|
|
|
private static IReadOnlyList<string> DepthFirstLexicographicOrder(RichGraph graph)
|
|
{
|
|
var ordered = new List<string>();
|
|
var visited = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
var adjacency = BuildAdjacency(graph);
|
|
var entries = FindEntryPoints(graph).OrderBy(e => e, StringComparer.Ordinal).ToList();
|
|
|
|
foreach (var entry in entries)
|
|
{
|
|
DfsVisit(entry, adjacency, visited, ordered);
|
|
}
|
|
|
|
// Append disconnected nodes deterministically
|
|
foreach (var nodeId in graph.Nodes.Select(n => n.Id).Distinct(StringComparer.Ordinal).OrderBy(n => n, StringComparer.Ordinal))
|
|
{
|
|
DfsVisit(nodeId, adjacency, visited, ordered);
|
|
}
|
|
|
|
return ordered;
|
|
}
|
|
|
|
private static void DfsVisit(
|
|
string node,
|
|
IReadOnlyDictionary<string, IReadOnlyList<string>> adjacency,
|
|
HashSet<string> visited,
|
|
List<string> result)
|
|
{
|
|
if (!visited.Add(node))
|
|
{
|
|
return;
|
|
}
|
|
|
|
result.Add(node);
|
|
|
|
if (!adjacency.TryGetValue(node, out var neighbors))
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var neighbor in neighbors)
|
|
{
|
|
DfsVisit(neighbor, adjacency, visited, result);
|
|
}
|
|
}
|
|
|
|
private static IReadOnlyList<string> LexicographicOrder(RichGraph graph)
|
|
{
|
|
return graph.Nodes
|
|
.Select(n => n.Id)
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.OrderBy(id => id, StringComparer.Ordinal)
|
|
.ToList();
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, IReadOnlyList<string>> BuildAdjacency(RichGraph graph)
|
|
{
|
|
var adjacency = new Dictionary<string, List<string>>(StringComparer.Ordinal);
|
|
|
|
foreach (var edge in graph.Edges)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(edge.From) || string.IsNullOrWhiteSpace(edge.To))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!adjacency.TryGetValue(edge.From, out var list))
|
|
{
|
|
list = new List<string>();
|
|
adjacency[edge.From] = list;
|
|
}
|
|
|
|
list.Add(edge.To);
|
|
}
|
|
|
|
// Deterministic neighbor traversal
|
|
foreach (var list in adjacency.Values)
|
|
{
|
|
list.Sort(StringComparer.Ordinal);
|
|
}
|
|
|
|
return adjacency.ToDictionary(kv => kv.Key, kv => (IReadOnlyList<string>)kv.Value, StringComparer.Ordinal);
|
|
}
|
|
|
|
private static IReadOnlyList<string> FindEntryPoints(RichGraph graph)
|
|
{
|
|
var nodeIds = graph.Nodes.Select(n => n.Id).Where(id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal).ToList();
|
|
var inbound = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
foreach (var edge in graph.Edges)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(edge.To))
|
|
{
|
|
inbound.Add(edge.To);
|
|
}
|
|
}
|
|
|
|
var roots = (graph.Roots ?? Array.Empty<RichGraphRoot>())
|
|
.Select(r => r.Id)
|
|
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
.ToList();
|
|
|
|
var semanticEntrypoints = graph.Nodes
|
|
.Where(IsEntrypointNode)
|
|
.Select(n => n.Id)
|
|
.ToList();
|
|
|
|
var entryPoints = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var root in roots)
|
|
{
|
|
entryPoints.Add(root);
|
|
}
|
|
|
|
foreach (var entry in semanticEntrypoints)
|
|
{
|
|
entryPoints.Add(entry);
|
|
}
|
|
|
|
foreach (var nodeId in nodeIds)
|
|
{
|
|
if (!inbound.Contains(nodeId))
|
|
{
|
|
entryPoints.Add(nodeId);
|
|
}
|
|
}
|
|
|
|
return entryPoints.OrderBy(id => id, StringComparer.Ordinal).ToList();
|
|
}
|
|
|
|
private static IReadOnlyList<string>? FindAnchorNodes(RichGraph graph, IReadOnlyList<string> nodeOrder)
|
|
{
|
|
var anchors = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
foreach (var root in graph.Roots ?? Array.Empty<RichGraphRoot>())
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(root.Id))
|
|
{
|
|
anchors.Add(root.Id);
|
|
}
|
|
}
|
|
|
|
foreach (var node in graph.Nodes)
|
|
{
|
|
if (IsEntrypointNode(node))
|
|
{
|
|
anchors.Add(node.Id);
|
|
}
|
|
}
|
|
|
|
if (anchors.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return nodeOrder.Where(anchors.Contains).ToList();
|
|
}
|
|
|
|
private static string ComputeCanonicalHash(
|
|
IReadOnlyList<CanonicalNode> nodes,
|
|
IReadOnlyList<CanonicalEdge> edges)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
foreach (var node in nodes)
|
|
{
|
|
sb.Append("N:");
|
|
sb.Append(node.Index);
|
|
sb.Append(':');
|
|
sb.Append(node.Id);
|
|
sb.Append(':');
|
|
sb.Append(node.NodeType);
|
|
sb.Append(';');
|
|
}
|
|
|
|
foreach (var edge in edges)
|
|
{
|
|
sb.Append("E:");
|
|
sb.Append(edge.SourceIndex);
|
|
sb.Append(':');
|
|
sb.Append(edge.TargetIndex);
|
|
sb.Append(':');
|
|
sb.Append(edge.EdgeType);
|
|
sb.Append(';');
|
|
}
|
|
|
|
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
|
var hash = SHA256.HashData(bytes);
|
|
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
|
}
|
|
|
|
private static bool IsEntrypointNode(RichGraphNode node)
|
|
{
|
|
if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.IsEntrypoint, out var value) != true ||
|
|
string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return bool.TryParse(value, out var result) && result;
|
|
}
|
|
}
|