Add Authority Advisory AI and API Lifecycle Configuration
- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings. - Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations. - Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration. - Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options. - Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations. - Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client. - Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Runtime;
|
||||
|
||||
public sealed class EntryTraceRuntimeReconciler
|
||||
{
|
||||
private static readonly HashSet<string> WrapperNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"tini",
|
||||
"tini-static",
|
||||
"tini64",
|
||||
"dumb-init",
|
||||
"dumbinit",
|
||||
"gosu",
|
||||
"su-exec",
|
||||
"chpst",
|
||||
"s6-supervise",
|
||||
"s6-svscan",
|
||||
"s6-svscanctl",
|
||||
"s6-rc-init",
|
||||
"runsv",
|
||||
"runsvdir",
|
||||
"supervisord",
|
||||
"sh",
|
||||
"bash",
|
||||
"dash",
|
||||
"ash",
|
||||
"env"
|
||||
};
|
||||
|
||||
public EntryTraceGraph Reconcile(EntryTraceGraph graph, ProcGraph? procGraph)
|
||||
{
|
||||
if (graph is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(graph));
|
||||
}
|
||||
|
||||
var diagnostics = graph.Diagnostics.ToBuilder();
|
||||
diagnostics.RemoveAll(d =>
|
||||
d.Reason is EntryTraceUnknownReason.RuntimeSnapshotUnavailable
|
||||
or EntryTraceUnknownReason.RuntimeProcessNotFound
|
||||
or EntryTraceUnknownReason.RuntimeMatch
|
||||
or EntryTraceUnknownReason.RuntimeMismatch);
|
||||
|
||||
if (procGraph is null || procGraph.Processes.Count == 0)
|
||||
{
|
||||
diagnostics.Add(new EntryTraceDiagnostic(
|
||||
EntryTraceDiagnosticSeverity.Info,
|
||||
EntryTraceUnknownReason.RuntimeSnapshotUnavailable,
|
||||
"Runtime process snapshot unavailable; static confidence retained.",
|
||||
Span: null,
|
||||
RelatedPath: null));
|
||||
|
||||
return graph with { Diagnostics = diagnostics.ToImmutable() };
|
||||
}
|
||||
|
||||
var runtimeTerminals = BuildRuntimeTerminals(procGraph);
|
||||
if (runtimeTerminals.Length == 0)
|
||||
{
|
||||
diagnostics.Add(new EntryTraceDiagnostic(
|
||||
EntryTraceDiagnosticSeverity.Warning,
|
||||
EntryTraceUnknownReason.RuntimeProcessNotFound,
|
||||
"Runtime process snapshot did not reveal a terminal executable.",
|
||||
Span: null,
|
||||
RelatedPath: null));
|
||||
|
||||
return graph with { Diagnostics = diagnostics.ToImmutable() };
|
||||
}
|
||||
|
||||
var planBuilder = ImmutableArray.CreateBuilder<EntryTracePlan>(graph.Plans.Length);
|
||||
var terminalBuilder = ImmutableArray.CreateBuilder<EntryTraceTerminal>(graph.Terminals.Length);
|
||||
var terminalIndexMap = BuildTerminalIndexMap(graph.Terminals, terminalBuilder);
|
||||
|
||||
foreach (var plan in graph.Plans)
|
||||
{
|
||||
var match = SelectBestRuntimeMatch(plan.TerminalPath, runtimeTerminals);
|
||||
var confidence = EvaluateConfidence(plan.TerminalPath, match?.Path);
|
||||
|
||||
planBuilder.Add(plan with { Confidence = confidence.Score });
|
||||
|
||||
if (terminalIndexMap.TryGetValue(plan.TerminalPath, out var indices) && indices.Count > 0)
|
||||
{
|
||||
var index = indices.Dequeue();
|
||||
terminalBuilder[index] = terminalBuilder[index] with { Confidence = confidence.Score };
|
||||
}
|
||||
|
||||
diagnostics.Add(BuildDiagnostic(confidence, plan.TerminalPath));
|
||||
}
|
||||
|
||||
// Update any terminals that were not tied to plans.
|
||||
for (var i = 0; i < terminalBuilder.Count; i++)
|
||||
{
|
||||
if (terminalBuilder[i].Confidence > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var terminal = terminalBuilder[i];
|
||||
var match = SelectBestRuntimeMatch(terminal.Path, runtimeTerminals);
|
||||
var confidence = EvaluateConfidence(terminal.Path, match?.Path);
|
||||
terminalBuilder[i] = terminal with { Confidence = confidence.Score };
|
||||
}
|
||||
|
||||
return graph with
|
||||
{
|
||||
Plans = planBuilder.ToImmutable(),
|
||||
Terminals = terminalBuilder.ToImmutable(),
|
||||
Diagnostics = diagnostics.ToImmutable()
|
||||
};
|
||||
}
|
||||
|
||||
private static ImmutableArray<RuntimeTerminal> BuildRuntimeTerminals(ProcGraph graph)
|
||||
{
|
||||
var allCandidates = new List<RuntimeTerminal>();
|
||||
|
||||
foreach (var process in graph.Processes.Values
|
||||
.OrderBy(p => p.StartTimeTicks == 0 ? ulong.MaxValue : p.StartTimeTicks)
|
||||
.ThenBy(p => p.Pid))
|
||||
{
|
||||
var normalizedPath = NormalizeRuntimePath(process);
|
||||
if (string.IsNullOrWhiteSpace(normalizedPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
allCandidates.Add(new RuntimeTerminal(normalizedPath, process));
|
||||
}
|
||||
|
||||
if (allCandidates.Count == 0)
|
||||
{
|
||||
return ImmutableArray<RuntimeTerminal>.Empty;
|
||||
}
|
||||
|
||||
var preferred = allCandidates.Where(candidate => !IsWrapper(candidate.Process)).ToList();
|
||||
var source = preferred.Count > 0 ? preferred : allCandidates;
|
||||
|
||||
var unique = new Dictionary<string, RuntimeTerminal>(StringComparer.Ordinal);
|
||||
foreach (var candidate in source)
|
||||
{
|
||||
if (!unique.ContainsKey(candidate.Path))
|
||||
{
|
||||
unique[candidate.Path] = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return unique.Values.OrderBy(candidate => candidate.Process.StartTimeTicks == 0 ? ulong.MaxValue : candidate.Process.StartTimeTicks)
|
||||
.ThenBy(candidate => candidate.Process.Pid)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static RuntimeTerminal? SelectBestRuntimeMatch(string predictedPath, ImmutableArray<RuntimeTerminal> runtimeTerminals)
|
||||
{
|
||||
if (runtimeTerminals.IsDefaultOrEmpty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RuntimeTerminal? best = null;
|
||||
double bestScore = double.MinValue;
|
||||
|
||||
foreach (var runtime in runtimeTerminals)
|
||||
{
|
||||
var (score, _, _) = EvaluateConfidence(predictedPath, runtime.Path);
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
best = runtime;
|
||||
if (score >= 95d)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static Dictionary<string, Queue<int>> BuildTerminalIndexMap(
|
||||
ImmutableArray<EntryTraceTerminal> terminals,
|
||||
ImmutableArray<EntryTraceTerminal>.Builder terminalBuilder)
|
||||
{
|
||||
var map = new Dictionary<string, Queue<int>>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < terminals.Length; i++)
|
||||
{
|
||||
var terminal = terminals[i];
|
||||
terminalBuilder.Add(terminal);
|
||||
|
||||
if (!map.TryGetValue(terminal.Path, out var indices))
|
||||
{
|
||||
indices = new Queue<int>();
|
||||
map[terminal.Path] = indices;
|
||||
}
|
||||
|
||||
indices.Enqueue(i);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static ConfidenceResult EvaluateConfidence(string predictedPath, string? runtimePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(runtimePath))
|
||||
{
|
||||
return new ConfidenceResult(60d, ConfidenceLevel.Low, string.Empty);
|
||||
}
|
||||
|
||||
var normalizedPredicted = NormalizeComparisonPath(predictedPath);
|
||||
var normalizedRuntime = NormalizeComparisonPath(runtimePath);
|
||||
|
||||
if (string.Equals(normalizedPredicted, normalizedRuntime, StringComparison.Ordinal))
|
||||
{
|
||||
return new ConfidenceResult(95d, ConfidenceLevel.High, runtimePath);
|
||||
}
|
||||
|
||||
var predictedName = Path.GetFileName(normalizedPredicted);
|
||||
var runtimeName = Path.GetFileName(normalizedRuntime);
|
||||
|
||||
if (!string.IsNullOrEmpty(predictedName) &&
|
||||
string.Equals(predictedName, runtimeName, StringComparison.Ordinal))
|
||||
{
|
||||
return new ConfidenceResult(90d, ConfidenceLevel.High, runtimePath);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(predictedName) &&
|
||||
string.Equals(predictedName, runtimeName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new ConfidenceResult(80d, ConfidenceLevel.Medium, runtimePath);
|
||||
}
|
||||
|
||||
return new ConfidenceResult(60d, ConfidenceLevel.Low, runtimePath);
|
||||
}
|
||||
|
||||
private static EntryTraceDiagnostic BuildDiagnostic(ConfidenceResult result, string predictedPath)
|
||||
{
|
||||
var runtimePath = string.IsNullOrWhiteSpace(result.RuntimePath) ? "<unknown>" : result.RuntimePath;
|
||||
var severity = result.Level == ConfidenceLevel.High
|
||||
? EntryTraceDiagnosticSeverity.Info
|
||||
: EntryTraceDiagnosticSeverity.Warning;
|
||||
var reason = result.Level == ConfidenceLevel.High
|
||||
? EntryTraceUnknownReason.RuntimeMatch
|
||||
: EntryTraceUnknownReason.RuntimeMismatch;
|
||||
var message = result.Level == ConfidenceLevel.High
|
||||
? $"Runtime process '{runtimePath}' matches EntryTrace prediction '{predictedPath}'."
|
||||
: $"Runtime process '{runtimePath}' diverges from EntryTrace prediction '{predictedPath}'.";
|
||||
|
||||
return new EntryTraceDiagnostic(
|
||||
severity,
|
||||
reason,
|
||||
message,
|
||||
Span: null,
|
||||
RelatedPath: string.IsNullOrWhiteSpace(result.RuntimePath) ? null : result.RuntimePath);
|
||||
}
|
||||
|
||||
private static bool IsWrapper(ProcProcess process)
|
||||
{
|
||||
var command = GetCommandName(process);
|
||||
return command.Length > 0 && WrapperNames.Contains(command);
|
||||
}
|
||||
|
||||
private static string GetCommandName(ProcProcess process)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(process.CommandName))
|
||||
{
|
||||
return process.CommandName;
|
||||
}
|
||||
|
||||
if (!process.CommandLine.IsDefaultOrEmpty && process.CommandLine.Length > 0)
|
||||
{
|
||||
return Path.GetFileName(process.CommandLine[0]);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(process.ExecutablePath))
|
||||
{
|
||||
return Path.GetFileName(process.ExecutablePath);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string NormalizeRuntimePath(ProcProcess process)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(process.ExecutablePath))
|
||||
{
|
||||
return process.ExecutablePath;
|
||||
}
|
||||
|
||||
if (!process.CommandLine.IsDefaultOrEmpty && process.CommandLine.Length > 0)
|
||||
{
|
||||
return process.CommandLine[0];
|
||||
}
|
||||
|
||||
return process.CommandName;
|
||||
}
|
||||
|
||||
private static string NormalizeComparisonPath(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var trimmed = path.Trim();
|
||||
var normalized = trimmed.Replace('\\', '/');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private readonly record struct RuntimeTerminal(string Path, ProcProcess Process);
|
||||
|
||||
private readonly record struct ConfidenceResult(double Score, ConfidenceLevel Level, string RuntimePath);
|
||||
|
||||
private enum ConfidenceLevel
|
||||
{
|
||||
High,
|
||||
Medium,
|
||||
Low
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Runtime;
|
||||
|
||||
public sealed class ProcFileSystemSnapshot : IProcSnapshotProvider
|
||||
{
|
||||
private readonly string _rootPath;
|
||||
|
||||
public ProcFileSystemSnapshot(string? rootPath = null)
|
||||
{
|
||||
_rootPath = string.IsNullOrWhiteSpace(rootPath) ? "/proc" : rootPath;
|
||||
}
|
||||
|
||||
public IEnumerable<int> EnumerateProcessIds()
|
||||
{
|
||||
if (!Directory.Exists(_rootPath))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var directory in Directory.EnumerateDirectories(_rootPath).OrderBy(d => d, StringComparer.Ordinal))
|
||||
{
|
||||
var name = Path.GetFileName(directory);
|
||||
if (int.TryParse(name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var pid))
|
||||
{
|
||||
yield return pid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadProcess(int pid, out ProcProcess process)
|
||||
{
|
||||
process = null!;
|
||||
try
|
||||
{
|
||||
var statPath = Path.Combine(_rootPath, pid.ToString(CultureInfo.InvariantCulture), "stat");
|
||||
var cmdPath = Path.Combine(_rootPath, pid.ToString(CultureInfo.InvariantCulture), "cmdline");
|
||||
var exePath = Path.Combine(_rootPath, pid.ToString(CultureInfo.InvariantCulture), "exe");
|
||||
|
||||
if (!File.Exists(statPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var statContent = File.ReadAllText(statPath);
|
||||
var commandName = ParseCommandName(statContent);
|
||||
var parentPid = ParseParentPid(statContent);
|
||||
var startTime = ParseStartTime(statContent);
|
||||
var commandLine = ReadCommandLine(cmdPath, statContent);
|
||||
var executable = ResolveExecutablePath(exePath, commandLine);
|
||||
|
||||
process = new ProcProcess(pid, parentPid, executable, commandLine, commandName, startTime);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ParseCommandName(string statContent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(statContent))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var openIndex = statContent.IndexOf('(');
|
||||
var closeIndex = statContent.LastIndexOf(')');
|
||||
if (openIndex >= 0 && closeIndex > openIndex)
|
||||
{
|
||||
return statContent.Substring(openIndex + 1, closeIndex - openIndex - 1);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static int ParseParentPid(string statContent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(statContent))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var closeIndex = statContent.LastIndexOf(')');
|
||||
if (closeIndex < 0 || closeIndex + 2 >= statContent.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var after = statContent.Substring(closeIndex + 2);
|
||||
var parts = after.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var parentPid)
|
||||
? parentPid
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static ulong ParseStartTime(string statContent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(statContent))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var closeIndex = statContent.LastIndexOf(')');
|
||||
if (closeIndex < 0 || closeIndex + 2 >= statContent.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var after = statContent.Substring(closeIndex + 2);
|
||||
var parts = after.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 19)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ulong.TryParse(parts[18], NumberStyles.Integer, CultureInfo.InvariantCulture, out var startTime)
|
||||
? startTime
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ReadCommandLine(string path, string statContent)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
if (bytes.Length > 0)
|
||||
{
|
||||
var segments = SplitNullTerminated(bytes)
|
||||
.Where(segment => segment.Length > 0)
|
||||
.ToImmutableArray();
|
||||
if (!segments.IsDefaultOrEmpty)
|
||||
{
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
var openIndex = statContent.IndexOf('(');
|
||||
var closeIndex = statContent.LastIndexOf(')');
|
||||
if (openIndex >= 0 && closeIndex > openIndex)
|
||||
{
|
||||
var command = statContent.Substring(openIndex + 1, closeIndex - openIndex - 1);
|
||||
return ImmutableArray.Create(command);
|
||||
}
|
||||
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
private static string ResolveExecutablePath(string path, ImmutableArray<string> commandLine)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = File.ResolveLinkTarget(path, returnFinalTarget: true);
|
||||
if (info is not null)
|
||||
{
|
||||
return info.FullName;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through to command line fallback
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
if (!string.IsNullOrWhiteSpace(fullPath))
|
||||
{
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore failures to resolve symlinks
|
||||
}
|
||||
|
||||
if (!commandLine.IsDefaultOrEmpty && commandLine[0].Contains('/'))
|
||||
{
|
||||
return commandLine[0];
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SplitNullTerminated(byte[] buffer)
|
||||
{
|
||||
var start = 0;
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
if (buffer[i] == 0)
|
||||
{
|
||||
var length = i - start;
|
||||
if (length > 0)
|
||||
{
|
||||
yield return Encoding.UTF8.GetString(buffer, start, length);
|
||||
}
|
||||
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (start < buffer.Length)
|
||||
{
|
||||
yield return Encoding.UTF8.GetString(buffer, start, buffer.Length - start);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Runtime;
|
||||
|
||||
public sealed record ProcGraph(
|
||||
int RootPid,
|
||||
ImmutableDictionary<int, ProcProcess> Processes,
|
||||
ImmutableDictionary<int, ImmutableArray<int>> Children);
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Runtime;
|
||||
|
||||
public interface IProcSnapshotProvider
|
||||
{
|
||||
IEnumerable<int> EnumerateProcessIds();
|
||||
|
||||
bool TryReadProcess(int pid, out ProcProcess process);
|
||||
}
|
||||
|
||||
public static class ProcGraphBuilder
|
||||
{
|
||||
public static ProcGraph? Build(IProcSnapshotProvider provider)
|
||||
{
|
||||
if (provider is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
}
|
||||
|
||||
var processes = new Dictionary<int, ProcProcess>();
|
||||
foreach (var pid in provider.EnumerateProcessIds().OrderBy(pid => pid))
|
||||
{
|
||||
if (pid <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (provider.TryReadProcess(pid, out var process))
|
||||
{
|
||||
processes[pid] = process;
|
||||
}
|
||||
}
|
||||
|
||||
if (processes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var rootPid = DetermineRootPid(processes);
|
||||
var children = BuildChildren(processes);
|
||||
return new ProcGraph(
|
||||
rootPid,
|
||||
processes.ToImmutableDictionary(pair => pair.Key, pair => pair.Value),
|
||||
children);
|
||||
}
|
||||
|
||||
private static int DetermineRootPid(Dictionary<int, ProcProcess> processes)
|
||||
{
|
||||
if (processes.ContainsKey(1))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var childPids = new HashSet<int>();
|
||||
foreach (var process in processes.Values)
|
||||
{
|
||||
if (processes.ContainsKey(process.ParentPid))
|
||||
{
|
||||
childPids.Add(process.Pid);
|
||||
}
|
||||
}
|
||||
|
||||
var rootCandidates = processes.Keys.Where(pid => !childPids.Contains(pid));
|
||||
return rootCandidates.OrderBy(pid => pid).FirstOrDefault(processes.Keys.Min());
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<int, ImmutableArray<int>> BuildChildren(Dictionary<int, ProcProcess> processes)
|
||||
{
|
||||
var map = new Dictionary<int, List<int>>(processes.Count);
|
||||
|
||||
foreach (var process in processes.Values)
|
||||
{
|
||||
if (!processes.ContainsKey(process.ParentPid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!map.TryGetValue(process.ParentPid, out var list))
|
||||
{
|
||||
list = new List<int>();
|
||||
map[process.ParentPid] = list;
|
||||
}
|
||||
|
||||
list.Add(process.Pid);
|
||||
}
|
||||
|
||||
return map.ToImmutableDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value
|
||||
.OrderBy(pid => NormalizeStartTime(processes[pid]))
|
||||
.ThenBy(pid => pid)
|
||||
.Select(pid => pid)
|
||||
.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static ulong NormalizeStartTime(ProcProcess process)
|
||||
{
|
||||
return process.StartTimeTicks == 0 ? ulong.MaxValue : process.StartTimeTicks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Runtime;
|
||||
|
||||
public sealed record ProcProcess(
|
||||
int Pid,
|
||||
int ParentPid,
|
||||
string ExecutablePath,
|
||||
ImmutableArray<string> CommandLine,
|
||||
string CommandName,
|
||||
ulong StartTimeTicks);
|
||||
Reference in New Issue
Block a user