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,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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user