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:
master
2025-12-18 16:19:16 +02:00
parent 00d2c99af9
commit 811f35cba7
114 changed files with 13702 additions and 268 deletions

View File

@@ -0,0 +1,91 @@
// -----------------------------------------------------------------------------
// NativeUnknownContext.cs
// Sprint: SPRINT_3500_0013_0001_native_unknowns
// Task: NUC-002
// Description: Native binary-specific context for unknowns classification.
// -----------------------------------------------------------------------------
namespace StellaOps.Unknowns.Core.Models;
/// <summary>
/// Context information specific to native binary unknowns.
/// Serialized as JSON in the Unknown.Context property.
/// </summary>
public sealed record NativeUnknownContext
{
/// <summary>
/// Binary format (elf, pe, macho).
/// </summary>
public required string Format { get; init; }
/// <summary>
/// File path within the container or filesystem.
/// </summary>
public required string FilePath { get; init; }
/// <summary>
/// Build-ID if available (gnu-build-id:..., pe-cv:..., macho-uuid:...).
/// Null if MissingBuildId.
/// </summary>
public string? BuildId { get; init; }
/// <summary>
/// CPU architecture (x86_64, aarch64, arm, i686, etc.).
/// </summary>
public string? Architecture { get; init; }
/// <summary>
/// Container layer digest where the binary was found.
/// </summary>
public string? LayerDigest { get; init; }
/// <summary>
/// Layer index (0-based, base layer first).
/// </summary>
public int? LayerIndex { get; init; }
/// <summary>
/// SHA-256 digest of the binary file.
/// </summary>
public string? FileDigest { get; init; }
/// <summary>
/// File size in bytes.
/// </summary>
public long? FileSize { get; init; }
/// <summary>
/// For UnresolvedNativeLibrary: the import that couldn't be resolved.
/// </summary>
public string? UnresolvedImport { get; init; }
/// <summary>
/// For HeuristicDependency: the dlopen/LoadLibrary string pattern detected.
/// </summary>
public string? HeuristicPattern { get; init; }
/// <summary>
/// For HeuristicDependency: confidence score [0,1].
/// </summary>
public double? HeuristicConfidence { get; init; }
/// <summary>
/// For UnsupportedBinaryFormat: reason why format is unsupported.
/// </summary>
public string? UnsupportedReason { get; init; }
/// <summary>
/// Image reference (digest or tag) containing this binary.
/// </summary>
public string? ImageRef { get; init; }
/// <summary>
/// Scan ID that discovered this unknown.
/// </summary>
public Guid? ScanId { get; init; }
/// <summary>
/// Timestamp when the unknown was classified.
/// </summary>
public DateTimeOffset ClassifiedAt { get; init; } = DateTimeOffset.UtcNow;
}

View File

@@ -174,7 +174,10 @@ public enum UnknownSubjectType
File,
/// <summary>A runtime component.</summary>
Runtime
Runtime,
/// <summary>A native binary (ELF, PE, Mach-O).</summary>
Binary
}
/// <summary>Classification of the unknown.</summary>
@@ -208,7 +211,24 @@ public enum UnknownKind
UnsupportedFormat,
/// <summary>Gap in transitive dependency chain.</summary>
TransitiveGap
TransitiveGap,
// Native binary classification (Sprint: SPRINT_3500_0013_0001)
/// <summary>Native binary has no build-id for identification.</summary>
MissingBuildId,
/// <summary>Build-ID not found in mapping index.</summary>
UnknownBuildId,
/// <summary>Native library dependency cannot be resolved.</summary>
UnresolvedNativeLibrary,
/// <summary>dlopen string-based heuristic dependency (with confidence).</summary>
HeuristicDependency,
/// <summary>Binary format not fully supported (unsupported PE/ELF/Mach-O variant).</summary>
UnsupportedBinaryFormat
}
/// <summary>Severity of the unknown's impact.</summary>

View File

