// ----------------------------------------------------------------------------- // GatewayBoundaryExtractorTests.cs // Sprint: SPRINT_3800_0002_0003_boundary_gateway // Description: Unit tests for GatewayBoundaryExtractor. // ----------------------------------------------------------------------------- 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 GatewayBoundaryExtractorTests { private readonly GatewayBoundaryExtractor _extractor; public GatewayBoundaryExtractorTests() { _extractor = new GatewayBoundaryExtractor( NullLogger.Instance); } #region Priority and CanHandle [Trait("Category", TestCategories.Unit)] [Fact] public void Priority_Returns250_HigherThanK8sExtractor() { Assert.Equal(250, _extractor.Priority); } [Trait("Category", TestCategories.Unit)] [Theory] [InlineData("gateway", true)] [InlineData("kong", true)] [InlineData("Kong", true)] [InlineData("envoy", true)] [InlineData("istio", true)] [InlineData("apigateway", true)] [InlineData("traefik", true)] [InlineData("k8s", false)] [InlineData("static", 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_WithKongAnnotations_ReturnsTrue() { var context = BoundaryExtractionContext.Empty with { Annotations = new Dictionary { ["kong.route.path"] = "/api" } }; Assert.True(_extractor.CanHandle(context)); } [Trait("Category", TestCategories.Unit)] [Fact] public void CanHandle_WithIstioAnnotations_ReturnsTrue() { var context = BoundaryExtractionContext.Empty with { Annotations = new Dictionary { ["istio.io/rev"] = "stable" } }; Assert.True(_extractor.CanHandle(context)); } [Trait("Category", TestCategories.Unit)] [Fact] public void CanHandle_WithTraefikAnnotations_ReturnsTrue() { var context = BoundaryExtractionContext.Empty with { Annotations = new Dictionary { ["traefik.http.routers.my-router.rule"] = "Host(`example.com`)" } }; 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 Gateway Type Detection [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithKongSource_ReturnsKongGatewaySource() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong" }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Equal("gateway:kong", result.Source); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithEnvoySource_ReturnsEnvoyGatewaySource() { var root = new RichGraphRoot("root-1", "envoy", null); var context = BoundaryExtractionContext.Empty with { Source = "envoy" }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Equal("gateway:envoy", result.Source); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithIstioAnnotations_ReturnsEnvoyGatewaySource() { var root = new RichGraphRoot("root-1", "gateway", null); var context = BoundaryExtractionContext.Empty with { Source = "gateway", Annotations = new Dictionary { ["istio.io/rev"] = "stable" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Equal("gateway:envoy", result.Source); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithApiGatewaySource_ReturnsAwsApigwSource() { var root = new RichGraphRoot("root-1", "apigateway", null); var context = BoundaryExtractionContext.Empty with { Source = "apigateway" }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Equal("gateway:aws-apigw", result.Source); } #endregion #region Exposure Detection [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_DefaultGateway_ReturnsPublicExposure() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong" }; 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); Assert.True(result.Exposure.BehindProxy); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithInternalFlag_ReturnsInternalExposure() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.internal"] = "true" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Exposure); Assert.Equal("internal", result.Exposure.Level); Assert.False(result.Exposure.InternetFacing); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithIstioMesh_ReturnsInternalExposure() { var root = new RichGraphRoot("root-1", "envoy", null); var context = BoundaryExtractionContext.Empty with { Source = "envoy", Annotations = new Dictionary { ["istio.io/mesh-config"] = "enabled" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Exposure); Assert.Equal("internal", result.Exposure.Level); Assert.False(result.Exposure.InternetFacing); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithAwsPrivateEndpoint_ReturnsInternalExposure() { var root = new RichGraphRoot("root-1", "apigateway", null); var context = BoundaryExtractionContext.Empty with { Source = "apigateway", Annotations = new Dictionary { ["apigateway.endpoint-type"] = "PRIVATE" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Exposure); Assert.Equal("internal", result.Exposure.Level); Assert.False(result.Exposure.InternetFacing); } #endregion #region Surface Detection [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithKongPath_ReturnsSurfaceWithPath() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.route.path"] = "/api/v1" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Surface); Assert.Equal("/api/v1", result.Surface.Path); Assert.Equal("api", result.Surface.Type); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithKongHost_ReturnsSurfaceWithHost() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.route.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); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithGrpcAnnotation_ReturnsGrpcProtocol() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.protocol.grpc"] = "true" } }; 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_WithWebsocketAnnotation_ReturnsWssProtocol() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.upgrade.websocket"] = "true" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Surface); Assert.Equal("wss", result.Surface.Protocol); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_DefaultProtocol_ReturnsHttps() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong" }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Surface); Assert.Equal("https", result.Surface.Protocol); Assert.Equal(443, result.Surface.Port); } #endregion #region Kong Auth Detection [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithKongJwtPlugin_ReturnsJwtAuth() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.jwt"] = "enabled" } }; 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_WithKongKeyAuth_ReturnsApiKeyAuth() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.key-auth"] = "enabled" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Auth); Assert.True(result.Auth.Required); Assert.Equal("api_key", result.Auth.Type); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithKongAcl_ReturnsRoles() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.jwt"] = "enabled", ["kong.plugin.acl.allow"] = "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); } #endregion #region Envoy/Istio Auth Detection [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithIstioJwt_ReturnsJwtAuth() { var root = new RichGraphRoot("root-1", "envoy", null); var context = BoundaryExtractionContext.Empty with { Source = "envoy", Annotations = new Dictionary { ["istio.io/requestauthentication.jwt"] = "enabled" } }; 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_WithIstioMtls_ReturnsMtlsAuth() { var root = new RichGraphRoot("root-1", "envoy", null); var context = BoundaryExtractionContext.Empty with { Source = "envoy", Annotations = new Dictionary { ["istio.io/peerauthentication.mtls"] = "STRICT" } }; 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_WithEnvoyOidc_ReturnsOAuth2Auth() { var root = new RichGraphRoot("root-1", "envoy", null); var context = BoundaryExtractionContext.Empty with { Source = "envoy", Annotations = new Dictionary { ["envoy.filter.oidc.provider"] = "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); Assert.Equal("https://auth.example.com", result.Auth.Provider); } #endregion #region AWS API Gateway Auth Detection [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithCognitoAuthorizer_ReturnsOAuth2Auth() { var root = new RichGraphRoot("root-1", "apigateway", null); var context = BoundaryExtractionContext.Empty with { Source = "apigateway", Annotations = new Dictionary { ["apigateway.authorizer.cognito"] = "user-pool-id" } }; 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); Assert.Equal("cognito", result.Auth.Provider); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithApiKeyRequired_ReturnsApiKeyAuth() { var root = new RichGraphRoot("root-1", "apigateway", null); var context = BoundaryExtractionContext.Empty with { Source = "apigateway", Annotations = new Dictionary { ["apigateway.api-key-required"] = "true" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Auth); Assert.True(result.Auth.Required); Assert.Equal("api_key", result.Auth.Type); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithLambdaAuthorizer_ReturnsCustomAuth() { var root = new RichGraphRoot("root-1", "apigateway", null); var context = BoundaryExtractionContext.Empty with { Source = "apigateway", Annotations = new Dictionary { ["apigateway.lambda-authorizer"] = "arn:aws:lambda:..." } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Auth); Assert.True(result.Auth.Required); Assert.Equal("custom", result.Auth.Type); Assert.Equal("lambda", result.Auth.Provider); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithIamAuthorizer_ReturnsIamAuth() { var root = new RichGraphRoot("root-1", "apigateway", null); var context = BoundaryExtractionContext.Empty with { Source = "apigateway", Annotations = new Dictionary { ["apigateway.iam-authorizer"] = "enabled" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Auth); Assert.True(result.Auth.Required); Assert.Equal("iam", result.Auth.Type); Assert.Equal("aws-iam", result.Auth.Provider); } #endregion #region Traefik Auth Detection [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithTraefikBasicAuth_ReturnsBasicAuth() { var root = new RichGraphRoot("root-1", "traefik", null); var context = BoundaryExtractionContext.Empty with { Source = "traefik", Annotations = new Dictionary { ["traefik.http.middlewares.auth.basicauth.users"] = "admin:$$apr1$$..." } }; 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_WithTraefikForwardAuth_ReturnsCustomAuth() { var root = new RichGraphRoot("root-1", "traefik", null); var context = BoundaryExtractionContext.Empty with { Source = "traefik", Annotations = new Dictionary { ["traefik.http.middlewares.auth.forwardauth.address"] = "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("custom", result.Auth.Type); Assert.Equal("https://auth.example.com", result.Auth.Provider); } #endregion #region Controls Detection [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithRateLimit_ReturnsRateLimitControl() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.rate-limiting"] = "100" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Controls); Assert.Contains(result.Controls, c => c.Type == "rate_limit"); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithIpRestriction_ReturnsIpAllowlistControl() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.ip-restriction.whitelist"] = "10.0.0.0/8" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Controls); Assert.Contains(result.Controls, c => c.Type == "ip_allowlist"); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithCors_ReturnsCorsControl() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.cors.origins"] = "https://example.com" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Controls); Assert.Contains(result.Controls, c => c.Type == "cors"); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithWaf_ReturnsWafControl() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.bot-detection"] = "enabled" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Controls); Assert.Contains(result.Controls, c => c.Type == "waf"); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithRequestValidation_ReturnsInputValidationControl() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.request-validation"] = "enabled" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Controls); Assert.Contains(result.Controls, c => c.Type == "input_validation"); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithMultipleControls_ReturnsAllControls() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.rate-limiting"] = "100", ["kong.plugin.cors.origins"] = "https://example.com", ["kong.plugin.bot-detection"] = "enabled" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.NotNull(result.Controls); Assert.Equal(3, result.Controls.Count); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithNoControls_ReturnsNullControls() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong" }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Null(result.Controls); } #endregion #region Confidence and Metadata [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_BaseConfidence_Returns0Point75() { var root = new RichGraphRoot("root-1", "gateway", null); var context = BoundaryExtractionContext.Empty with { Source = "gateway" }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Equal(0.75, result.Confidence, precision: 2); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithKnownGateway_IncreasesConfidence() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong" }; 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_WithAuthAndRouteInfo_MaximizesConfidence() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.jwt"] = "enabled", ["kong.route.path"] = "/api/v1" } }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Equal(0.95, result.Confidence, precision: 2); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_ReturnsNetworkKind() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong" }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Equal("network", result.Kind); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_BuildsEvidenceRef_WithGatewayType() { var root = new RichGraphRoot("root-123", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Namespace = "production", EnvironmentId = "env-456" }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Equal("gateway/kong/production/env-456/root-123", result.EvidenceRef); } #endregion #region ExtractAsync [Trait("Category", TestCategories.Unit)] [Fact] public async Task ExtractAsync_ReturnsSameResultAsExtract() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong", Annotations = new Dictionary { ["kong.plugin.jwt"] = "enabled" } }; 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 = "kong" }; 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); } [Trait("Category", TestCategories.Unit)] [Fact] public void Extract_WithNoAuth_ReturnsNullAuth() { var root = new RichGraphRoot("root-1", "kong", null); var context = BoundaryExtractionContext.Empty with { Source = "kong" }; var result = _extractor.Extract(root, null, context); Assert.NotNull(result); Assert.Null(result.Auth); } #endregion }