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,341 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Analyzers.Native.Index;
using StellaOps.Scanner.Emit.Native;
using Xunit;
namespace StellaOps.Scanner.Emit.Tests.Native;
/// <summary>
/// Unit tests for <see cref="NativePurlBuilder"/>.
/// Sprint: SPRINT_3500_0012_0001
/// Task: BSE-008
/// </summary>
public sealed class NativePurlBuilderTests
{
private readonly NativePurlBuilder _builder = new();
#region FromIndexResult Tests
[Fact]
public void FromIndexResult_ReturnsPurlFromResult()
{
var result = new BuildIdLookupResult(
BuildId: "gnu-build-id:abc123",
Purl: "pkg:deb/debian/libc6@2.31",
Version: "2.31",
SourceDistro: "debian",
Confidence: BuildIdConfidence.Exact,
IndexedAt: DateTimeOffset.UtcNow);
var purl = _builder.FromIndexResult(result);
Assert.Equal("pkg:deb/debian/libc6@2.31", purl);
}
[Fact]
public void FromIndexResult_ThrowsForNull()
{
Assert.Throws<ArgumentNullException>(() => _builder.FromIndexResult(null!));
}
#endregion
#region FromUnresolvedBinary Tests
[Fact]
public void FromUnresolvedBinary_GeneratesGenericPurl()
{
var metadata = new NativeBinaryMetadata
{
Format = "elf",
FilePath = "/usr/lib/libssl.so.3"
};
var purl = _builder.FromUnresolvedBinary(metadata);
Assert.StartsWith("pkg:generic/libssl.so.3@unknown", purl);
}
[Fact]
public void FromUnresolvedBinary_IncludesBuildId()
{
var metadata = new NativeBinaryMetadata
{
Format = "elf",
FilePath = "/usr/lib/libssl.so.3",
BuildId = "gnu-build-id:abc123def456"
};
var purl = _builder.FromUnresolvedBinary(metadata);
Assert.Contains("build-id=gnu-build-id%3Aabc123def456", purl);
}
[Fact]
public void FromUnresolvedBinary_IncludesArchitecture()
{
var metadata = new NativeBinaryMetadata
{
Format = "elf",
FilePath = "/usr/lib/libssl.so.3",
Architecture = "x86_64"
};
var purl = _builder.FromUnresolvedBinary(metadata);
Assert.Contains("arch=x86_64", purl);
}
[Fact]
public void FromUnresolvedBinary_IncludesPlatform()
{
var metadata = new NativeBinaryMetadata
{
Format = "elf",
FilePath = "/usr/lib/libssl.so.3",
Platform = "linux"
};
var purl = _builder.FromUnresolvedBinary(metadata);
Assert.Contains("os=linux", purl);
}
[Fact]
public void FromUnresolvedBinary_SortsQualifiersAlphabetically()
{
var metadata = new NativeBinaryMetadata
{
Format = "elf",
FilePath = "/usr/lib/libssl.so.3",
BuildId = "gnu-build-id:abc",
Architecture = "x86_64",
Platform = "linux"
};
var purl = _builder.FromUnresolvedBinary(metadata);
// arch < build-id < os (alphabetical)
var archIndex = purl.IndexOf("arch=", StringComparison.Ordinal);
var buildIdIndex = purl.IndexOf("build-id=", StringComparison.Ordinal);
var osIndex = purl.IndexOf("os=", StringComparison.Ordinal);
Assert.True(archIndex < buildIdIndex);
Assert.True(buildIdIndex < osIndex);
}
#endregion
#region FromDistroPackage Tests
[Theory]
[InlineData("deb", "debian", "pkg:deb/debian/libc6@2.31")]
[InlineData("debian", "debian", "pkg:deb/debian/libc6@2.31")]
[InlineData("ubuntu", "ubuntu", "pkg:deb/ubuntu/libc6@2.31")]
[InlineData("rpm", "fedora", "pkg:rpm/fedora/libc6@2.31")]
[InlineData("apk", "alpine", "pkg:apk/alpine/libc6@2.31")]
[InlineData("pacman", "arch", "pkg:pacman/arch/libc6@2.31")]
public void FromDistroPackage_MapsDistroToPurlType(string distro, string distroName, string expectedPrefix)
{
var purl = _builder.FromDistroPackage(distro, distroName, "libc6", "2.31");
Assert.StartsWith(expectedPrefix, purl);
}
[Fact]
public void FromDistroPackage_IncludesArchitecture()
{
var purl = _builder.FromDistroPackage("deb", "debian", "libc6", "2.31", "amd64");
Assert.Equal("pkg:deb/debian/libc6@2.31?arch=amd64", purl);
}
[Fact]
public void FromDistroPackage_ThrowsForNullDistro()
{
Assert.ThrowsAny<ArgumentException>(() =>
_builder.FromDistroPackage(null!, "debian", "libc6", "2.31"));
}
[Fact]
public void FromDistroPackage_ThrowsForNullPackageName()
{
Assert.ThrowsAny<ArgumentException>(() =>
_builder.FromDistroPackage("deb", "debian", null!, "2.31"));
}
#endregion
}
/// <summary>
/// Unit tests for <see cref="NativeComponentEmitter"/>.
/// Sprint: SPRINT_3500_0012_0001
/// Task: BSE-008
/// </summary>
public sealed class NativeComponentEmitterTests
{
#region EmitAsync Tests
[Fact]
public async Task EmitAsync_UsesIndexMatch_WhenFound()
{
var index = new FakeBuildIdIndex();
index.AddEntry("gnu-build-id:abc123", new BuildIdLookupResult(
BuildId: "gnu-build-id:abc123",
Purl: "pkg:deb/debian/libc6@2.31",
Version: "2.31",
SourceDistro: "debian",
Confidence: BuildIdConfidence.Exact,
IndexedAt: DateTimeOffset.UtcNow));
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
var metadata = new NativeBinaryMetadata
{
Format = "elf",
FilePath = "/usr/lib/libc.so.6",
BuildId = "gnu-build-id:abc123"
};
var result = await emitter.EmitAsync(metadata);
Assert.True(result.IndexMatch);
Assert.Equal("pkg:deb/debian/libc6@2.31", result.Purl);
Assert.Equal("2.31", result.Version);
Assert.NotNull(result.LookupResult);
}
[Fact]
public async Task EmitAsync_FallsBackToGenericPurl_WhenNotFound()
{
var index = new FakeBuildIdIndex();
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
var metadata = new NativeBinaryMetadata
{
Format = "elf",
FilePath = "/usr/lib/libcustom.so",
BuildId = "gnu-build-id:notfound"
};
var result = await emitter.EmitAsync(metadata);
Assert.False(result.IndexMatch);
Assert.StartsWith("pkg:generic/libcustom.so@unknown", result.Purl);
Assert.Null(result.LookupResult);
}
[Fact]
public async Task EmitAsync_ExtractsFilename()
{
var index = new FakeBuildIdIndex();
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
var metadata = new NativeBinaryMetadata
{
Format = "elf",
FilePath = "/very/deep/path/to/libfoo.so.1.2.3"
};
var result = await emitter.EmitAsync(metadata);
Assert.Equal("libfoo.so.1.2.3", result.Name);
}
[Fact]
public async Task EmitAsync_UsesProductVersion_WhenNotInIndex()
{
var index = new FakeBuildIdIndex();
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
var metadata = new NativeBinaryMetadata
{
Format = "pe",
FilePath = "C:\\Windows\\System32\\kernel32.dll",
ProductVersion = "10.0.19041.1"
};
var result = await emitter.EmitAsync(metadata);
Assert.Equal("10.0.19041.1", result.Version);
}
#endregion
#region EmitBatchAsync Tests
[Fact]
public async Task EmitBatchAsync_ProcessesMultipleBinaries()
{
var index = new FakeBuildIdIndex();
index.AddEntry("gnu-build-id:aaa", new BuildIdLookupResult(
"gnu-build-id:aaa", "pkg:deb/debian/liba@1.0", "1.0", "debian", BuildIdConfidence.Exact, DateTimeOffset.UtcNow));
index.AddEntry("gnu-build-id:bbb", new BuildIdLookupResult(
"gnu-build-id:bbb", "pkg:deb/debian/libb@2.0", "2.0", "debian", BuildIdConfidence.Exact, DateTimeOffset.UtcNow));
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
var metadataList = new[]
{
new NativeBinaryMetadata { Format = "elf", FilePath = "/lib/liba.so", BuildId = "gnu-build-id:aaa" },
new NativeBinaryMetadata { Format = "elf", FilePath = "/lib/libb.so", BuildId = "gnu-build-id:bbb" },
new NativeBinaryMetadata { Format = "elf", FilePath = "/lib/libc.so", BuildId = "gnu-build-id:ccc" }
};
var results = await emitter.EmitBatchAsync(metadataList);
Assert.Equal(3, results.Count);
Assert.Equal(2, results.Count(r => r.IndexMatch));
Assert.Equal(1, results.Count(r => !r.IndexMatch));
}
[Fact]
public async Task EmitBatchAsync_ReturnsEmptyForEmptyInput()
{
var index = new FakeBuildIdIndex();
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
var results = await emitter.EmitBatchAsync(Array.Empty<NativeBinaryMetadata>());
Assert.Empty(results);
}
#endregion
#region Test Helpers
private sealed class FakeBuildIdIndex : IBuildIdIndex
{
private readonly Dictionary<string, BuildIdLookupResult> _entries = new(StringComparer.OrdinalIgnoreCase);
public int Count => _entries.Count;
public bool IsLoaded => true;
public void AddEntry(string buildId, BuildIdLookupResult result)
{
_entries[buildId] = result;
}
public Task<BuildIdLookupResult?> LookupAsync(string buildId, CancellationToken cancellationToken = default)
{
_entries.TryGetValue(buildId, out var result);
return Task.FromResult(result);
}
public Task<IReadOnlyList<BuildIdLookupResult>> BatchLookupAsync(
IEnumerable<string> buildIds,
CancellationToken cancellationToken = default)
{
var results = buildIds
.Where(id => _entries.ContainsKey(id))
.Select(id => _entries[id])
.ToList();
return Task.FromResult<IReadOnlyList<BuildIdLookupResult>>(results);
}
public Task LoadAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
}
#endregion
}