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:
@@ -52,4 +52,10 @@ public sealed record NativeBinaryMetadata
|
||||
|
||||
/// <summary>Signature details (Authenticode, codesign, etc.)</summary>
|
||||
public string? SignatureDetails { get; init; }
|
||||
|
||||
/// <summary>Imported libraries (DLL names for PE, SO names for ELF, dylib names for Mach-O)</summary>
|
||||
public IReadOnlyList<string>? Imports { get; init; }
|
||||
|
||||
/// <summary>Exported symbols (for dependency analysis)</summary>
|
||||
public IReadOnlyList<string>? Exports { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// NativeComponentMapper.cs
|
||||
// Sprint: SPRINT_3500_0012_0001_binary_sbom_emission
|
||||
// Task: BSE-004
|
||||
// Description: Maps native binaries to container layer fragments for SBOM.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Analyzers.Native.Index;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Maps native binary components to container layer fragments.
|
||||
/// Generates dependency relationships and layer ownership metadata.
|
||||
/// </summary>
|
||||
public sealed class NativeComponentMapper
|
||||
{
|
||||
private readonly INativeComponentEmitter _emitter;
|
||||
|
||||
public NativeComponentMapper(INativeComponentEmitter emitter)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(emitter);
|
||||
_emitter = emitter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a container layer's native binaries to SBOM components.
|
||||
/// </summary>
|
||||
/// <param name="layerDigest">Layer digest (sha256:...)</param>
|
||||
/// <param name="binaries">Native binaries discovered in the layer</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Layer mapping result</returns>
|
||||
public async Task<LayerComponentMapping> MapLayerAsync(
|
||||
string layerDigest,
|
||||
IReadOnlyList<NativeBinaryMetadata> binaries,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(layerDigest);
|
||||
ArgumentNullException.ThrowIfNull(binaries);
|
||||
|
||||
var components = new List<NativeComponentEmitResult>(binaries.Count);
|
||||
var unresolvedCount = 0;
|
||||
|
||||
foreach (var binary in binaries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var result = await _emitter.EmitAsync(binary, cancellationToken).ConfigureAwait(false);
|
||||
components.Add(result);
|
||||
|
||||
if (!result.IndexMatch)
|
||||
{
|
||||
unresolvedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return new LayerComponentMapping(
|
||||
LayerDigest: layerDigest,
|
||||
Components: components,
|
||||
TotalCount: components.Count,
|
||||
ResolvedCount: components.Count - unresolvedCount,
|
||||
UnresolvedCount: unresolvedCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps all layers in a container image to SBOM components.
|
||||
/// Deduplicates components that appear in multiple layers.
|
||||
/// </summary>
|
||||
/// <param name="imageLayers">Ordered list of layer digests (base to top)</param>
|
||||
/// <param name="binariesByLayer">Binaries discovered per layer</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Image mapping result with deduplication</returns>
|
||||
public async Task<ImageComponentMapping> MapImageAsync(
|
||||
IReadOnlyList<string> imageLayers,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<NativeBinaryMetadata>> binariesByLayer,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(imageLayers);
|
||||
ArgumentNullException.ThrowIfNull(binariesByLayer);
|
||||
|
||||
var layerMappings = new List<LayerComponentMapping>(imageLayers.Count);
|
||||
var seenPurls = new HashSet<string>(StringComparer.Ordinal);
|
||||
var uniqueComponents = new List<NativeComponentEmitResult>();
|
||||
var duplicateCount = 0;
|
||||
|
||||
foreach (var layerDigest in imageLayers)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!binariesByLayer.TryGetValue(layerDigest, out var binaries))
|
||||
{
|
||||
// Empty layer, skip
|
||||
layerMappings.Add(new LayerComponentMapping(
|
||||
LayerDigest: layerDigest,
|
||||
Components: Array.Empty<NativeComponentEmitResult>(),
|
||||
TotalCount: 0,
|
||||
ResolvedCount: 0,
|
||||
UnresolvedCount: 0));
|
||||
continue;
|
||||
}
|
||||
|
||||
var layerMapping = await MapLayerAsync(layerDigest, binaries, cancellationToken).ConfigureAwait(false);
|
||||
layerMappings.Add(layerMapping);
|
||||
|
||||
// Track unique components for the final image SBOM
|
||||
foreach (var component in layerMapping.Components)
|
||||
{
|
||||
if (seenPurls.Add(component.Purl))
|
||||
{
|
||||
uniqueComponents.Add(component);
|
||||
}
|
||||
else
|
||||
{
|
||||
duplicateCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ImageComponentMapping(
|
||||
Layers: layerMappings,
|
||||
UniqueComponents: uniqueComponents,
|
||||
TotalBinaryCount: layerMappings.Sum(l => l.TotalCount),
|
||||
UniqueBinaryCount: uniqueComponents.Count,
|
||||
DuplicateCount: duplicateCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes dependency relationships between native binaries.
|
||||
/// Uses import table analysis to determine which binaries depend on which.
|
||||
/// </summary>
|
||||
/// <param name="components">Components to analyze</param>
|
||||
/// <returns>Dependency edges (from PURL to list of dependency PURLs)</returns>
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>> ComputeDependencies(
|
||||
IReadOnlyList<NativeComponentEmitResult> components)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(components);
|
||||
|
||||
// Build lookup by filename for dependency resolution
|
||||
var byFilename = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var component in components)
|
||||
{
|
||||
var filename = Path.GetFileName(component.Metadata.FilePath);
|
||||
if (!string.IsNullOrWhiteSpace(filename))
|
||||
{
|
||||
byFilename.TryAdd(filename, component.Purl);
|
||||
}
|
||||
}
|
||||
|
||||
var dependencies = new Dictionary<string, IReadOnlyList<string>>();
|
||||
|
||||
foreach (var component in components)
|
||||
{
|
||||
var deps = new List<string>();
|
||||
|
||||
// Use imports from metadata if available
|
||||
if (component.Metadata.Imports is { Count: > 0 })
|
||||
{
|
||||
foreach (var import in component.Metadata.Imports)
|
||||
{
|
||||
var importName = Path.GetFileName(import);
|
||||
if (byFilename.TryGetValue(importName, out var depPurl))
|
||||
{
|
||||
deps.Add(depPurl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deps.Count > 0)
|
||||
{
|
||||
dependencies[component.Purl] = deps;
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of mapping a single container layer to SBOM components.
|
||||
/// </summary>
|
||||
public sealed record LayerComponentMapping(
|
||||
string LayerDigest,
|
||||
IReadOnlyList<NativeComponentEmitResult> Components,
|
||||
int TotalCount,
|
||||
int ResolvedCount,
|
||||
int UnresolvedCount);
|
||||
|
||||
/// <summary>
|
||||
/// Result of mapping an entire container image to SBOM components.
|
||||
/// </summary>
|
||||
public sealed record ImageComponentMapping(
|
||||
IReadOnlyList<LayerComponentMapping> Layers,
|
||||
IReadOnlyList<NativeComponentEmitResult> UniqueComponents,
|
||||
int TotalBinaryCount,
|
||||
int UniqueBinaryCount,
|
||||
int DuplicateCount);
|
||||
Reference in New Issue
Block a user