Add comprehensive tests for PathConfidenceScorer, PathEnumerator, ShellSymbolicExecutor, and SymbolicState

- Implemented unit tests for PathConfidenceScorer to evaluate path scoring under various conditions, including empty constraints, known and unknown constraints, environmental dependencies, and custom weights.
- Developed tests for PathEnumerator to ensure correct path enumeration from simple scripts, handling known environments, and respecting maximum paths and depth limits.
- Created tests for ShellSymbolicExecutor to validate execution of shell scripts, including handling of commands, branching, and environment tracking.
- Added tests for SymbolicState to verify state management, variable handling, constraint addition, and environment dependency collection.
This commit is contained in:
StellaOps Bot
2025-12-20 14:03:31 +02:00
parent 0ada1b583f
commit ce8cdcd23d
71 changed files with 12438 additions and 3349 deletions

View File

@@ -1,3 +1,5 @@
// Licensed to StellaOps under the AGPL-3.0-or-later license.
using System.Collections.Immutable;
using StellaOps.Scanner.EntryTrace.Mesh;
using StellaOps.Scanner.EntryTrace.Semantic;
@@ -6,366 +8,234 @@ using Xunit;
namespace StellaOps.Scanner.EntryTrace.Tests.Mesh;
/// <summary>
/// Unit tests for MeshEntrypointGraph and related types.
/// Part of Sprint 0412 - Task TEST-002.
/// Unit tests for <see cref="MeshEntrypointGraph"/> and related records.
/// </summary>
public sealed class MeshEntrypointGraphTests
{
[Fact]
public void MeshEntrypointGraph_Creation_SetsProperties()
public void FindPath_DirectConnection_ReturnsPath()
{
// Arrange & Act
// Arrange
var frontend = CreateServiceNode("frontend");
var backend = CreateServiceNode("backend");
var edge = new CrossContainerEdge
{
FromServiceId = "frontend",
ToServiceId = "backend",
Port = 8080,
Protocol = "HTTP"
};
var graph = new MeshEntrypointGraph
{
MeshId = "test-mesh",
Services = ImmutableArray.Create(frontend, backend),
Edges = ImmutableArray.Create(edge),
IngressPaths = ImmutableArray<IngressPath>.Empty,
Type = MeshType.Kubernetes,
Namespace = "default",
Services = CreateServiceNodes(3),
Edges = [],
IngressPaths = [],
AnalyzedAt = DateTime.UtcNow.ToString("O")
AnalyzedAt = "2025-12-20T12:00:00Z"
};
// Act
var path = graph.FindPath("frontend", "backend");
// Assert
Assert.Equal("test-mesh", graph.MeshId);
Assert.Equal(MeshType.Kubernetes, graph.Type);
Assert.Equal("default", graph.Namespace);
Assert.Equal(3, graph.Services.Length);
Assert.NotNull(path);
Assert.Equal("frontend", path.Source.ServiceId);
Assert.Equal("backend", path.Target.ServiceId);
Assert.Single(path.Hops);
Assert.Equal(1, path.HopCount);
}
[Fact]
public void MeshEntrypointGraph_FindPathsToService_FindsDirectPath()
public void FindPath_MultiHop_ReturnsShortestPath()
{
// Arrange
var services = CreateServiceNodes(3);
var edges = new[]
{
new CrossContainerEdge
{
EdgeId = "a->b",
SourceServiceId = "svc-0",
TargetServiceId = "svc-1",
TargetPort = 8080
},
new CrossContainerEdge
{
EdgeId = "b->c",
SourceServiceId = "svc-1",
TargetServiceId = "svc-2",
TargetPort = 8080
}
}.ToImmutableArray();
var api = CreateServiceNode("api");
var cache = CreateServiceNode("cache");
var db = CreateServiceNode("db");
var ingressPaths = new[]
var apiToCache = new CrossContainerEdge
{
new IngressPath
{
IngressName = "main-ingress",
Host = "example.com",
Path = "/",
TargetServiceId = "svc-0",
TargetPort = 8080
}
}.ToImmutableArray();
FromServiceId = "api",
ToServiceId = "cache",
Port = 6379,
Protocol = "TCP"
};
var cacheToDb = new CrossContainerEdge
{
FromServiceId = "cache",
ToServiceId = "db",
Port = 5432,
Protocol = "TCP"
};
var graph = new MeshEntrypointGraph
{
MeshId = "test",
MeshId = "test-mesh",
Services = ImmutableArray.Create(api, cache, db),
Edges = ImmutableArray.Create(apiToCache, cacheToDb),
IngressPaths = ImmutableArray<IngressPath>.Empty,
Type = MeshType.Kubernetes,
Services = services,
Edges = edges,
IngressPaths = ingressPaths,
AnalyzedAt = DateTime.UtcNow.ToString("O")
AnalyzedAt = "2025-12-20T12:00:00Z"
};
// Act
var paths = graph.FindPathsToService("svc-2", maxDepth: 5);
var path = graph.FindPath("api", "db");
// Assert
Assert.Single(paths);
Assert.Equal(2, paths[0].Hops.Length);
Assert.True(paths[0].IsExternallyExposed);
Assert.NotNull(path);
Assert.Equal(2, path.HopCount);
Assert.Equal("api", path.Source.ServiceId);
Assert.Equal("db", path.Target.ServiceId);
}
[Fact]
public void MeshEntrypointGraph_FindPathsToService_RespectsMaxDepth()
public void FindPath_NoConnection_ReturnsNull()
{
// Arrange - Long chain of services
var services = CreateServiceNodes(10);
var edges = new List<CrossContainerEdge>();
for (var i = 0; i < 9; i++)
{
edges.Add(new CrossContainerEdge
{
EdgeId = $"svc-{i}->svc-{i + 1}",
SourceServiceId = $"svc-{i}",
TargetServiceId = $"svc-{i + 1}",
TargetPort = 8080
});
}
// Arrange
var frontend = CreateServiceNode("frontend");
var isolated = CreateServiceNode("isolated");
var graph = new MeshEntrypointGraph
{
MeshId = "test",
MeshId = "test-mesh",
Services = ImmutableArray.Create(frontend, isolated),
Edges = ImmutableArray<CrossContainerEdge>.Empty,
IngressPaths = ImmutableArray<IngressPath>.Empty,
Type = MeshType.Kubernetes,
Services = services,
Edges = edges.ToImmutableArray(),
IngressPaths = [],
AnalyzedAt = DateTime.UtcNow.ToString("O")
};
// Act - Limit depth to 3
var paths = graph.FindPathsToService("svc-9", maxDepth: 3);
// Assert - Should not find path since it requires 9 hops
Assert.Empty(paths);
}
[Fact]
public void MeshEntrypointGraph_FindPathsToService_NoPathExists()
{
// Arrange - Disconnected services
var services = CreateServiceNodes(2);
var graph = new MeshEntrypointGraph
{
MeshId = "test",
Type = MeshType.Kubernetes,
Services = services,
Edges = [],
IngressPaths = [],
AnalyzedAt = DateTime.UtcNow.ToString("O")
AnalyzedAt = "2025-12-20T12:00:00Z"
};
// Act
var paths = graph.FindPathsToService("svc-1", maxDepth: 5);
var path = graph.FindPath("frontend", "isolated");
// Assert
Assert.Empty(paths);
Assert.Null(path);
}
[Fact]
public void ServiceNode_Creation_SetsProperties()
public void FindPath_SameService_ReturnsNull()
{
// Arrange & Act
var node = new ServiceNode
// Arrange
var frontend = CreateServiceNode("frontend");
var graph = new MeshEntrypointGraph
{
ServiceId = "my-service",
ContainerName = "app",
ImageDigest = "sha256:abc123",
ImageReference = "myapp:v1.0.0",
Entrypoints = [],
ExposedPorts = [8080, 8443],
InternalDns = ["my-service.default.svc.cluster.local"],
Labels = new Dictionary<string, string> { ["app"] = "my-app" }.ToImmutableDictionary(),
Replicas = 3
MeshId = "test-mesh",
Services = ImmutableArray.Create(frontend),
Edges = ImmutableArray<CrossContainerEdge>.Empty,
IngressPaths = ImmutableArray<IngressPath>.Empty,
Type = MeshType.Kubernetes,
AnalyzedAt = "2025-12-20T12:00:00Z"
};
// Act
var path = graph.FindPath("frontend", "frontend");
// Assert
Assert.Equal("my-service", node.ServiceId);
Assert.Equal("app", node.ContainerName);
Assert.Equal(2, node.ExposedPorts.Length);
Assert.Equal(3, node.Replicas);
Assert.Null(path);
}
[Fact]
public void CrossContainerEdge_Creation_SetsProperties()
public void FindPath_ServiceNotFound_ReturnsNull()
{
// Arrange & Act
// Arrange
var frontend = CreateServiceNode("frontend");
var graph = new MeshEntrypointGraph
{
MeshId = "test-mesh",
Services = ImmutableArray.Create(frontend),
Edges = ImmutableArray<CrossContainerEdge>.Empty,
IngressPaths = ImmutableArray<IngressPath>.Empty,
Type = MeshType.Kubernetes,
AnalyzedAt = "2025-12-20T12:00:00Z"
};
// Act
var path = graph.FindPath("frontend", "nonexistent");
// Assert
Assert.Null(path);
}
[Fact]
public void FindPathsToService_WithIngress_ReturnsIngressPaths()
{
// Arrange
var frontend = CreateServiceNode("frontend");
var backend = CreateServiceNode("backend");
var edge = new CrossContainerEdge
{
EdgeId = "frontend->backend",
SourceServiceId = "frontend",
TargetServiceId = "backend",
SourcePort = 0,
TargetPort = 8080,
Protocol = "http",
IsExplicit = true
FromServiceId = "frontend",
ToServiceId = "backend",
Port = 8080,
Protocol = "HTTP"
};
// Assert
Assert.Equal("frontend->backend", edge.EdgeId);
Assert.Equal("frontend", edge.SourceServiceId);
Assert.Equal("backend", edge.TargetServiceId);
Assert.Equal(8080, edge.TargetPort);
Assert.True(edge.IsExplicit);
}
[Fact]
public void CrossContainerPath_TracksHops()
{
// Arrange
var hops = new[]
{
new CrossContainerEdge
{
EdgeId = "a->b",
SourceServiceId = "a",
TargetServiceId = "b",
TargetPort = 8080
},
new CrossContainerEdge
{
EdgeId = "b->c",
SourceServiceId = "b",
TargetServiceId = "c",
TargetPort = 9090
}
}.ToImmutableArray();
// Act
var path = new CrossContainerPath
{
PathId = "path-1",
SourceServiceId = "a",
TargetServiceId = "c",
Hops = hops,
IsExternallyExposed = true,
VulnerableComponents = ["pkg:npm/lodash@4.17.20"],
TotalLatencyEstimateMs = 10
};
// Assert
Assert.Equal(2, path.Hops.Length);
Assert.True(path.IsExternallyExposed);
Assert.Single(path.VulnerableComponents);
}
[Fact]
public void IngressPath_TracksExternalExposure()
{
// Arrange & Act
var ingress = new IngressPath
{
IngressName = "main-ingress",
Host = "api.example.com",
Path = "/v1",
TargetServiceId = "api-gateway",
TargetPort = 8080,
TlsEnabled = true,
TlsSecretName = "api-tls-secret",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/rewrite-target"] = "/"
}.ToImmutableDictionary()
Path = "/api/*",
TargetServiceId = "frontend",
TargetPort = 80
};
// Assert
Assert.Equal("main-ingress", ingress.IngressName);
Assert.Equal("api.example.com", ingress.Host);
Assert.True(ingress.TlsEnabled);
Assert.NotNull(ingress.TlsSecretName);
}
[Fact]
public void MeshEntrypointGraphBuilder_BuildsGraph()
{
// Arrange
var builder = new MeshEntrypointGraphBuilder("test-mesh", MeshType.DockerCompose);
// Act
var graph = builder
.WithNamespace("my-project")
.WithService(new ServiceNode
{
ServiceId = "web",
ContainerName = "web",
ImageDigest = "sha256:abc",
Entrypoints = [],
ExposedPorts = [80]
})
.WithService(new ServiceNode
{
ServiceId = "db",
ContainerName = "db",
ImageDigest = "sha256:def",
Entrypoints = [],
ExposedPorts = [5432]
})
.WithEdge(new CrossContainerEdge
{
EdgeId = "web->db",
SourceServiceId = "web",
TargetServiceId = "db",
TargetPort = 5432
})
.Build();
// Assert
Assert.Equal("test-mesh", graph.MeshId);
Assert.Equal(MeshType.DockerCompose, graph.Type);
Assert.Equal(2, graph.Services.Length);
Assert.Single(graph.Edges);
}
[Fact]
public void MeshType_AllValuesAreDistinct()
{
// Assert
var values = Enum.GetValues<MeshType>();
var distinctCount = values.Distinct().Count();
Assert.Equal(values.Length, distinctCount);
}
[Fact]
public void MeshEntrypointGraph_MultiplePaths_FindsAll()
{
// Arrange - Diamond pattern: A -> B -> D, A -> C -> D
var services = new[]
{
CreateServiceNode("A"),
CreateServiceNode("B"),
CreateServiceNode("C"),
CreateServiceNode("D")
}.ToImmutableArray();
var edges = new[]
{
CreateEdge("A", "B"),
CreateEdge("A", "C"),
CreateEdge("B", "D"),
CreateEdge("C", "D")
}.ToImmutableArray();
var ingress = new[]
{
new IngressPath
{
IngressName = "main",
Host = "test.com",
Path = "/",
TargetServiceId = "A",
TargetPort = 80
}
}.ToImmutableArray();
var graph = new MeshEntrypointGraph
{
MeshId = "diamond",
MeshId = "test-mesh",
Services = ImmutableArray.Create(frontend, backend),
Edges = ImmutableArray.Create(edge),
IngressPaths = ImmutableArray.Create(ingress),
Type = MeshType.Kubernetes,
Services = services,
Edges = edges,
IngressPaths = ingress,
AnalyzedAt = DateTime.UtcNow.ToString("O")
AnalyzedAt = "2025-12-20T12:00:00Z"
};
// Act
var paths = graph.FindPathsToService("D", maxDepth: 5);
var paths = graph.FindPathsToService("backend");
// Assert - Should find both paths: A->B->D and A->C->D
Assert.Equal(2, paths.Length);
Assert.All(paths, p => Assert.True(p.IsExternallyExposed));
// Assert
Assert.NotEmpty(paths);
Assert.True(paths[0].IsIngressExposed);
Assert.NotNull(paths[0].IngressPath);
Assert.Equal("api.example.com", paths[0].IngressPath.Host);
}
#region Helper Methods
private static ImmutableArray<ServiceNode> CreateServiceNodes(int count)
[Fact]
public void FindPathsToService_NoIngress_ReturnsEmpty()
{
var builder = ImmutableArray.CreateBuilder<ServiceNode>(count);
for (var i = 0; i < count; i++)
// Arrange
var frontend = CreateServiceNode("frontend");
var backend = CreateServiceNode("backend");
var edge = new CrossContainerEdge
{
builder.Add(CreateServiceNode($"svc-{i}"));
}
return builder.ToImmutable();
FromServiceId = "frontend",
ToServiceId = "backend",
Port = 8080,
Protocol = "HTTP"
};
var graph = new MeshEntrypointGraph
{
MeshId = "test-mesh",
Services = ImmutableArray.Create(frontend, backend),
Edges = ImmutableArray.Create(edge),
IngressPaths = ImmutableArray<IngressPath>.Empty,
Type = MeshType.Kubernetes,
AnalyzedAt = "2025-12-20T12:00:00Z"
};
// Act
var paths = graph.FindPathsToService("backend");
// Assert
Assert.Empty(paths);
}
private static ServiceNode CreateServiceNode(string serviceId)
@@ -374,23 +244,324 @@ public sealed class MeshEntrypointGraphTests
{
ServiceId = serviceId,
ContainerName = serviceId,
ImageDigest = $"sha256:{serviceId}",
ImageReference = $"{serviceId}:latest",
Entrypoints = [],
ExposedPorts = [8080]
ImageDigest = "sha256:" + serviceId,
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray.Create(8080)
};
}
}
private static CrossContainerEdge CreateEdge(string from, string to)
/// <summary>
/// Unit tests for <see cref="ServiceNode"/>.
/// </summary>
public sealed class ServiceNodeTests
{
[Fact]
public void InternalDns_DefaultsToEmpty()
{
return new CrossContainerEdge
// Arrange
var node = new ServiceNode
{
EdgeId = $"{from}->{to}",
SourceServiceId = from,
TargetServiceId = to,
ServiceId = "myapp",
ContainerName = "myapp",
ImageDigest = "sha256:abc",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty
};
// Assert
Assert.Empty(node.InternalDns);
}
[Fact]
public void VulnerableComponents_DefaultsToEmpty()
{
// Arrange
var node = new ServiceNode
{
ServiceId = "myapp",
ContainerName = "myapp",
ImageDigest = "sha256:abc",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty
};
// Assert
Assert.Empty(node.VulnerableComponents);
}
[Fact]
public void Replicas_DefaultsToOne()
{
// Arrange
var node = new ServiceNode
{
ServiceId = "myapp",
ContainerName = "myapp",
ImageDigest = "sha256:abc",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty
};
// Assert
Assert.Equal(1, node.Replicas);
}
[Fact]
public void IsSidecar_DefaultsToFalse()
{
// Arrange
var node = new ServiceNode
{
ServiceId = "myapp",
ContainerName = "myapp",
ImageDigest = "sha256:abc",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty
};
// Assert
Assert.False(node.IsSidecar);
}
}
/// <summary>
/// Unit tests for <see cref="CrossContainerEdge"/>.
/// </summary>
public sealed class CrossContainerEdgeTests
{
[Fact]
public void Confidence_DefaultsToOne()
{
// Arrange
var edge = new CrossContainerEdge
{
FromServiceId = "frontend",
ToServiceId = "backend",
Port = 8080,
Protocol = "HTTP"
};
// Assert
Assert.Equal(1.0f, edge.Confidence);
}
[Fact]
public void Source_DefaultsToManifest()
{
// Arrange
var edge = new CrossContainerEdge
{
FromServiceId = "frontend",
ToServiceId = "backend",
Port = 8080,
Protocol = "HTTP"
};
// Assert
Assert.Equal(EdgeSource.Manifest, edge.Source);
}
[Fact]
public void IsExternal_DefaultsToFalse()
{
// Arrange
var edge = new CrossContainerEdge
{
FromServiceId = "frontend",
ToServiceId = "backend",
Port = 8080,
Protocol = "HTTP"
};
// Assert
Assert.False(edge.IsExternal);
}
}
/// <summary>
/// Unit tests for <see cref="CrossContainerPath"/>.
/// </summary>
public sealed class CrossContainerPathTests
{
[Fact]
public void GetAllVulnerableComponents_CombinesSourceAndTarget()
{
// Arrange
var source = new ServiceNode
{
ServiceId = "frontend",
ContainerName = "frontend",
ImageDigest = "sha256:aaa",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty,
VulnerableComponents = ImmutableArray.Create("pkg:npm/lodash@4.17.20")
};
var target = new ServiceNode
{
ServiceId = "backend",
ContainerName = "backend",
ImageDigest = "sha256:bbb",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty,
VulnerableComponents = ImmutableArray.Create("pkg:maven/log4j/log4j-core@2.14.1")
};
var path = new CrossContainerPath
{
Source = source,
Target = target,
Hops = ImmutableArray<CrossContainerEdge>.Empty,
HopCount = 0,
IsIngressExposed = false,
ReachabilityConfidence = 1.0f
};
// Act
var allVulns = path.GetAllVulnerableComponents();
// Assert
Assert.Equal(2, allVulns.Length);
Assert.Contains("pkg:npm/lodash@4.17.20", allVulns);
Assert.Contains("pkg:maven/log4j/log4j-core@2.14.1", allVulns);
}
[Fact]
public void GetAllVulnerableComponents_DeduplicatesComponents()
{
// Arrange
var sharedVuln = "pkg:npm/lodash@4.17.20";
var source = new ServiceNode
{
ServiceId = "frontend",
ContainerName = "frontend",
ImageDigest = "sha256:aaa",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty,
VulnerableComponents = ImmutableArray.Create(sharedVuln)
};
var target = new ServiceNode
{
ServiceId = "backend",
ContainerName = "backend",
ImageDigest = "sha256:bbb",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty,
VulnerableComponents = ImmutableArray.Create(sharedVuln)
};
var path = new CrossContainerPath
{
Source = source,
Target = target,
Hops = ImmutableArray<CrossContainerEdge>.Empty,
HopCount = 0,
IsIngressExposed = false,
ReachabilityConfidence = 1.0f
};
// Act
var allVulns = path.GetAllVulnerableComponents();
// Assert
Assert.Single(allVulns);
}
[Fact]
public void VulnerableComponents_DefaultsToEmpty()
{
// Arrange
var source = new ServiceNode
{
ServiceId = "frontend",
ContainerName = "frontend",
ImageDigest = "sha256:aaa",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty
};
var target = new ServiceNode
{
ServiceId = "backend",
ContainerName = "backend",
ImageDigest = "sha256:bbb",
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
ExposedPorts = ImmutableArray<int>.Empty
};
var path = new CrossContainerPath
{
Source = source,
Target = target,
Hops = ImmutableArray<CrossContainerEdge>.Empty,
HopCount = 0,
IsIngressExposed = false,
ReachabilityConfidence = 1.0f
};
// Assert
Assert.Empty(path.VulnerableComponents);
}
}
/// <summary>
/// Unit tests for <see cref="IngressPath"/>.
/// </summary>
public sealed class IngressPathTests
{
[Fact]
public void TlsEnabled_DefaultsToFalse()
{
// Arrange
var ingress = new IngressPath
{
IngressName = "main-ingress",
Host = "api.example.com",
Path = "/api/*",
TargetServiceId = "backend",
TargetPort = 8080
};
// Assert
Assert.False(ingress.TlsEnabled);
}
#endregion
[Fact]
public void TlsSecretName_IsNull_WhenTlsDisabled()
{
// Arrange
var ingress = new IngressPath
{
IngressName = "main-ingress",
Host = "api.example.com",
Path = "/api/*",
TargetServiceId = "backend",
TargetPort = 8080
};
// Assert
Assert.Null(ingress.TlsSecretName);
}
[Fact]
public void CanHaveAnnotations()
{
// Arrange
var ingress = new IngressPath
{
IngressName = "main-ingress",
Host = "api.example.com",
Path = "/api/*",
TargetServiceId = "backend",
TargetPort = 8080,
Annotations = ImmutableDictionary<string, string>.Empty
.Add("nginx.ingress.kubernetes.io/rewrite-target", "/")
};
// Assert
Assert.NotNull(ingress.Annotations);
Assert.Contains("nginx.ingress.kubernetes.io/rewrite-target", ingress.Annotations.Keys);
}
}