This commit is contained in:
StellaOps Bot
2025-12-18 20:37:12 +02:00
278 changed files with 35930 additions and 1134 deletions

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Signals.Models;
using StellaOps.Signals.Persistence;
namespace StellaOps.Signals.Services;
/// <summary>
/// Synchronizes canonical callgraph documents to relational tables.
/// </summary>
public sealed class CallGraphSyncService : ICallGraphSyncService
{
private readonly ICallGraphProjectionRepository _projectionRepository;
private readonly ILogger<CallGraphSyncService> _logger;
private readonly TimeProvider _timeProvider;
public CallGraphSyncService(
ICallGraphProjectionRepository projectionRepository,
TimeProvider timeProvider,
ILogger<CallGraphSyncService> logger)
{
_projectionRepository = projectionRepository ?? throw new ArgumentNullException(nameof(projectionRepository));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<CallGraphSyncResult> SyncAsync(
Guid scanId,
string artifactDigest,
CallgraphDocument document,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(document);
ArgumentException.ThrowIfNullOrWhiteSpace(artifactDigest);
var stopwatch = Stopwatch.StartNew();
_logger.LogInformation(
"Starting callgraph projection for scan {ScanId}, artifact {ArtifactDigest}, nodes={NodeCount}, edges={EdgeCount}",
scanId, artifactDigest, document.Nodes.Count, document.Edges.Count);
try
{
var sbomDigest = TryGetMetadataValue(document.Metadata, "sbomDigest");
if (string.IsNullOrWhiteSpace(sbomDigest))
{
sbomDigest = string.IsNullOrWhiteSpace(document.GraphHash) ? null : document.GraphHash;
}
var repoUri = TryGetMetadataValue(document.Metadata, "repoUri");
var commitSha = TryGetMetadataValue(document.Metadata, "commitSha")?.ToLowerInvariant();
var sortedNodes = document.Nodes
.OrderBy(n => n.Id, StringComparer.Ordinal)
.ToList();
var sortedEdges = document.Edges
.OrderBy(e => e.SourceId, StringComparer.Ordinal)
.ThenBy(e => e.TargetId, StringComparer.Ordinal)
.ThenBy(e => (int)e.Kind)
.ThenBy(e => (int)e.Reason)
.ThenBy(e => e.Offset ?? -1)
.ThenBy(e => e.Type, StringComparer.Ordinal)
.ToList();
var sortedEntrypoints = document.Entrypoints
.OrderBy(e => (int)e.Phase)
.ThenBy(e => e.Order)
.ThenBy(e => e.NodeId, StringComparer.Ordinal)
.ThenBy(e => e.Kind)
.ThenBy(e => e.Framework)
.ThenBy(e => e.Route, StringComparer.Ordinal)
.ThenBy(e => e.HttpMethod, StringComparer.Ordinal)
.ThenBy(e => e.Source, StringComparer.Ordinal)
.ToList();
// Step 1: Upsert scan record
await _projectionRepository.UpsertScanAsync(
scanId,
artifactDigest,
sbomDigest: sbomDigest,
repoUri: repoUri,
commitSha: commitSha,
cancellationToken: cancellationToken).ConfigureAwait(false);
// Step 2: Project nodes in stable order
var nodesProjected = await _projectionRepository.UpsertNodesAsync(
scanId,
sortedNodes,
cancellationToken).ConfigureAwait(false);
// Step 3: Project edges in stable order
var edgesProjected = await _projectionRepository.UpsertEdgesAsync(
scanId,
sortedEdges,
cancellationToken).ConfigureAwait(false);
// Step 4: Project entrypoints in stable order
var entrypointsProjected = 0;
if (sortedEntrypoints is { Count: > 0 })
{
entrypointsProjected = await _projectionRepository.UpsertEntrypointsAsync(
scanId,
sortedEntrypoints,
cancellationToken).ConfigureAwait(false);
}
// Step 5: Mark scan as completed
await _projectionRepository.CompleteScanAsync(scanId, cancellationToken).ConfigureAwait(false);
stopwatch.Stop();
_logger.LogInformation(
"Completed callgraph projection for scan {ScanId}: nodes={NodesProjected}, edges={EdgesProjected}, entrypoints={EntrypointsProjected}, duration={DurationMs}ms",
scanId, nodesProjected, edgesProjected, entrypointsProjected, stopwatch.ElapsedMilliseconds);
return new CallGraphSyncResult(
ScanId: scanId,
NodesProjected: nodesProjected,
EdgesProjected: edgesProjected,
EntrypointsProjected: entrypointsProjected,
WasUpdated: nodesProjected > 0 || edgesProjected > 0,
DurationMs: stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(
ex,
"Failed callgraph projection for scan {ScanId} after {DurationMs}ms: {ErrorMessage}",
scanId, stopwatch.ElapsedMilliseconds, ex.Message);
await _projectionRepository.FailScanAsync(scanId, ex.Message, cancellationToken).ConfigureAwait(false);
throw;
}
}
/// <inheritdoc />
public async Task DeleteByScanAsync(Guid scanId, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Deleting callgraph projection for scan {ScanId}", scanId);
await _projectionRepository.DeleteScanAsync(scanId, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Deleted callgraph projection for scan {ScanId}", scanId);
}
private static string? TryGetMetadataValue(IReadOnlyDictionary<string, string?>? metadata, string key)
{
if (metadata is null || string.IsNullOrWhiteSpace(key))
{
return null;
}
if (!metadata.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
{
return null;
}
return value.Trim();
}
}