Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/K8sBoundaryExtractorTests.cs

803 lines
24 KiB
C#

// -----------------------------------------------------------------------------
// K8sBoundaryExtractorTests.cs
// Sprint: SPRINT_3800_0002_0002_boundary_k8s
// Description: Unit tests for K8sBoundaryExtractor.
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Reachability.Boundary;
using StellaOps.Scanner.Reachability.Gates;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Reachability.Tests;
public class K8sBoundaryExtractorTests
{
private readonly K8sBoundaryExtractor _extractor;
public K8sBoundaryExtractorTests()
{
_extractor = new K8sBoundaryExtractor(
NullLogger<K8sBoundaryExtractor>.Instance);
}
#region Priority and CanHandle
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Priority_Returns200_HigherThanRichGraphExtractor()
{
Assert.Equal(200, _extractor.Priority);
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("k8s", true)]
[InlineData("K8S", true)]
[InlineData("kubernetes", true)]
[InlineData("Kubernetes", true)]
[InlineData("static", false)]
[InlineData("runtime", false)]
public void CanHandle_WithSource_ReturnsExpected(string source, bool expected)
{
var context = BoundaryExtractionContext.Empty with { Source = source };
Assert.Equal(expected, _extractor.CanHandle(context));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CanHandle_WithK8sAnnotations_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
{
Annotations = new Dictionary<string, string>
{
["kubernetes.io/ingress.class"] = "nginx"
}
};
Assert.True(_extractor.CanHandle(context));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CanHandle_WithIngressAnnotation_ReturnsTrue()
{
var context = BoundaryExtractionContext.Empty with
{
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/rewrite-target"] = "/"
}
};
Assert.True(_extractor.CanHandle(context));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CanHandle_WithEmptyAnnotations_ReturnsFalse()
{
var context = BoundaryExtractionContext.Empty;
Assert.False(_extractor.CanHandle(context));
}
#endregion
#region Extract - Exposure Detection
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithInternetFacing_ReturnsPublicExposure()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
IsInternetFacing = true
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Exposure);
Assert.Equal("public", result.Exposure.Level);
Assert.True(result.Exposure.InternetFacing);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithIngressClass_ReturnsInternetFacing()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["kubernetes.io/ingress.class"] = "nginx"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Exposure);
Assert.True(result.Exposure.InternetFacing);
Assert.True(result.Exposure.BehindProxy);
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("LoadBalancer", "public", true)]
[InlineData("NodePort", "internal", false)]
[InlineData("ClusterIP", "private", false)]
public void Extract_WithServiceType_ReturnsExpectedExposure(
string serviceType, string expectedLevel, bool expectedInternetFacing)
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["service.type"] = serviceType
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Exposure);
Assert.Equal(expectedLevel, result.Exposure.Level);
Assert.Equal(expectedInternetFacing, result.Exposure.InternetFacing);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithExternalPorts_ReturnsInternalLevel()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
PortBindings = new Dictionary<int, string> { [443] = "https" }
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Exposure);
Assert.Equal("internal", result.Exposure.Level);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithDmzZone_ReturnsInternalLevel()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
NetworkZone = "dmz"
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Exposure);
Assert.Equal("internal", result.Exposure.Level);
Assert.Equal("dmz", result.Exposure.Zone);
}
#endregion
#region Extract - Surface Detection
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithServicePath_ReturnsSurfaceWithPath()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["service.path"] = "/api/v1"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Surface);
Assert.Equal("/api/v1", result.Surface.Path);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithRewriteTarget_ReturnsSurfaceWithPath()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/rewrite-target"] = "/backend"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Surface);
Assert.Equal("/backend", result.Surface.Path);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithNamespace_ReturnsSurfaceWithNamespacePath()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Namespace = "production"
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Surface);
Assert.Equal("/production", result.Surface.Path);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithTlsAnnotation_ReturnsHttpsProtocol()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["cert-manager.io/cluster-issuer"] = "letsencrypt"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Surface);
Assert.Equal("https", result.Surface.Protocol);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithGrpcAnnotation_ReturnsGrpcProtocol()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["grpc.service"] = "UserService"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Surface);
Assert.Equal("grpc", result.Surface.Protocol);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithPortBinding_ReturnsSurfaceWithPort()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
PortBindings = new Dictionary<int, string> { [8080] = "http" }
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Surface);
Assert.Equal(8080, result.Surface.Port);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithIngressHost_ReturnsSurfaceWithHost()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["ingress.host"] = "api.example.com"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Surface);
Assert.Equal("api.example.com", result.Surface.Host);
}
#endregion
#region Extract - Auth Detection
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithBasicAuth_ReturnsBasicAuthType()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/auth-secret"] = "basic-auth-secret"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Auth);
Assert.True(result.Auth.Required);
Assert.Equal("basic", result.Auth.Type);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithOAuth_ReturnsOAuth2Type()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/oauth2-signin"] = "https://auth.example.com"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Auth);
Assert.True(result.Auth.Required);
Assert.Equal("oauth2", result.Auth.Type);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithMtls_ReturnsMtlsType()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/auth-tls-secret"] = "client-certs"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Auth);
Assert.True(result.Auth.Required);
Assert.Equal("mtls", result.Auth.Type);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithExplicitAuthType_ReturnsSpecifiedType()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/auth-type"] = "jwt"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Auth);
Assert.True(result.Auth.Required);
Assert.Equal("jwt", result.Auth.Type);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithAuthRoles_ReturnsRolesList()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/auth-type"] = "oauth2",
["nginx.ingress.kubernetes.io/auth-roles"] = "admin,editor,viewer"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Auth);
Assert.NotNull(result.Auth.Roles);
Assert.Equal(3, result.Auth.Roles.Count);
Assert.Contains("admin", result.Auth.Roles);
Assert.Contains("editor", result.Auth.Roles);
Assert.Contains("viewer", result.Auth.Roles);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithNoAuth_ReturnsNullAuth()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s"
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.Null(result.Auth);
}
#endregion
#region Extract - Controls Detection
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithNetworkPolicy_ReturnsNetworkPolicyControl()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Namespace = "production",
Annotations = new Dictionary<string, string>
{
["network.policy.enabled"] = "true"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Controls);
var control = Assert.Single(result.Controls);
Assert.Equal("network_policy", control.Type);
Assert.True(control.Active);
Assert.Equal("production", control.Config);
Assert.Equal("high", control.Effectiveness);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithRateLimit_ReturnsRateLimitControl()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/rate-limit"] = "100"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Controls);
var control = Assert.Single(result.Controls);
Assert.Equal("rate_limit", control.Type);
Assert.True(control.Active);
Assert.Equal("medium", control.Effectiveness);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithIpAllowlist_ReturnsIpAllowlistControl()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/whitelist-source-range"] = "10.0.0.0/8"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Controls);
var control = Assert.Single(result.Controls);
Assert.Equal("ip_allowlist", control.Type);
Assert.True(control.Active);
Assert.Equal("high", control.Effectiveness);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithWaf_ReturnsWafControl()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/enable-modsecurity"] = "true"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Controls);
var control = Assert.Single(result.Controls);
Assert.Equal("waf", control.Type);
Assert.True(control.Active);
Assert.Equal("high", control.Effectiveness);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithMultipleControls_ReturnsAllControls()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["network.policy.enabled"] = "true",
["nginx.ingress.kubernetes.io/rate-limit"] = "100",
["nginx.ingress.kubernetes.io/enable-modsecurity"] = "true"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.NotNull(result.Controls);
Assert.Equal(3, result.Controls.Count);
Assert.Contains(result.Controls, c => c.Type == "network_policy");
Assert.Contains(result.Controls, c => c.Type == "rate_limit");
Assert.Contains(result.Controls, c => c.Type == "waf");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithNoControls_ReturnsNullControls()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s"
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.Null(result.Controls);
}
#endregion
#region Extract - Confidence and Metadata
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_BaseConfidence_Returns0Point7()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s"
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.Equal(0.7, result.Confidence, precision: 2);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithIngressAnnotation_IncreasesConfidence()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["nginx.ingress.kubernetes.io/rewrite-target"] = "/"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.Equal(0.85, result.Confidence, precision: 2);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithServiceType_IncreasesConfidence()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["service.type"] = "ClusterIP"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.Equal(0.8, result.Confidence, precision: 2);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_MaxConfidence_CapsAt0Point95()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Annotations = new Dictionary<string, string>
{
["kubernetes.io/ingress.class"] = "nginx",
["service.type"] = "LoadBalancer"
}
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.True(result.Confidence <= 0.95);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_ReturnsK8sSource()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s"
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.Equal("k8s", result.Source);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_BuildsEvidenceRef_WithNamespaceAndEnvironment()
{
var root = new RichGraphRoot("root-123", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Namespace = "production",
EnvironmentId = "env-456"
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.Equal("k8s/production/env-456/root-123", result.EvidenceRef);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_ReturnsNetworkKind()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s"
};
var result = _extractor.Extract(root, null, context);
Assert.NotNull(result);
Assert.Equal("network", result.Kind);
}
#endregion
#region ExtractAsync
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_ReturnsSameResultAsExtract()
{
var root = new RichGraphRoot("root-1", "k8s", null);
var context = BoundaryExtractionContext.Empty with
{
Source = "k8s",
Namespace = "production",
Annotations = new Dictionary<string, string>
{
["kubernetes.io/ingress.class"] = "nginx"
}
};
var syncResult = _extractor.Extract(root, null, context);
var asyncResult = await _extractor.ExtractAsync(root, null, context);
Assert.NotNull(syncResult);
Assert.NotNull(asyncResult);
Assert.Equal(syncResult.Kind, asyncResult.Kind);
Assert.Equal(syncResult.Source, asyncResult.Source);
Assert.Equal(syncResult.Confidence, asyncResult.Confidence);
}
#endregion
#region Edge Cases
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WithNullRoot_ThrowsArgumentNullException()
{
var context = BoundaryExtractionContext.Empty with { Source = "k8s" };
Assert.Throws<ArgumentNullException>(() => _extractor.Extract(null!, null, context));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Extract_WhenCannotHandle_ReturnsNull()
{
var root = new RichGraphRoot("root-1", "static", null);
var context = BoundaryExtractionContext.Empty with { Source = "static" };
var result = _extractor.Extract(root, null, context);
Assert.Null(result);
}
#endregion
}