notify doctors work, audit work, new product advisory sprints

This commit is contained in:
master
2026-01-13 08:36:29 +02:00
parent b8868a5f13
commit 9ca7cb183e
343 changed files with 24492 additions and 3544 deletions

View File

@@ -13,9 +13,10 @@ public sealed class ElfHardeningExtractor : IHardeningExtractor
{
private readonly TimeProvider _timeProvider;
public ElfHardeningExtractor(TimeProvider? timeProvider = null)
public ElfHardeningExtractor(TimeProvider timeProvider)
{
_timeProvider = timeProvider ?? TimeProvider.System;
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
}
// ELF magic bytes

View File

@@ -19,9 +19,10 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor
{
private readonly TimeProvider _timeProvider;
public MachoHardeningExtractor(TimeProvider? timeProvider = null)
public MachoHardeningExtractor(TimeProvider timeProvider)
{
_timeProvider = timeProvider ?? TimeProvider.System;
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
}
// Mach-O magic numbers
@@ -105,7 +106,7 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor
// Handle universal binaries - just extract first architecture for now
if (magic is FAT_MAGIC or FAT_CIGAM)
{
var fatResult = ExtractFromFat(data, path, digest);
var fatResult = await ExtractFromFatAsync(data, path, digest, ct);
if (fatResult is not null)
return fatResult;
return CreateResult(path, digest, [], ["Universal binary: no supported architectures"]);
@@ -233,7 +234,7 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor
/// <summary>
/// Extract hardening info from the first slice of a universal (fat) binary.
/// </summary>
private BinaryHardeningFlags? ExtractFromFat(byte[] data, string path, string digest)
private async Task<BinaryHardeningFlags?> ExtractFromFatAsync(byte[] data, string path, string digest, CancellationToken ct)
{
if (data.Length < 8) return null;
@@ -254,7 +255,7 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor
// Extract first architecture and re-parse
var sliceData = data.AsSpan((int)archOffset, (int)archSize).ToArray();
using var sliceStream = new MemoryStream(sliceData);
return ExtractAsync(sliceStream, path, digest).GetAwaiter().GetResult();
return await ExtractAsync(sliceStream, path, digest, ct);
}
private static uint ReadUInt32(byte[] data, int offset, bool littleEndian)

View File

@@ -21,9 +21,10 @@ public sealed class PeHardeningExtractor : IHardeningExtractor
{
private readonly TimeProvider _timeProvider;
public PeHardeningExtractor(TimeProvider? timeProvider = null)
public PeHardeningExtractor(TimeProvider timeProvider)
{
_timeProvider = timeProvider ?? TimeProvider.System;
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
}
// PE magic bytes: MZ (DOS header)

View File

@@ -32,16 +32,17 @@ public sealed class OfflineBuildIdIndex : IBuildIdIndex
public OfflineBuildIdIndex(
IOptions<BuildIdIndexOptions> options,
ILogger<OfflineBuildIdIndex> logger,
IDsseSigningService? dsseSigningService = null,
TimeProvider? timeProvider = null)
TimeProvider timeProvider,
IDsseSigningService? dsseSigningService = null)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(timeProvider);
_options = options.Value;
_logger = logger;
_dsseSigningService = dsseSigningService;
_timeProvider = timeProvider ?? TimeProvider.System;
_timeProvider = timeProvider;
}
/// <inheritdoc />

View File

@@ -514,7 +514,8 @@ public static class PeImportParser
{
IgnoreWhitespace = true,
IgnoreComments = true,
DtdProcessing = DtdProcessing.Ignore,
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
});
while (reader.Read())

View File

@@ -0,0 +1,30 @@
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
internal static class CaptureDurationTimer
{
internal static Task RunAsync(
TimeSpan duration,
Func<CancellationToken, Task> stopAsync,
CancellationToken captureToken,
CancellationToken stopToken)
{
ArgumentNullException.ThrowIfNull(stopAsync);
return RunCoreAsync(duration, stopAsync, captureToken, stopToken);
}
private static async Task RunCoreAsync(
TimeSpan duration,
Func<CancellationToken, Task> stopAsync,
CancellationToken captureToken,
CancellationToken stopToken)
{
try
{
await Task.Delay(duration, captureToken).ConfigureAwait(false);
await stopAsync(stopToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
}
}

View File

@@ -1,3 +1,5 @@
using StellaOps.Determinism;
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
/// <summary>
@@ -159,16 +161,19 @@ public static class RuntimeCaptureAdapterFactory
/// Creates the appropriate capture adapter for the current platform.
/// </summary>
/// <returns>Platform-specific adapter or null if no adapter available.</returns>
public static IRuntimeCaptureAdapter? CreateForCurrentPlatform()
public static IRuntimeCaptureAdapter? CreateForCurrentPlatform(TimeProvider timeProvider, IGuidProvider guidProvider)
{
ArgumentNullException.ThrowIfNull(timeProvider);
ArgumentNullException.ThrowIfNull(guidProvider);
if (OperatingSystem.IsLinux())
return new LinuxEbpfCaptureAdapter();
return new LinuxEbpfCaptureAdapter(timeProvider, guidProvider);
if (OperatingSystem.IsWindows())
return new WindowsEtwCaptureAdapter();
return new WindowsEtwCaptureAdapter(timeProvider, guidProvider);
if (OperatingSystem.IsMacOS())
return new MacOsDyldCaptureAdapter();
return new MacOsDyldCaptureAdapter(timeProvider, guidProvider);
return null;
}
@@ -177,18 +182,21 @@ public static class RuntimeCaptureAdapterFactory
/// Gets all available adapters for the current platform.
/// </summary>
/// <returns>List of available adapters.</returns>
public static IReadOnlyList<IRuntimeCaptureAdapter> GetAvailableAdapters()
public static IReadOnlyList<IRuntimeCaptureAdapter> GetAvailableAdapters(TimeProvider timeProvider, IGuidProvider guidProvider)
{
ArgumentNullException.ThrowIfNull(timeProvider);
ArgumentNullException.ThrowIfNull(guidProvider);
var adapters = new List<IRuntimeCaptureAdapter>();
if (OperatingSystem.IsLinux())
adapters.Add(new LinuxEbpfCaptureAdapter());
adapters.Add(new LinuxEbpfCaptureAdapter(timeProvider, guidProvider));
if (OperatingSystem.IsWindows())
adapters.Add(new WindowsEtwCaptureAdapter());
adapters.Add(new WindowsEtwCaptureAdapter(timeProvider, guidProvider));
if (OperatingSystem.IsMacOS())
adapters.Add(new MacOsDyldCaptureAdapter());
adapters.Add(new MacOsDyldCaptureAdapter(timeProvider, guidProvider));
return adapters;
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
@@ -32,6 +33,7 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
private DateTime _startTime;
private CancellationTokenSource? _captureCts;
private Task? _captureTask;
private Task? _durationTask;
private Process? _bpftraceProcess;
private long _droppedEvents;
private int _redactedPaths;
@@ -39,11 +41,12 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
/// <summary>
/// Creates a new Linux eBPF capture adapter.
/// </summary>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
/// <param name="timeProvider">Time provider for deterministic timestamps.</param>
/// <param name="guidProvider">Optional GUID provider for deterministic session IDs.</param>
public LinuxEbpfCaptureAdapter(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null)
public LinuxEbpfCaptureAdapter(TimeProvider timeProvider, IGuidProvider? guidProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
@@ -184,18 +187,11 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
}
// Start the duration timer
_ = Task.Run(async () =>
{
try
{
await Task.Delay(options.MaxCaptureDuration, _captureCts.Token);
await StopCaptureAsync(CancellationToken.None);
}
catch (OperationCanceledException)
{
// Expected when capture is stopped manually
}
}, _captureCts.Token);
_durationTask = CaptureDurationTimer.RunAsync(
options.MaxCaptureDuration,
ct => StopCaptureAsync(ct),
_captureCts.Token,
cancellationToken);
SetState(CaptureState.Running);
return SessionId;
@@ -254,6 +250,22 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
}
}
if (_durationTask != null)
{
try
{
await _durationTask.WaitAsync(TimeSpan.FromSeconds(2), cancellationToken);
}
catch (TimeoutException)
{
// Timer did not complete in time
}
catch (OperationCanceledException)
{
// Expected when canceling
}
}
var session = new RuntimeCaptureSession(
SessionId: SessionId ?? "unknown",
StartTime: _startTime,
@@ -333,20 +345,7 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
// Build bpftrace script for dlopen tracing
var script = BuildBpftraceScript();
var psi = new ProcessStartInfo
{
FileName = "bpftrace",
Arguments = $"-e '{script}'",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
if (_options?.TargetProcessId != null)
{
psi.Arguments = $"-p {_options.TargetProcessId} -e '{script}'";
}
var psi = CreateBpftraceStartInfo(script, _options?.TargetProcessId);
_bpftraceProcess = new Process { StartInfo = psi };
_bpftraceProcess.OutputDataReceived += OnBpftraceOutput;
@@ -420,8 +419,8 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
{
return new RuntimeLoadEvent(
Timestamp: _timeProvider.GetUtcNow().UtcDateTime,
ProcessId: int.Parse(parts[1]),
ThreadId: int.Parse(parts[2]),
ProcessId: int.Parse(parts[1], CultureInfo.InvariantCulture),
ThreadId: int.Parse(parts[2], CultureInfo.InvariantCulture),
LoadType: RuntimeLoadType.Dlopen,
RequestedPath: parts[3],
ResolvedPath: null, // Set on return probe
@@ -512,14 +511,17 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
{
try
{
if (!IsSafeToken(command))
return false;
var psi = new ProcessStartInfo
{
FileName = "which",
Arguments = command,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add(command);
using var process = Process.Start(psi);
if (process == null)
@@ -545,8 +547,8 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
if (match.Success)
{
return new Version(
int.Parse(match.Groups[1].Value),
int.Parse(match.Groups[2].Value));
int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture),
int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture));
}
}
}
@@ -577,14 +579,17 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
{
try
{
if (!IsSafeToken(capability))
return false;
var psi = new ProcessStartInfo
{
FileName = "capsh",
Arguments = $"--print",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add("--print");
using var process = Process.Start(psi);
if (process == null)
@@ -609,4 +614,43 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter
return Regex.IsMatch(path, regexPattern, RegexOptions.IgnoreCase);
}
private static ProcessStartInfo CreateBpftraceStartInfo(string script, int? targetProcessId)
{
if (string.IsNullOrWhiteSpace(script))
throw new ArgumentException("Script cannot be empty.", nameof(script));
var psi = new ProcessStartInfo
{
FileName = "bpftrace",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
if (targetProcessId is int pid && pid > 0)
{
psi.ArgumentList.Add("-p");
psi.ArgumentList.Add(pid.ToString(CultureInfo.InvariantCulture));
}
psi.ArgumentList.Add("-e");
psi.ArgumentList.Add(script);
return psi;
}
private static bool IsSafeToken(string value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
foreach (var ch in value)
{
if (!char.IsAsciiLetterOrDigit(ch) && ch != '-' && ch != '_' && ch != '.')
return false;
}
return true;
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
@@ -33,6 +34,7 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
private DateTime _startTime;
private CancellationTokenSource? _captureCts;
private Task? _captureTask;
private Task? _durationTask;
private Process? _dtraceProcess;
private long _droppedEvents;
private int _redactedPaths;
@@ -40,11 +42,12 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
/// <summary>
/// Creates a new macOS dyld capture adapter.
/// </summary>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
/// <param name="timeProvider">Time provider for deterministic timestamps.</param>
/// <param name="guidProvider">Optional GUID provider for deterministic session IDs.</param>
public MacOsDyldCaptureAdapter(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null)
public MacOsDyldCaptureAdapter(TimeProvider timeProvider, IGuidProvider? guidProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
@@ -188,18 +191,11 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
}
// Start the duration timer
_ = Task.Run(async () =>
{
try
{
await Task.Delay(options.MaxCaptureDuration, _captureCts.Token);
await StopCaptureAsync(CancellationToken.None);
}
catch (OperationCanceledException)
{
// Expected when capture is stopped manually
}
}, _captureCts.Token);
_durationTask = CaptureDurationTimer.RunAsync(
options.MaxCaptureDuration,
ct => StopCaptureAsync(ct),
_captureCts.Token,
cancellationToken);
SetState(CaptureState.Running);
return SessionId;
@@ -258,6 +254,22 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
}
}
if (_durationTask != null)
{
try
{
await _durationTask.WaitAsync(TimeSpan.FromSeconds(2), cancellationToken);
}
catch (TimeoutException)
{
// Timer did not complete in time
}
catch (OperationCanceledException)
{
// Expected when canceling
}
}
var session = new RuntimeCaptureSession(
SessionId: SessionId ?? "unknown",
StartTime: _startTime,
@@ -337,20 +349,7 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
// Build dtrace script for dyld tracing
var script = BuildDtraceScript();
var psi = new ProcessStartInfo
{
FileName = "dtrace",
Arguments = $"-n '{script}'",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
if (_options?.TargetProcessId != null)
{
psi.Arguments = $"-p {_options.TargetProcessId} -n '{script}'";
}
var psi = CreateDtraceStartInfo(script, _options?.TargetProcessId);
_dtraceProcess = new Process { StartInfo = psi };
_dtraceProcess.OutputDataReceived += OnDtraceOutput;
@@ -432,8 +431,8 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
return new RuntimeLoadEvent(
Timestamp: _timeProvider.GetUtcNow().UtcDateTime,
ProcessId: int.Parse(parts[1]),
ThreadId: int.Parse(parts[2]),
ProcessId: int.Parse(parts[1], CultureInfo.InvariantCulture),
ThreadId: int.Parse(parts[2], CultureInfo.InvariantCulture),
LoadType: loadType,
RequestedPath: parts[3],
ResolvedPath: null, // Set on return probe
@@ -524,14 +523,17 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
{
try
{
if (!IsSafeToken(command))
return false;
var psi = new ProcessStartInfo
{
FileName = "which",
Arguments = command,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add(command);
using var process = Process.Start(psi);
if (process == null)
@@ -557,14 +559,17 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
{
try
{
if (!IsSafeToken("csrutil"))
return SipStatus.Unknown;
var psi = new ProcessStartInfo
{
FileName = "csrutil",
Arguments = "status",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add("status");
using var process = Process.Start(psi);
if (process == null)
@@ -610,4 +615,43 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter
return Regex.IsMatch(path, regexPattern, RegexOptions.IgnoreCase);
}
private static ProcessStartInfo CreateDtraceStartInfo(string script, int? targetProcessId)
{
if (string.IsNullOrWhiteSpace(script))
throw new ArgumentException("Script cannot be empty.", nameof(script));
var psi = new ProcessStartInfo
{
FileName = "dtrace",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
if (targetProcessId is int pid && pid > 0)
{
psi.ArgumentList.Add("-p");
psi.ArgumentList.Add(pid.ToString(CultureInfo.InvariantCulture));
}
psi.ArgumentList.Add("-n");
psi.ArgumentList.Add(script);
return psi;
}
private static bool IsSafeToken(string value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
foreach (var ch in value)
{
if (!char.IsAsciiLetterOrDigit(ch) && ch != '-' && ch != '_' && ch != '.')
return false;
}
return true;
}
}

View File

@@ -48,14 +48,15 @@ public static class RuntimeEvidenceAggregator
/// <param name="runtimeEvidence">Runtime capture evidence.</param>
/// <param name="staticEdges">Static analysis dependency edges.</param>
/// <param name="heuristicEdges">Heuristic analysis edges.</param>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
/// <param name="timeProvider">Time provider for deterministic timestamps.</param>
/// <returns>Merged evidence document.</returns>
public static MergedEvidence MergeWithStaticAnalysis(
RuntimeEvidence runtimeEvidence,
IEnumerable<Observations.NativeObservationDeclaredEdge> staticEdges,
IEnumerable<Observations.NativeObservationHeuristicEdge> heuristicEdges,
TimeProvider? timeProvider = null)
TimeProvider timeProvider)
{
ArgumentNullException.ThrowIfNull(timeProvider);
var staticList = staticEdges.ToList();
var heuristicList = heuristicEdges.ToList();
@@ -142,7 +143,6 @@ public static class RuntimeEvidenceAggregator
}
}
var tp = timeProvider ?? TimeProvider.System;
return new MergedEvidence(
ConfirmedEdges: confirmedEdges,
StaticOnlyEdges: staticOnlyEdges,
@@ -151,7 +151,7 @@ public static class RuntimeEvidenceAggregator
TotalRuntimeEvents: runtimeEvidence.Sessions.Sum(s => s.Events.Count),
TotalDroppedEvents: runtimeEvidence.Sessions.Sum(s => s.TotalEventsDropped),
CaptureStartTime: runtimeEvidence.Sessions.Min(s => s.StartTime),
CaptureEndTime: runtimeEvidence.Sessions.Max(s => s.EndTime ?? tp.GetUtcNow().UtcDateTime));
CaptureEndTime: runtimeEvidence.Sessions.Max(s => s.EndTime ?? timeProvider.GetUtcNow().UtcDateTime));
}
/// <summary>

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Runtime.Versioning;
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
@@ -274,12 +275,14 @@ public sealed record CollapsedStack
/// Format: "container@digest;buildid=xxx;func;... count"
/// </summary>
/// <param name="line">The collapsed stack line to parse.</param>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
public static CollapsedStack? Parse(string line, TimeProvider? timeProvider = null)
/// <param name="timeProvider">Time provider for deterministic timestamps.</param>
public static CollapsedStack? Parse(string line, TimeProvider timeProvider)
{
if (string.IsNullOrWhiteSpace(line))
return null;
ArgumentNullException.ThrowIfNull(timeProvider);
var lastSpace = line.LastIndexOf(' ');
if (lastSpace < 0)
return null;
@@ -287,7 +290,7 @@ public sealed record CollapsedStack
var stackPart = line[..lastSpace];
var countPart = line[(lastSpace + 1)..];
if (!int.TryParse(countPart, out var count))
if (!int.TryParse(countPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count))
return null;
var firstSemi = stackPart.IndexOf(';');
@@ -307,8 +310,7 @@ public sealed record CollapsedStack
}
}
var tp = timeProvider ?? TimeProvider.System;
var now = tp.GetUtcNow().UtcDateTime;
var now = timeProvider.GetUtcNow().UtcDateTime;
return new CollapsedStack
{
ContainerIdentifier = container,

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Principal;
@@ -31,20 +32,19 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
private DateTime _startTime;
private CancellationTokenSource? _captureCts;
private Task? _captureTask;
#pragma warning disable CS0649 // Field is never assigned (assigned via Start/Stop ETW)
private Process? _logmanProcess;
#pragma warning restore CS0649
private Task? _durationTask;
private long _droppedEvents;
private int _redactedPaths;
/// <summary>
/// Creates a new Windows ETW capture adapter.
/// </summary>
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
/// <param name="timeProvider">Time provider for deterministic timestamps.</param>
/// <param name="guidProvider">Optional GUID provider for deterministic session IDs.</param>
public WindowsEtwCaptureAdapter(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null)
public WindowsEtwCaptureAdapter(TimeProvider timeProvider, IGuidProvider? guidProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
}
@@ -106,16 +106,21 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
requiresElevation = true;
}
// Check for logman.exe (built-in ETW management tool)
var logmanPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.System),
"logman.exe");
// Check for logman.exe and tracerpt.exe (built-in ETW tools)
var systemDir = Environment.GetFolderPath(Environment.SpecialFolder.System);
var logmanPath = Path.Combine(systemDir, "logman.exe");
var tracerptPath = Path.Combine(systemDir, "tracerpt.exe");
if (!File.Exists(logmanPath))
{
missingDeps.Add("logman.exe");
}
if (!File.Exists(tracerptPath))
{
missingDeps.Add("tracerpt.exe");
}
if (missingDeps.Count > 0)
{
return Task.FromResult(new AdapterAvailability(
@@ -178,18 +183,11 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
}
// Start the duration timer
_ = Task.Run(async () =>
{
try
{
await Task.Delay(options.MaxCaptureDuration, _captureCts.Token);
await StopCaptureAsync(CancellationToken.None);
}
catch (OperationCanceledException)
{
// Expected when capture is stopped manually
}
}, _captureCts.Token);
_durationTask = CaptureDurationTimer.RunAsync(
options.MaxCaptureDuration,
ct => StopCaptureAsync(ct),
_captureCts.Token,
cancellationToken);
SetState(CaptureState.Running);
return SessionId;
@@ -220,20 +218,6 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
// Stop ETW session
await StopEtwSessionAsync(cancellationToken);
// Kill logman process if running
if (_logmanProcess is { HasExited: false })
{
try
{
_logmanProcess.Kill(true);
await _logmanProcess.WaitForExitAsync(cancellationToken);
}
catch
{
// Ignore kill errors
}
}
// Wait for capture task
if (_captureTask != null)
{
@@ -251,6 +235,22 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
}
}
if (_durationTask != null)
{
try
{
await _durationTask.WaitAsync(TimeSpan.FromSeconds(2), cancellationToken);
}
catch (TimeoutException)
{
// Timer did not complete in time
}
catch (OperationCanceledException)
{
// Expected when canceling
}
}
var session = new RuntimeCaptureSession(
SessionId: SessionId ?? "unknown",
StartTime: _startTime,
@@ -298,7 +298,6 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
}
_captureCts?.Dispose();
_logmanProcess?.Dispose();
}
private async Task RunSandboxCaptureAsync(CancellationToken cancellationToken)
@@ -367,17 +366,17 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
{
// Create ETW session for kernel image load events
// Provider GUID for Microsoft-Windows-Kernel-Process: {22FB2CD6-0E7B-422B-A0C7-2FAD1FD0E716}
var args = $"create trace \"{sessionName}\" -o \"{etlPath}\" -p \"Microsoft-Windows-Kernel-Process\" 0x10 -ets";
var psi = new ProcessStartInfo
{
FileName = "logman.exe",
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
var psi = CreateLogmanStartInfo(
sessionName,
"create",
"trace",
sessionName,
"-o",
etlPath,
"-p",
"Microsoft-Windows-Kernel-Process",
"0x10",
"-ets");
using var process = Process.Start(psi);
if (process == null)
@@ -399,15 +398,11 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
var sessionName = $"StellaOps_ImageLoad_{SessionId}";
var psi = new ProcessStartInfo
{
FileName = "logman.exe",
Arguments = $"stop \"{sessionName}\" -ets",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
var psi = CreateLogmanStartInfo(
sessionName,
"stop",
sessionName,
"-ets");
try
{
@@ -433,15 +428,7 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
try
{
var psi = new ProcessStartInfo
{
FileName = "tracerpt.exe",
Arguments = $"\"{etlPath}\" -o \"{xmlPath}\" -of XML -summary -report",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
var psi = CreateTracerptStartInfo(etlPath, xmlPath);
using var process = Process.Start(psi);
if (process != null)
@@ -482,8 +469,13 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
foreach (Match match in imageLoadPattern.Matches(content))
{
var imagePath = match.Groups[1].Value;
var processId = int.Parse(match.Groups[2].Value);
var imageBase = Convert.ToUInt64(match.Groups[3].Value, 16);
var processId = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture);
var imageBaseText = match.Groups[3].Value;
if (imageBaseText.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
imageBaseText = imageBaseText[2..];
}
var imageBase = ulong.Parse(imageBaseText, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
// Skip if filtering by process and doesn't match
if (_options?.TargetProcessId != null && processId != _options.TargetProcessId)
@@ -597,6 +589,81 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter
}
}
private static ProcessStartInfo CreateLogmanStartInfo(string sessionName, params string[] args)
{
if (string.IsNullOrWhiteSpace(sessionName))
throw new ArgumentException("Session name cannot be empty.", nameof(sessionName));
if (!IsSafeToken(sessionName))
throw new InvalidOperationException("Session name contains unsupported characters.");
var psi = new ProcessStartInfo
{
FileName = GetSystemToolPath("logman.exe"),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
foreach (var arg in args)
{
psi.ArgumentList.Add(arg);
}
return psi;
}
private static ProcessStartInfo CreateTracerptStartInfo(string etlPath, string xmlPath)
{
var psi = new ProcessStartInfo
{
FileName = GetSystemToolPath("tracerpt.exe"),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add(etlPath);
psi.ArgumentList.Add("-o");
psi.ArgumentList.Add(xmlPath);
psi.ArgumentList.Add("-of");
psi.ArgumentList.Add("XML");
psi.ArgumentList.Add("-summary");
psi.ArgumentList.Add("-report");
return psi;
}
private static string GetSystemToolPath(string toolName)
{
if (!IsSafeToken(toolName))
throw new InvalidOperationException("Tool name contains unsupported characters.");
var systemDir = Environment.GetFolderPath(Environment.SpecialFolder.System);
var path = Path.Combine(systemDir, toolName);
if (!File.Exists(path))
throw new FileNotFoundException($"Required tool not found: {toolName}", path);
return path;
}
private static bool IsSafeToken(string value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
foreach (var ch in value)
{
if (!char.IsAsciiLetterOrDigit(ch) && ch != '-' && ch != '_' && ch != '.')
return false;
}
return true;
}
private static bool MatchGlob(string path, string pattern)
{
var regexPattern = "^" + Regex.Escape(pattern)

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Determinism;
using StellaOps.Scanner.Analyzers.Native.Plugin;
using StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
@@ -84,6 +85,7 @@ public static class ServiceCollectionExtensions
this IServiceCollection services,
Action<RuntimeCaptureOptions>? configure = null)
{
services.AddDeterminismDefaults();
var optionsBuilder = services.AddOptions<RuntimeCaptureOptions>();
if (configure != null)
@@ -94,7 +96,9 @@ public static class ServiceCollectionExtensions
// Register platform-appropriate capture adapter
services.TryAddSingleton<IRuntimeCaptureAdapter>(sp =>
{
var adapter = RuntimeCaptureAdapterFactory.CreateForCurrentPlatform();
var timeProvider = sp.GetRequiredService<TimeProvider>();
var guidProvider = sp.GetRequiredService<IGuidProvider>();
var adapter = RuntimeCaptureAdapterFactory.CreateForCurrentPlatform(timeProvider, guidProvider);
if (adapter == null)
{
throw new PlatformNotSupportedException(