up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-27 07:46:56 +02:00
parent d63af51f84
commit ea970ead2a
302 changed files with 43161 additions and 1534 deletions

View File

@@ -0,0 +1,275 @@
namespace StellaOps.Scanner.Analyzers.Native.Observations;
/// <summary>
/// Builder for constructing NativeObservationDocument from analysis results.
/// </summary>
public sealed class NativeObservationBuilder
{
private NativeObservationBinary? _binary;
private readonly List<NativeObservationEntrypoint> _entrypoints = [];
private readonly List<NativeObservationDeclaredEdge> _declaredEdges = [];
private readonly List<NativeObservationHeuristicEdge> _heuristicEdges = [];
private readonly List<NativeObservationRuntimeEdge> _runtimeEdges = [];
private NativeObservationEnvironment _environment = new();
private readonly List<NativeObservationResolution> _resolutions = [];
/// <summary>
/// Sets the binary identity and metadata.
/// </summary>
public NativeObservationBuilder WithBinary(
string path,
NativeFormat format,
string? sha256 = null,
string? architecture = null,
string? buildId = null,
bool isUniversal = false,
string? subsystem = null,
bool is64Bit = true)
{
_binary = new NativeObservationBinary
{
Path = path,
Format = format.ToString().ToLowerInvariant(),
Sha256 = sha256,
Architecture = architecture,
BuildId = buildId,
IsUniversal = isUniversal,
Subsystem = subsystem,
Is64Bit = is64Bit,
};
return this;
}
/// <summary>
/// Adds an entrypoint.
/// </summary>
public NativeObservationBuilder AddEntrypoint(
string type,
string? symbol = null,
long? address = null,
IEnumerable<string>? conditions = null)
{
_entrypoints.Add(new NativeObservationEntrypoint
{
Type = type,
Symbol = symbol,
Address = address,
Conditions = conditions?.ToList() ?? [],
});
return this;
}
/// <summary>
/// Adds ELF declared dependencies from dynamic section.
/// </summary>
public NativeObservationBuilder AddElfDependencies(ElfDynamicInfo elfInfo)
{
foreach (var dep in elfInfo.Dependencies)
{
_declaredEdges.Add(new NativeObservationDeclaredEdge
{
Target = dep.Soname,
Reason = dep.ReasonCode,
VersionNeeds = dep.VersionNeeds.Select(v => new NativeObservationVersionNeed
{
Version = v.Version,
Hash = v.Hash,
}).ToList(),
});
}
_environment = _environment with
{
Interpreter = elfInfo.Interpreter,
Rpath = elfInfo.Rpath.Count > 0 ? elfInfo.Rpath : null,
Runpath = elfInfo.Runpath.Count > 0 ? elfInfo.Runpath : null,
};
return this;
}
/// <summary>
/// Adds PE declared dependencies from import tables.
/// </summary>
public NativeObservationBuilder AddPeDependencies(PeImportInfo peInfo)
{
foreach (var dep in peInfo.Dependencies)
{
_declaredEdges.Add(new NativeObservationDeclaredEdge
{
Target = dep.DllName,
Reason = dep.ReasonCode,
Imports = dep.ImportedFunctions.Count > 0 ? dep.ImportedFunctions : null,
});
}
foreach (var delayDep in peInfo.DelayLoadDependencies)
{
_declaredEdges.Add(new NativeObservationDeclaredEdge
{
Target = delayDep.DllName,
Reason = delayDep.ReasonCode,
Imports = delayDep.ImportedFunctions.Count > 0 ? delayDep.ImportedFunctions : null,
});
}
if (peInfo.SxsDependencies.Count > 0)
{
_environment = _environment with
{
SxsDependencies = peInfo.SxsDependencies.Select(s => new NativeObservationSxsDependency
{
Name = s.Name,
Version = s.Version,
PublicKeyToken = s.PublicKeyToken,
ProcessorArchitecture = s.ProcessorArchitecture,
}).ToList(),
};
}
return this;
}
/// <summary>
/// Adds Mach-O declared dependencies from load commands.
/// </summary>
public NativeObservationBuilder AddMachODependencies(MachOImportInfo machoInfo)
{
var allRpaths = new List<string>();
foreach (var slice in machoInfo.Slices)
{
foreach (var dep in slice.Dependencies)
{
// Avoid duplicates across slices
if (_declaredEdges.Any(e => e.Target == dep.Path && e.Reason == dep.ReasonCode))
{
continue;
}
_declaredEdges.Add(new NativeObservationDeclaredEdge
{
Target = dep.Path,
Reason = dep.ReasonCode,
Version = dep.CurrentVersion,
CompatVersion = dep.CompatibilityVersion,
});
}
allRpaths.AddRange(slice.Rpaths.Where(r => !allRpaths.Contains(r)));
}
if (allRpaths.Count > 0)
{
_environment = _environment with
{
MachORpaths = allRpaths,
};
}
return this;
}
/// <summary>
/// Adds heuristic scan results.
/// </summary>
public NativeObservationBuilder AddHeuristicResults(HeuristicScanResult scanResult)
{
foreach (var edge in scanResult.Edges)
{
_heuristicEdges.Add(new NativeObservationHeuristicEdge
{
Target = edge.LibraryName,
Reason = edge.ReasonCode,
Confidence = edge.Confidence.ToString().ToLowerInvariant(),
Context = edge.Context,
Offset = edge.FileOffset,
});
}
if (scanResult.PluginConfigs.Count > 0)
{
_environment = _environment with
{
PluginConfigs = scanResult.PluginConfigs,
};
}
return this;
}
/// <summary>
/// Adds a resolution result with explain trace.
/// </summary>
public NativeObservationBuilder AddResolution(ResolveResult result)
{
_resolutions.Add(new NativeObservationResolution
{
Requested = result.RequestedName,
Resolved = result.Resolved,
ResolvedPath = result.ResolvedPath,
Steps = result.Steps.Select(s => new NativeObservationResolutionStep
{
SearchPath = s.SearchPath,
Reason = s.SearchReason,
Found = s.Found,
}).ToList(),
});
return this;
}
/// <summary>
/// Sets the default search paths for the platform.
/// </summary>
public NativeObservationBuilder WithDefaultSearchPaths(IEnumerable<string> paths)
{
_environment = _environment with
{
DefaultSearchPaths = paths.ToList(),
};
return this;
}
/// <summary>
/// Adds a runtime-observed dependency edge.
/// </summary>
public NativeObservationBuilder AddRuntimeEdge(
string target,
string reasonCode,
HeuristicConfidence confidence,
DateTime? firstObserved = null,
int? observationCount = null)
{
_runtimeEdges.Add(new NativeObservationRuntimeEdge
{
Target = target,
Reason = reasonCode,
Confidence = confidence.ToString().ToLowerInvariant(),
FirstObserved = firstObserved,
ObservationCount = observationCount,
});
return this;
}
/// <summary>
/// Builds the observation document.
/// </summary>
public NativeObservationDocument Build()
{
if (_binary is null)
{
throw new InvalidOperationException("Binary information must be set before building.");
}
return new NativeObservationDocument
{
Binary = _binary,
Entrypoints = _entrypoints,
DeclaredEdges = _declaredEdges,
HeuristicEdges = _heuristicEdges,
RuntimeEdges = _runtimeEdges,
Environment = _environment,
Resolution = _resolutions,
};
}
}

