partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

@@ -0,0 +1,310 @@
// Licensed to StellaOps under the BUSL-1.1 license.
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Reachability.Core;
using StellaOps.ReachGraph.WebService.Services;
using Xunit;
namespace StellaOps.ReachGraph.WebService.Tests;
public class InMemorySignalsAdapterTests
{
private readonly FakeTimeProvider _timeProvider = new(new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero));
[Fact]
public async Task QueryAsync_ReturnsNotObserved_WhenNoFacts()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
var symbol = new SymbolRef
{
Namespace = "System",
TypeName = "String",
MemberName = "Trim"
};
// Act
var result = await adapter.QueryAsync(
symbol,
"sha256:test",
TimeSpan.FromDays(7),
"tenant1",
CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.WasObserved.Should().BeFalse();
result.HitCount.Should().Be(0);
}
[Fact]
public async Task QueryAsync_ReturnsObserved_WhenFactsExist()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
var symbol = new SymbolRef
{
Namespace = "MyApp",
TypeName = "Service",
MemberName = "Process"
};
adapter.RecordObservation(
"sha256:test",
"tenant1",
symbol,
_timeProvider.GetUtcNow().AddHours(-1),
hitCount: 100,
environment: "production",
serviceName: "api-gateway");
// Act
var result = await adapter.QueryAsync(
symbol,
"sha256:test",
TimeSpan.FromDays(7),
"tenant1",
CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.WasObserved.Should().BeTrue();
result.HitCount.Should().Be(100);
result.FirstSeen.Should().NotBeNull();
result.LastSeen.Should().NotBeNull();
}
[Fact]
public async Task QueryAsync_ReturnsNotObserved_WhenOutsideWindow()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
var symbol = new SymbolRef
{
Namespace = "MyApp",
TypeName = "Service",
MemberName = "Process"
};
// Record observation 10 days ago
adapter.RecordObservation(
"sha256:test",
"tenant1",
symbol,
_timeProvider.GetUtcNow().AddDays(-10),
hitCount: 50);
// Act - query with 7-day window
var result = await adapter.QueryAsync(
symbol,
"sha256:test",
TimeSpan.FromDays(7),
"tenant1",
CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.WasObserved.Should().BeFalse();
}
[Fact]
public async Task QueryAsync_AggregatesMultipleObservations()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
var symbol = new SymbolRef
{
Namespace = "MyApp",
TypeName = "Service",
MemberName = "Process"
};
adapter.RecordObservation(
"sha256:test",
"tenant1",
symbol,
_timeProvider.GetUtcNow().AddHours(-2),
hitCount: 50);
adapter.RecordObservation(
"sha256:test",
"tenant1",
symbol,
_timeProvider.GetUtcNow().AddHours(-1),
hitCount: 75);
// Act
var result = await adapter.QueryAsync(
symbol,
"sha256:test",
TimeSpan.FromDays(7),
"tenant1",
CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.WasObserved.Should().BeTrue();
result.HitCount.Should().Be(125); // 50 + 75
}
[Fact]
public async Task QueryAsync_IncludesContexts()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
var symbol = new SymbolRef
{
Namespace = "MyApp",
TypeName = "Service",
MemberName = "Process"
};
adapter.RecordObservation(
"sha256:test",
"tenant1",
symbol,
_timeProvider.GetUtcNow().AddMinutes(-30),
hitCount: 10,
environment: "production",
serviceName: "api-gateway",
traceId: "trace-001");
// Act
var result = await adapter.QueryAsync(
symbol,
"sha256:test",
TimeSpan.FromDays(7),
"tenant1",
CancellationToken.None);
// Assert
result.Contexts.Should().NotBeEmpty();
result.Contexts[0].Environment.Should().Be("production");
result.Contexts[0].Service.Should().Be("api-gateway");
result.Contexts[0].TraceId.Should().Be("trace-001");
}
[Fact]
public async Task QueryAsync_IsolatesByTenant()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
var symbol = new SymbolRef
{
Namespace = "MyApp",
TypeName = "Service",
MemberName = "Process"
};
adapter.RecordObservation(
"sha256:test",
"tenant1",
symbol,
_timeProvider.GetUtcNow().AddMinutes(-30),
hitCount: 100);
// Act - query different tenant
var result = await adapter.QueryAsync(
symbol,
"sha256:test",
TimeSpan.FromDays(7),
"tenant2",
CancellationToken.None);
// Assert
result.WasObserved.Should().BeFalse();
}
[Fact]
public async Task HasFactsAsync_ReturnsTrue_WhenFactsExist()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
var symbol = new SymbolRef
{
Namespace = "MyApp",
TypeName = "Service",
MemberName = "Process"
};
adapter.RecordObservation(
"sha256:test",
"tenant1",
symbol,
_timeProvider.GetUtcNow(),
hitCount: 1);
// Act
var result = await adapter.HasFactsAsync("sha256:test", "tenant1", CancellationToken.None);
// Assert
result.Should().BeTrue();
}
[Fact]
public async Task HasFactsAsync_ReturnsFalse_WhenNoFacts()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
// Act
var result = await adapter.HasFactsAsync("sha256:test", "tenant1", CancellationToken.None);
// Assert
result.Should().BeFalse();
}
[Fact]
public async Task GetMetadataAsync_ReturnsMetadata_WhenFactsExist()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
var symbol = new SymbolRef
{
Namespace = "MyApp",
TypeName = "Service",
MemberName = "Process"
};
adapter.RecordObservation(
"sha256:test",
"tenant1",
symbol,
_timeProvider.GetUtcNow().AddDays(-3),
hitCount: 50,
environment: "production");
adapter.RecordObservation(
"sha256:test",
"tenant1",
symbol,
_timeProvider.GetUtcNow().AddDays(-1),
hitCount: 100,
environment: "staging");
// Act
var metadata = await adapter.GetMetadataAsync("sha256:test", "tenant1", CancellationToken.None);
// Assert
metadata.Should().NotBeNull();
metadata!.ArtifactDigest.Should().Be("sha256:test");
metadata.TotalObservations.Should().Be(150);
metadata.Environments.Should().Contain("production");
metadata.Environments.Should().Contain("staging");
}
[Fact]
public async Task GetMetadataAsync_ReturnsNull_WhenNoFacts()
{
// Arrange
var adapter = new InMemorySignalsAdapter(_timeProvider);
// Act
var metadata = await adapter.GetMetadataAsync("sha256:test", "tenant1", CancellationToken.None);
// Assert
metadata.Should().BeNull();
}
}

