feat: Add VEX Status Chip component and integration tests for reachability drift detection

- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips.
- Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling.
- Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation.
- Updated project references to include the new Reachability Drift library.
This commit is contained in:
StellaOps Bot
2025-12-20 01:26:42 +02:00
parent edc91ea96f
commit 5fc469ad98
159 changed files with 41116 additions and 2305 deletions

View File

@@ -19,10 +19,22 @@ public static class BoundaryServiceCollectionExtensions
/// </summary>
public static IServiceCollection AddBoundaryExtractors(this IServiceCollection services)
{
// Register base extractor
// Register base extractor (Priority 100 - fallback)
services.TryAddSingleton<RichGraphBoundaryExtractor>();
services.TryAddSingleton<IBoundaryProofExtractor, RichGraphBoundaryExtractor>();
// Register IaC extractor (Priority 150 - for Terraform/CloudFormation/Pulumi/Helm sources)
services.TryAddSingleton<IacBoundaryExtractor>();
services.AddSingleton<IBoundaryProofExtractor, IacBoundaryExtractor>();
// Register K8s extractor (Priority 200 - higher precedence for K8s sources)
services.TryAddSingleton<K8sBoundaryExtractor>();
services.AddSingleton<IBoundaryProofExtractor, K8sBoundaryExtractor>();
// Register Gateway extractor (Priority 250 - higher precedence for API gateway sources)
services.TryAddSingleton<GatewayBoundaryExtractor>();
services.AddSingleton<IBoundaryProofExtractor, GatewayBoundaryExtractor>();
// Register composite extractor that uses all available extractors
services.TryAddSingleton<CompositeBoundaryExtractor>();

View File

@@ -0,0 +1,769 @@
// -----------------------------------------------------------------------------
// GatewayBoundaryExtractor.cs
// Sprint: SPRINT_3800_0002_0003_boundary_gateway
// Description: Extracts boundary proof from API Gateway metadata.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.SmartDiff.Detection;
namespace StellaOps.Scanner.Reachability.Boundary;
/// <summary>
/// Extracts boundary proof from API Gateway deployment metadata.
/// Parses Kong, Envoy/Istio, AWS API Gateway, and Traefik configurations.
/// </summary>
public sealed class GatewayBoundaryExtractor : IBoundaryProofExtractor
{
private readonly ILogger<GatewayBoundaryExtractor> _logger;
private readonly TimeProvider _timeProvider;
// Gateway source identifiers
private static readonly string[] GatewaySources =
[
"gateway",
"kong",
"envoy",
"istio",
"apigateway",
"traefik"
];
// Gateway annotation prefixes
private static readonly string[] GatewayAnnotationPrefixes =
[
"kong.",
"konghq.com/",
"envoy.",
"istio.io/",
"apigateway.",
"traefik.",
"getambassador.io/"
];
public GatewayBoundaryExtractor(
ILogger<GatewayBoundaryExtractor> logger,
TimeProvider? timeProvider = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
public int Priority => 250; // Higher than K8sBoundaryExtractor (200)
/// <inheritdoc />
public bool CanHandle(BoundaryExtractionContext context)
{
// Handle when source is a known gateway
if (GatewaySources.Any(s =>
string.Equals(context.Source, s, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
// Also handle if annotations contain gateway-specific keys
return context.Annotations.Keys.Any(k =>
GatewayAnnotationPrefixes.Any(prefix =>
k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)));
}
/// <inheritdoc />
public Task<BoundaryProof?> ExtractAsync(
RichGraphRoot root,
RichGraphNode? rootNode,
BoundaryExtractionContext context,
CancellationToken cancellationToken = default)
{
return Task.FromResult(Extract(root, rootNode, context));
}
/// <inheritdoc />
public BoundaryProof? Extract(
RichGraphRoot root,
RichGraphNode? rootNode,
BoundaryExtractionContext context)
{
ArgumentNullException.ThrowIfNull(root);
if (!CanHandle(context))
{
return null;
}
try
{
var annotations = context.Annotations;
var gatewayType = DetectGatewayType(context);
var exposure = DetermineExposure(context, gatewayType);
var surface = DetermineSurface(context, annotations, gatewayType);
var auth = DetectAuth(annotations, gatewayType);
var controls = DetectControls(annotations, gatewayType);
var confidence = CalculateConfidence(annotations, gatewayType);
_logger.LogDebug(
"Gateway boundary extraction: gateway={Gateway}, exposure={ExposureLevel}, confidence={Confidence:F2}",
gatewayType,
exposure.Level,
confidence);
return new BoundaryProof
{
Kind = "network",
Surface = surface,
Exposure = exposure,
Auth = auth,
Controls = controls.Count > 0 ? controls : null,
LastSeen = _timeProvider.GetUtcNow(),
Confidence = confidence,
Source = $"gateway:{gatewayType}",
EvidenceRef = BuildEvidenceRef(context, root.Id, gatewayType)
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Gateway boundary extraction failed for root {RootId}", root.Id);
return null;
}
}
private string DetectGatewayType(BoundaryExtractionContext context)
{
var source = context.Source?.ToLowerInvariant() ?? string.Empty;
var annotations = context.Annotations;
// Check source first
if (source.Contains("kong"))
return "kong";
if (source.Contains("envoy") || source.Contains("istio"))
return "envoy";
if (source.Contains("apigateway"))
return "aws-apigw";
if (source.Contains("traefik"))
return "traefik";
// Check annotations
if (annotations.Keys.Any(k => k.StartsWith("kong.", StringComparison.OrdinalIgnoreCase) ||
k.StartsWith("konghq.com/", StringComparison.OrdinalIgnoreCase)))
return "kong";
if (annotations.Keys.Any(k => k.StartsWith("istio.io/", StringComparison.OrdinalIgnoreCase) ||
k.StartsWith("envoy.", StringComparison.OrdinalIgnoreCase)))
return "envoy";
if (annotations.Keys.Any(k => k.StartsWith("apigateway.", StringComparison.OrdinalIgnoreCase)))
return "aws-apigw";
if (annotations.Keys.Any(k => k.StartsWith("traefik.", StringComparison.OrdinalIgnoreCase)))
return "traefik";
if (annotations.Keys.Any(k => k.StartsWith("getambassador.io/", StringComparison.OrdinalIgnoreCase)))
return "ambassador";
return "generic";
}
private BoundaryExposure DetermineExposure(BoundaryExtractionContext context, string gatewayType)
{
var annotations = context.Annotations;
var level = "public"; // API gateways are typically internet-facing
var internetFacing = true;
var behindProxy = true; // Gateway is the proxy
List<string>? clientTypes = ["browser", "api_client", "mobile"];
// Check for internal-only configurations
if (annotations.TryGetValue($"{gatewayType}.internal", out var isInternal) ||
annotations.TryGetValue("internal", out isInternal))
{
if (bool.TryParse(isInternal, out var internalFlag) && internalFlag)
{
level = "internal";
internetFacing = false;
clientTypes = ["service"];
}
}
// Istio mesh internal
if (gatewayType == "envoy" &&
annotations.Keys.Any(k => k.Contains("mesh", StringComparison.OrdinalIgnoreCase)))
{
level = "internal";
internetFacing = false;
clientTypes = ["service"];
}
// AWS internal API
if (gatewayType == "aws-apigw" &&
annotations.TryGetValue("apigateway.endpoint-type", out var endpointType))
{
if (endpointType.Equals("PRIVATE", StringComparison.OrdinalIgnoreCase))
{
level = "internal";
internetFacing = false;
clientTypes = ["service"];
}
}
return new BoundaryExposure
{
Level = level,
InternetFacing = internetFacing,
Zone = context.NetworkZone,
BehindProxy = behindProxy,
ClientTypes = clientTypes
};
}
private BoundarySurface DetermineSurface(
BoundaryExtractionContext context,
IReadOnlyDictionary<string, string> annotations,
string gatewayType)
{
string? path = null;
string protocol = "https";
int? port = null;
string? host = null;
// Gateway-specific path extraction
path = gatewayType switch
{
"kong" => TryGetAnnotation(annotations, "kong.route.path", "kong.path", "konghq.com/path"),
"envoy" => TryGetAnnotation(annotations, "envoy.route.prefix", "istio.io/path"),
"aws-apigw" => TryGetAnnotation(annotations, "apigateway.path", "apigateway.resource-path"),
"traefik" => TryGetAnnotation(annotations, "traefik.http.routers.path", "traefik.path"),
_ => TryGetAnnotation(annotations, "path", "route.path")
};
// Default path from namespace
path ??= !string.IsNullOrEmpty(context.Namespace) ? $"/{context.Namespace}" : "/";
// Host extraction
host = gatewayType switch
{
"kong" => TryGetAnnotation(annotations, "kong.route.host", "konghq.com/host"),
"envoy" => TryGetAnnotation(annotations, "istio.io/host", "envoy.virtual-host"),
"aws-apigw" => TryGetAnnotation(annotations, "apigateway.domain-name"),
"traefik" => TryGetAnnotation(annotations, "traefik.http.routers.host"),
_ => TryGetAnnotation(annotations, "host")
};
// Protocol - gateways typically use HTTPS
if (annotations.Keys.Any(k => k.Contains("grpc", StringComparison.OrdinalIgnoreCase)))
{
protocol = "grpc";
}
else if (annotations.Keys.Any(k => k.Contains("websocket", StringComparison.OrdinalIgnoreCase)))
{
protocol = "wss";
}
// Port from bindings
if (context.PortBindings.Count > 0)
{
port = context.PortBindings.Keys.FirstOrDefault();
}
else
{
// Default gateway ports
port = protocol switch
{
"https" => 443,
"grpc" => 443,
"wss" => 443,
_ => 80
};
}
return new BoundarySurface
{
Type = "api",
Protocol = protocol,
Port = port,
Host = host,
Path = path
};
}
private BoundaryAuth? DetectAuth(
IReadOnlyDictionary<string, string> annotations,
string gatewayType)
{
string? authType = null;
var required = false;
List<string>? roles = null;
string? provider = null;
bool? mfaRequired = null;
switch (gatewayType)
{
case "kong":
(authType, required, roles, provider) = DetectKongAuth(annotations);
break;
case "envoy":
(authType, required, roles, provider) = DetectEnvoyAuth(annotations);
break;
case "aws-apigw":
(authType, required, roles, provider) = DetectAwsApigwAuth(annotations);
break;
case "traefik":
(authType, required, roles, provider) = DetectTraefikAuth(annotations);
break;
default:
(authType, required, roles, provider) = DetectGenericAuth(annotations);
break;
}
if (!required)
{
return null;
}
return new BoundaryAuth
{
Required = required,
Type = authType,
Roles = roles,
Provider = provider,
MfaRequired = mfaRequired
};
}
private static (string? authType, bool required, List<string>? roles, string? provider) DetectKongAuth(
IReadOnlyDictionary<string, string> annotations)
{
string? authType = null;
var required = false;
List<string>? roles = null;
string? provider = null;
foreach (var (key, value) in annotations)
{
// JWT plugin
if (key.Contains("jwt", StringComparison.OrdinalIgnoreCase) &&
(key.Contains("plugin", StringComparison.OrdinalIgnoreCase) ||
key.Contains("kong", StringComparison.OrdinalIgnoreCase)))
{
authType = "jwt";
required = true;
}
// OAuth2 plugin
if (key.Contains("oauth2", StringComparison.OrdinalIgnoreCase))
{
authType = "oauth2";
required = true;
}
// Key-auth plugin
if (key.Contains("key-auth", StringComparison.OrdinalIgnoreCase))
{
authType = "api_key";
required = true;
}
// Basic auth plugin
if (key.Contains("basic-auth", StringComparison.OrdinalIgnoreCase))
{
authType = "basic";
required = true;
}
// ACL plugin for roles
if (key.Contains("acl", StringComparison.OrdinalIgnoreCase) &&
key.Contains("allow", StringComparison.OrdinalIgnoreCase))
{
roles = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
}
}
return (authType, required, roles, provider);
}
private static (string? authType, bool required, List<string>? roles, string? provider) DetectEnvoyAuth(
IReadOnlyDictionary<string, string> annotations)
{
string? authType = null;
var required = false;
List<string>? roles = null;
string? provider = null;
foreach (var (key, value) in annotations)
{
// Istio JWT policy
if (key.Contains("jwt", StringComparison.OrdinalIgnoreCase) ||
key.Contains("requestauthentication", StringComparison.OrdinalIgnoreCase))
{
authType = "jwt";
required = true;
}
// Istio AuthorizationPolicy
if (key.Contains("authorizationpolicy", StringComparison.OrdinalIgnoreCase))
{
authType ??= "rbac";
required = true;
}
// mTLS
if (key.Contains("mtls", StringComparison.OrdinalIgnoreCase) ||
key.Contains("peerauthentication", StringComparison.OrdinalIgnoreCase))
{
authType = "mtls";
required = true;
}
// OIDC filter
if (key.Contains("oidc", StringComparison.OrdinalIgnoreCase))
{
authType = "oauth2";
required = true;
if (value.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
provider = value;
}
}
}
return (authType, required, roles, provider);
}
private static (string? authType, bool required, List<string>? roles, string? provider) DetectAwsApigwAuth(
IReadOnlyDictionary<string, string> annotations)
{
string? authType = null;
var required = false;
List<string>? roles = null;
string? provider = null;
foreach (var (key, value) in annotations)
{
// Cognito authorizer
if (key.Contains("cognito", StringComparison.OrdinalIgnoreCase))
{
authType = "oauth2";
required = true;
provider = "cognito";
}
// Lambda authorizer
if (key.Contains("lambda-authorizer", StringComparison.OrdinalIgnoreCase) ||
(key.Contains("authorizer", StringComparison.OrdinalIgnoreCase) &&
value.Contains("lambda", StringComparison.OrdinalIgnoreCase)))
{
authType = "custom";
required = true;
provider = "lambda";
}
// API key required
if (key.Contains("api-key-required", StringComparison.OrdinalIgnoreCase))
{
if (bool.TryParse(value, out var keyRequired) && keyRequired)
{
authType = "api_key";
required = true;
}
}
// IAM authorizer
if (key.Contains("iam", StringComparison.OrdinalIgnoreCase) &&
key.Contains("authorizer", StringComparison.OrdinalIgnoreCase))
{
authType = "iam";
required = true;
provider = "aws-iam";
}
}
return (authType, required, roles, provider);
}
private static (string? authType, bool required, List<string>? roles, string? provider) DetectTraefikAuth(
IReadOnlyDictionary<string, string> annotations)
{
string? authType = null;
var required = false;
List<string>? roles = null;
string? provider = null;
foreach (var (key, value) in annotations)
{
// Basic auth middleware
if (key.Contains("basicauth", StringComparison.OrdinalIgnoreCase))
{
authType = "basic";
required = true;
}
// Digest auth middleware
if (key.Contains("digestauth", StringComparison.OrdinalIgnoreCase))
{
authType = "digest";
required = true;
}
// Forward auth middleware (external auth)
if (key.Contains("forwardauth", StringComparison.OrdinalIgnoreCase))
{
authType = "custom";
required = true;
if (value.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
provider = value;
}
}
// OAuth middleware plugin
if (key.Contains("oauth", StringComparison.OrdinalIgnoreCase))
{
authType = "oauth2";
required = true;
}
}
return (authType, required, roles, provider);
}
private static (string? authType, bool required, List<string>? roles, string? provider) DetectGenericAuth(
IReadOnlyDictionary<string, string> annotations)
{
string? authType = null;
var required = false;
List<string>? roles = null;
string? provider = null;
foreach (var (key, value) in annotations)
{
if (key.Contains("auth", StringComparison.OrdinalIgnoreCase))
{
if (key.Contains("jwt", StringComparison.OrdinalIgnoreCase))
authType = "jwt";
else if (key.Contains("oauth", StringComparison.OrdinalIgnoreCase))
authType = "oauth2";
else if (key.Contains("basic", StringComparison.OrdinalIgnoreCase))
authType = "basic";
else if (key.Contains("api-key", StringComparison.OrdinalIgnoreCase))
authType = "api_key";
else
authType = "custom";
required = true;
}
}
return (authType, required, roles, provider);
}
private List<BoundaryControl> DetectControls(
IReadOnlyDictionary<string, string> annotations,
string gatewayType)
{
var controls = new List<BoundaryControl>();
var now = _timeProvider.GetUtcNow();
// Rate limiting
var hasRateLimit = annotations.Keys.Any(k =>
k.Contains("rate-limit", StringComparison.OrdinalIgnoreCase) ||
k.Contains("ratelimit", StringComparison.OrdinalIgnoreCase) ||
k.Contains("throttle", StringComparison.OrdinalIgnoreCase) ||
// Kong
k.Contains("kong.plugin.rate-limiting", StringComparison.OrdinalIgnoreCase) ||
// Istio
k.Contains("ratelimit.config", StringComparison.OrdinalIgnoreCase) ||
// AWS
k.Contains("apigateway.throttle", StringComparison.OrdinalIgnoreCase));
if (hasRateLimit)
{
controls.Add(new BoundaryControl
{
Type = "rate_limit",
Active = true,
Config = gatewayType,
Effectiveness = "medium",
VerifiedAt = now
});
}
// IP restrictions
var hasIpRestriction = annotations.Keys.Any(k =>
k.Contains("ip-restriction", StringComparison.OrdinalIgnoreCase) ||
k.Contains("whitelist", StringComparison.OrdinalIgnoreCase) ||
k.Contains("allowlist", StringComparison.OrdinalIgnoreCase) ||
k.Contains("blacklist", StringComparison.OrdinalIgnoreCase) ||
k.Contains("denylist", StringComparison.OrdinalIgnoreCase));
if (hasIpRestriction)
{
controls.Add(new BoundaryControl
{
Type = "ip_allowlist",
Active = true,
Config = gatewayType,
Effectiveness = "high",
VerifiedAt = now
});
}
// CORS
var hasCors = annotations.Keys.Any(k =>
k.Contains("cors", StringComparison.OrdinalIgnoreCase));
if (hasCors)
{
controls.Add(new BoundaryControl
{
Type = "cors",
Active = true,
Config = gatewayType,
Effectiveness = "low",
VerifiedAt = now
});
}
// Request size limit
var hasSizeLimit = annotations.Keys.Any(k =>
k.Contains("request-size", StringComparison.OrdinalIgnoreCase) ||
k.Contains("body-limit", StringComparison.OrdinalIgnoreCase) ||
k.Contains("max-body", StringComparison.OrdinalIgnoreCase));
if (hasSizeLimit)
{
controls.Add(new BoundaryControl
{
Type = "request_size_limit",
Active = true,
Config = gatewayType,
Effectiveness = "low",
VerifiedAt = now
});
}
// WAF / Bot protection
var hasWaf = annotations.Keys.Any(k =>
k.Contains("waf", StringComparison.OrdinalIgnoreCase) ||
k.Contains("bot", StringComparison.OrdinalIgnoreCase) ||
k.Contains("modsecurity", StringComparison.OrdinalIgnoreCase) ||
// Kong
k.Contains("kong.plugin.bot-detection", StringComparison.OrdinalIgnoreCase) ||
// AWS
k.Contains("apigateway.waf", StringComparison.OrdinalIgnoreCase));
if (hasWaf)
{
controls.Add(new BoundaryControl
{
Type = "waf",
Active = true,
Config = gatewayType,
Effectiveness = "high",
VerifiedAt = now
});
}
// Request transformation / validation
var hasValidation = annotations.Keys.Any(k =>
k.Contains("request-validation", StringComparison.OrdinalIgnoreCase) ||
k.Contains("request-transformer", StringComparison.OrdinalIgnoreCase) ||
k.Contains("validate", StringComparison.OrdinalIgnoreCase));
if (hasValidation)
{
controls.Add(new BoundaryControl
{
Type = "input_validation",
Active = true,
Config = gatewayType,
Effectiveness = "medium",
VerifiedAt = now
});
}
return controls;
}
private static double CalculateConfidence(
IReadOnlyDictionary<string, string> annotations,
string gatewayType)
{
// Base confidence from gateway source
var confidence = 0.75;
// Higher confidence if we have specific gateway annotations
if (gatewayType != "generic")
{
confidence += 0.1;
}
// Higher confidence if we have auth information
if (annotations.Keys.Any(k =>
k.Contains("auth", StringComparison.OrdinalIgnoreCase) ||
k.Contains("jwt", StringComparison.OrdinalIgnoreCase) ||
k.Contains("oauth", StringComparison.OrdinalIgnoreCase)))
{
confidence += 0.05;
}
// Higher confidence if we have routing information
if (annotations.Keys.Any(k =>
k.Contains("route", StringComparison.OrdinalIgnoreCase) ||
k.Contains("path", StringComparison.OrdinalIgnoreCase) ||
k.Contains("host", StringComparison.OrdinalIgnoreCase)))
{
confidence += 0.05;
}
// Cap at 0.95
return Math.Min(confidence, 0.95);
}
private static string? TryGetAnnotation(
IReadOnlyDictionary<string, string> annotations,
params string[] keys)
{
foreach (var key in keys)
{
if (annotations.TryGetValue(key, out var value) && !string.IsNullOrEmpty(value))
{
return value;
}
// Also try case-insensitive match
var match = annotations.FirstOrDefault(kv =>
kv.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(match.Value))
{
return match.Value;
}
}
return null;
}
private static string BuildEvidenceRef(
BoundaryExtractionContext context,
string rootId,
string gatewayType)
{
var parts = new List<string> { "gateway", gatewayType };
if (!string.IsNullOrEmpty(context.Namespace))
{
parts.Add(context.Namespace);
}
if (!string.IsNullOrEmpty(context.EnvironmentId))
{
parts.Add(context.EnvironmentId);
}
parts.Add(rootId);
return string.Join("/", parts);
}
}

View File

@@ -0,0 +1,838 @@
// -----------------------------------------------------------------------------
// IacBoundaryExtractor.cs
// Sprint: SPRINT_3800_0002_0004_boundary_iac
// Description: Extracts boundary proof from Infrastructure-as-Code metadata.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.SmartDiff.Detection;
namespace StellaOps.Scanner.Reachability.Boundary;
/// <summary>
/// Extracts boundary proof from Infrastructure-as-Code configurations.
/// Parses Terraform, CloudFormation, Pulumi, and Helm chart configurations.
/// </summary>
public sealed class IacBoundaryExtractor : IBoundaryProofExtractor
{
private readonly ILogger<IacBoundaryExtractor> _logger;
private readonly TimeProvider _timeProvider;
// IaC source identifiers
private static readonly string[] IacSources =
[
"terraform",
"cloudformation",
"cfn",
"pulumi",
"helm",
"iac",
"infrastructure"
];
// IaC annotation prefixes
private static readonly string[] IacAnnotationPrefixes =
[
"terraform.",
"cloudformation.",
"cfn.",
"pulumi.",
"helm.",
"aws::",
"azure.",
"gcp."
];
public IacBoundaryExtractor(
ILogger<IacBoundaryExtractor> logger,
TimeProvider? timeProvider = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
public int Priority => 150; // Between base (100) and K8s (200) - IaC is declarative intent
/// <inheritdoc />
public bool CanHandle(BoundaryExtractionContext context)
{
// Handle when source is a known IaC tool
if (IacSources.Any(s =>
string.Equals(context.Source, s, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
// Also handle if annotations contain IaC-specific keys
return context.Annotations.Keys.Any(k =>
IacAnnotationPrefixes.Any(prefix =>
k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)));
}
/// <inheritdoc />
public Task<BoundaryProof?> ExtractAsync(
RichGraphRoot root,
RichGraphNode? rootNode,
BoundaryExtractionContext context,
CancellationToken cancellationToken = default)
{
return Task.FromResult(Extract(root, rootNode, context));
}
/// <inheritdoc />
public BoundaryProof? Extract(
RichGraphRoot root,
RichGraphNode? rootNode,
BoundaryExtractionContext context)
{
ArgumentNullException.ThrowIfNull(root);
if (!CanHandle(context))
{
return null;
}
try
{
var annotations = context.Annotations;
var iacType = DetectIacType(context);
var exposure = DetermineExposure(context, annotations, iacType);
var surface = DetermineSurface(context, annotations, iacType);
var auth = DetectAuth(annotations, iacType);
var controls = DetectControls(annotations, iacType);
var confidence = CalculateConfidence(annotations, iacType);
_logger.LogDebug(
"IaC boundary extraction: iac={IacType}, exposure={ExposureLevel}, confidence={Confidence:F2}",
iacType,
exposure.Level,
confidence);
return new BoundaryProof
{
Kind = "network",
Surface = surface,
Exposure = exposure,
Auth = auth,
Controls = controls.Count > 0 ? controls : null,
LastSeen = _timeProvider.GetUtcNow(),
Confidence = confidence,
Source = $"iac:{iacType}",
EvidenceRef = BuildEvidenceRef(context, root.Id, iacType)
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "IaC boundary extraction failed for root {RootId}", root.Id);
return null;
}
}
private string DetectIacType(BoundaryExtractionContext context)
{
var source = context.Source?.ToLowerInvariant() ?? string.Empty;
var annotations = context.Annotations;
// Check source first
if (source.Contains("terraform"))
return "terraform";
if (source.Contains("cloudformation") || source.Contains("cfn"))
return "cloudformation";
if (source.Contains("pulumi"))
return "pulumi";
if (source.Contains("helm"))
return "helm";
// Check annotations
if (annotations.Keys.Any(k => k.StartsWith("terraform.", StringComparison.OrdinalIgnoreCase)))
return "terraform";
if (annotations.Keys.Any(k =>
k.StartsWith("cloudformation.", StringComparison.OrdinalIgnoreCase) ||
k.StartsWith("cfn.", StringComparison.OrdinalIgnoreCase) ||
k.Contains("AWS::", StringComparison.Ordinal)))
return "cloudformation";
if (annotations.Keys.Any(k => k.StartsWith("pulumi.", StringComparison.OrdinalIgnoreCase)))
return "pulumi";
if (annotations.Keys.Any(k => k.StartsWith("helm.", StringComparison.OrdinalIgnoreCase)))
return "helm";
// Check for cloud provider patterns
if (annotations.Keys.Any(k => k.StartsWith("aws:", StringComparison.OrdinalIgnoreCase)))
return "terraform"; // Assume Terraform for AWS resources
if (annotations.Keys.Any(k => k.StartsWith("azure.", StringComparison.OrdinalIgnoreCase)))
return "terraform";
if (annotations.Keys.Any(k => k.StartsWith("gcp.", StringComparison.OrdinalIgnoreCase)))
return "terraform";
return "generic";
}
private BoundaryExposure DetermineExposure(
BoundaryExtractionContext context,
IReadOnlyDictionary<string, string> annotations,
string iacType)
{
var level = "private";
var internetFacing = false;
var behindProxy = false;
List<string>? clientTypes = ["service"];
// Check for public internet exposure indicators
var hasPublicExposure = false;
switch (iacType)
{
case "terraform":
hasPublicExposure = DetectTerraformPublicExposure(annotations);
break;
case "cloudformation":
hasPublicExposure = DetectCloudFormationPublicExposure(annotations);
break;
case "pulumi":
hasPublicExposure = DetectPulumiPublicExposure(annotations);
break;
case "helm":
hasPublicExposure = DetectHelmPublicExposure(annotations);
break;
default:
hasPublicExposure = DetectGenericPublicExposure(annotations);
break;
}
if (hasPublicExposure || context.IsInternetFacing == true)
{
level = "public";
internetFacing = true;
clientTypes = ["browser", "api_client"];
}
else if (annotations.Keys.Any(k =>
k.Contains("internal", StringComparison.OrdinalIgnoreCase) ||
k.Contains("private", StringComparison.OrdinalIgnoreCase)))
{
level = "internal";
clientTypes = ["service"];
}
// Check for load balancer (implies behind proxy)
if (annotations.Keys.Any(k =>
k.Contains("load_balancer", StringComparison.OrdinalIgnoreCase) ||
k.Contains("loadbalancer", StringComparison.OrdinalIgnoreCase) ||
k.Contains("alb", StringComparison.OrdinalIgnoreCase) ||
k.Contains("elb", StringComparison.OrdinalIgnoreCase)))
{
behindProxy = true;
}
return new BoundaryExposure
{
Level = level,
InternetFacing = internetFacing,
Zone = context.NetworkZone,
BehindProxy = behindProxy,
ClientTypes = clientTypes
};
}
private static bool DetectTerraformPublicExposure(IReadOnlyDictionary<string, string> annotations)
{
// Check for internet-facing resources
foreach (var (key, value) in annotations)
{
// Security group with 0.0.0.0/0
if (key.Contains("security_group", StringComparison.OrdinalIgnoreCase) &&
key.Contains("ingress", StringComparison.OrdinalIgnoreCase))
{
if (value.Contains("0.0.0.0/0") || value.Contains("::/0"))
return true;
}
// Internet-facing ALB
if (key.Contains("aws_lb", StringComparison.OrdinalIgnoreCase) &&
key.Contains("internal", StringComparison.OrdinalIgnoreCase))
{
if (value.Equals("false", StringComparison.OrdinalIgnoreCase))
return true;
}
// Public subnet
if (key.Contains("map_public_ip", StringComparison.OrdinalIgnoreCase))
{
if (value.Equals("true", StringComparison.OrdinalIgnoreCase))
return true;
}
// Public IP association
if (key.Contains("public_ip", StringComparison.OrdinalIgnoreCase) ||
key.Contains("eip", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static bool DetectCloudFormationPublicExposure(IReadOnlyDictionary<string, string> annotations)
{
foreach (var (key, value) in annotations)
{
// Security group with public CIDR
if (key.Contains("SecurityGroup", StringComparison.OrdinalIgnoreCase))
{
if (value.Contains("0.0.0.0/0") || value.Contains("::/0"))
return true;
}
// Internet-facing ELB/ALB
if ((key.Contains("LoadBalancer", StringComparison.OrdinalIgnoreCase) ||
key.Contains("ELB", StringComparison.OrdinalIgnoreCase)) &&
key.Contains("Scheme", StringComparison.OrdinalIgnoreCase))
{
if (value.Equals("internet-facing", StringComparison.OrdinalIgnoreCase))
return true;
}
// API Gateway
if (key.Contains("ApiGateway", StringComparison.OrdinalIgnoreCase))
{
return true;
}
// CloudFront distribution
if (key.Contains("CloudFront", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static bool DetectPulumiPublicExposure(IReadOnlyDictionary<string, string> annotations)
{
foreach (var (key, value) in annotations)
{
// Public security group rule
if (key.Contains("SecurityGroup", StringComparison.OrdinalIgnoreCase))
{
if (value.Contains("0.0.0.0/0") || value.Contains("::/0"))
return true;
}
// Internet-facing load balancer
if (key.Contains("LoadBalancer", StringComparison.OrdinalIgnoreCase) &&
key.Contains("internal", StringComparison.OrdinalIgnoreCase))
{
if (value.Equals("false", StringComparison.OrdinalIgnoreCase))
return true;
}
// Public tags
if (key.Contains("tags", StringComparison.OrdinalIgnoreCase) &&
key.Contains("public", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static bool DetectHelmPublicExposure(IReadOnlyDictionary<string, string> annotations)
{
foreach (var (key, value) in annotations)
{
// Ingress enabled
if (key.Contains("ingress.enabled", StringComparison.OrdinalIgnoreCase))
{
if (value.Equals("true", StringComparison.OrdinalIgnoreCase))
return true;
}
// LoadBalancer service
if (key.Contains("service.type", StringComparison.OrdinalIgnoreCase))
{
if (value.Equals("LoadBalancer", StringComparison.OrdinalIgnoreCase))
return true;
}
// NodePort service
if (key.Contains("service.type", StringComparison.OrdinalIgnoreCase))
{
if (value.Equals("NodePort", StringComparison.OrdinalIgnoreCase))
return true;
}
}
return false;
}
private static bool DetectGenericPublicExposure(IReadOnlyDictionary<string, string> annotations)
{
foreach (var (key, value) in annotations)
{
// Generic public indicators
if (key.Contains("public", StringComparison.OrdinalIgnoreCase) ||
key.Contains("internet", StringComparison.OrdinalIgnoreCase) ||
key.Contains("external", StringComparison.OrdinalIgnoreCase))
{
if (value.Equals("true", StringComparison.OrdinalIgnoreCase))
return true;
}
// CIDR 0.0.0.0/0
if (value.Contains("0.0.0.0/0"))
return true;
}
return false;
}
private static BoundarySurface DetermineSurface(
BoundaryExtractionContext context,
IReadOnlyDictionary<string, string> annotations,
string iacType)
{
string? path = null;
string protocol = "https";
int? port = null;
string? host = null;
// IaC-specific path/host extraction
path = iacType switch
{
"terraform" => TryGetAnnotation(annotations, "terraform.resource.path", "path"),
"cloudformation" => TryGetAnnotation(annotations, "cloudformation.path", "path"),
"pulumi" => TryGetAnnotation(annotations, "pulumi.path", "path"),
"helm" => TryGetAnnotation(annotations, "helm.values.ingress.path", "ingress.path"),
_ => TryGetAnnotation(annotations, "path")
};
// Default path
path ??= !string.IsNullOrEmpty(context.Namespace) ? $"/{context.Namespace}" : "/";
// Host extraction
host = iacType switch
{
"terraform" => TryGetAnnotation(annotations, "terraform.resource.domain", "domain"),
"cloudformation" => TryGetAnnotation(annotations, "cloudformation.domain", "domain"),
"pulumi" => TryGetAnnotation(annotations, "pulumi.domain", "domain"),
"helm" => TryGetAnnotation(annotations, "helm.values.ingress.host", "ingress.host"),
_ => TryGetAnnotation(annotations, "domain", "host")
};
// Port extraction
var portStr = TryGetAnnotation(annotations, "port", "listener.port", "service.port");
if (portStr != null && int.TryParse(portStr, out var parsedPort))
{
port = parsedPort;
}
else if (context.PortBindings.Count > 0)
{
port = context.PortBindings.Keys.FirstOrDefault();
}
// Determine protocol from annotations
if (annotations.Keys.Any(k => k.Contains("grpc", StringComparison.OrdinalIgnoreCase)))
{
protocol = "grpc";
}
else if (annotations.Keys.Any(k =>
k.Contains("tcp", StringComparison.OrdinalIgnoreCase) &&
!k.Contains("https", StringComparison.OrdinalIgnoreCase)))
{
protocol = "tcp";
}
return new BoundarySurface
{
Type = "infrastructure",
Protocol = protocol,
Port = port,
Host = host,
Path = path
};
}
private static BoundaryAuth? DetectAuth(
IReadOnlyDictionary<string, string> annotations,
string iacType)
{
string? authType = null;
var required = false;
List<string>? roles = null;
string? provider = null;
switch (iacType)
{
case "terraform":
case "cloudformation":
case "pulumi":
(authType, required, provider) = DetectCloudAuth(annotations);
break;
case "helm":
(authType, required, provider) = DetectHelmAuth(annotations);
break;
default:
(authType, required, provider) = DetectGenericAuth(annotations);
break;
}
if (!required)
{
return null;
}
return new BoundaryAuth
{
Required = required,
Type = authType,
Roles = roles,
Provider = provider,
MfaRequired = null
};
}
private static (string? authType, bool required, string? provider) DetectCloudAuth(
IReadOnlyDictionary<string, string> annotations)
{
string? authType = null;
var required = false;
string? provider = null;
foreach (var (key, value) in annotations)
{
// IAM authentication
if (key.Contains("iam", StringComparison.OrdinalIgnoreCase) &&
(key.Contains("auth", StringComparison.OrdinalIgnoreCase) ||
key.Contains("policy", StringComparison.OrdinalIgnoreCase)))
{
authType = "iam";
required = true;
provider = "aws-iam";
}
// Cognito authentication
if (key.Contains("cognito", StringComparison.OrdinalIgnoreCase))
{
authType = "oauth2";
required = true;
provider = "cognito";
}
// Azure AD authentication
if (key.Contains("azure_ad", StringComparison.OrdinalIgnoreCase) ||
key.Contains("aad", StringComparison.OrdinalIgnoreCase))
{
authType = "oauth2";
required = true;
provider = "azure-ad";
}
// GCP IAM
if (key.Contains("gcp", StringComparison.OrdinalIgnoreCase) &&
key.Contains("iam", StringComparison.OrdinalIgnoreCase))
{
authType = "iam";
required = true;
provider = "gcp-iam";
}
// mTLS
if (key.Contains("mtls", StringComparison.OrdinalIgnoreCase) ||
key.Contains("client_certificate", StringComparison.OrdinalIgnoreCase))
{
authType = "mtls";
required = true;
}
}
return (authType, required, provider);
}
private static (string? authType, bool required, string? provider) DetectHelmAuth(
IReadOnlyDictionary<string, string> annotations)
{
string? authType = null;
var required = false;
string? provider = null;
foreach (var (key, value) in annotations)
{
// OAuth2 proxy
if (key.Contains("oauth2-proxy", StringComparison.OrdinalIgnoreCase))
{
authType = "oauth2";
required = true;
}
// Basic auth
if (key.Contains("auth.enabled", StringComparison.OrdinalIgnoreCase) &&
value.Equals("true", StringComparison.OrdinalIgnoreCase))
{
authType ??= "basic";
required = true;
}
// TLS/mTLS
if (key.Contains("tls.enabled", StringComparison.OrdinalIgnoreCase) &&
value.Equals("true", StringComparison.OrdinalIgnoreCase))
{
if (key.Contains("mtls", StringComparison.OrdinalIgnoreCase))
{
authType = "mtls";
required = true;
}
}
}
return (authType, required, provider);
}
private static (string? authType, bool required, string? provider) DetectGenericAuth(
IReadOnlyDictionary<string, string> annotations)
{
string? authType = null;
var required = false;
string? provider = null;
foreach (var (key, _) in annotations)
{
if (key.Contains("auth", StringComparison.OrdinalIgnoreCase))
{
authType = "custom";
required = true;
break;
}
}
return (authType, required, provider);
}
private List<BoundaryControl> DetectControls(
IReadOnlyDictionary<string, string> annotations,
string iacType)
{
var controls = new List<BoundaryControl>();
var now = _timeProvider.GetUtcNow();
// Security Groups / Firewall Rules
var hasSecurityGroup = annotations.Keys.Any(k =>
k.Contains("security_group", StringComparison.OrdinalIgnoreCase) ||
k.Contains("SecurityGroup", StringComparison.OrdinalIgnoreCase) ||
k.Contains("firewall", StringComparison.OrdinalIgnoreCase) ||
k.Contains("nsg", StringComparison.OrdinalIgnoreCase)); // Azure NSG
if (hasSecurityGroup)
{
controls.Add(new BoundaryControl
{
Type = "security_group",
Active = true,
Config = iacType,
Effectiveness = "high",
VerifiedAt = now
});
}
// WAF
var hasWaf = annotations.Keys.Any(k =>
k.Contains("waf", StringComparison.OrdinalIgnoreCase) ||
k.Contains("WebACL", StringComparison.OrdinalIgnoreCase) ||
k.Contains("ApplicationGateway", StringComparison.OrdinalIgnoreCase));
if (hasWaf)
{
controls.Add(new BoundaryControl
{
Type = "waf",
Active = true,
Config = iacType,
Effectiveness = "high",
VerifiedAt = now
});
}
// VPC / Network isolation
var hasVpc = annotations.Keys.Any(k =>
k.Contains("vpc", StringComparison.OrdinalIgnoreCase) ||
k.Contains("vnet", StringComparison.OrdinalIgnoreCase) ||
k.Contains("subnet", StringComparison.OrdinalIgnoreCase));
if (hasVpc)
{
controls.Add(new BoundaryControl
{
Type = "network_isolation",
Active = true,
Config = iacType,
Effectiveness = "medium",
VerifiedAt = now
});
}
// NACL / Network ACL
var hasNacl = annotations.Keys.Any(k =>
k.Contains("nacl", StringComparison.OrdinalIgnoreCase) ||
k.Contains("network_acl", StringComparison.OrdinalIgnoreCase) ||
k.Contains("NetworkAcl", StringComparison.OrdinalIgnoreCase));
if (hasNacl)
{
controls.Add(new BoundaryControl
{
Type = "network_acl",
Active = true,
Config = iacType,
Effectiveness = "high",
VerifiedAt = now
});
}
// DDoS Protection
var hasDdos = annotations.Keys.Any(k =>
k.Contains("ddos", StringComparison.OrdinalIgnoreCase) ||
k.Contains("shield", StringComparison.OrdinalIgnoreCase));
if (hasDdos)
{
controls.Add(new BoundaryControl
{
Type = "ddos_protection",
Active = true,
Config = iacType,
Effectiveness = "high",
VerifiedAt = now
});
}
// Encryption in transit
var hasEncryption = annotations.Keys.Any(k =>
k.Contains("ssl", StringComparison.OrdinalIgnoreCase) ||
k.Contains("tls", StringComparison.OrdinalIgnoreCase) ||
k.Contains("https_only", StringComparison.OrdinalIgnoreCase));
if (hasEncryption)
{
controls.Add(new BoundaryControl
{
Type = "encryption_in_transit",
Active = true,
Config = iacType,
Effectiveness = "high",
VerifiedAt = now
});
}
// Private endpoints
var hasPrivateEndpoint = annotations.Keys.Any(k =>
k.Contains("private_endpoint", StringComparison.OrdinalIgnoreCase) ||
k.Contains("PrivateLink", StringComparison.OrdinalIgnoreCase) ||
k.Contains("vpc_endpoint", StringComparison.OrdinalIgnoreCase));
if (hasPrivateEndpoint)
{
controls.Add(new BoundaryControl
{
Type = "private_endpoint",
Active = true,
Config = iacType,
Effectiveness = "high",
VerifiedAt = now
});
}
return controls;
}
private static double CalculateConfidence(
IReadOnlyDictionary<string, string> annotations,
string iacType)
{
// Base confidence - IaC is declarative intent, lower than runtime
var confidence = 0.6;
// Higher confidence for known IaC tools
if (iacType != "generic")
{
confidence += 0.1;
}
// Higher confidence if we have security-related resources
if (annotations.Keys.Any(k =>
k.Contains("security", StringComparison.OrdinalIgnoreCase) ||
k.Contains("firewall", StringComparison.OrdinalIgnoreCase) ||
k.Contains("waf", StringComparison.OrdinalIgnoreCase)))
{
confidence += 0.1;
}
// Higher confidence if we have network configuration
if (annotations.Keys.Any(k =>
k.Contains("vpc", StringComparison.OrdinalIgnoreCase) ||
k.Contains("subnet", StringComparison.OrdinalIgnoreCase) ||
k.Contains("network", StringComparison.OrdinalIgnoreCase)))
{
confidence += 0.05;
}
// Cap at 0.85 - IaC is not runtime state
return Math.Min(confidence, 0.85);
}
private static string? TryGetAnnotation(
IReadOnlyDictionary<string, string> annotations,
params string[] keys)
{
foreach (var key in keys)
{
if (annotations.TryGetValue(key, out var value) && !string.IsNullOrEmpty(value))
{
return value;
}
// Also try case-insensitive match
var match = annotations.FirstOrDefault(kv =>
kv.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(match.Value))
{
return match.Value;
}
}
return null;
}
private static string BuildEvidenceRef(
BoundaryExtractionContext context,
string rootId,
string iacType)
{
var parts = new List<string> { "iac", iacType };
if (!string.IsNullOrEmpty(context.Namespace))
{
parts.Add(context.Namespace);
}
if (!string.IsNullOrEmpty(context.EnvironmentId))
{
parts.Add(context.EnvironmentId);
}
parts.Add(rootId);
return string.Join("/", parts);
}
}

View File

@@ -0,0 +1,462 @@
// -----------------------------------------------------------------------------
// K8sBoundaryExtractor.cs
// Sprint: SPRINT_3800_0002_0002_boundary_k8s
// Description: Extracts boundary proof from Kubernetes metadata.
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.SmartDiff.Detection;
namespace StellaOps.Scanner.Reachability.Boundary;
/// <summary>
/// Extracts boundary proof from Kubernetes deployment metadata.
/// Parses Ingress, Service, and NetworkPolicy resources to determine exposure.
/// </summary>
public sealed class K8sBoundaryExtractor : IBoundaryProofExtractor
{
private readonly ILogger<K8sBoundaryExtractor> _logger;
private readonly TimeProvider _timeProvider;
// Well-known annotations for TLS
private static readonly string[] TlsAnnotations =
[
"nginx.ingress.kubernetes.io/ssl-redirect",
"nginx.ingress.kubernetes.io/force-ssl-redirect",
"cert-manager.io/cluster-issuer",
"cert-manager.io/issuer",
"kubernetes.io/tls-acme"
];
public K8sBoundaryExtractor(
ILogger<K8sBoundaryExtractor> logger,
TimeProvider? timeProvider = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
public int Priority => 200; // Higher than RichGraphBoundaryExtractor (100)
/// <inheritdoc />
public bool CanHandle(BoundaryExtractionContext context)
{
// Handle when source is K8s or when we have K8s-specific annotations
if (string.Equals(context.Source, "k8s", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.Source, "kubernetes", StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Also handle if annotations contain K8s-specific keys
return context.Annotations.Keys.Any(k =>
k.Contains("kubernetes.io", StringComparison.OrdinalIgnoreCase) ||
k.Contains("ingress", StringComparison.OrdinalIgnoreCase) ||
k.Contains("k8s", StringComparison.OrdinalIgnoreCase));
}
/// <inheritdoc />
public Task<BoundaryProof?> ExtractAsync(
RichGraphRoot root,
RichGraphNode? rootNode,
BoundaryExtractionContext context,
CancellationToken cancellationToken = default)
{
return Task.FromResult(Extract(root, rootNode, context));
}
/// <inheritdoc />
public BoundaryProof? Extract(
RichGraphRoot root,
RichGraphNode? rootNode,
BoundaryExtractionContext context)
{
ArgumentNullException.ThrowIfNull(root);
if (!CanHandle(context))
{
return null;
}
try
{
var annotations = context.Annotations;
var exposure = DetermineExposure(context);
var surface = DetermineSurface(context, annotations, exposure);
var auth = DetectAuth(annotations);
var controls = DetectControls(annotations, context);
var confidence = CalculateConfidence(exposure, annotations);
_logger.LogDebug(
"K8s boundary extraction: exposure={ExposureLevel}, surface={SurfaceType}, confidence={Confidence:F2}",
exposure.Level,
surface.Type,
confidence);
return new BoundaryProof
{
Kind = DetermineKind(exposure),
Surface = surface,
Exposure = exposure,
Auth = auth,
Controls = controls.Count > 0 ? controls : null,
LastSeen = _timeProvider.GetUtcNow(),
Confidence = confidence,
Source = "k8s",
EvidenceRef = BuildEvidenceRef(context, root.Id)
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "K8s boundary extraction failed for root {RootId}", root.Id);
return null;
}
}
private BoundaryExposure DetermineExposure(BoundaryExtractionContext context)
{
var annotations = context.Annotations;
var level = "private";
var internetFacing = false;
var behindProxy = false;
List<string>? clientTypes = null;
// Check explicit internet-facing flag
if (context.IsInternetFacing == true)
{
level = "public";
internetFacing = true;
clientTypes = ["browser", "api_client"];
}
// Ingress class indicates external exposure
else if (annotations.ContainsKey("kubernetes.io/ingress.class") ||
annotations.Keys.Any(k => k.Contains("ingress", StringComparison.OrdinalIgnoreCase)))
{
level = "public";
internetFacing = true;
behindProxy = true; // ingress controller acts as proxy
clientTypes = ["browser", "api_client"];
}
// Check for LoadBalancer service type
else if (annotations.TryGetValue("service.type", out var serviceType))
{
(level, internetFacing, clientTypes) = serviceType.ToLowerInvariant() switch
{
"loadbalancer" => ("public", true, new List<string> { "api_client", "service" }),
"nodeport" => ("internal", false, new List<string> { "service" }),
"clusterip" => ("private", false, new List<string> { "service" }),
_ => ("private", false, new List<string> { "service" })
};
}
// Check port bindings for common external ports
else if (context.PortBindings.Count > 0)
{
var externalPorts = new HashSet<int> { 80, 443, 8080, 8443 };
if (context.PortBindings.Keys.Any(p => externalPorts.Contains(p)))
{
level = "internal";
clientTypes = ["service"];
}
}
// Default based on network zone
else
{
level = context.NetworkZone switch
{
"dmz" => "internal",
"trusted" or "internal" => "private",
_ => "private"
};
clientTypes = ["service"];
}
return new BoundaryExposure
{
Level = level,
InternetFacing = internetFacing,
Zone = context.NetworkZone,
BehindProxy = behindProxy,
ClientTypes = clientTypes
};
}
private static BoundarySurface DetermineSurface(
BoundaryExtractionContext context,
IReadOnlyDictionary<string, string> annotations,
BoundaryExposure exposure)
{
string? path = null;
string protocol = "https";
int? port = null;
string? host = null;
// Try to extract path from annotations
if (annotations.TryGetValue("service.path", out var servicePath))
{
path = servicePath;
}
else if (annotations.TryGetValue("nginx.ingress.kubernetes.io/rewrite-target", out var rewrite))
{
path = rewrite;
}
else if (!string.IsNullOrEmpty(context.Namespace))
{
path = $"/{context.Namespace}";
}
// Determine protocol
var hasTls = TlsAnnotations.Any(ta =>
annotations.ContainsKey(ta) ||
annotations.Keys.Any(k => k.Contains("tls", StringComparison.OrdinalIgnoreCase)));
protocol = hasTls || exposure.InternetFacing ? "https" : "http";
// Check for grpc
if (annotations.Keys.Any(k => k.Contains("grpc", StringComparison.OrdinalIgnoreCase)))
{
protocol = "grpc";
}
// Get port from bindings
if (context.PortBindings.Count > 0)
{
port = context.PortBindings.Keys.FirstOrDefault();
}
// Get host from annotations
if (annotations.TryGetValue("ingress.host", out var ingressHost))
{
host = ingressHost;
}
return new BoundarySurface
{
Type = exposure.InternetFacing ? "api" : "service",
Protocol = protocol,
Port = port,
Host = host,
Path = path
};
}
private BoundaryAuth? DetectAuth(IReadOnlyDictionary<string, string> annotations)
{
string? authType = null;
var required = false;
List<string>? roles = null;
string? provider = null;
bool? mfaRequired = null;
// Check for auth annotations
foreach (var (key, value) in annotations)
{
// Check auth type annotations
if (key.Contains("auth-type", StringComparison.OrdinalIgnoreCase))
{
authType = value.ToLowerInvariant();
required = true;
}
// Check for basic auth
if (key.Contains("auth-secret", StringComparison.OrdinalIgnoreCase) ||
key.Contains("basic-auth", StringComparison.OrdinalIgnoreCase))
{
authType ??= "basic";
required = true;
}
// Check for OAuth/OIDC
if (key.Contains("oauth", StringComparison.OrdinalIgnoreCase) ||
key.Contains("oidc", StringComparison.OrdinalIgnoreCase))
{
authType = "oauth2";
required = true;
}
// Check for client cert auth
if (key.Contains("client-certificate", StringComparison.OrdinalIgnoreCase) ||
key.Contains("auth-tls", StringComparison.OrdinalIgnoreCase))
{
authType = "mtls";
required = true;
}
// Check for API key auth
if (key.Contains("api-key", StringComparison.OrdinalIgnoreCase))
{
authType = "api_key";
required = true;
}
// Check for auth provider
if (key.Contains("auth-url", StringComparison.OrdinalIgnoreCase))
{
provider = value;
}
// Check for role requirements
if (key.Contains("auth-roles", StringComparison.OrdinalIgnoreCase))
{
roles = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
}
// Check for MFA requirement
if (key.Contains("mfa", StringComparison.OrdinalIgnoreCase))
{
mfaRequired = bool.TryParse(value, out var mfa) ? mfa : true;
}
}
if (!required)
{
return null;
}
return new BoundaryAuth
{
Required = required,
Type = authType,
Roles = roles,
Provider = provider,
MfaRequired = mfaRequired
};
}
private List<BoundaryControl> DetectControls(
IReadOnlyDictionary<string, string> annotations,
BoundaryExtractionContext context)
{
var controls = new List<BoundaryControl>();
var now = _timeProvider.GetUtcNow();
// Check for NetworkPolicy
if (annotations.ContainsKey("network.policy.enabled") ||
annotations.Keys.Any(k => k.Contains("networkpolicy", StringComparison.OrdinalIgnoreCase)))
{
controls.Add(new BoundaryControl
{
Type = "network_policy",
Active = true,
Config = context.Namespace ?? "default",
Effectiveness = "high",
VerifiedAt = now
});
}
// Check for rate limiting
if (annotations.Keys.Any(k =>
k.Contains("rate-limit", StringComparison.OrdinalIgnoreCase) ||
k.Contains("ratelimit", StringComparison.OrdinalIgnoreCase)))
{
var rateValue = annotations.FirstOrDefault(kv =>
kv.Key.Contains("rate", StringComparison.OrdinalIgnoreCase)).Value ?? "default";
controls.Add(new BoundaryControl
{
Type = "rate_limit",
Active = true,
Config = rateValue,
Effectiveness = "medium",
VerifiedAt = now
});
}
// Check for IP whitelist
if (annotations.Keys.Any(k =>
k.Contains("whitelist", StringComparison.OrdinalIgnoreCase) ||
k.Contains("allowlist", StringComparison.OrdinalIgnoreCase)))
{
controls.Add(new BoundaryControl
{
Type = "ip_allowlist",
Active = true,
Config = "ingress",
Effectiveness = "high",
VerifiedAt = now
});
}
// Check for WAF
if (annotations.Keys.Any(k =>
k.Contains("waf", StringComparison.OrdinalIgnoreCase) ||
k.Contains("modsecurity", StringComparison.OrdinalIgnoreCase)))
{
controls.Add(new BoundaryControl
{
Type = "waf",
Active = true,
Config = "ingress",
Effectiveness = "high",
VerifiedAt = now
});
}
// Check for input validation
if (annotations.Keys.Any(k =>
k.Contains("validation", StringComparison.OrdinalIgnoreCase)))
{
controls.Add(new BoundaryControl
{
Type = "input_validation",
Active = true,
Effectiveness = "medium",
VerifiedAt = now
});
}
return controls;
}
private static string DetermineKind(BoundaryExposure exposure)
{
return exposure.InternetFacing ? "network" : "network";
}
private static double CalculateConfidence(
BoundaryExposure exposure,
IReadOnlyDictionary<string, string> annotations)
{
// Base confidence from K8s source
var confidence = 0.7;
// Higher confidence if we have explicit ingress annotations
if (annotations.Keys.Any(k => k.Contains("ingress", StringComparison.OrdinalIgnoreCase)))
{
confidence += 0.15;
}
// Higher confidence if we have service type
if (annotations.ContainsKey("service.type"))
{
confidence += 0.1;
}
// Cap at 0.95 - K8s extraction is high confidence but not runtime-verified
return Math.Min(confidence, 0.95);
}
private static string BuildEvidenceRef(BoundaryExtractionContext context, string rootId)
{
var parts = new List<string> { "k8s" };
if (!string.IsNullOrEmpty(context.Namespace))
{
parts.Add(context.Namespace);
}
if (!string.IsNullOrEmpty(context.EnvironmentId))
{
parts.Add(context.EnvironmentId);
}
parts.Add(rootId);
return string.Join("/", parts);
}
}