View File

@@ -0,0 +1,294 @@
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Analyzers.Native.Observations;
/// <summary>
/// AOC-compliant observation document for native binary analysis.
/// Contains entrypoints, dependency edges, and environment profiles.
/// </summary>
public sealed record NativeObservationDocument
{
/// <summary>Schema identifier for this observation format.</summary>
[JsonPropertyName("$schema")]
public string Schema { get; init; } = "stellaops.native.observation@1";
/// <summary>Binary identity (path, hash, format).</summary>
[JsonPropertyName("binary")]
public required NativeObservationBinary Binary { get; init; }
/// <summary>Detected entrypoints in this binary.</summary>
[JsonPropertyName("entrypoints")]
public IReadOnlyList<NativeObservationEntrypoint> Entrypoints { get; init; } = [];
/// <summary>Declared dependency edges from import tables/load commands.</summary>
[JsonPropertyName("declared_edges")]
public IReadOnlyList<NativeObservationDeclaredEdge> DeclaredEdges { get; init; } = [];
/// <summary>Heuristically detected edges (dlopen strings, etc).</summary>
[JsonPropertyName("heuristic_edges")]
public IReadOnlyList<NativeObservationHeuristicEdge> HeuristicEdges { get; init; } = [];
/// <summary>Runtime-observed edges from capture sessions.</summary>
[JsonPropertyName("runtime_edges")]
public IReadOnlyList<NativeObservationRuntimeEdge> RuntimeEdges { get; init; } = [];
/// <summary>Environment profile (search paths, interpreter, loader metadata).</summary>
[JsonPropertyName("environment")]
public required NativeObservationEnvironment Environment { get; init; }
/// <summary>Resolver results for dependency resolution explain traces.</summary>
[JsonPropertyName("resolution")]
public IReadOnlyList<NativeObservationResolution> Resolution { get; init; } = [];
}
/// <summary>
/// Binary identity and metadata.
/// </summary>
public sealed record NativeObservationBinary
{
/// <summary>Path to the binary within the image/filesystem.</summary>
[JsonPropertyName("path")]
public required string Path { get; init; }
/// <summary>SHA256 hash of the binary content.</summary>
[JsonPropertyName("sha256")]
public string? Sha256 { get; init; }
/// <summary>Native format (elf, pe, macho).</summary>
[JsonPropertyName("format")]
public required string Format { get; init; }
/// <summary>Target architecture (x86_64, arm64, etc).</summary>
[JsonPropertyName("architecture")]
public string? Architecture { get; init; }
/// <summary>Build ID (ELF) or UUID (Mach-O).</summary>
[JsonPropertyName("build_id")]
public string? BuildId { get; init; }
/// <summary>True if this is a universal/fat binary (Mach-O).</summary>
[JsonPropertyName("is_universal")]
public bool IsUniversal { get; init; }
/// <summary>PE subsystem (windows_gui, windows_console, etc).</summary>
[JsonPropertyName("subsystem")]
public string? Subsystem { get; init; }
/// <summary>True if this is a 64-bit binary.</summary>
[JsonPropertyName("is_64bit")]
public bool Is64Bit { get; init; }
}
/// <summary>
/// Entrypoint detected in the binary.
/// </summary>
public sealed record NativeObservationEntrypoint
{
/// <summary>Entrypoint type (main, init_array, constructor, dllmain, etc).</summary>
[JsonPropertyName("type")]
public required string Type { get; init; }
/// <summary>Symbol name if available.</summary>
[JsonPropertyName("symbol")]
public string? Symbol { get; init; }
/// <summary>Virtual address of the entrypoint.</summary>
[JsonPropertyName("address")]
public long? Address { get; init; }
/// <summary>Condition set for this entrypoint.</summary>
[JsonPropertyName("conditions")]
public IReadOnlyList<string> Conditions { get; init; } = [];
}
/// <summary>
/// Declared dependency edge from import table/load command.
/// </summary>
public sealed record NativeObservationDeclaredEdge
{
/// <summary>Target library name or path.</summary>
[JsonPropertyName("target")]
public required string Target { get; init; }
/// <summary>Reason code (elf-dtneeded, pe-import, pe-delayimport, macho-loadlib, etc).</summary>
[JsonPropertyName("reason")]
public required string Reason { get; init; }
/// <summary>Version information if available.</summary>
[JsonPropertyName("version")]
public string? Version { get; init; }
/// <summary>Compatibility version (Mach-O).</summary>
[JsonPropertyName("compat_version")]
public string? CompatVersion { get; init; }
/// <summary>Imported functions (PE).</summary>
[JsonPropertyName("imports")]
public IReadOnlyList<string>? Imports { get; init; }
/// <summary>Version needs (ELF).</summary>
[JsonPropertyName("version_needs")]
public IReadOnlyList<NativeObservationVersionNeed>? VersionNeeds { get; init; }
}
/// <summary>
/// ELF version need record.
/// </summary>
public sealed record NativeObservationVersionNeed
{
/// <summary>Version string.</summary>
[JsonPropertyName("version")]
public required string Version { get; init; }
/// <summary>Version hash.</summary>
[JsonPropertyName("hash")]
public uint Hash { get; init; }
}
/// <summary>
/// Heuristically detected dependency edge.
/// </summary>
public sealed record NativeObservationHeuristicEdge
{
/// <summary>Target library name.</summary>
[JsonPropertyName("target")]
public required string Target { get; init; }
/// <summary>Reason code (string-dlopen, string-loadlibrary, config-plugin, go-cgo-import, rust-ffi).</summary>
[JsonPropertyName("reason")]
public required string Reason { get; init; }
/// <summary>Confidence level (low, medium, high).</summary>
[JsonPropertyName("confidence")]
public required string Confidence { get; init; }
/// <summary>Context about how it was detected.</summary>
[JsonPropertyName("context")]
public string? Context { get; init; }
/// <summary>File offset where the string was found.</summary>
[JsonPropertyName("offset")]
public long? Offset { get; init; }
}
/// <summary>
/// Runtime-observed dependency edge from capture session.
/// </summary>
public sealed record NativeObservationRuntimeEdge
{
/// <summary>Target library path or name.</summary>
[JsonPropertyName("target")]
public required string Target { get; init; }
/// <summary>Reason code (runtime-dlopen, runtime-loadlibrary, runtime-dylib, etc).</summary>
[JsonPropertyName("reason")]
public required string Reason { get; init; }
/// <summary>Confidence level (always high for runtime evidence).</summary>
[JsonPropertyName("confidence")]
public required string Confidence { get; init; }
/// <summary>First time this edge was observed.</summary>
[JsonPropertyName("first_observed")]
public DateTime? FirstObserved { get; init; }
/// <summary>Number of times this edge was observed.</summary>
[JsonPropertyName("observation_count")]
public int? ObservationCount { get; init; }
}
/// <summary>
/// Environment profile with search paths and loader metadata.
/// </summary>
public sealed record NativeObservationEnvironment
{
/// <summary>Dynamic linker/interpreter path.</summary>
[JsonPropertyName("interpreter")]
public string? Interpreter { get; init; }
/// <summary>DT_RPATH entries (ELF).</summary>
[JsonPropertyName("rpath")]
public IReadOnlyList<string>? Rpath { get; init; }
/// <summary>DT_RUNPATH entries (ELF).</summary>
[JsonPropertyName("runpath")]
public IReadOnlyList<string>? Runpath { get; init; }
/// <summary>LC_RPATH entries (Mach-O).</summary>
[JsonPropertyName("macho_rpaths")]
public IReadOnlyList<string>? MachORpaths { get; init; }
/// <summary>Default search paths for the platform.</summary>
[JsonPropertyName("default_search_paths")]
public IReadOnlyList<string>? DefaultSearchPaths { get; init; }
/// <summary>Plugin configuration files referenced.</summary>
[JsonPropertyName("plugin_configs")]
public IReadOnlyList<string>? PluginConfigs { get; init; }
/// <summary>SxS dependencies (PE).</summary>
[JsonPropertyName("sxs_dependencies")]
public IReadOnlyList<NativeObservationSxsDependency>? SxsDependencies { get; init; }
}
/// <summary>
/// Windows Side-by-Side (SxS) dependency.
/// </summary>
public sealed record NativeObservationSxsDependency
{
/// <summary>Assembly name.</summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>Assembly version.</summary>
[JsonPropertyName("version")]
public string? Version { get; init; }
/// <summary>Public key token.</summary>
[JsonPropertyName("public_key_token")]
public string? PublicKeyToken { get; init; }
/// <summary>Processor architecture.</summary>
[JsonPropertyName("processor_architecture")]
public string? ProcessorArchitecture { get; init; }
}
/// <summary>
/// Resolution result for a dependency with explain trace.
/// </summary>
public sealed record NativeObservationResolution
{
/// <summary>The library name that was resolved.</summary>
[JsonPropertyName("requested")]
public required string Requested { get; init; }
/// <summary>Whether resolution succeeded.</summary>
[JsonPropertyName("resolved")]
public bool Resolved { get; init; }
/// <summary>The final resolved path if found.</summary>
[JsonPropertyName("resolved_path")]
public string? ResolvedPath { get; init; }
/// <summary>Resolution steps taken (explain trace).</summary>
[JsonPropertyName("steps")]
public IReadOnlyList<NativeObservationResolutionStep> Steps { get; init; } = [];
}
/// <summary>
/// A single step in the resolution explain trace.
/// </summary>
public sealed record NativeObservationResolutionStep
{
/// <summary>The path that was searched.</summary>
[JsonPropertyName("search_path")]
public required string SearchPath { get; init; }
/// <summary>Why this path was searched (rpath, runpath, default, etc).</summary>
[JsonPropertyName("reason")]
public required string Reason { get; init; }
/// <summary>Whether the library was found at this path.</summary>
[JsonPropertyName("found")]
public bool Found { get; init; }
}

