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:
@@ -1,11 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.EntryTrace.Diagnostics;
|
||||
using StellaOps.Scanner.EntryTrace.Parsing;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.EntryTrace.Diagnostics;
|
||||
using StellaOps.Scanner.EntryTrace.FileSystem;
|
||||
using StellaOps.Scanner.EntryTrace.Parsing;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace;
|
||||
|
||||
@@ -73,13 +76,17 @@ public sealed class EntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
{
|
||||
private readonly EntrypointSpecification _entrypoint;
|
||||
private readonly EntryTraceContext _context;
|
||||
private readonly EntryTraceAnalyzerOptions _options;
|
||||
private readonly EntryTraceMetrics _metrics;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ImmutableArray<string> _pathEntries;
|
||||
private readonly List<EntryTraceNode> _nodes = new();
|
||||
private readonly List<EntryTraceEdge> _edges = new();
|
||||
private readonly List<EntryTraceDiagnostic> _diagnostics = new();
|
||||
private readonly EntryTraceAnalyzerOptions _options;
|
||||
private readonly EntryTraceMetrics _metrics;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ImmutableArray<string> _pathEntries;
|
||||
private readonly ImmutableArray<EntryTraceCandidate> _candidates;
|
||||
private readonly List<EntryTraceNode> _nodes = new();
|
||||
private readonly List<EntryTraceEdge> _edges = new();
|
||||
private readonly List<EntryTraceDiagnostic> _diagnostics = new();
|
||||
private readonly List<EntryTracePlan> _plans = new();
|
||||
private readonly List<EntryTraceTerminal> _terminals = new();
|
||||
private readonly HashSet<string> _terminalKeys = new(StringComparer.Ordinal);
|
||||
private readonly HashSet<string> _visitedScripts = new(StringComparer.Ordinal);
|
||||
private readonly HashSet<string> _visitedCommands = new(StringComparer.Ordinal);
|
||||
private int _nextNodeId = 1;
|
||||
@@ -89,15 +96,16 @@ public sealed class EntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
EntryTraceContext context,
|
||||
EntryTraceAnalyzerOptions options,
|
||||
EntryTraceMetrics metrics,
|
||||
ILogger logger)
|
||||
{
|
||||
_entrypoint = entrypoint;
|
||||
_context = context;
|
||||
_options = options;
|
||||
_metrics = metrics;
|
||||
_logger = logger;
|
||||
_pathEntries = DeterminePath(context);
|
||||
}
|
||||
ILogger logger)
|
||||
{
|
||||
_entrypoint = entrypoint;
|
||||
_context = context;
|
||||
_options = options;
|
||||
_metrics = metrics;
|
||||
_logger = logger;
|
||||
_pathEntries = DeterminePath(context);
|
||||
_candidates = context.Candidates;
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> DeterminePath(EntryTraceContext context)
|
||||
{
|
||||
@@ -114,46 +122,65 @@ public sealed class EntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
public EntryTraceGraph BuildGraph()
|
||||
{
|
||||
var initialArgs = ComposeInitialCommand(_entrypoint);
|
||||
if (initialArgs.Length == 0)
|
||||
{
|
||||
_diagnostics.Add(new EntryTraceDiagnostic(
|
||||
EntryTraceDiagnosticSeverity.Error,
|
||||
EntryTraceUnknownReason.CommandNotFound,
|
||||
"ENTRYPOINT/CMD yielded no executable command.",
|
||||
Span: null,
|
||||
RelatedPath: null));
|
||||
return ToGraph(EntryTraceOutcome.Unresolved);
|
||||
}
|
||||
public EntryTraceGraph BuildGraph()
|
||||
{
|
||||
var initialArgs = ComposeInitialCommand(_entrypoint);
|
||||
if (initialArgs.Length == 0)
|
||||
{
|
||||
if (_candidates.Length == 0)
|
||||
{
|
||||
_diagnostics.Add(new EntryTraceDiagnostic(
|
||||
EntryTraceDiagnosticSeverity.Warning,
|
||||
EntryTraceUnknownReason.CommandNotFound,
|
||||
"No ENTRYPOINT/CMD declared and no fallback candidates were discovered.",
|
||||
Span: null,
|
||||
RelatedPath: null));
|
||||
return ToGraph(DetermineOutcome());
|
||||
}
|
||||
|
||||
foreach (var candidate in _candidates)
|
||||
{
|
||||
_diagnostics.Add(new EntryTraceDiagnostic(
|
||||
EntryTraceDiagnosticSeverity.Info,
|
||||
MapCandidateReason(candidate.Source),
|
||||
CreateCandidateMessage(candidate),
|
||||
Span: null,
|
||||
RelatedPath: candidate.Evidence?.Path));
|
||||
|
||||
ResolveCommand(candidate.Command, parent: null, originSpan: null, depth: 0, relationship: candidate.Source);
|
||||
}
|
||||
|
||||
return ToGraph(DetermineOutcome());
|
||||
}
|
||||
|
||||
ResolveCommand(initialArgs, parent: null, originSpan: null, depth: 0, relationship: "entrypoint");
|
||||
|
||||
var outcome = DetermineOutcome();
|
||||
return ToGraph(outcome);
|
||||
}
|
||||
|
||||
private EntryTraceOutcome DetermineOutcome()
|
||||
{
|
||||
var hasErrors = _diagnostics.Any(d => d.Severity == EntryTraceDiagnosticSeverity.Error);
|
||||
if (hasErrors)
|
||||
{
|
||||
return EntryTraceOutcome.Unresolved;
|
||||
}
|
||||
|
||||
var hasWarnings = _diagnostics.Any(d => d.Severity == EntryTraceDiagnosticSeverity.Warning);
|
||||
return hasWarnings ? EntryTraceOutcome.PartiallyResolved : EntryTraceOutcome.Resolved;
|
||||
}
|
||||
|
||||
ResolveCommand(initialArgs, parent: null, originSpan: null, depth: 0, relationship: "entrypoint");
|
||||
|
||||
var outcome = DetermineOutcome();
|
||||
return ToGraph(outcome);
|
||||
}
|
||||
|
||||
private EntryTraceOutcome DetermineOutcome()
|
||||
{
|
||||
if (_diagnostics.Count == 0)
|
||||
{
|
||||
return EntryTraceOutcome.Resolved;
|
||||
}
|
||||
|
||||
return _diagnostics.Any(d => d.Severity == EntryTraceDiagnosticSeverity.Error)
|
||||
? EntryTraceOutcome.Unresolved
|
||||
: EntryTraceOutcome.PartiallyResolved;
|
||||
}
|
||||
|
||||
private EntryTraceGraph ToGraph(EntryTraceOutcome outcome)
|
||||
{
|
||||
return new EntryTraceGraph(
|
||||
outcome,
|
||||
_nodes.ToImmutableArray(),
|
||||
_edges.ToImmutableArray(),
|
||||
_diagnostics.ToImmutableArray());
|
||||
}
|
||||
private EntryTraceGraph ToGraph(EntryTraceOutcome outcome)
|
||||
{
|
||||
return new EntryTraceGraph(
|
||||
outcome,
|
||||
_nodes.ToImmutableArray(),
|
||||
_edges.ToImmutableArray(),
|
||||
_diagnostics.ToImmutableArray(),
|
||||
_plans.ToImmutableArray(),
|
||||
_terminals.ToImmutableArray());
|
||||
}
|
||||
|
||||
private ImmutableArray<string> ComposeInitialCommand(EntrypointSpecification specification)
|
||||
{
|
||||
@@ -185,17 +212,17 @@ public sealed class EntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
private void ResolveCommand(
|
||||
ImmutableArray<string> arguments,
|
||||
EntryTraceNode? parent,
|
||||
EntryTraceSpan? originSpan,
|
||||
int depth,
|
||||
string relationship)
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
private void ResolveCommand(
|
||||
ImmutableArray<string> arguments,
|
||||
EntryTraceNode? parent,
|
||||
EntryTraceSpan? originSpan,
|
||||
int depth,
|
||||
string relationship)
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (depth >= _options.MaxDepth)
|
||||
{
|
||||
@@ -242,18 +269,19 @@ public sealed class EntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryFollowInterpreter(node, descriptor, arguments, depth))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryFollowShell(node, descriptor, arguments, depth))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Terminal executable.
|
||||
}
|
||||
if (TryFollowInterpreter(node, descriptor, arguments, depth))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryFollowShell(node, descriptor, arguments, depth))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClassifyTerminal(node, descriptor, arguments);
|
||||
// Terminal executable.
|
||||
}
|
||||
|
||||
private bool TryResolveExecutable(
|
||||
string commandName,
|
||||
@@ -497,16 +525,16 @@ public sealed class EntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HandleJava(
|
||||
EntryTraceNode node,
|
||||
ImmutableArray<string> arguments,
|
||||
RootFileDescriptor descriptor,
|
||||
int depth)
|
||||
{
|
||||
if (arguments.Length < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
private bool HandleJava(
|
||||
EntryTraceNode node,
|
||||
ImmutableArray<string> arguments,
|
||||
RootFileDescriptor descriptor,
|
||||
int depth)
|
||||
{
|
||||
if (arguments.Length < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string? jar = null;
|
||||
string? mainClass = null;
|
||||
@@ -526,40 +554,42 @@ public sealed class EntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if (jar is not null)
|
||||
{
|
||||
if (!_context.FileSystem.TryResolveExecutable(jar, Array.Empty<string>(), out var jarDescriptor))
|
||||
{
|
||||
_diagnostics.Add(new EntryTraceDiagnostic(
|
||||
EntryTraceDiagnosticSeverity.Warning,
|
||||
EntryTraceUnknownReason.JarNotFound,
|
||||
$"Java JAR '{jar}' not found.",
|
||||
Span: null,
|
||||
RelatedPath: jar));
|
||||
}
|
||||
else
|
||||
{
|
||||
var jarNode = AddNode(
|
||||
EntryTraceNodeKind.Executable,
|
||||
jarDescriptor.Path,
|
||||
ImmutableArray<string>.Empty,
|
||||
EntryTraceInterpreterKind.Java,
|
||||
new EntryTraceEvidence(jarDescriptor.Path, jarDescriptor.LayerDigest, "jar", null),
|
||||
null);
|
||||
_edges.Add(new EntryTraceEdge(node.Id, jarNode.Id, "executes", null));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mainClass is not null)
|
||||
{
|
||||
_edges.Add(new EntryTraceEdge(node.Id, node.Id, "java-main", new Dictionary<string, string>
|
||||
{
|
||||
["class"] = mainClass
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
if (jar is not null)
|
||||
{
|
||||
if (!_context.FileSystem.TryResolveExecutable(jar, _pathEntries, out var jarDescriptor) &&
|
||||
!_context.FileSystem.TryResolveExecutable(jar, Array.Empty<string>(), out jarDescriptor))
|
||||
{
|
||||
_diagnostics.Add(new EntryTraceDiagnostic(
|
||||
EntryTraceDiagnosticSeverity.Warning,
|
||||
EntryTraceUnknownReason.JarNotFound,
|
||||
$"Java JAR '{jar}' not found.",
|
||||
Span: null,
|
||||
RelatedPath: jar));
|
||||
return true;
|
||||
}
|
||||
|
||||
var jarNode = AddNode(
|
||||
EntryTraceNodeKind.Executable,
|
||||
jarDescriptor.Path,
|
||||
ImmutableArray<string>.Empty,
|
||||
EntryTraceInterpreterKind.Java,
|
||||
new EntryTraceEvidence(jarDescriptor.Path, jarDescriptor.LayerDigest, "jar", null),
|
||||
null);
|
||||
|
||||
_edges.Add(new EntryTraceEdge(node.Id, jarNode.Id, "executes", null));
|
||||
ClassifyTerminal(jarNode, jarDescriptor, arguments);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mainClass is not null)
|
||||
{
|
||||
_edges.Add(new EntryTraceEdge(node.Id, node.Id, "java-main", new Dictionary<string, string>
|
||||
{
|
||||
["class"] = mainClass
|
||||
}));
|
||||
ClassifyTerminal(node, descriptor, arguments);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1018,26 +1048,325 @@ public sealed class EntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
return content.Contains("#!/bin/sh", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private EntryTraceNode AddNode(
|
||||
EntryTraceNodeKind kind,
|
||||
string displayName,
|
||||
ImmutableArray<string> arguments,
|
||||
EntryTraceInterpreterKind interpreterKind,
|
||||
EntryTraceEvidence? evidence,
|
||||
EntryTraceSpan? span)
|
||||
{
|
||||
var node = new EntryTraceNode(
|
||||
_nextNodeId++,
|
||||
kind,
|
||||
displayName,
|
||||
arguments,
|
||||
interpreterKind,
|
||||
evidence,
|
||||
span);
|
||||
_nodes.Add(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
private EntryTraceNode AddNode(
|
||||
EntryTraceNodeKind kind,
|
||||
string displayName,
|
||||
ImmutableArray<string> arguments,
|
||||
EntryTraceInterpreterKind interpreterKind,
|
||||
EntryTraceEvidence? evidence,
|
||||
EntryTraceSpan? span,
|
||||
ImmutableDictionary<string, string>? metadata = null)
|
||||
{
|
||||
var node = new EntryTraceNode(
|
||||
_nextNodeId++,
|
||||
kind,
|
||||
displayName,
|
||||
arguments,
|
||||
interpreterKind,
|
||||
evidence,
|
||||
span,
|
||||
metadata);
|
||||
_nodes.Add(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
private void ClassifyTerminal(
|
||||
EntryTraceNode node,
|
||||
RootFileDescriptor descriptor,
|
||||
ImmutableArray<string> arguments)
|
||||
{
|
||||
var signature = CreateCommandSignature(arguments, node.DisplayName);
|
||||
var key = $"{descriptor.Path}|{_context.User}|{_context.WorkingDirectory}|{signature}";
|
||||
if (!_terminalKeys.Add(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var evidence = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
double score = descriptor.IsExecutable ? 50d : 40d;
|
||||
string? runtime = null;
|
||||
var type = EntryTraceTerminalType.Unknown;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(descriptor.ShebangInterpreter))
|
||||
{
|
||||
var shebang = descriptor.ShebangInterpreter!;
|
||||
evidence["shebang"] = shebang;
|
||||
runtime = InferRuntimeFromShebang(shebang);
|
||||
type = EntryTraceTerminalType.Script;
|
||||
score += 15d;
|
||||
}
|
||||
|
||||
if (_context.FileSystem.TryReadBytes(descriptor.Path, 2_097_152, out _, out var binaryContent))
|
||||
{
|
||||
var span = binaryContent.Span;
|
||||
if (TryClassifyElf(span, evidence, ref runtime))
|
||||
{
|
||||
type = EntryTraceTerminalType.Native;
|
||||
score += 15d;
|
||||
}
|
||||
else if (TryClassifyPe(span, evidence, ref runtime))
|
||||
{
|
||||
type = EntryTraceTerminalType.Managed;
|
||||
score += 15d;
|
||||
}
|
||||
else if (IsZipArchive(span))
|
||||
{
|
||||
runtime ??= "java";
|
||||
type = EntryTraceTerminalType.Managed;
|
||||
score += 10d;
|
||||
if (TryReadJarManifest(span, descriptor.Path, evidence))
|
||||
{
|
||||
score += 5d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime ??= InferRuntimeFromCommand(node.DisplayName, arguments);
|
||||
|
||||
if (runtime is "go" or "rust")
|
||||
{
|
||||
type = EntryTraceTerminalType.Native;
|
||||
score += 10d;
|
||||
}
|
||||
else if (runtime is ".net" or "java" or "python" or "node" or "ruby" or "php" or "php-fpm")
|
||||
{
|
||||
if (type == EntryTraceTerminalType.Unknown)
|
||||
{
|
||||
type = EntryTraceTerminalType.Managed;
|
||||
}
|
||||
|
||||
score += 5d;
|
||||
}
|
||||
|
||||
if (runtime is "shell")
|
||||
{
|
||||
type = EntryTraceTerminalType.Script;
|
||||
}
|
||||
|
||||
if (type == EntryTraceTerminalType.Unknown)
|
||||
{
|
||||
type = EntryTraceTerminalType.Native;
|
||||
}
|
||||
|
||||
var boundedScore = Math.Min(95d, score);
|
||||
var terminal = new EntryTraceTerminal(
|
||||
descriptor.Path,
|
||||
type,
|
||||
runtime,
|
||||
boundedScore,
|
||||
evidence.ToImmutable(),
|
||||
_context.User,
|
||||
_context.WorkingDirectory,
|
||||
arguments.IsDefault ? ImmutableArray<string>.Empty : arguments);
|
||||
|
||||
var plan = new EntryTracePlan(
|
||||
terminal.Arguments,
|
||||
_context.Environment,
|
||||
_context.WorkingDirectory,
|
||||
_context.User,
|
||||
terminal.Path,
|
||||
terminal.Type,
|
||||
terminal.Runtime,
|
||||
terminal.Confidence,
|
||||
terminal.Evidence);
|
||||
|
||||
_terminals.Add(terminal);
|
||||
_plans.Add(plan);
|
||||
}
|
||||
|
||||
private static string CreateCommandSignature(ImmutableArray<string> command, string displayName)
|
||||
{
|
||||
if (command.IsDefaultOrEmpty || command.Length == 0)
|
||||
{
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return string.Join('\u001F', command);
|
||||
}
|
||||
|
||||
private static EntryTraceUnknownReason MapCandidateReason(string source)
|
||||
=> source switch
|
||||
{
|
||||
"history" => EntryTraceUnknownReason.InferredEntrypointFromHistory,
|
||||
"service-directory" => EntryTraceUnknownReason.InferredEntrypointFromServices,
|
||||
"supervisor" => EntryTraceUnknownReason.InferredEntrypointFromSupervisor,
|
||||
"entrypoint-script" => EntryTraceUnknownReason.InferredEntrypointFromEntrypointScript,
|
||||
_ => EntryTraceUnknownReason.CommandNotFound
|
||||
};
|
||||
|
||||
private static string CreateCandidateMessage(EntryTraceCandidate candidate)
|
||||
{
|
||||
var primary = candidate.Command.Length > 0 ? candidate.Command[0] : candidate.Source;
|
||||
return candidate.Source switch
|
||||
{
|
||||
"history" => "Inferred entrypoint from image history.",
|
||||
"service-directory" => $"Inferred service run script '{primary}'.",
|
||||
"supervisor" => candidate.Description is null
|
||||
? "Inferred supervisor command."
|
||||
: $"Inferred supervisor program '{candidate.Description}'.",
|
||||
"entrypoint-script" => $"Inferred entrypoint script '{primary}'.",
|
||||
_ => "Inferred entrypoint candidate."
|
||||
};
|
||||
}
|
||||
|
||||
private static string? InferRuntimeFromShebang(string shebang)
|
||||
{
|
||||
var normalized = shebang.ToLowerInvariant();
|
||||
if (normalized.Contains("python"))
|
||||
{
|
||||
return "python";
|
||||
}
|
||||
|
||||
if (normalized.Contains("node"))
|
||||
{
|
||||
return "node";
|
||||
}
|
||||
|
||||
if (normalized.Contains("ruby"))
|
||||
{
|
||||
return "ruby";
|
||||
}
|
||||
|
||||
if (normalized.Contains("php-fpm"))
|
||||
{
|
||||
return "php-fpm";
|
||||
}
|
||||
|
||||
if (normalized.Contains("php"))
|
||||
{
|
||||
return "php";
|
||||
}
|
||||
|
||||
if (normalized.Contains("sh") || normalized.Contains("bash"))
|
||||
{
|
||||
return "shell";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? InferRuntimeFromCommand(string commandName, ImmutableArray<string> arguments)
|
||||
{
|
||||
var normalized = commandName.ToLowerInvariant();
|
||||
if (normalized == "java" || arguments.Any(arg => arg.Equals("-jar", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return "java";
|
||||
}
|
||||
|
||||
if (normalized.Contains("dotnet") || normalized.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".net";
|
||||
}
|
||||
|
||||
if (normalized.Contains("python") || normalized.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "python";
|
||||
}
|
||||
|
||||
if (normalized.Contains("node") || normalized.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "node";
|
||||
}
|
||||
|
||||
if (normalized.Contains("go"))
|
||||
{
|
||||
return "go";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool TryClassifyElf(ReadOnlySpan<byte> span, ImmutableDictionary<string, string>.Builder evidence, ref string? runtime)
|
||||
{
|
||||
if (span.Length < 4 || span[0] != 0x7F || span[1] != (byte)'E' || span[2] != (byte)'L' || span[3] != (byte)'F')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
evidence["binary.format"] = "ELF";
|
||||
|
||||
if (ContainsAscii(span, "Go build ID") || ContainsAscii(span, ".gopclntab"))
|
||||
{
|
||||
runtime = "go";
|
||||
evidence["runtime"] = "go";
|
||||
}
|
||||
else if (ContainsAscii(span, "rust_eh_personality") || ContainsAscii(span, ".rustc"))
|
||||
{
|
||||
runtime = "rust";
|
||||
evidence["runtime"] = "rust";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ContainsAscii(ReadOnlySpan<byte> span, string value)
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(value);
|
||||
return span.IndexOf(bytes) >= 0;
|
||||
}
|
||||
|
||||
private bool TryClassifyPe(ReadOnlySpan<byte> span, ImmutableDictionary<string, string>.Builder evidence, ref string? runtime)
|
||||
{
|
||||
if (span.Length < 2 || span[0] != 'M' || span[1] != 'Z')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
evidence["binary.format"] = "PE";
|
||||
if (ContainsAscii(span, "BSJB") || ContainsAscii(span, "CLR"))
|
||||
{
|
||||
runtime = ".net";
|
||||
evidence["pe.cli"] = "true";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryReadJarManifest(ReadOnlySpan<byte> span, string path, ImmutableDictionary<string, string>.Builder evidence)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = new MemoryStream(span.ToArray(), writable: false);
|
||||
using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false);
|
||||
var manifestEntry = archive.GetEntry("META-INF/MANIFEST.MF");
|
||||
if (manifestEntry is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(manifestEntry.Open(), Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
var content = reader.ReadToEnd();
|
||||
evidence["jar.manifest"] = "true";
|
||||
|
||||
foreach (var line in content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var separator = line.IndexOf(':');
|
||||
if (separator <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = line[..separator].Trim();
|
||||
var value = line[(separator + 1)..].Trim();
|
||||
|
||||
if (key.Equals("Main-Class", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
evidence["jar.main-class"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to read jar manifest for {JarPath}.", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsZipArchive(ReadOnlySpan<byte> span)
|
||||
=> span.Length >= 4 && span[0] == 0x50 && span[1] == 0x4B && span[2] == 0x03 && span[3] == 0x04;
|
||||
|
||||
private static string CombineUnixPath(string baseDirectory, string relative)
|
||||
{
|
||||
var normalizedBase = NormalizeUnixPath(baseDirectory);
|
||||
|
||||
Reference in New Issue
Block a user