View File

@@ -0,0 +1,270 @@
// Licensed to StellaOps under the BUSL-1.1 license.
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Reachability.Core;
using StellaOps.ReachGraph.Schema;
using StellaOps.ReachGraph.WebService.Services;
using Xunit;
namespace StellaOps.ReachGraph.WebService.Tests;
public class ReachGraphStoreAdapterTests
{
private readonly FakeTimeProvider _timeProvider = new(DateTimeOffset.UtcNow);
private readonly InMemoryReachGraphStoreService _storeService = new();
[Fact]
public async Task QueryAsync_ReturnsNotReachable_WhenGraphNotFound()
{
// Arrange
var adapter = CreateAdapter();
var symbol = new SymbolRef
{
Namespace = "System",
TypeName = "String",
MemberName = "Trim"
};
// Act
var result = await adapter.QueryAsync(symbol, "sha256:notfound", CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.IsReachable.Should().BeFalse();
result.Symbol.Should().Be(symbol);
result.ArtifactDigest.Should().Be("sha256:notfound");
}
[Fact]
public async Task QueryAsync_ReturnsReachable_WhenSymbolFoundInGraph()
{
// Arrange
var graph = CreateTestGraph("sha256:test123");
await _storeService.UpsertAsync(graph, "tenant1", CancellationToken.None);
var adapter = CreateAdapter();
var symbol = new SymbolRef
{
Namespace = "MyApp",
TypeName = "VulnerableClass",
MemberName = "Execute"
};
// Act
var result = await adapter.QueryAsync(symbol, "sha256:test123", CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.IsReachable.Should().BeTrue();
result.DistanceFromEntrypoint.Should().BeGreaterThanOrEqualTo(0);
}
[Fact]
public async Task QueryAsync_ReturnsNotReachable_WhenSymbolNotInGraph()
{
// Arrange
var graph = CreateTestGraph("sha256:test123");
await _storeService.UpsertAsync(graph, "tenant1", CancellationToken.None);
var adapter = CreateAdapter();
var symbol = new SymbolRef
{
Namespace = "NonExistent",
TypeName = "Class",
MemberName = "Method"
};
// Act
var result = await adapter.QueryAsync(symbol, "sha256:test123", CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.IsReachable.Should().BeFalse();
}
[Fact]
public async Task HasGraphAsync_ReturnsTrue_WhenGraphExists()
{
// Arrange
var graph = CreateTestGraph("sha256:exists123");
await _storeService.UpsertAsync(graph, "tenant1", CancellationToken.None);
var adapter = CreateAdapter();
// Act
var result = await adapter.HasGraphAsync("sha256:exists123", CancellationToken.None);
// Assert
result.Should().BeTrue();
}
[Fact]
public async Task HasGraphAsync_ReturnsFalse_WhenGraphNotExists()
{
// Arrange
var adapter = CreateAdapter();
// Act
var result = await adapter.HasGraphAsync("sha256:doesnotexist", CancellationToken.None);
// Assert
result.Should().BeFalse();
}
[Fact]
public async Task GetMetadataAsync_ReturnsMetadata_WhenGraphExists()
{
// Arrange
var graph = CreateTestGraph("sha256:metadata123");
await _storeService.UpsertAsync(graph, "tenant1", CancellationToken.None);
var adapter = CreateAdapter();
// Act
var metadata = await adapter.GetMetadataAsync("sha256:metadata123", CancellationToken.None);
// Assert
metadata.Should().NotBeNull();
metadata!.ArtifactDigest.Should().Be("sha256:metadata123");
metadata.NodeCount.Should().BeGreaterThan(0);
metadata.EdgeCount.Should().BeGreaterThan(0);
}
[Fact]
public async Task GetMetadataAsync_ReturnsNull_WhenGraphNotExists()
{
// Arrange
var adapter = CreateAdapter();
// Act
var metadata = await adapter.GetMetadataAsync("sha256:notfound", CancellationToken.None);
// Assert
metadata.Should().BeNull();
}
private ReachGraphStoreAdapter CreateAdapter()
{
return new ReachGraphStoreAdapter(
_storeService,
_timeProvider,
NullLogger<ReachGraphStoreAdapter>.Instance);
}
private static ReachGraphMinimal CreateTestGraph(string artifactDigest)
{
var entrypoint = new ReachGraphNode
{
Id = "entry-main",
Ref = "MyApp.Program.Main",
Kind = "method",
Depth = 0
};
var vulnerableClass = new ReachGraphNode
{
Id = "vulnerable-class",
Ref = "MyApp.VulnerableClass.Execute",
Kind = "method",
Depth = 1
};
var otherNode = new ReachGraphNode
{
Id = "other-node",
Ref = "MyApp.OtherClass.DoWork",
Kind = "method",
Depth = 2
};
var edges = ImmutableArray.Create(
new ReachGraphEdge
{
Source = "entry-main",
Target = "vulnerable-class"
},
new ReachGraphEdge
{
Source = "entry-main",
Target = "other-node"
});
return new ReachGraphMinimal
{
Artifact = new ReachGraphArtifact
{
Name = "test-artifact",
Digest = artifactDigest,
Env = "test"
},
Scope = new ReachGraphScope
{
Entrypoints = ImmutableArray.Create("entry-main"),
Selectors = ImmutableArray<string>.Empty,
Cves = null
},
Signature = null,
Nodes = ImmutableArray.Create(entrypoint, vulnerableClass, otherNode),
Edges = edges
};
}
}
/// <summary>
/// In-memory implementation of IReachGraphStoreService for testing.
/// </summary>
internal sealed class InMemoryReachGraphStoreService : IReachGraphStoreService
{
private readonly Dictionary<string, ReachGraphMinimal> _graphs = new();
public Task<ReachGraphStoreResult> UpsertAsync(
ReachGraphMinimal graph,
string? tenantId,
CancellationToken ct)
{
var digest = graph.Artifact.Digest;
var created = !_graphs.ContainsKey(digest);
_graphs[digest] = graph;
return Task.FromResult(new ReachGraphStoreResult
{
Digest = digest,
ArtifactDigest = digest,
Created = created,
NodeCount = graph.Nodes.Length,
EdgeCount = graph.Edges.Length,
StoredAt = DateTimeOffset.UtcNow
});
}
public Task<ReachGraphMinimal?> GetByDigestAsync(
string digest,
string? tenantId,
CancellationToken ct)
{
_graphs.TryGetValue(digest, out var graph);
return Task.FromResult(graph);
}
public Task<ReachGraphMinimal?> GetByArtifactAsync(
string artifactDigest,
string? tenantId,
CancellationToken ct)
{
var graph = _graphs.Values.FirstOrDefault(g => g.Artifact.Digest == artifactDigest);
return Task.FromResult(graph);
}
public Task<bool> ExistsAsync(string digest, string? tenantId, CancellationToken ct)
{
return Task.FromResult(_graphs.ContainsKey(digest));
}
public Task<bool> DeleteAsync(string digest, string? tenantId, CancellationToken ct)
{
return Task.FromResult(_graphs.Remove(digest));
}
}