Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
247 lines
9.6 KiB
C#
247 lines
9.6 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Signals.Options;
|
|
using StellaOps.Signals.Services;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Signals.Tests;
|
|
|
|
public class EdgeBundleIngestionServiceTests
|
|
{
|
|
private readonly EdgeBundleIngestionService _service;
|
|
private const string TestTenantId = "test-tenant";
|
|
private const string TestGraphHash = "blake3:abc123def456";
|
|
|
|
public EdgeBundleIngestionServiceTests()
|
|
{
|
|
var opts = new SignalsOptions();
|
|
opts.Storage.RootPath = Path.GetTempPath();
|
|
var options = Microsoft.Extensions.Options.Options.Create(opts);
|
|
_service = new EdgeBundleIngestionService(NullLogger<EdgeBundleIngestionService>.Instance, options);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IngestAsync_ParsesBundleAndStoresDocument()
|
|
{
|
|
// Arrange
|
|
var bundle = new
|
|
{
|
|
schema = "edge-bundle-v1",
|
|
bundleId = "bundle:test123",
|
|
graphHash = TestGraphHash,
|
|
bundleReason = "RuntimeHits",
|
|
generatedAt = DateTimeOffset.UtcNow.ToString("O"),
|
|
edges = new[]
|
|
{
|
|
new { from = "func_a", to = "func_b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.9 },
|
|
new { from = "func_b", to = "func_c", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.8 }
|
|
}
|
|
};
|
|
var bundleJson = JsonSerializer.Serialize(bundle);
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(bundleJson));
|
|
|
|
// Act
|
|
var result = await _service.IngestAsync(TestTenantId, stream, null);
|
|
|
|
// Assert
|
|
Assert.Equal("bundle:test123", result.BundleId);
|
|
Assert.Equal(TestGraphHash, result.GraphHash);
|
|
Assert.Equal("RuntimeHits", result.BundleReason);
|
|
Assert.Equal(2, result.EdgeCount);
|
|
Assert.Equal(0, result.RevokedCount);
|
|
Assert.False(result.Quarantined);
|
|
Assert.Contains("cas://reachability/edges/", result.CasUri);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IngestAsync_TracksRevokedEdgesForQuarantine()
|
|
{
|
|
// Arrange
|
|
var bundle = new
|
|
{
|
|
schema = "edge-bundle-v1",
|
|
bundleId = "bundle:revoked123",
|
|
graphHash = TestGraphHash,
|
|
bundleReason = "Revoked",
|
|
edges = new[]
|
|
{
|
|
new { from = "func_a", to = "func_b", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 },
|
|
new { from = "func_b", to = "func_c", kind = "call", reason = "TargetRemoved", revoked = true, confidence = 1.0 },
|
|
new { from = "func_c", to = "func_d", kind = "call", reason = "LowConfidence", revoked = false, confidence = 0.3 }
|
|
}
|
|
};
|
|
var bundleJson = JsonSerializer.Serialize(bundle);
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(bundleJson));
|
|
|
|
// Act
|
|
var result = await _service.IngestAsync(TestTenantId, stream, null);
|
|
|
|
// Assert
|
|
Assert.Equal(3, result.EdgeCount);
|
|
Assert.Equal(2, result.RevokedCount);
|
|
Assert.True(result.Quarantined);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IsEdgeRevokedAsync_ReturnsTrueForRevokedEdges()
|
|
{
|
|
// Arrange
|
|
var bundle = new
|
|
{
|
|
bundleId = "bundle:revoke-check",
|
|
graphHash = TestGraphHash,
|
|
bundleReason = "Revoked",
|
|
edges = new[]
|
|
{
|
|
new { from = "vuln_func", to = "patched_func", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 }
|
|
}
|
|
};
|
|
var bundleJson = JsonSerializer.Serialize(bundle);
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(bundleJson));
|
|
await _service.IngestAsync(TestTenantId, stream, null);
|
|
|
|
// Act & Assert
|
|
Assert.True(await _service.IsEdgeRevokedAsync(TestTenantId, TestGraphHash, "vuln_func", "patched_func"));
|
|
Assert.False(await _service.IsEdgeRevokedAsync(TestTenantId, TestGraphHash, "other_func", "some_func"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBundlesForGraphAsync_ReturnsAllBundlesForGraph()
|
|
{
|
|
// Arrange - ingest multiple bundles
|
|
var graphHash = $"blake3:graph_{Guid.NewGuid():N}";
|
|
|
|
var bundle1 = new { bundleId = "bundle:1", graphHash, bundleReason = "RuntimeHits", edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 1.0 } } };
|
|
var bundle2 = new { bundleId = "bundle:2", graphHash, bundleReason = "InitArray", edges = new[] { new { from = "c", to = "d", kind = "call", reason = "InitArray", revoked = false, confidence = 1.0 } } };
|
|
|
|
using var stream1 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle1)));
|
|
using var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle2)));
|
|
|
|
await _service.IngestAsync(TestTenantId, stream1, null);
|
|
await _service.IngestAsync(TestTenantId, stream2, null);
|
|
|
|
// Act
|
|
var bundles = await _service.GetBundlesForGraphAsync(TestTenantId, graphHash);
|
|
|
|
// Assert
|
|
Assert.Equal(2, bundles.Length);
|
|
Assert.Contains(bundles, b => b.BundleId == "bundle:1");
|
|
Assert.Contains(bundles, b => b.BundleId == "bundle:2");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetRevokedEdgesAsync_ReturnsOnlyRevokedEdges()
|
|
{
|
|
// Arrange
|
|
var graphHash = $"blake3:revoked_graph_{Guid.NewGuid():N}";
|
|
var bundle = new
|
|
{
|
|
bundleId = "bundle:mixed",
|
|
graphHash,
|
|
bundleReason = "Revoked",
|
|
edges = new[]
|
|
{
|
|
new { from = "a", to = "b", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 },
|
|
new { from = "c", to = "d", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.9 },
|
|
new { from = "e", to = "f", kind = "call", reason = "Revoked", revoked = true, confidence = 1.0 }
|
|
}
|
|
};
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle)));
|
|
await _service.IngestAsync(TestTenantId, stream, null);
|
|
|
|
// Act
|
|
var revokedEdges = await _service.GetRevokedEdgesAsync(TestTenantId, graphHash);
|
|
|
|
// Assert
|
|
Assert.Equal(2, revokedEdges.Length);
|
|
Assert.All(revokedEdges, e => Assert.True(e.Revoked));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IngestAsync_WithDsseStream_SetsVerifiedAndDsseFields()
|
|
{
|
|
// Arrange
|
|
var bundle = new
|
|
{
|
|
bundleId = "bundle:verified",
|
|
graphHash = TestGraphHash,
|
|
bundleReason = "RuntimeHits",
|
|
edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 1.0 } }
|
|
};
|
|
var dsseEnvelope = new
|
|
{
|
|
payloadType = "application/vnd.stellaops.edgebundle.predicate+json",
|
|
payload = Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")),
|
|
signatures = new[] { new { keyid = "test", sig = "abc123" } }
|
|
};
|
|
|
|
using var bundleStream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle)));
|
|
using var dsseStream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(dsseEnvelope)));
|
|
|
|
// Act
|
|
var result = await _service.IngestAsync(TestTenantId, bundleStream, dsseStream);
|
|
|
|
// Assert
|
|
Assert.NotNull(result.DsseCasUri);
|
|
Assert.EndsWith(".dsse", result.DsseCasUri);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IngestAsync_ThrowsOnMissingGraphHash()
|
|
{
|
|
// Arrange
|
|
var bundle = new
|
|
{
|
|
bundleId = "bundle:no-graph",
|
|
bundleReason = "RuntimeHits",
|
|
edges = Array.Empty<object>()
|
|
};
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle)));
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _service.IngestAsync(TestTenantId, stream, null));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IngestAsync_UpdatesExistingBundleWithSameId()
|
|
{
|
|
// Arrange
|
|
var graphHash = $"blake3:update_test_{Guid.NewGuid():N}";
|
|
var bundle1 = new
|
|
{
|
|
bundleId = "bundle:same-id",
|
|
graphHash,
|
|
bundleReason = "RuntimeHits",
|
|
edges = new[] { new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.5 } }
|
|
};
|
|
var bundle2 = new
|
|
{
|
|
bundleId = "bundle:same-id",
|
|
graphHash,
|
|
bundleReason = "RuntimeHits",
|
|
edges = new[]
|
|
{
|
|
new { from = "a", to = "b", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.9 },
|
|
new { from = "c", to = "d", kind = "call", reason = "RuntimeHit", revoked = false, confidence = 0.8 }
|
|
}
|
|
};
|
|
|
|
using var stream1 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle1)));
|
|
using var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(bundle2)));
|
|
|
|
// Act
|
|
await _service.IngestAsync(TestTenantId, stream1, null);
|
|
await _service.IngestAsync(TestTenantId, stream2, null);
|
|
|
|
// Assert
|
|
var bundles = await _service.GetBundlesForGraphAsync(TestTenantId, graphHash);
|
|
Assert.Single(bundles);
|
|
Assert.Equal(2, bundles[0].Edges.Count); // Updated to have 2 edges
|
|
}
|
|
}
|