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.
197 lines
7.3 KiB
C#
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
|
|
);
|
|
}
|
|
}
|