- 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.
568 lines
15 KiB
C#
568 lines
15 KiB
C#
// 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;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Scanner.EntryTrace.Tests.Mesh;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="MeshEntrypointGraph"/> and related records.
|
|
/// </summary>
|
|
public sealed class MeshEntrypointGraphTests
|
|
{
|
|
[Fact]
|
|
public void FindPath_DirectConnection_ReturnsPath()
|
|
{
|
|
// 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,
|
|
AnalyzedAt = "2025-12-20T12:00:00Z"
|
|
};
|
|
|
|
// Act
|
|
var path = graph.FindPath("frontend", "backend");
|
|
|
|
// Assert
|
|
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 FindPath_MultiHop_ReturnsShortestPath()
|
|
{
|
|
// Arrange
|
|
var api = CreateServiceNode("api");
|
|
var cache = CreateServiceNode("cache");
|
|
var db = CreateServiceNode("db");
|
|
|
|
var apiToCache = new CrossContainerEdge
|
|
{
|
|
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-mesh",
|
|
Services = ImmutableArray.Create(api, cache, db),
|
|
Edges = ImmutableArray.Create(apiToCache, cacheToDb),
|
|
IngressPaths = ImmutableArray<IngressPath>.Empty,
|
|
Type = MeshType.Kubernetes,
|
|
AnalyzedAt = "2025-12-20T12:00:00Z"
|
|
};
|
|
|
|
// Act
|
|
var path = graph.FindPath("api", "db");
|
|
|
|
// Assert
|
|
Assert.NotNull(path);
|
|
Assert.Equal(2, path.HopCount);
|
|
Assert.Equal("api", path.Source.ServiceId);
|
|
Assert.Equal("db", path.Target.ServiceId);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindPath_NoConnection_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var frontend = CreateServiceNode("frontend");
|
|
var isolated = CreateServiceNode("isolated");
|
|
|
|
var graph = new MeshEntrypointGraph
|
|
{
|
|
MeshId = "test-mesh",
|
|
Services = ImmutableArray.Create(frontend, isolated),
|
|
Edges = ImmutableArray<CrossContainerEdge>.Empty,
|
|
IngressPaths = ImmutableArray<IngressPath>.Empty,
|
|
Type = MeshType.Kubernetes,
|
|
AnalyzedAt = "2025-12-20T12:00:00Z"
|
|
};
|
|
|
|
// Act
|
|
var path = graph.FindPath("frontend", "isolated");
|
|
|
|
// Assert
|
|
Assert.Null(path);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindPath_SameService_ReturnsNull()
|
|
{
|
|
// 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", "frontend");
|
|
|
|
// Assert
|
|
Assert.Null(path);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindPath_ServiceNotFound_ReturnsNull()
|
|
{
|
|
// 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
|
|
{
|
|
FromServiceId = "frontend",
|
|
ToServiceId = "backend",
|
|
Port = 8080,
|
|
Protocol = "HTTP"
|
|
};
|
|
|
|
var ingress = new IngressPath
|
|
{
|
|
IngressName = "main-ingress",
|
|
Host = "api.example.com",
|
|
Path = "/api/*",
|
|
TargetServiceId = "frontend",
|
|
TargetPort = 80
|
|
};
|
|
|
|
var graph = new MeshEntrypointGraph
|
|
{
|
|
MeshId = "test-mesh",
|
|
Services = ImmutableArray.Create(frontend, backend),
|
|
Edges = ImmutableArray.Create(edge),
|
|
IngressPaths = ImmutableArray.Create(ingress),
|
|
Type = MeshType.Kubernetes,
|
|
AnalyzedAt = "2025-12-20T12:00:00Z"
|
|
};
|
|
|
|
// Act
|
|
var paths = graph.FindPathsToService("backend");
|
|
|
|
// Assert
|
|
Assert.NotEmpty(paths);
|
|
Assert.True(paths[0].IsIngressExposed);
|
|
Assert.NotNull(paths[0].IngressPath);
|
|
Assert.Equal("api.example.com", paths[0].IngressPath.Host);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindPathsToService_NoIngress_ReturnsEmpty()
|
|
{
|
|
// 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,
|
|
AnalyzedAt = "2025-12-20T12:00:00Z"
|
|
};
|
|
|
|
// Act
|
|
var paths = graph.FindPathsToService("backend");
|
|
|
|
// Assert
|
|
Assert.Empty(paths);
|
|
}
|
|
|
|
private static ServiceNode CreateServiceNode(string serviceId)
|
|
{
|
|
return new ServiceNode
|
|
{
|
|
ServiceId = serviceId,
|
|
ContainerName = serviceId,
|
|
ImageDigest = "sha256:" + serviceId,
|
|
Entrypoints = ImmutableArray<SemanticEntrypoint>.Empty,
|
|
ExposedPorts = ImmutableArray.Create(8080)
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="ServiceNode"/>.
|
|
/// </summary>
|
|
public sealed class ServiceNodeTests
|
|
{
|
|
[Fact]
|
|
public void InternalDns_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.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);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|