using Microsoft.Extensions.Logging; using StellaOps.Scanner.Analyzers.Native.Index; namespace StellaOps.Scanner.Emit.Native; /// /// Emits native binary components for SBOM generation. /// Uses the Build-ID index to resolve PURLs when possible. /// public sealed class NativeComponentEmitter : INativeComponentEmitter { private readonly IBuildIdIndex _buildIdIndex; private readonly NativePurlBuilder _purlBuilder; private readonly ILogger _logger; /// /// Creates a new native component emitter. /// public NativeComponentEmitter( IBuildIdIndex buildIdIndex, ILogger logger) { ArgumentNullException.ThrowIfNull(buildIdIndex); ArgumentNullException.ThrowIfNull(logger); _buildIdIndex = buildIdIndex; _purlBuilder = new NativePurlBuilder(); _logger = logger; } /// public async Task EmitAsync( NativeBinaryMetadata metadata, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(metadata); // Try to resolve via Build-ID index BuildIdLookupResult? lookupResult = null; if (!string.IsNullOrWhiteSpace(metadata.BuildId)) { lookupResult = await _buildIdIndex.LookupAsync(metadata.BuildId, cancellationToken).ConfigureAwait(false); } string purl; string? version = null; bool indexMatch = false; if (lookupResult is not null) { // Index match - use the resolved PURL purl = _purlBuilder.FromIndexResult(lookupResult); version = lookupResult.Version; indexMatch = true; _logger.LogDebug( "Resolved binary {FilePath} via Build-ID index: {Purl}", metadata.FilePath, purl); } else { // No match - generate generic PURL purl = _purlBuilder.FromUnresolvedBinary(metadata); version = metadata.ProductVersion ?? metadata.FileVersion; _logger.LogDebug( "Unresolved binary {FilePath}, generated generic PURL: {Purl}", metadata.FilePath, purl); } var name = Path.GetFileName(metadata.FilePath); return new NativeComponentEmitResult( Purl: purl, Name: name, Version: version, Metadata: metadata, IndexMatch: indexMatch, LookupResult: lookupResult); } /// public async Task> EmitBatchAsync( IEnumerable metadataList, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(metadataList); var metadataArray = metadataList.ToArray(); if (metadataArray.Length == 0) { return Array.Empty(); } // Batch lookup for all Build-IDs var buildIds = metadataArray .Where(m => !string.IsNullOrWhiteSpace(m.BuildId)) .Select(m => m.BuildId!) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); var lookupResults = await _buildIdIndex.BatchLookupAsync(buildIds, cancellationToken).ConfigureAwait(false); var lookupMap = lookupResults.ToDictionary( r => r.BuildId, StringComparer.OrdinalIgnoreCase); _logger.LogDebug( "Batch lookup: {Total} binaries, {Resolved} resolved via index", metadataArray.Length, lookupMap.Count); // Emit components var results = new List(metadataArray.Length); foreach (var metadata in metadataArray) { BuildIdLookupResult? lookupResult = null; if (!string.IsNullOrWhiteSpace(metadata.BuildId) && lookupMap.TryGetValue(metadata.BuildId, out var result)) { lookupResult = result; } string purl; string? version = null; bool indexMatch = false; if (lookupResult is not null) { purl = _purlBuilder.FromIndexResult(lookupResult); version = lookupResult.Version; indexMatch = true; } else { purl = _purlBuilder.FromUnresolvedBinary(metadata); version = metadata.ProductVersion ?? metadata.FileVersion; } results.Add(new NativeComponentEmitResult( Purl: purl, Name: Path.GetFileName(metadata.FilePath), Version: version, Metadata: metadata, IndexMatch: indexMatch, LookupResult: lookupResult)); } return results; } }