- 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.
224 lines
8.9 KiB
C#
224 lines
8.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Signals.Models;
|
|
using StellaOps.Signals.Options;
|
|
using StellaOps.Signals.Parsing;
|
|
using StellaOps.Signals.Persistence;
|
|
using StellaOps.Signals.Services;
|
|
using StellaOps.Signals.Storage;
|
|
using StellaOps.Signals.Storage.Models;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Signals.Tests;
|
|
|
|
public class CallgraphIngestionServiceTests
|
|
{
|
|
private readonly InMemoryCallgraphRepository _repository = new();
|
|
private readonly InMemoryArtifactStore _artifactStore = new();
|
|
private readonly CallgraphNormalizationService _normalizer = new();
|
|
private readonly TimeProvider _timeProvider = TimeProvider.System;
|
|
|
|
[Fact]
|
|
public async Task IngestAsync_normalizes_graph_and_persists_manifest_hash()
|
|
{
|
|
var parser = new StubParser("java");
|
|
var resolver = new StubParserResolver(parser);
|
|
var options = Microsoft.Extensions.Options.Options.Create(new SignalsOptions());
|
|
var reachabilityStore = new InMemoryReachabilityStoreRepository(_timeProvider);
|
|
var callGraphSyncService = new StubCallGraphSyncService();
|
|
var service = new CallgraphIngestionService(
|
|
resolver,
|
|
_artifactStore,
|
|
_repository,
|
|
reachabilityStore,
|
|
_normalizer,
|
|
callGraphSyncService,
|
|
options,
|
|
_timeProvider,
|
|
NullLogger<CallgraphIngestionService>.Instance);
|
|
|
|
var artifactJson = @"{""nodes"":[{""id"":""com/example/Foo.bar:(I)V"",""kind"":""fn""}],
|
|
""edges"":[{""source"":""com/example/Foo.bar:(I)V"",""target"":""com/example/Foo.bar:(I)V""}]}";
|
|
|
|
var request = new CallgraphIngestRequest(
|
|
Language: "java",
|
|
Component: "demo",
|
|
Version: "1.0.0",
|
|
ArtifactFileName: "graph.json",
|
|
ArtifactContentType: "application/json",
|
|
ArtifactContentBase64: Convert.ToBase64String(Encoding.UTF8.GetBytes(artifactJson)),
|
|
SchemaVersion: null,
|
|
Metadata: new Dictionary<string, string?> { ["source"] = "test" },
|
|
Analyzer: new Dictionary<string, string?> { ["name"] = "stub" });
|
|
|
|
var response = await service.IngestAsync(request, CancellationToken.None);
|
|
|
|
response.CallgraphId.Should().NotBeNullOrWhiteSpace();
|
|
response.GraphHash.Should().NotBeNullOrWhiteSpace();
|
|
response.NodeCount.Should().Be(1);
|
|
response.EdgeCount.Should().Be(1);
|
|
response.ManifestCasUri.Should().Be("cas://signals/manifests/graph.json");
|
|
|
|
var stored = _repository.LastUpserted!;
|
|
stored.Artifact.Hash.Should().Be(response.ArtifactHash);
|
|
stored.Nodes[0].Namespace.Should().Be("com.example.Foo");
|
|
stored.Nodes[0].Language.Should().Be("java");
|
|
stored.Metadata!["schemaVersion"].Should().Be("1.0");
|
|
stored.Metadata!["analyzer.name"].Should().Be("stub");
|
|
stored.Artifact.GraphHash.Should().Be(response.GraphHash);
|
|
|
|
var storedNodes = await reachabilityStore.GetFuncNodesByGraphAsync(response.GraphHash, CancellationToken.None);
|
|
storedNodes.Should().HaveCount(1);
|
|
storedNodes[0].SymbolId.Should().Be("com/example/Foo.bar:(I)V");
|
|
|
|
var storedEdges = await reachabilityStore.GetCallEdgesByGraphAsync(response.GraphHash, CancellationToken.None);
|
|
storedEdges.Should().HaveCount(1);
|
|
storedEdges[0].SourceId.Should().Be("com/example/Foo.bar:(I)V");
|
|
storedEdges[0].TargetId.Should().Be("com/example/Foo.bar:(I)V");
|
|
}
|
|
|
|
private sealed class StubParser : ICallgraphParser
|
|
{
|
|
public StubParser(string language) => Language = language;
|
|
public string Language { get; }
|
|
public Task<CallgraphParseResult> ParseAsync(Stream artifactStream, CancellationToken cancellationToken)
|
|
{
|
|
artifactStream.Position = 0;
|
|
using var doc = JsonDocument.Parse(artifactStream);
|
|
var nodes = new List<CallgraphNode>();
|
|
foreach (var node in doc.RootElement.GetProperty("nodes").EnumerateArray())
|
|
{
|
|
nodes.Add(new CallgraphNode(node.GetProperty("id").GetString()!, "", "function", null, null, null));
|
|
}
|
|
|
|
var edges = new List<CallgraphEdge>();
|
|
foreach (var edge in doc.RootElement.GetProperty("edges").EnumerateArray())
|
|
{
|
|
edges.Add(new CallgraphEdge(
|
|
edge.GetProperty("source").GetString()!,
|
|
edge.GetProperty("target").GetString()!,
|
|
"call"));
|
|
}
|
|
|
|
return Task.FromResult(new CallgraphParseResult(nodes, edges, Array.Empty<CallgraphRoot>(), "1.0", "1.0", null));
|
|
}
|
|
}
|
|
|
|
private sealed class StubParserResolver : ICallgraphParserResolver
|
|
{
|
|
private readonly ICallgraphParser _parser;
|
|
public StubParserResolver(ICallgraphParser parser) => _parser = parser;
|
|
public ICallgraphParser Resolve(string language) => _parser;
|
|
}
|
|
|
|
private sealed class InMemoryArtifactStore : ICallgraphArtifactStore
|
|
{
|
|
private readonly Dictionary<string, byte[]> artifacts = new(StringComparer.Ordinal);
|
|
private readonly Dictionary<string, byte[]> manifests = new(StringComparer.Ordinal);
|
|
|
|
public Task<StoredCallgraphArtifact> SaveAsync(CallgraphArtifactSaveRequest request, Stream content, CancellationToken cancellationToken)
|
|
{
|
|
using var ms = new MemoryStream();
|
|
content.CopyTo(ms);
|
|
artifacts[request.Hash] = ms.ToArray();
|
|
|
|
if (request.ManifestContent is not null)
|
|
{
|
|
using var manifestMs = new MemoryStream();
|
|
request.ManifestContent.CopyTo(manifestMs);
|
|
manifests[request.Hash] = manifestMs.ToArray();
|
|
}
|
|
|
|
var path = $"cas://signals/artifacts/{request.FileName}";
|
|
var manifestPath = "cas://signals/manifests/graph.json";
|
|
|
|
return Task.FromResult(new StoredCallgraphArtifact(
|
|
Path: path,
|
|
Length: ms.Length,
|
|
Hash: request.Hash,
|
|
ContentType: request.ContentType,
|
|
CasUri: path,
|
|
ManifestPath: manifestPath,
|
|
ManifestCasUri: manifestPath));
|
|
}
|
|
|
|
public Task<Stream?> GetAsync(string hash, string? fileName = null, CancellationToken cancellationToken = default)
|
|
{
|
|
if (artifacts.TryGetValue(hash, out var bytes))
|
|
{
|
|
return Task.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
|
|
}
|
|
|
|
return Task.FromResult<Stream?>(null);
|
|
}
|
|
|
|
public Task<Stream?> GetManifestAsync(string hash, CancellationToken cancellationToken = default)
|
|
{
|
|
if (manifests.TryGetValue(hash, out var bytes))
|
|
{
|
|
return Task.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
|
|
}
|
|
|
|
return Task.FromResult<Stream?>(null);
|
|
}
|
|
|
|
public Task<bool> ExistsAsync(string hash, CancellationToken cancellationToken = default)
|
|
{
|
|
return Task.FromResult(artifacts.ContainsKey(hash));
|
|
}
|
|
}
|
|
|
|
private sealed class InMemoryCallgraphRepository : ICallgraphRepository
|
|
{
|
|
public CallgraphDocument? LastUpserted { get; private set; }
|
|
|
|
public Task<CallgraphDocument?> GetByIdAsync(string id, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(LastUpserted?.Id == id ? LastUpserted : null);
|
|
}
|
|
|
|
public Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken)
|
|
{
|
|
LastUpserted = document;
|
|
return Task.FromResult(document);
|
|
}
|
|
}
|
|
|
|
private sealed class StubCallGraphSyncService : ICallGraphSyncService
|
|
{
|
|
public CallGraphSyncResult? LastSyncResult { get; private set; }
|
|
public CallgraphDocument? LastSyncedDocument { get; private set; }
|
|
|
|
public Task<CallGraphSyncResult> SyncAsync(
|
|
Guid scanId,
|
|
string artifactDigest,
|
|
CallgraphDocument document,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
LastSyncedDocument = document;
|
|
var result = new CallGraphSyncResult(
|
|
ScanId: scanId,
|
|
NodesProjected: document.Nodes.Count,
|
|
EdgesProjected: document.Edges.Count,
|
|
EntrypointsProjected: document.Entrypoints.Count,
|
|
WasUpdated: true,
|
|
DurationMs: 1);
|
|
LastSyncResult = result;
|
|
return Task.FromResult(result);
|
|
}
|
|
|
|
public Task DeleteByScanAsync(Guid scanId, CancellationToken cancellationToken = default)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
}
|