View File

@@ -0,0 +1,136 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Analyzers.Native.Observations;
/// <summary>
/// Serializes NativeObservationDocument to JSON and computes content hashes.
/// </summary>
public static class NativeObservationSerializer
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
};
private static readonly JsonSerializerOptions PrettySerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
};
/// <summary>
/// Serializes the observation document to compact JSON.
/// </summary>
public static string Serialize(NativeObservationDocument document)
{
ArgumentNullException.ThrowIfNull(document);
return JsonSerializer.Serialize(document, SerializerOptions);
}
/// <summary>
/// Serializes the observation document to pretty-printed JSON.
/// </summary>
public static string SerializePretty(NativeObservationDocument document)
{
ArgumentNullException.ThrowIfNull(document);
return JsonSerializer.Serialize(document, PrettySerializerOptions);
}
/// <summary>
/// Serializes the observation document to a UTF-8 byte array.
/// </summary>
public static byte[] SerializeToBytes(NativeObservationDocument document)
{
ArgumentNullException.ThrowIfNull(document);
return JsonSerializer.SerializeToUtf8Bytes(document, SerializerOptions);
}
/// <summary>
/// Deserializes a JSON string to an observation document.
/// </summary>
public static NativeObservationDocument? Deserialize(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return null;
}
return JsonSerializer.Deserialize<NativeObservationDocument>(json, SerializerOptions);
}
/// <summary>
/// Deserializes a UTF-8 byte array to an observation document.
/// </summary>
public static NativeObservationDocument? Deserialize(ReadOnlySpan<byte> json)
{
if (json.IsEmpty)
{
return null;
}
return JsonSerializer.Deserialize<NativeObservationDocument>(json, SerializerOptions);
}
/// <summary>
/// Computes SHA256 hash of the serialized document.
/// </summary>
public static string ComputeSha256(NativeObservationDocument document)
{
var bytes = SerializeToBytes(document);
return ComputeSha256(bytes);
}
/// <summary>
/// Computes SHA256 hash of a JSON string.
/// </summary>
public static string ComputeSha256(string json)
{
var bytes = Encoding.UTF8.GetBytes(json);
return ComputeSha256(bytes);
}
/// <summary>
/// Computes SHA256 hash of a byte array.
/// </summary>
public static string ComputeSha256(byte[] data)
{
var hash = SHA256.HashData(data);
return Convert.ToHexString(hash).ToLowerInvariant();
}
/// <summary>
/// Writes the observation document to a stream.
/// </summary>
public static async Task WriteAsync(
NativeObservationDocument document,
Stream stream,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(document);
ArgumentNullException.ThrowIfNull(stream);
await JsonSerializer.SerializeAsync(stream, document, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Reads an observation document from a stream.
/// </summary>
public static async Task<NativeObservationDocument?> ReadAsync(
Stream stream,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stream);
return await JsonSerializer.DeserializeAsync<NativeObservationDocument>(
stream, SerializerOptions, cancellationToken).ConfigureAwait(false);
}
}