feat: add Attestation Chain and Triage Evidence API clients and models

- Implemented Attestation Chain API client with methods for verifying, fetching, and managing attestation chains.
- Created models for Attestation Chain, including DSSE envelope structures and verification results.
- Developed Triage Evidence API client for fetching finding evidence, including methods for evidence retrieval by CVE and component.
- Added models for Triage Evidence, encapsulating evidence responses, entry points, boundary proofs, and VEX evidence.
- Introduced mock implementations for both API clients to facilitate testing and development.
This commit is contained in:
master
2025-12-18 13:15:13 +02:00
parent 7d5250238c
commit 00d2c99af9
118 changed files with 13463 additions and 151 deletions

View File

@@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Signals.Models;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Services;
using StellaOps.Signals.Storage.Postgres.Repositories;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Signals.Storage.Postgres.Tests;
/// <summary>
/// Integration tests for callgraph projection to relational tables.
/// </summary>
[Collection(SignalsPostgresCollection.Name)]
public sealed class CallGraphProjectionIntegrationTests
{
private readonly SignalsPostgresFixture _fixture;
private readonly ITestOutputHelper _output;
public CallGraphProjectionIntegrationTests(SignalsPostgresFixture fixture, ITestOutputHelper output)
{
_fixture = fixture;
_output = output;
}
[Fact]
public async Task SyncAsync_ProjectsNodesToRelationalTable()
{
// Arrange
var dataSource = await CreateDataSourceAsync();
var repository = new PostgresCallGraphProjectionRepository(
dataSource,
NullLogger<PostgresCallGraphProjectionRepository>.Instance);
var service = new CallGraphSyncService(
repository,
TimeProvider.System,
NullLogger<CallGraphSyncService>.Instance);
var scanId = Guid.NewGuid();
var document = CreateSampleDocument();
// Act
var result = await service.SyncAsync(scanId, "sha256:test-digest", document);
// Assert
Assert.True(result.WasUpdated);
Assert.Equal(document.Nodes.Count, result.NodesProjected);
Assert.Equal(document.Edges.Count, result.EdgesProjected);
Assert.Equal(document.Entrypoints.Count, result.EntrypointsProjected);
Assert.True(result.DurationMs >= 0);
_output.WriteLine($"Projected {result.NodesProjected} nodes, {result.EdgesProjected} edges, {result.EntrypointsProjected} entrypoints in {result.DurationMs}ms");
}
[Fact]
public async Task SyncAsync_IsIdempotent_DoesNotCreateDuplicates()
{
// Arrange
var dataSource = await CreateDataSourceAsync();
var repository = new PostgresCallGraphProjectionRepository(
dataSource,
NullLogger<PostgresCallGraphProjectionRepository>.Instance);
var service = new CallGraphSyncService(
repository,
TimeProvider.System,
NullLogger<CallGraphSyncService>.Instance);
var scanId = Guid.NewGuid();
var document = CreateSampleDocument();
// Act - project twice
var result1 = await service.SyncAsync(scanId, "sha256:test-digest", document);
var result2 = await service.SyncAsync(scanId, "sha256:test-digest", document);
// Assert - second run should update, not duplicate
Assert.Equal(result1.NodesProjected, result2.NodesProjected);
Assert.Equal(result1.EdgesProjected, result2.EdgesProjected);
}
[Fact]
public async Task SyncAsync_WithEntrypoints_ProjectsEntrypointsCorrectly()
{
// Arrange
var dataSource = await CreateDataSourceAsync();
var repository = new PostgresCallGraphProjectionRepository(
dataSource,
NullLogger<PostgresCallGraphProjectionRepository>.Instance);
var service = new CallGraphSyncService(
repository,
TimeProvider.System,
NullLogger<CallGraphSyncService>.Instance);
var scanId = Guid.NewGuid();
var document = new CallgraphDocument
{
Id = Guid.NewGuid().ToString("N"),
Language = "csharp",
GraphHash = "test-hash",
Nodes = new List<CallgraphNode>
{
new() { Id = "node-1", Name = "GetUsers", Namespace = "Api.Controllers" },
new() { Id = "node-2", Name = "CreateUser", Namespace = "Api.Controllers" }
},
Edges = new List<CallgraphEdge>(),
Entrypoints = new List<CallgraphEntrypoint>
{
new() { NodeId = "node-1", Kind = EntrypointKind.Http, Route = "/api/users", HttpMethod = "GET", Order = 0 },
new() { NodeId = "node-2", Kind = EntrypointKind.Http, Route = "/api/users", HttpMethod = "POST", Order = 1 }
}
};
// Act
var result = await service.SyncAsync(scanId, "sha256:test-digest", document);
// Assert
Assert.Equal(2, result.EntrypointsProjected);
_output.WriteLine($"Projected {result.EntrypointsProjected} HTTP entrypoints");
}
[Fact]
public async Task DeleteByScanAsync_RemovesAllProjectedData()
{
// Arrange
var dataSource = await CreateDataSourceAsync();
var repository = new PostgresCallGraphProjectionRepository(
dataSource,
NullLogger<PostgresCallGraphProjectionRepository>.Instance);
var queryRepository = new PostgresCallGraphQueryRepository(
dataSource,
NullLogger<PostgresCallGraphQueryRepository>.Instance);
var service = new CallGraphSyncService(
repository,
TimeProvider.System,
NullLogger<CallGraphSyncService>.Instance);
var scanId = Guid.NewGuid();
var document = CreateSampleDocument();
// Project first
await service.SyncAsync(scanId, "sha256:test-digest", document);
// Act
await service.DeleteByScanAsync(scanId);
// Assert - query should return empty stats
var stats = await queryRepository.GetStatsAsync(scanId);
Assert.Equal(0, stats.NodeCount);
Assert.Equal(0, stats.EdgeCount);
}
[Fact]
public async Task QueryRepository_CanQueryProjectedData()
{
// Arrange
var dataSource = await CreateDataSourceAsync();
var repository = new PostgresCallGraphProjectionRepository(
dataSource,
NullLogger<PostgresCallGraphProjectionRepository>.Instance);
var queryRepository = new PostgresCallGraphQueryRepository(
dataSource,
NullLogger<PostgresCallGraphQueryRepository>.Instance);
var service = new CallGraphSyncService(
repository,
TimeProvider.System,
NullLogger<CallGraphSyncService>.Instance);
var scanId = Guid.NewGuid();
var document = CreateSampleDocument();
// Project
await service.SyncAsync(scanId, "sha256:test-digest", document);
// Act
var stats = await queryRepository.GetStatsAsync(scanId);
// Assert
Assert.Equal(document.Nodes.Count, stats.NodeCount);
Assert.Equal(document.Edges.Count, stats.EdgeCount);
_output.WriteLine($"Query returned: {stats.NodeCount} nodes, {stats.EdgeCount} edges");
}
private async Task<SignalsDataSource> CreateDataSourceAsync()
{
var connectionString = _fixture.GetConnectionString();
var options = new Microsoft.Extensions.Options.OptionsWrapper<StellaOps.Infrastructure.Postgres.Options.PostgresOptions>(
new StellaOps.Infrastructure.Postgres.Options.PostgresOptions { ConnectionString = connectionString });
var dataSource = new SignalsDataSource(options);
// Run migration
await _fixture.RunMigrationsAsync();
return dataSource;
}
private static CallgraphDocument CreateSampleDocument()
{
return new CallgraphDocument
{
Id = Guid.NewGuid().ToString("N"),
Language = "csharp",
GraphHash = "sha256:sample-graph-hash",
Nodes = new List<CallgraphNode>
{
new() { Id = "node-1", Name = "Main", Kind = "method", Namespace = "Program", Visibility = SymbolVisibility.Public, IsEntrypointCandidate = true },
new() { Id = "node-2", Name = "DoWork", Kind = "method", Namespace = "Service", Visibility = SymbolVisibility.Internal },
new() { Id = "node-3", Name = "ProcessData", Kind = "method", Namespace = "Core", Visibility = SymbolVisibility.Private }
},
Edges = new List<CallgraphEdge>
{
new() { SourceId = "node-1", TargetId = "node-2", Kind = EdgeKind.Static, Reason = EdgeReason.DirectCall, Weight = 1.0 },
new() { SourceId = "node-2", TargetId = "node-3", Kind = EdgeKind.Static, Reason = EdgeReason.DirectCall, Weight = 1.0 }
},
Entrypoints = new List<CallgraphEntrypoint>
{
new() { NodeId = "node-1", Kind = EntrypointKind.Main, Phase = EntrypointPhase.AppStart, Order = 0 }
}
};
}
}