@@ -0,0 +1,244 @@
// -----------------------------------------------------------------------------
// NativeUnknownClassifier.cs
// Sprint: SPRINT_3500_0013_0001_native_unknowns
// Task: NUC-003
// Description: Classification service for native binary unknowns.
// -----------------------------------------------------------------------------
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Unknowns.Core.Models;
namespace StellaOps.Unknowns.Core.Services;
/// <summary>
/// Classifies native binary gaps as Unknowns for the registry.
/// </summary>
public sealed class NativeUnknownClassifier
{
private readonly TimeProvider _timeProvider;
public NativeUnknownClassifier(TimeProvider timeProvider)
{
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
}
/// <summary>
/// Classify a binary with no build-id.
/// </summary>
public Unknown ClassifyMissingBuildId(
string tenantId,
NativeUnknownContext context)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentNullException.ThrowIfNull(context);
var now = _timeProvider.GetUtcNow();
var subjectHash = ComputeSubjectHash(context.FilePath, context.LayerDigest);
return new Unknown
{
Id = Guid.CreateVersion7(),
TenantId = tenantId,
SubjectHash = subjectHash,
SubjectType = UnknownSubjectType.Binary,
SubjectRef = context.FilePath,
Kind = UnknownKind.MissingBuildId,
Severity = UnknownSeverity.Medium,
Context = SerializeContext(context with { ClassifiedAt = now }),
ValidFrom = now,
SysFrom = now
};
}
/// <summary>
/// Classify a binary with build-id not found in the mapping index.
/// </summary>
public Unknown ClassifyUnknownBuildId(
string tenantId,
NativeUnknownContext context)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentNullException.ThrowIfNull(context);
if (string.IsNullOrWhiteSpace(context.BuildId))
{
throw new ArgumentException("BuildId is required for UnknownBuildId classification", nameof(context));
}
var now = _timeProvider.GetUtcNow();
var subjectHash = ComputeSubjectHash(context.BuildId, context.LayerDigest);
return new Unknown
{
Id = Guid.CreateVersion7(),
TenantId = tenantId,
SubjectHash = subjectHash,
SubjectType = UnknownSubjectType.Binary,
SubjectRef = context.BuildId,
Kind = UnknownKind.UnknownBuildId,
Severity = UnknownSeverity.Low,
Context = SerializeContext(context with { ClassifiedAt = now }),
ValidFrom = now,
SysFrom = now
};
}
/// <summary>
/// Classify an unresolved native library import.
/// </summary>
public Unknown ClassifyUnresolvedLibrary(
string tenantId,
NativeUnknownContext context)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentNullException.ThrowIfNull(context);
if (string.IsNullOrWhiteSpace(context.UnresolvedImport))
{
throw new ArgumentException("UnresolvedImport is required", nameof(context));
}
var now = _timeProvider.GetUtcNow();
var subjectHash = ComputeSubjectHash(context.UnresolvedImport, context.FilePath);
return new Unknown
{
Id = Guid.CreateVersion7(),
TenantId = tenantId,
SubjectHash = subjectHash,
SubjectType = UnknownSubjectType.Binary,
SubjectRef = context.UnresolvedImport,
Kind = UnknownKind.UnresolvedNativeLibrary,
Severity = UnknownSeverity.Low,
Context = SerializeContext(context with { ClassifiedAt = now }),
ValidFrom = now,
SysFrom = now
};
}
/// <summary>
/// Classify a heuristic (dlopen-based) dependency.
/// </summary>
public Unknown ClassifyHeuristicDependency(
string tenantId,
NativeUnknownContext context)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentNullException.ThrowIfNull(context);
if (string.IsNullOrWhiteSpace(context.HeuristicPattern))
{
throw new ArgumentException("HeuristicPattern is required", nameof(context));
}
var now = _timeProvider.GetUtcNow();
var subjectHash = ComputeSubjectHash(context.HeuristicPattern, context.FilePath);
// Severity based on confidence
var severity = context.HeuristicConfidence switch
{
>= 0.8 => UnknownSeverity.Info,
>= 0.5 => UnknownSeverity.Low,
_ => UnknownSeverity.Medium
};
return new Unknown
{
Id = Guid.CreateVersion7(),
TenantId = tenantId,
SubjectHash = subjectHash,
SubjectType = UnknownSubjectType.Binary,
SubjectRef = context.HeuristicPattern,
Kind = UnknownKind.HeuristicDependency,
Severity = severity,
Context = SerializeContext(context with { ClassifiedAt = now }),
ValidFrom = now,
SysFrom = now
};
}
/// <summary>
/// Classify an unsupported binary format.
/// </summary>
public Unknown ClassifyUnsupportedFormat(
string tenantId,
NativeUnknownContext context)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentNullException.ThrowIfNull(context);
var now = _timeProvider.GetUtcNow();
var subjectHash = ComputeSubjectHash(context.FilePath, context.Format);
return new Unknown
{
Id = Guid.CreateVersion7(),
TenantId = tenantId,
SubjectHash = subjectHash,
SubjectType = UnknownSubjectType.Binary,
SubjectRef = context.FilePath,
Kind = UnknownKind.UnsupportedBinaryFormat,
Severity = UnknownSeverity.Info,
Context = SerializeContext(context with { ClassifiedAt = now }),
ValidFrom = now,
SysFrom = now
};
}
/// <summary>
/// Batch classify multiple native binary contexts.
/// </summary>
public IReadOnlyList<Unknown> ClassifyBatch(
string tenantId,
IEnumerable<(UnknownKind kind, NativeUnknownContext context)> items)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentNullException.ThrowIfNull(items);
var results = new List<Unknown>();
foreach (var (kind, context) in items)
{
var unknown = kind switch
{
UnknownKind.MissingBuildId => ClassifyMissingBuildId(tenantId, context),
UnknownKind.UnknownBuildId => ClassifyUnknownBuildId(tenantId, context),
UnknownKind.UnresolvedNativeLibrary => ClassifyUnresolvedLibrary(tenantId, context),
UnknownKind.HeuristicDependency => ClassifyHeuristicDependency(tenantId, context),
UnknownKind.UnsupportedBinaryFormat => ClassifyUnsupportedFormat(tenantId, context),
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unsupported UnknownKind for native classification")
};
results.Add(unknown);
}
return results;
}
private static string ComputeSubjectHash(string primary, string? secondary)
{
var input = string.IsNullOrEmpty(secondary)
? primary
: $"{primary}|{secondary}";
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
private static JsonDocument SerializeContext(NativeUnknownContext context)
{
var json = JsonSerializer.Serialize(context, NativeUnknownContextJsonContext.Default.NativeUnknownContext);
return JsonDocument.Parse(json);
}
}
/// <summary>
/// Source-generated JSON context for NativeUnknownContext serialization.
/// </summary>
[System.Text.Json.Serialization.JsonSerializable(typeof(NativeUnknownContext))]
internal partial class NativeUnknownContextJsonContext : System.Text.Json.Serialization.JsonSerializerContext
{
}