notify doctors work, audit work, new product advisory sprints
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -514,7 +514,8 @@ public static class PeImportParser
|
||||
{
|
||||
IgnoreWhitespace = true,
|
||||
IgnoreComments = true,
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
DtdProcessing = DtdProcessing.Prohibit,
|
||||
XmlResolver = null,
|
||||
});
|
||||
|
||||
while (reader.Read())
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user