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,9 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.EntryTrace.FileSystem;
|
||||
using StellaOps.Scanner.EntryTrace.Parsing;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace;
|
||||
|
||||
/// <summary>
|
||||
/// Combines OCI configuration and root filesystem data into the context required by the EntryTrace analyzer.
|
||||
@@ -31,22 +36,447 @@ public static class EntryTraceImageContextFactory
|
||||
var workingDir = NormalizeWorkingDirectory(config.WorkingDirectory);
|
||||
var user = NormalizeUser(config.User);
|
||||
|
||||
var context = new EntryTraceContext(
|
||||
fileSystem,
|
||||
environment,
|
||||
path,
|
||||
workingDir,
|
||||
user,
|
||||
imageDigest,
|
||||
scanId,
|
||||
logger);
|
||||
|
||||
var entrypoint = EntrypointSpecification.FromExecForm(
|
||||
config.Entrypoint.IsDefaultOrEmpty ? null : config.Entrypoint,
|
||||
config.Command.IsDefaultOrEmpty ? null : config.Command);
|
||||
|
||||
return new EntryTraceImageContext(entrypoint, context);
|
||||
}
|
||||
var context = new EntryTraceContext(
|
||||
fileSystem,
|
||||
environment,
|
||||
path,
|
||||
workingDir,
|
||||
user,
|
||||
imageDigest,
|
||||
scanId,
|
||||
logger);
|
||||
|
||||
var candidates = BuildFallbackCandidates(config, fileSystem, logger);
|
||||
context = context with { Candidates = candidates };
|
||||
|
||||
var entrypoint = EntrypointSpecification.FromExecForm(
|
||||
config.Entrypoint.IsDefaultOrEmpty ? null : config.Entrypoint,
|
||||
config.Command.IsDefaultOrEmpty ? null : config.Command);
|
||||
|
||||
return new EntryTraceImageContext(entrypoint, context);
|
||||
}
|
||||
|
||||
private static ImmutableArray<EntryTraceCandidate> BuildFallbackCandidates(
|
||||
OciImageConfig config,
|
||||
IRootFileSystem fileSystem,
|
||||
ILogger? logger)
|
||||
{
|
||||
if (config.Entrypoint.Length > 0 || config.Command.Length > 0)
|
||||
{
|
||||
return ImmutableArray<EntryTraceCandidate>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<EntryTraceCandidate>();
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
void AddCandidate(ImmutableArray<string> command, string source, EntryTraceEvidence? evidence, string? description)
|
||||
{
|
||||
if (command.IsDefaultOrEmpty || command.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var signature = CreateSignature(command);
|
||||
if (!seen.Add(signature))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
builder.Add(new EntryTraceCandidate(command, source, evidence, description));
|
||||
}
|
||||
|
||||
if (TryAddHistoryCandidate(config, AddCandidate, logger))
|
||||
{
|
||||
// Preserve first viable history candidate only.
|
||||
}
|
||||
|
||||
CollectEntrypointScripts(fileSystem, AddCandidate);
|
||||
CollectSupervisorCommands(fileSystem, AddCandidate);
|
||||
CollectServiceRunScripts(fileSystem, AddCandidate);
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static bool TryAddHistoryCandidate(
|
||||
OciImageConfig config,
|
||||
Action<ImmutableArray<string>, string, EntryTraceEvidence?, string?> addCandidate,
|
||||
ILogger? logger)
|
||||
{
|
||||
if (config.History.IsDefaultOrEmpty || config.History.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = config.History.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var entry = config.History[i];
|
||||
if (TryExtractHistoryCommand(entry?.CreatedBy, out var command))
|
||||
{
|
||||
var metadata = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
if (!string.IsNullOrWhiteSpace(entry?.CreatedBy))
|
||||
{
|
||||
metadata["created_by"] = entry!.CreatedBy!;
|
||||
}
|
||||
|
||||
var evidence = new EntryTraceEvidence(
|
||||
Path: "/image/history",
|
||||
LayerDigest: null,
|
||||
Source: "history",
|
||||
metadata.Count > 0 ? metadata.ToImmutable() : null);
|
||||
|
||||
addCandidate(command, "history", evidence, null);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void CollectEntrypointScripts(
|
||||
IRootFileSystem fileSystem,
|
||||
Action<ImmutableArray<string>, string, EntryTraceEvidence?, string?> addCandidate)
|
||||
{
|
||||
const string directory = "/usr/local/bin";
|
||||
if (!fileSystem.DirectoryExists(directory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var entry in fileSystem.EnumerateDirectory(directory))
|
||||
{
|
||||
if (entry.IsDirectory || !entry.IsExecutable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = Path.GetFileName(entry.Path);
|
||||
if (!name.Contains("entrypoint", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var evidence = new EntryTraceEvidence(entry.Path, entry.LayerDigest, "entrypoint-script", null);
|
||||
addCandidate(ImmutableArray.Create(entry.Path), "entrypoint-script", evidence, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectSupervisorCommands(
|
||||
IRootFileSystem fileSystem,
|
||||
Action<ImmutableArray<string>, string, EntryTraceEvidence?, string?> addCandidate)
|
||||
{
|
||||
const string root = "/etc/supervisor";
|
||||
if (!fileSystem.DirectoryExists(root))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pending = new Stack<string>();
|
||||
pending.Push(root);
|
||||
|
||||
while (pending.Count > 0)
|
||||
{
|
||||
var current = pending.Pop();
|
||||
foreach (var entry in fileSystem.EnumerateDirectory(current))
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
pending.Push(entry.Path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.Path.EndsWith(".conf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fileSystem.TryReadAllText(entry.Path, out var descriptor, out var content))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var (command, program) in ExtractSupervisorCommands(content))
|
||||
{
|
||||
var metadataBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
if (!string.IsNullOrWhiteSpace(program))
|
||||
{
|
||||
metadataBuilder["program"] = program!;
|
||||
}
|
||||
|
||||
var evidence = new EntryTraceEvidence(
|
||||
entry.Path,
|
||||
descriptor.LayerDigest,
|
||||
"supervisor",
|
||||
metadataBuilder.Count > 0 ? metadataBuilder.ToImmutable() : null);
|
||||
|
||||
addCandidate(command, "supervisor", evidence, program);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectServiceRunScripts(
|
||||
IRootFileSystem fileSystem,
|
||||
Action<ImmutableArray<string>, string, EntryTraceEvidence?, string?> addCandidate)
|
||||
{
|
||||
string[] roots =
|
||||
{
|
||||
"/etc/services.d",
|
||||
"/etc/services",
|
||||
"/service",
|
||||
"/s6",
|
||||
"/etc/s6",
|
||||
"/etc/s6-overlay/s6-rc.d"
|
||||
};
|
||||
|
||||
foreach (var root in roots)
|
||||
{
|
||||
if (!fileSystem.DirectoryExists(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pending = new Stack<string>();
|
||||
pending.Push(root);
|
||||
|
||||
while (pending.Count > 0)
|
||||
{
|
||||
var current = pending.Pop();
|
||||
foreach (var entry in fileSystem.EnumerateDirectory(current))
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
pending.Push(entry.Path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.IsExecutable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.Path.EndsWith("/run", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var metadata = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
var directory = Path.GetDirectoryName(entry.Path);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
metadata["service_dir"] = directory!.Replace('\\', '/');
|
||||
}
|
||||
|
||||
var evidence = new EntryTraceEvidence(
|
||||
entry.Path,
|
||||
entry.LayerDigest,
|
||||
"service-directory",
|
||||
metadata.Count > 0 ? metadata.ToImmutable() : null);
|
||||
addCandidate(ImmutableArray.Create(entry.Path), "service-directory", evidence, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<(ImmutableArray<string> Command, string? Program)> ExtractSupervisorCommands(string content)
|
||||
{
|
||||
var results = new List<(ImmutableArray<string>, string?)>();
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
string? currentProgram = null;
|
||||
foreach (var rawLine in content.Split('\n'))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (line.Length == 0 || line.StartsWith("#", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("[", StringComparison.Ordinal) && line.EndsWith("]", StringComparison.Ordinal))
|
||||
{
|
||||
var section = line[1..^1].Trim();
|
||||
if (section.StartsWith("program:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
currentProgram = section["program:".Length..].Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
currentProgram = null;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.StartsWith("command", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var equalsIndex = line.IndexOf('=', StringComparison.Ordinal);
|
||||
if (equalsIndex < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var commandText = line[(equalsIndex + 1)..].Trim();
|
||||
if (commandText.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryTokenizeShellCommand(commandText, out var command))
|
||||
{
|
||||
results.Add((command, currentProgram));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static bool TryExtractHistoryCommand(string? createdBy, out ImmutableArray<string> command)
|
||||
{
|
||||
command = ImmutableArray<string>.Empty;
|
||||
if (string.IsNullOrWhiteSpace(createdBy))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var text = createdBy.Trim();
|
||||
|
||||
if (TryParseHistoryJsonCommand(text, "CMD", out command) ||
|
||||
TryParseHistoryJsonCommand(text, "ENTRYPOINT", out command))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var shIndex = text.IndexOf("/bin/sh", StringComparison.OrdinalIgnoreCase);
|
||||
if (shIndex >= 0)
|
||||
{
|
||||
var dashC = text.IndexOf("-c", shIndex, StringComparison.OrdinalIgnoreCase);
|
||||
if (dashC >= 0)
|
||||
{
|
||||
var after = text[(dashC + 2)..].Trim();
|
||||
if (after.StartsWith("#(nop)", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
after = after[6..].Trim();
|
||||
}
|
||||
|
||||
if (after.StartsWith("CMD", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
after = after[3..].Trim();
|
||||
}
|
||||
else if (after.StartsWith("ENTRYPOINT", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
after = after[10..].Trim();
|
||||
}
|
||||
|
||||
if (after.StartsWith("[", StringComparison.Ordinal))
|
||||
{
|
||||
if (TryParseJsonArray(after, out command))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (TryTokenizeShellCommand(after, out command))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseHistoryJsonCommand(string text, string keyword, out ImmutableArray<string> command)
|
||||
{
|
||||
command = ImmutableArray<string>.Empty;
|
||||
var index = text.IndexOf(keyword, StringComparison.OrdinalIgnoreCase);
|
||||
if (index < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bracket = text.IndexOf('[', index);
|
||||
if (bracket < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var end = text.IndexOf(']', bracket);
|
||||
if (end < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var json = text.Substring(bracket, end - bracket + 1);
|
||||
return TryParseJsonArray(json, out command);
|
||||
}
|
||||
|
||||
private static bool TryParseJsonArray(string json, out ImmutableArray<string> command)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
command = ImmutableArray<string>.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<string>();
|
||||
foreach (var element in document.RootElement.EnumerateArray())
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
builder.Add(element.GetString() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
command = builder.ToImmutable();
|
||||
return command.Length > 0;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
command = ImmutableArray<string>.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryTokenizeShellCommand(string commandText, out ImmutableArray<string> command)
|
||||
{
|
||||
var tokenizer = new ShellTokenizer();
|
||||
var tokens = tokenizer.Tokenize(commandText);
|
||||
var builder = ImmutableArray.CreateBuilder<string>();
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
switch (token.Kind)
|
||||
{
|
||||
case ShellTokenKind.Word:
|
||||
case ShellTokenKind.SingleQuoted:
|
||||
case ShellTokenKind.DoubleQuoted:
|
||||
builder.Add(token.Value);
|
||||
break;
|
||||
case ShellTokenKind.Operator:
|
||||
case ShellTokenKind.NewLine:
|
||||
case ShellTokenKind.EndOfFile:
|
||||
if (builder.Count > 0)
|
||||
{
|
||||
command = builder.ToImmutable();
|
||||
return command.Length > 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
command = builder.ToImmutable();
|
||||
return command.Length > 0;
|
||||
}
|
||||
|
||||
private static string CreateSignature(ImmutableArray<string> command)
|
||||
=> string.Join('\u001F', command);
|
||||
|
||||
private static ImmutableDictionary<string, string> BuildEnvironment(ImmutableArray<string> raw)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user