Files
git.stella-ops.org/src/SbomService/__Libraries/StellaOps.SbomService.Lineage/Services/LineageGraphService.cs
master a4badc275e UI work to fill SBOM sourcing management gap. UI planning remaining functionality exposure. Work on CI/Tests stabilization
Introduces CGS determinism test runs to CI workflows for Windows, macOS, Linux, Alpine, and Debian, fulfilling CGS-008 cross-platform requirements. Updates local-ci scripts to support new smoke steps, test timeouts, progress intervals, and project slicing for improved test isolation and diagnostics.
2025-12-29 19:12:38 +02:00

197 lines
7.3 KiB
C#

using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using StellaOps.SbomService.Lineage.Domain;
using StellaOps.SbomService.Lineage.Repositories;
using System.Text.Json;
namespace StellaOps.SbomService.Lineage.Services;
/// <summary>
/// Implementation of lineage graph service with caching and enrichment.
/// </summary>
public sealed class LineageGraphService : ILineageGraphService
{
private readonly ISbomLineageEdgeRepository _edgeRepository;
private readonly IVexDeltaRepository _deltaRepository;
private readonly ISbomVerdictLinkRepository _verdictRepository;
private readonly IDistributedCache? _cache;
private readonly ILogger<LineageGraphService> _logger;
private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(10);
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
public LineageGraphService(
ISbomLineageEdgeRepository edgeRepository,
IVexDeltaRepository deltaRepository,
ISbomVerdictLinkRepository verdictRepository,
ILogger<LineageGraphService> logger,
IDistributedCache? cache = null)
{
_edgeRepository = edgeRepository;
_deltaRepository = deltaRepository;
_verdictRepository = verdictRepository;
_cache = cache;
_logger = logger;
}
public async ValueTask<LineageGraphResponse> GetLineageAsync(
string artifactDigest,
Guid tenantId,
LineageQueryOptions options,
CancellationToken ct = default)
{
// Try cache first
var cacheKey = $"lineage:{tenantId}:{artifactDigest}:{options.MaxDepth}";
if (_cache != null)
{
var cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached != null)
{
var response = JsonSerializer.Deserialize<LineageGraphResponse>(cached, SerializerOptions);
if (response != null)
{
_logger.LogDebug("Cache hit for lineage {Digest}", artifactDigest);
return response;
}
}
}
// Build graph
var graph = await _edgeRepository.GetGraphAsync(artifactDigest, tenantId, options.MaxDepth, ct);
// Enrich with verdict data if requested
var enrichment = new Dictionary<string, NodeEnrichment>();
if (options.IncludeVerdicts)
{
foreach (var node in graph.Nodes.Where(n => n.SbomVersionId.HasValue))
{
var verdicts = await _verdictRepository.GetBySbomVersionAsync(
node.SbomVersionId!.Value,
tenantId,
ct);
var affected = verdicts.Where(v => v.VerdictStatus == VexStatus.Affected).ToList();
var high = affected.Where(v => v.ConfidenceScore >= 0.8m).ToList();
enrichment[node.ArtifactDigest] = new NodeEnrichment(
VulnerabilityCount: verdicts.Count,
HighSeverityCount: high.Count,
AffectedCount: affected.Count,
TopCves: affected
.OrderByDescending(v => v.ConfidenceScore)
.Take(5)
.Select(v => v.Cve)
.ToList()
);
}
}
var result = new LineageGraphResponse(graph, enrichment);
// Cache the result
if (_cache != null)
{
var json = JsonSerializer.Serialize(result, SerializerOptions);
await _cache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = CacheExpiry
}, ct);
}
return result;
}
public async ValueTask<LineageDiffResponse> GetDiffAsync(
string fromDigest,
string toDigest,
Guid tenantId,
CancellationToken ct = default)
{
// Try cache first
var cacheKey = $"lineage:compare:{tenantId}:{fromDigest}:{toDigest}";
if (_cache != null)
{
var cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached != null)
{
var response = JsonSerializer.Deserialize<LineageDiffResponse>(cached, SerializerOptions);
if (response != null)
{
_logger.LogDebug("Cache hit for diff {From} -> {To}", fromDigest, toDigest);
return response;
}
}
}
// Get VEX deltas
var deltas = await _deltaRepository.GetDeltasAsync(fromDigest, toDigest, tenantId, ct);
var statusChanges = deltas.Where(d => d.FromStatus != d.ToStatus).ToList();
var newVulns = deltas.Count(d => d.FromStatus == VexStatus.Unknown && d.ToStatus == VexStatus.Affected);
var resolved = deltas.Count(d => d.FromStatus == VexStatus.Affected && d.ToStatus == VexStatus.Fixed);
var affectedToNot = deltas.Count(d => d.FromStatus == VexStatus.Affected && d.ToStatus == VexStatus.NotAffected);
var notToAffected = deltas.Count(d => d.FromStatus == VexStatus.NotAffected && d.ToStatus == VexStatus.Affected);
var vexDiff = new VexDiff(
StatusChanges: statusChanges,
NewVulnerabilities: newVulns,
ResolvedVulnerabilities: resolved,
AffectedToNotAffected: affectedToNot,
NotAffectedToAffected: notToAffected
);
// TODO: Implement SBOM diff by comparing component lists
var sbomDiff = new SbomDiff([], [], []);
// TODO: Implement reachability diff if requested
ReachabilityDiff? reachDiff = null;
var result = new LineageDiffResponse(
FromDigest: fromDigest,
ToDigest: toDigest,
SbomDifferences: sbomDiff,
VexDifferences: vexDiff,
ReachabilityDifferences: reachDiff
);
// Cache the result
if (_cache != null)
{
var json = JsonSerializer.Serialize(result, SerializerOptions);
await _cache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = CacheExpiry
}, ct);
}
return result;
}
public async ValueTask<ExportResult> ExportEvidencePackAsync(
ExportRequest request,
Guid tenantId,
CancellationToken ct = default)
{
_logger.LogInformation("Exporting evidence pack for {Digest}", request.ArtifactDigest);
// TODO: Implement evidence pack generation
// 1. Get lineage graph if requested
// 2. Get verdicts if requested
// 3. Get reachability data if requested
// 4. Bundle into archive (tar.gz or zip)
// 5. Sign with Sigstore if requested
// 6. Upload to storage and return download URL
// Placeholder implementation
var downloadUrl = $"https://evidence.stellaops.example/exports/{Guid.NewGuid()}.tar.gz";
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
return new ExportResult(
DownloadUrl: downloadUrl,
ExpiresAt: expiresAt,
SizeBytes: 0,
SignatureDigest: request.SignWithSigstore ? "sha256:placeholder" : null
);
}
}