feat(telemetry): add telemetry client and services for tracking events
- Implemented TelemetryClient to handle event queuing and flushing to the telemetry endpoint. - Created TtfsTelemetryService for emitting specific telemetry events related to TTFS. - Added tests for TelemetryClient to ensure event queuing and flushing functionality. - Introduced models for reachability drift detection, including DriftResult and DriftedSink. - Developed DriftApiService for interacting with the drift detection API. - Updated FirstSignalCardComponent to emit telemetry events on signal appearance. - Enhanced localization support for first signal component with i18n strings.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BoundaryExtractionContext.cs
|
||||
// Sprint: SPRINT_3800_0002_0001_boundary_richgraph
|
||||
// Description: Context for boundary extraction with environment hints.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Boundary;
|
||||
|
||||
/// <summary>
|
||||
/// Context for boundary extraction, providing environment hints and detected gates.
|
||||
/// </summary>
|
||||
public sealed record BoundaryExtractionContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Empty context for simple extractions.
|
||||
/// </summary>
|
||||
public static readonly BoundaryExtractionContext Empty = new();
|
||||
|
||||
/// <summary>
|
||||
/// Environment identifier (e.g., "production", "staging").
|
||||
/// </summary>
|
||||
public string? EnvironmentId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Deployment namespace or context (e.g., "default", "kube-system").
|
||||
/// </summary>
|
||||
public string? Namespace { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional annotations from deployment metadata.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> Annotations { get; init; } =
|
||||
new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gates detected by gate detection analysis.
|
||||
/// </summary>
|
||||
public IReadOnlyList<DetectedGate> DetectedGates { get; init; } =
|
||||
Array.Empty<DetectedGate>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the service is known to be internet-facing.
|
||||
/// </summary>
|
||||
public bool? IsInternetFacing { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Network zone (e.g., "dmz", "internal", "trusted").
|
||||
/// </summary>
|
||||
public string? NetworkZone { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Known port bindings (port → protocol).
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<int, string> PortBindings { get; init; } =
|
||||
new Dictionary<int, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp for the context (for cache invalidation).
|
||||
/// </summary>
|
||||
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Source of this context (e.g., "k8s", "iac", "runtime").
|
||||
/// </summary>
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a context from detected gates.
|
||||
/// </summary>
|
||||
public static BoundaryExtractionContext FromGates(IReadOnlyList<DetectedGate> gates) =>
|
||||
new() { DetectedGates = gates };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a context with environment hints.
|
||||
/// </summary>
|
||||
public static BoundaryExtractionContext ForEnvironment(
|
||||
string environmentId,
|
||||
bool? isInternetFacing = null,
|
||||
string? networkZone = null) =>
|
||||
new()
|
||||
{
|
||||
EnvironmentId = environmentId,
|
||||
IsInternetFacing = isInternetFacing,
|
||||
NetworkZone = networkZone
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BoundaryServiceCollectionExtensions.cs
|
||||
// Sprint: SPRINT_3800_0002_0001_boundary_richgraph
|
||||
// Description: DI registration for boundary proof extractors.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Boundary;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering boundary proof extractors.
|
||||
/// </summary>
|
||||
public static class BoundaryServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds boundary proof extraction services.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddBoundaryExtractors(this IServiceCollection services)
|
||||
{
|
||||
// Register base extractor
|
||||
services.TryAddSingleton<RichGraphBoundaryExtractor>();
|
||||
services.TryAddSingleton<IBoundaryProofExtractor, RichGraphBoundaryExtractor>();
|
||||
|
||||
// Register composite extractor that uses all available extractors
|
||||
services.TryAddSingleton<CompositeBoundaryExtractor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom boundary proof extractor.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddBoundaryExtractor<TExtractor>(this IServiceCollection services)
|
||||
where TExtractor : class, IBoundaryProofExtractor
|
||||
{
|
||||
services.AddSingleton<IBoundaryProofExtractor, TExtractor>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CompositeBoundaryExtractor.cs
|
||||
// Sprint: SPRINT_3800_0002_0001_boundary_richgraph
|
||||
// Description: Composite extractor that aggregates results from multiple extractors.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
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>
|
||||
/// Composite boundary extractor that selects the best result from multiple extractors.
|
||||
/// Extractors are sorted by priority and the first successful extraction is used.
|
||||
/// </summary>
|
||||
public sealed class CompositeBoundaryExtractor : IBoundaryProofExtractor
|
||||
{
|
||||
private readonly IEnumerable<IBoundaryProofExtractor> _extractors;
|
||||
private readonly ILogger<CompositeBoundaryExtractor> _logger;
|
||||
|
||||
public CompositeBoundaryExtractor(
|
||||
IEnumerable<IBoundaryProofExtractor> extractors,
|
||||
ILogger<CompositeBoundaryExtractor> logger)
|
||||
{
|
||||
_extractors = extractors ?? throw new ArgumentNullException(nameof(extractors));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority => int.MaxValue; // Composite has highest priority
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(BoundaryExtractionContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BoundaryProof?> ExtractAsync(
|
||||
RichGraphRoot root,
|
||||
RichGraphNode? rootNode,
|
||||
BoundaryExtractionContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sortedExtractors = _extractors
|
||||
.Where(e => e != this) // Avoid recursion
|
||||
.Where(e => e.CanHandle(context))
|
||||
.OrderByDescending(e => e.Priority)
|
||||
.ToList();
|
||||
|
||||
if (sortedExtractors.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("No extractors available for context {Source}", context.Source);
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var extractor in sortedExtractors)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var result = await extractor.ExtractAsync(root, rootNode, context, cancellationToken);
|
||||
if (result is not null)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Boundary extracted by {Extractor} with confidence {Confidence:F2}",
|
||||
extractor.GetType().Name,
|
||||
result.Confidence);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Extractor {Extractor} failed", extractor.GetType().Name);
|
||||
// Continue to next extractor
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public BoundaryProof? Extract(
|
||||
RichGraphRoot root,
|
||||
RichGraphNode? rootNode,
|
||||
BoundaryExtractionContext context)
|
||||
{
|
||||
var sortedExtractors = _extractors
|
||||
.Where(e => e != this)
|
||||
.Where(e => e.CanHandle(context))
|
||||
.OrderByDescending(e => e.Priority)
|
||||
.ToList();
|
||||
|
||||
foreach (var extractor in sortedExtractors)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = extractor.Extract(root, rootNode, context);
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Extractor {Extractor} failed", extractor.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IBoundaryProofExtractor.cs
|
||||
// Sprint: SPRINT_3800_0002_0001_boundary_richgraph
|
||||
// Description: Interface for extracting boundary proofs from various sources.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.SmartDiff.Detection;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Boundary;
|
||||
|
||||
/// <summary>
|
||||
/// Extracts boundary proof (exposure, auth, controls) from reachability data.
|
||||
/// </summary>
|
||||
public interface IBoundaryProofExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts boundary proof for a RichGraph root/entrypoint.
|
||||
/// </summary>
|
||||
/// <param name="root">The RichGraph root representing the entrypoint.</param>
|
||||
/// <param name="rootNode">Optional root node with additional metadata.</param>
|
||||
/// <param name="context">Extraction context with environment hints.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Boundary proof if extractable; otherwise null.</returns>
|
||||
Task<BoundaryProof?> ExtractAsync(
|
||||
RichGraphRoot root,
|
||||
RichGraphNode? rootNode,
|
||||
BoundaryExtractionContext context,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Synchronous extraction for contexts where async is not needed.
|
||||
/// </summary>
|
||||
BoundaryProof? Extract(
|
||||
RichGraphRoot root,
|
||||
RichGraphNode? rootNode,
|
||||
BoundaryExtractionContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority of this extractor (higher = preferred).
|
||||
/// </summary>
|
||||
int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this extractor can handle the given context.
|
||||
/// </summary>
|
||||
bool CanHandle(BoundaryExtractionContext context);
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// RichGraphBoundaryExtractor.cs
|
||||
// Sprint: SPRINT_3800_0002_0001_boundary_richgraph
|
||||
// Description: Extracts boundary proof from RichGraph roots and node annotations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
using StellaOps.Scanner.SmartDiff.Detection;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Boundary;
|
||||
|
||||
/// <summary>
|
||||
/// Extracts boundary proof from RichGraph roots and node annotations.
|
||||
/// This is the base extractor that infers exposure from static analysis data.
|
||||
/// </summary>
|
||||
public sealed class RichGraphBoundaryExtractor : IBoundaryProofExtractor
|
||||
{
|
||||
private readonly ILogger<RichGraphBoundaryExtractor> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RichGraphBoundaryExtractor(
|
||||
ILogger<RichGraphBoundaryExtractor> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority => 100; // Base extractor, lowest priority
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(BoundaryExtractionContext context) => true; // Always handles as fallback
|
||||
|
||||
/// <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);
|
||||
|
||||
try
|
||||
{
|
||||
var surface = InferSurface(root, rootNode);
|
||||
var exposure = InferExposure(root, rootNode, context);
|
||||
var auth = InferAuth(context.DetectedGates, rootNode);
|
||||
var controls = InferControls(context.DetectedGates);
|
||||
var confidence = CalculateConfidence(surface, exposure, context);
|
||||
|
||||
return new BoundaryProof
|
||||
{
|
||||
Kind = InferBoundaryKind(surface),
|
||||
Surface = surface,
|
||||
Exposure = exposure,
|
||||
Auth = auth,
|
||||
Controls = controls.Count > 0 ? controls : null,
|
||||
LastSeen = _timeProvider.GetUtcNow(),
|
||||
Confidence = confidence,
|
||||
Source = "static_analysis",
|
||||
EvidenceRef = root.Id
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to extract boundary proof for root {RootId}", root.Id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BoundarySurface InferSurface(RichGraphRoot root, RichGraphNode? rootNode)
|
||||
{
|
||||
var (surfaceType, protocol) = InferSurfaceTypeAndProtocol(root, rootNode);
|
||||
var port = InferPort(rootNode, protocol);
|
||||
var path = InferPath(rootNode);
|
||||
|
||||
return new BoundarySurface
|
||||
{
|
||||
Type = surfaceType,
|
||||
Protocol = protocol,
|
||||
Port = port,
|
||||
Path = path
|
||||
};
|
||||
}
|
||||
|
||||
private (string type, string? protocol) InferSurfaceTypeAndProtocol(RichGraphRoot root, RichGraphNode? rootNode)
|
||||
{
|
||||
var nodeKind = rootNode?.Kind?.ToLowerInvariant() ?? "";
|
||||
var display = rootNode?.Display?.ToLowerInvariant() ?? "";
|
||||
var phase = root.Phase?.ToLowerInvariant() ?? "runtime";
|
||||
|
||||
// HTTP/HTTPS detection
|
||||
if (ContainsAny(nodeKind, display, "http", "rest", "api", "web", "controller", "endpoint"))
|
||||
{
|
||||
return ("api", "https");
|
||||
}
|
||||
|
||||
// gRPC detection
|
||||
if (ContainsAny(nodeKind, display, "grpc", "protobuf", "proto"))
|
||||
{
|
||||
return ("api", "grpc");
|
||||
}
|
||||
|
||||
// GraphQL detection
|
||||
if (ContainsAny(nodeKind, display, "graphql", "gql", "query", "mutation"))
|
||||
{
|
||||
return ("api", "https");
|
||||
}
|
||||
|
||||
// WebSocket detection
|
||||
if (ContainsAny(nodeKind, display, "websocket", "ws", "socket"))
|
||||
{
|
||||
return ("socket", "wss");
|
||||
}
|
||||
|
||||
// CLI detection
|
||||
if (ContainsAny(nodeKind, display, "cli", "command", "console", "main"))
|
||||
{
|
||||
return ("cli", null);
|
||||
}
|
||||
|
||||
// Scheduled/background detection
|
||||
if (ContainsAny(nodeKind, display, "scheduled", "cron", "timer", "background", "worker"))
|
||||
{
|
||||
return ("scheduled", null);
|
||||
}
|
||||
|
||||
// Library detection
|
||||
if (phase == "library" || ContainsAny(nodeKind, display, "library", "lib", "internal"))
|
||||
{
|
||||
return ("library", null);
|
||||
}
|
||||
|
||||
// Default to API for runtime phase
|
||||
return phase == "runtime" ? ("api", "https") : ("library", null);
|
||||
}
|
||||
|
||||
private static int? InferPort(RichGraphNode? rootNode, string? protocol)
|
||||
{
|
||||
// Try to get port from node attributes
|
||||
if (rootNode?.Attributes?.TryGetValue("port", out var portStr) == true &&
|
||||
int.TryParse(portStr, out var port))
|
||||
{
|
||||
return port;
|
||||
}
|
||||
|
||||
// Default ports by protocol
|
||||
return protocol?.ToLowerInvariant() switch
|
||||
{
|
||||
"https" => 443,
|
||||
"http" => 80,
|
||||
"grpc" => 443,
|
||||
"wss" => 443,
|
||||
"ws" => 80,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static string? InferPath(RichGraphNode? rootNode)
|
||||
{
|
||||
// Try to get route from node attributes
|
||||
if (rootNode?.Attributes?.TryGetValue("route", out var route) == true)
|
||||
{
|
||||
return route;
|
||||
}
|
||||
|
||||
if (rootNode?.Attributes?.TryGetValue("path", out var path) == true)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private BoundaryExposure InferExposure(
|
||||
RichGraphRoot root,
|
||||
RichGraphNode? rootNode,
|
||||
BoundaryExtractionContext context)
|
||||
{
|
||||
// Use context hints if available
|
||||
var isInternetFacing = context.IsInternetFacing ?? InferInternetFacing(rootNode);
|
||||
var level = InferExposureLevel(rootNode, isInternetFacing);
|
||||
var zone = context.NetworkZone ?? InferNetworkZone(isInternetFacing, level);
|
||||
|
||||
return new BoundaryExposure
|
||||
{
|
||||
Level = level,
|
||||
InternetFacing = isInternetFacing,
|
||||
Zone = zone
|
||||
};
|
||||
}
|
||||
|
||||
private static bool InferInternetFacing(RichGraphNode? rootNode)
|
||||
{
|
||||
if (rootNode?.Attributes?.TryGetValue("internet_facing", out var value) == true)
|
||||
{
|
||||
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Assume public APIs are internet-facing unless specified otherwise
|
||||
var kind = rootNode?.Kind?.ToLowerInvariant() ?? "";
|
||||
return kind.Contains("public") || kind.Contains("external");
|
||||
}
|
||||
|
||||
private static string InferExposureLevel(RichGraphNode? rootNode, bool isInternetFacing)
|
||||
{
|
||||
var kind = rootNode?.Kind?.ToLowerInvariant() ?? "";
|
||||
|
||||
if (kind.Contains("public") || isInternetFacing)
|
||||
return "public";
|
||||
if (kind.Contains("internal"))
|
||||
return "internal";
|
||||
if (kind.Contains("private") || kind.Contains("localhost"))
|
||||
return "private";
|
||||
|
||||
// Default to internal for most services
|
||||
return isInternetFacing ? "public" : "internal";
|
||||
}
|
||||
|
||||
private static string InferNetworkZone(bool isInternetFacing, string level)
|
||||
{
|
||||
if (isInternetFacing || level == "public")
|
||||
return "dmz";
|
||||
if (level == "internal")
|
||||
return "internal";
|
||||
return "trusted";
|
||||
}
|
||||
|
||||
private static BoundaryAuth? InferAuth(IReadOnlyList<DetectedGate>? gates, RichGraphNode? rootNode)
|
||||
{
|
||||
var authGates = gates?.Where(g =>
|
||||
g.Type == GateType.AuthRequired || g.Type == GateType.AdminOnly).ToList();
|
||||
|
||||
if (authGates is not { Count: > 0 })
|
||||
{
|
||||
// Check node attributes for auth hints
|
||||
if (rootNode?.Attributes?.TryGetValue("auth", out var authAttr) == true)
|
||||
{
|
||||
var required = !string.Equals(authAttr, "none", StringComparison.OrdinalIgnoreCase);
|
||||
return new BoundaryAuth
|
||||
{
|
||||
Required = required,
|
||||
Type = required ? authAttr : null
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var hasAdminGate = authGates.Any(g => g.Type == GateType.AdminOnly);
|
||||
var roles = hasAdminGate ? new[] { "admin" } : null;
|
||||
|
||||
return new BoundaryAuth
|
||||
{
|
||||
Required = true,
|
||||
Type = InferAuthType(authGates),
|
||||
Roles = roles
|
||||
};
|
||||
}
|
||||
|
||||
private static string? InferAuthType(IReadOnlyList<DetectedGate> authGates)
|
||||
{
|
||||
var details = authGates
|
||||
.Select(g => g.Detail.ToLowerInvariant())
|
||||
.ToList();
|
||||
|
||||
if (details.Any(d => d.Contains("jwt")))
|
||||
return "jwt";
|
||||
if (details.Any(d => d.Contains("oauth")))
|
||||
return "oauth2";
|
||||
if (details.Any(d => d.Contains("api_key") || d.Contains("apikey")))
|
||||
return "api_key";
|
||||
if (details.Any(d => d.Contains("basic")))
|
||||
return "basic";
|
||||
if (details.Any(d => d.Contains("session")))
|
||||
return "session";
|
||||
|
||||
return "required";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<BoundaryControl> InferControls(IReadOnlyList<DetectedGate>? gates)
|
||||
{
|
||||
var controls = new List<BoundaryControl>();
|
||||
|
||||
if (gates is null)
|
||||
return controls;
|
||||
|
||||
foreach (var gate in gates)
|
||||
{
|
||||
var control = gate.Type switch
|
||||
{
|
||||
GateType.FeatureFlag => new BoundaryControl
|
||||
{
|
||||
Type = "feature_flag",
|
||||
Active = true,
|
||||
Config = gate.Detail,
|
||||
Effectiveness = "high"
|
||||
},
|
||||
GateType.NonDefaultConfig => new BoundaryControl
|
||||
{
|
||||
Type = "config_gate",
|
||||
Active = true,
|
||||
Config = gate.Detail,
|
||||
Effectiveness = "medium"
|
||||
},
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (control is not null)
|
||||
{
|
||||
controls.Add(control);
|
||||
}
|
||||
}
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
private static string InferBoundaryKind(BoundarySurface surface)
|
||||
{
|
||||
return surface.Type switch
|
||||
{
|
||||
"api" => "network",
|
||||
"socket" => "network",
|
||||
"cli" => "process",
|
||||
"scheduled" => "process",
|
||||
"library" => "library",
|
||||
"file" => "file",
|
||||
_ => "network"
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateConfidence(
|
||||
BoundarySurface surface,
|
||||
BoundaryExposure exposure,
|
||||
BoundaryExtractionContext context)
|
||||
{
|
||||
var baseConfidence = 0.6; // Base confidence for static analysis
|
||||
|
||||
// Increase confidence if we have context hints
|
||||
if (context.IsInternetFacing.HasValue)
|
||||
baseConfidence += 0.1;
|
||||
|
||||
if (!string.IsNullOrEmpty(context.NetworkZone))
|
||||
baseConfidence += 0.1;
|
||||
|
||||
if (context.DetectedGates is { Count: > 0 })
|
||||
baseConfidence += 0.1;
|
||||
|
||||
// Lower confidence for inferred values
|
||||
if (string.IsNullOrEmpty(surface.Protocol))
|
||||
baseConfidence -= 0.1;
|
||||
|
||||
return Math.Clamp(baseConfidence, 0.1, 0.95);
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string primary, string secondary, params string[] terms)
|
||||
{
|
||||
foreach (var term in terms)
|
||||
{
|
||||
if (primary.Contains(term, StringComparison.OrdinalIgnoreCase) ||
|
||||
secondary.Contains(term, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user