// ----------------------------------------------------------------------------- // 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.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 { ["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 { ["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 { ["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 { ["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 { [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 { ["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 { ["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 { ["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 { ["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 { [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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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 { ["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(() => _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 }