968 lines
28 KiB
C#
968 lines
28 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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<GatewayBoundaryExtractor>.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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<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);
|
|
}
|
|
|
|
[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
|
|
}
|