feat(telemetry): add telemetry client and services for tracking events
- Implemented TelemetryClient to handle event queuing and flushing to the telemetry endpoint. - Created TtfsTelemetryService for emitting specific telemetry events related to TTFS. - Added tests for TelemetryClient to ensure event queuing and flushing functionality. - Introduced models for reachability drift detection, including DriftResult and DriftedSink. - Developed DriftApiService for interacting with the drift detection API. - Updated FirstSignalCardComponent to emit telemetry events on signal appearance. - Enhanced localization support for first signal component with i18n strings.
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// NativeAnalyzerExecutor.cs
|
||||
// Sprint: SPRINT_3500_0014_0001_native_analyzer_integration
|
||||
// Task: NAI-001
|
||||
// Description: Executes native binary analysis during container scans.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Emit.Native;
|
||||
using StellaOps.Scanner.Worker.Diagnostics;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing;
|
||||
|
||||
/// <summary>
|
||||
/// Executes native binary analysis during container scans.
|
||||
/// Discovers binaries, extracts metadata, correlates with Build-ID index,
|
||||
/// and emits SBOM components.
|
||||
/// </summary>
|
||||
public sealed class NativeAnalyzerExecutor
|
||||
{
|
||||
private readonly NativeBinaryDiscovery _discovery;
|
||||
private readonly INativeComponentEmitter _emitter;
|
||||
private readonly NativeAnalyzerOptions _options;
|
||||
private readonly ILogger<NativeAnalyzerExecutor> _logger;
|
||||
private readonly ScannerWorkerMetrics _metrics;
|
||||
|
||||
public NativeAnalyzerExecutor(
|
||||
NativeBinaryDiscovery discovery,
|
||||
INativeComponentEmitter emitter,
|
||||
IOptions<NativeAnalyzerOptions> options,
|
||||
ILogger<NativeAnalyzerExecutor> logger,
|
||||
ScannerWorkerMetrics metrics)
|
||||
{
|
||||
_discovery = discovery ?? throw new ArgumentNullException(nameof(discovery));
|
||||
_emitter = emitter ?? throw new ArgumentNullException(nameof(emitter));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes native binaries in the container filesystem.
|
||||
/// </summary>
|
||||
/// <param name="rootPath">Path to the extracted container filesystem.</param>
|
||||
/// <param name="context">Scan job context.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Analysis result with discovered components.</returns>
|
||||
public async Task<NativeAnalysisResult> ExecuteAsync(
|
||||
string rootPath,
|
||||
ScanJobContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
_logger.LogDebug("Native analyzer is disabled");
|
||||
return NativeAnalysisResult.Empty;
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(_options.TotalAnalysisTimeout);
|
||||
|
||||
// Discover binaries
|
||||
var discovered = await _discovery.DiscoverAsync(rootPath, cts.Token).ConfigureAwait(false);
|
||||
|
||||
if (discovered.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("No native binaries discovered in {RootPath}", rootPath);
|
||||
return NativeAnalysisResult.Empty;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting native analysis of {Count} binaries for job {JobId}",
|
||||
discovered.Count,
|
||||
context.JobId);
|
||||
|
||||
// Convert to metadata and emit
|
||||
var metadataList = new List<NativeBinaryMetadata>(discovered.Count);
|
||||
foreach (var binary in discovered)
|
||||
{
|
||||
var metadata = await ExtractMetadataAsync(binary, cts.Token).ConfigureAwait(false);
|
||||
if (metadata is not null)
|
||||
{
|
||||
metadataList.Add(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch emit components
|
||||
var emitResults = await _emitter.EmitBatchAsync(metadataList, cts.Token).ConfigureAwait(false);
|
||||
|
||||
sw.Stop();
|
||||
|
||||
var result = new NativeAnalysisResult
|
||||
{
|
||||
DiscoveredCount = discovered.Count,
|
||||
AnalyzedCount = metadataList.Count,
|
||||
ResolvedCount = emitResults.Count(r => r.IndexMatch),
|
||||
UnresolvedCount = emitResults.Count(r => !r.IndexMatch),
|
||||
Components = emitResults,
|
||||
ElapsedMs = sw.ElapsedMilliseconds
|
||||
};
|
||||
|
||||
_metrics.RecordNativeAnalysis(result);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Native analysis complete for job {JobId}: {Resolved}/{Analyzed} resolved in {ElapsedMs}ms",
|
||||
context.JobId,
|
||||
result.ResolvedCount,
|
||||
result.AnalyzedCount,
|
||||
result.ElapsedMs);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Native analysis timed out for job {JobId} after {ElapsedMs}ms",
|
||||
context.JobId,
|
||||
sw.ElapsedMilliseconds);
|
||||
|
||||
return new NativeAnalysisResult
|
||||
{
|
||||
TimedOut = true,
|
||||
ElapsedMs = sw.ElapsedMilliseconds
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Native analysis failed for job {JobId}", context.JobId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NativeBinaryMetadata?> ExtractMetadataAsync(
|
||||
DiscoveredBinary binary,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(_options.SingleBinaryTimeout);
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
// Read binary header to extract Build-ID and other metadata
|
||||
var buildId = ExtractBuildId(binary);
|
||||
|
||||
return new NativeBinaryMetadata
|
||||
{
|
||||
Format = binary.Format.ToString().ToLowerInvariant(),
|
||||
FilePath = binary.RelativePath,
|
||||
BuildId = buildId,
|
||||
Architecture = DetectArchitecture(binary),
|
||||
Platform = DetectPlatform(binary)
|
||||
};
|
||||
}, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogDebug("Extraction timed out for binary: {Path}", binary.RelativePath);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to extract metadata from: {Path}", binary.RelativePath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? ExtractBuildId(DiscoveredBinary binary)
|
||||
{
|
||||
if (binary.Format != BinaryFormat.Elf)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read ELF to find .note.gnu.build-id section
|
||||
using var fs = File.OpenRead(binary.AbsolutePath);
|
||||
using var reader = new BinaryReader(fs);
|
||||
|
||||
// Skip to ELF header
|
||||
var magic = reader.ReadBytes(4);
|
||||
if (magic.Length < 4 ||
|
||||
magic[0] != 0x7F || magic[1] != 0x45 || magic[2] != 0x4C || magic[3] != 0x46)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var elfClass = reader.ReadByte(); // 1 = 32-bit, 2 = 64-bit
|
||||
var is64Bit = elfClass == 2;
|
||||
|
||||
// Skip to section headers (simplified - real implementation would parse properly)
|
||||
// For now, return null - full implementation is in the Analyzers.Native project
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? DetectArchitecture(DiscoveredBinary binary)
|
||||
{
|
||||
if (binary.Format != BinaryFormat.Elf)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var fs = File.OpenRead(binary.AbsolutePath);
|
||||
Span<byte> header = stackalloc byte[20];
|
||||
if (fs.Read(header) < 20)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// e_machine is at offset 18 (2 bytes, little-endian typically)
|
||||
var machine = BitConverter.ToUInt16(header[18..20]);
|
||||
|
||||
return machine switch
|
||||
{
|
||||
0x03 => "i386",
|
||||
0x3E => "x86_64",
|
||||
0x28 => "arm",
|
||||
0xB7 => "aarch64",
|
||||
0xF3 => "riscv",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? DetectPlatform(DiscoveredBinary binary)
|
||||
{
|
||||
return binary.Format switch
|
||||
{
|
||||
BinaryFormat.Elf => "linux",
|
||||
BinaryFormat.Pe => "windows",
|
||||
BinaryFormat.MachO => "darwin",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of native binary analysis.
|
||||
/// </summary>
|
||||
public sealed record NativeAnalysisResult
|
||||
{
|
||||
public static readonly NativeAnalysisResult Empty = new();
|
||||
|
||||
/// <summary>Number of binaries discovered in filesystem.</summary>
|
||||
public int DiscoveredCount { get; init; }
|
||||
|
||||
/// <summary>Number of binaries successfully analyzed.</summary>
|
||||
public int AnalyzedCount { get; init; }
|
||||
|
||||
/// <summary>Number of binaries resolved via Build-ID index.</summary>
|
||||
public int ResolvedCount { get; init; }
|
||||
|
||||
/// <summary>Number of binaries not found in Build-ID index.</summary>
|
||||
public int UnresolvedCount { get; init; }
|
||||
|
||||
/// <summary>Whether the analysis timed out.</summary>
|
||||
public bool TimedOut { get; init; }
|
||||
|
||||
/// <summary>Total elapsed time in milliseconds.</summary>
|
||||
public long ElapsedMs { get; init; }
|
||||
|
||||
/// <summary>Emitted component results.</summary>
|
||||
public IReadOnlyList<NativeComponentEmitResult> Components { get; init; } = Array.Empty<NativeComponentEmitResult>();
|
||||
}
|
||||
Reference in New Issue
Block a user