using System.Collections.Immutable; using System.Globalization; using StellaOps.Scanner.EntryTrace.Semantic; namespace StellaOps.Scanner.EntryTrace.Mesh; /// /// Orchestrator for mesh entrypoint analysis. /// Coordinates manifest parsers with semantic entrypoint analysis /// to produce a complete mesh entrypoint graph. /// /// /// Part of Sprint 0412 - Temporal & Mesh Entrypoint (Task MESH-008). /// public sealed class MeshEntrypointAnalyzer { private readonly IReadOnlyList _parsers; private readonly ISemanticEntrypointAnalyzer? _semanticAnalyzer; /// /// Creates a new mesh entrypoint analyzer with the default parsers. /// public MeshEntrypointAnalyzer() : this([new KubernetesManifestParser(), new DockerComposeParser()], null) { } /// /// Creates a new mesh entrypoint analyzer with custom parsers. /// public MeshEntrypointAnalyzer( IReadOnlyList parsers, ISemanticEntrypointAnalyzer? semanticAnalyzer = null) { _parsers = parsers ?? throw new ArgumentNullException(nameof(parsers)); _semanticAnalyzer = semanticAnalyzer; } /// /// Analyzes a single manifest file. /// public async Task AnalyzeAsync( string manifestPath, string content, MeshAnalysisOptions? options = null, CancellationToken cancellationToken = default) { return await AnalyzeMultipleAsync( new Dictionary { [manifestPath] = content }, options, cancellationToken); } /// /// Analyzes multiple manifest files and produces a combined mesh graph. /// public async Task AnalyzeMultipleAsync( IReadOnlyDictionary manifests, MeshAnalysisOptions? options = null, CancellationToken cancellationToken = default) { options ??= MeshAnalysisOptions.Default; var errors = new List(); var graphs = new List(); // Group manifests by parser type var manifestsByParser = new Dictionary>(); foreach (var (path, content) in manifests) { var parser = FindParser(path, content); if (parser is null) { errors.Add(new MeshAnalysisError { FilePath = path, ErrorCode = "MESH001", Message = "No suitable parser found for manifest format" }); continue; } if (!manifestsByParser.TryGetValue(parser, out var parserManifests)) { parserManifests = []; manifestsByParser[parser] = parserManifests; } parserManifests[path] = content; } // Parse each group foreach (var (parser, parserManifests) in manifestsByParser) { cancellationToken.ThrowIfCancellationRequested(); try { var parseOptions = new ManifestParseOptions { Namespace = options.Namespace, MeshId = options.MeshId, InferEdgesFromEnv = options.InferEdgesFromEnv, IncludeSidecars = options.IncludeSidecars }; var graph = await parser.ParseMultipleAsync( parserManifests, parseOptions, cancellationToken); graphs.Add(graph); } catch (Exception ex) { foreach (var path in parserManifests.Keys) { errors.Add(new MeshAnalysisError { FilePath = path, ErrorCode = "MESH002", Message = $"Failed to parse manifest: {ex.Message}" }); } } } // Merge graphs var mergedGraph = MergeGraphs(graphs, options); // Enrich with semantic analysis if available if (_semanticAnalyzer is not null && options.PerformSemanticAnalysis) { mergedGraph = await EnrichWithSemanticAnalysisAsync( mergedGraph, options, cancellationToken); } // Calculate security metrics var metrics = CalculateSecurityMetrics(mergedGraph); return new MeshAnalysisResult { Graph = mergedGraph, Metrics = metrics, Errors = errors.ToImmutableArray(), AnalyzedAt = DateTime.UtcNow }; } /// /// Finds the most vulnerable paths from ingress to target services. /// public ImmutableArray FindVulnerablePaths( MeshEntrypointGraph graph, string targetServiceId, VulnerablePathCriteria? criteria = null) { criteria ??= VulnerablePathCriteria.Default; var allPaths = graph.FindPathsToService(targetServiceId); // Filter and score paths var scoredPaths = allPaths .Select(path => (Path: path, Score: ScorePath(path, graph, criteria))) .Where(x => x.Score >= criteria.MinimumScore) .OrderByDescending(x => x.Score) .Take(criteria.MaxResults) .Select(x => x.Path); return scoredPaths.ToImmutableArray(); } /// /// Identifies blast radius for a compromised service. /// public BlastRadiusAnalysis AnalyzeBlastRadius( MeshEntrypointGraph graph, string compromisedServiceId) { var directlyReachable = new HashSet(); var transitivelyReachable = new HashSet(); var ingressExposed = new List(); // Find all services reachable from compromised service var toVisit = new Queue<(string ServiceId, int Depth)>(); var visited = new HashSet(); toVisit.Enqueue((compromisedServiceId, 0)); visited.Add(compromisedServiceId); while (toVisit.Count > 0) { var (currentId, depth) = toVisit.Dequeue(); var outboundEdges = graph.Edges .Where(e => e.FromServiceId == currentId); foreach (var edge in outboundEdges) { if (depth == 0) { directlyReachable.Add(edge.ToServiceId); } else { transitivelyReachable.Add(edge.ToServiceId); } if (visited.Add(edge.ToServiceId)) { toVisit.Enqueue((edge.ToServiceId, depth + 1)); } } } // Check if compromised service is ingress-exposed ingressExposed.AddRange( graph.IngressPaths.Where(p => p.TargetServiceId == compromisedServiceId)); // Calculate severity based on reach var severity = CalculateBlastRadiusSeverity( directlyReachable.Count, transitivelyReachable.Count, ingressExposed.Count, graph.Services.Length); return new BlastRadiusAnalysis { CompromisedServiceId = compromisedServiceId, DirectlyReachableServices = directlyReachable.ToImmutableArray(), TransitivelyReachableServices = transitivelyReachable.ToImmutableArray(), IngressExposure = ingressExposed.ToImmutableArray(), TotalReach = directlyReachable.Count + transitivelyReachable.Count, TotalServices = graph.Services.Length, Severity = severity }; } private IManifestParser? FindParser(string path, string content) { foreach (var parser in _parsers) { if (parser.CanParse(path, content)) return parser; } return null; } private MeshEntrypointGraph MergeGraphs( IReadOnlyList graphs, MeshAnalysisOptions options) { if (graphs.Count == 0) { return new MeshEntrypointGraph { MeshId = options.MeshId ?? "empty", Type = MeshType.Kubernetes, Services = ImmutableArray.Empty, Edges = ImmutableArray.Empty, IngressPaths = ImmutableArray.Empty, AnalyzedAt = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture) }; } if (graphs.Count == 1) return graphs[0]; // Merge all graphs var services = new List(); var edges = new List(); var ingressPaths = new List(); foreach (var graph in graphs) { services.AddRange(graph.Services); edges.AddRange(graph.Edges); ingressPaths.AddRange(graph.IngressPaths); } // Deduplicate by ID var uniqueServices = services .GroupBy(s => s.ServiceId) .Select(g => g.First()) .ToImmutableArray(); var uniqueEdges = edges .GroupBy(e => $"{e.FromServiceId}:{e.ToServiceId}:{e.Port}") .Select(g => g.First()) .ToImmutableArray(); var uniqueIngress = ingressPaths .GroupBy(i => $"{i.Host}{i.Path}{i.TargetServiceId}") .Select(g => g.First()) .ToImmutableArray(); return new MeshEntrypointGraph { MeshId = options.MeshId ?? graphs[0].MeshId, Type = graphs[0].Type, Namespace = options.Namespace ?? graphs[0].Namespace, Services = uniqueServices, Edges = uniqueEdges, IngressPaths = uniqueIngress, AnalyzedAt = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture) }; } private async Task EnrichWithSemanticAnalysisAsync( MeshEntrypointGraph graph, MeshAnalysisOptions options, CancellationToken cancellationToken) { if (_semanticAnalyzer is null) return graph; var enrichedServices = new List(); foreach (var service in graph.Services) { cancellationToken.ThrowIfCancellationRequested(); try { var entrypoints = await _semanticAnalyzer.AnalyzeContainerAsync( service.ImageDigest ?? service.ImageReference ?? "", cancellationToken); var enriched = service with { Entrypoints = entrypoints.ToImmutableArray(), VulnerableComponents = await _semanticAnalyzer.GetVulnerableComponentsAsync( service.ImageDigest ?? "", cancellationToken) }; enrichedServices.Add(enriched); } catch { // Keep original service on analysis failure enrichedServices.Add(service); } } return graph with { Services = enrichedServices.ToImmutableArray() }; } private static MeshSecurityMetrics CalculateSecurityMetrics(MeshEntrypointGraph graph) { var totalServices = graph.Services.Length; var totalEdges = graph.Edges.Length; var ingressCount = graph.IngressPaths.Length; // Calculate exposure score var exposedServices = graph.Services .Where(s => graph.IngressPaths.Any(p => p.TargetServiceId == s.ServiceId)) .Count(); var exposureRatio = totalServices > 0 ? (double)exposedServices / totalServices : 0; // Calculate connectivity density var maxEdges = totalServices * (totalServices - 1); var connectivityDensity = maxEdges > 0 ? (double)totalEdges / maxEdges : 0; // Calculate vulnerable service ratio var vulnerableServices = graph.Services .Where(s => s.VulnerableComponents.Length > 0) .Count(); var vulnerableRatio = totalServices > 0 ? (double)vulnerableServices / totalServices : 0; // Calculate critical path count (paths from ingress to vulnerable services) var criticalPathCount = 0; foreach (var vulnerable in graph.Services.Where(s => s.VulnerableComponents.Length > 0)) { var paths = graph.FindPathsToService(vulnerable.ServiceId); criticalPathCount += paths.Length; } // Overall risk score (0-100) var riskScore = CalculateOverallRiskScore( exposureRatio, connectivityDensity, vulnerableRatio, criticalPathCount, totalServices); return new MeshSecurityMetrics { TotalServices = totalServices, TotalEdges = totalEdges, IngressPointCount = ingressCount, ExposedServiceCount = exposedServices, VulnerableServiceCount = vulnerableServices, CriticalPathCount = criticalPathCount, ExposureRatio = exposureRatio, ConnectivityDensity = connectivityDensity, VulnerableRatio = vulnerableRatio, OverallRiskScore = riskScore }; } private static double CalculateOverallRiskScore( double exposureRatio, double connectivityDensity, double vulnerableRatio, int criticalPathCount, int totalServices) { // Weighted scoring var score = 0.0; // Exposure (25% weight) score += exposureRatio * 25; // Vulnerability (30% weight) score += vulnerableRatio * 30; // Connectivity (15% weight) - higher connectivity = more lateral movement risk score += connectivityDensity * 15; // Critical paths (30% weight) - normalized var criticalPathNormalized = totalServices > 0 ? Math.Min(1.0, criticalPathCount / (totalServices * 2.0)) : 0; score += criticalPathNormalized * 30; return Math.Min(100, score); } private static double ScorePath( CrossContainerPath path, MeshEntrypointGraph graph, VulnerablePathCriteria criteria) { var score = 0.0; // Base score for path existence score += 10; // Shorter paths are more critical var lengthFactor = Math.Max(0, criteria.MaxDepth - path.Hops.Length + 1); score += lengthFactor * 5; // Check for vulnerable components along path foreach (var hop in path.Hops) { var service = graph.Services.FirstOrDefault(s => s.ServiceId == hop.ToServiceId); if (service?.VulnerableComponents.Length > 0) { score += 20; } } // External ingress exposure if (path.IsIngressExposed) { score += 25; } return score; } private static BlastRadiusSeverity CalculateBlastRadiusSeverity( int directCount, int transitiveCount, int ingressCount, int totalServices) { if (totalServices == 0) return BlastRadiusSeverity.None; var reachRatio = (double)(directCount + transitiveCount) / totalServices; return (reachRatio, ingressCount) switch { ( >= 0.5, > 0) => BlastRadiusSeverity.Critical, ( >= 0.3, > 0) => BlastRadiusSeverity.High, ( >= 0.3, 0) => BlastRadiusSeverity.Medium, ( >= 0.1, _) => BlastRadiusSeverity.Medium, ( > 0, _) => BlastRadiusSeverity.Low, _ => BlastRadiusSeverity.None }; } } /// /// Options for mesh entrypoint analysis. /// public sealed record MeshAnalysisOptions { public static readonly MeshAnalysisOptions Default = new(); /// /// Optional namespace filter. /// public string? Namespace { get; init; } /// /// Optional mesh identifier. /// public string? MeshId { get; init; } /// /// Whether to infer edges from environment variables. /// public bool InferEdgesFromEnv { get; init; } = true; /// /// Whether to include sidecar containers. /// public bool IncludeSidecars { get; init; } = true; /// /// Whether to perform semantic entrypoint analysis. /// public bool PerformSemanticAnalysis { get; init; } = true; } /// /// Result of mesh entrypoint analysis. /// public sealed record MeshAnalysisResult { /// /// The analyzed mesh graph. /// public required MeshEntrypointGraph Graph { get; init; } /// /// Security metrics for the mesh. /// public required MeshSecurityMetrics Metrics { get; init; } /// /// Errors encountered during analysis. /// public ImmutableArray Errors { get; init; } = ImmutableArray.Empty; /// /// When the analysis was performed. /// public DateTime AnalyzedAt { get; init; } } /// /// Security metrics for a mesh. /// public sealed record MeshSecurityMetrics { public int TotalServices { get; init; } public int TotalEdges { get; init; } public int IngressPointCount { get; init; } public int ExposedServiceCount { get; init; } public int VulnerableServiceCount { get; init; } public int CriticalPathCount { get; init; } public double ExposureRatio { get; init; } public double ConnectivityDensity { get; init; } public double VulnerableRatio { get; init; } public double OverallRiskScore { get; init; } } /// /// Error encountered during mesh analysis. /// public sealed record MeshAnalysisError { public required string FilePath { get; init; } public required string ErrorCode { get; init; } public required string Message { get; init; } public int? Line { get; init; } public int? Column { get; init; } } /// /// Criteria for finding vulnerable paths. /// public sealed record VulnerablePathCriteria { public static readonly VulnerablePathCriteria Default = new(); public int MaxDepth { get; init; } = 5; public int MaxResults { get; init; } = 10; public double MinimumScore { get; init; } = 10; } /// /// Analysis of blast radius for a compromised service. /// public sealed record BlastRadiusAnalysis { public required string CompromisedServiceId { get; init; } public ImmutableArray DirectlyReachableServices { get; init; } public ImmutableArray TransitivelyReachableServices { get; init; } public ImmutableArray IngressExposure { get; init; } public int TotalReach { get; init; } public int TotalServices { get; init; } public BlastRadiusSeverity Severity { get; init; } } /// /// Severity levels for blast radius. /// public enum BlastRadiusSeverity { None = 0, Low = 1, Medium = 2, High = 3, Critical = 4 } /// /// Interface for semantic entrypoint analysis (to be implemented by Semantic module integration). /// public interface ISemanticEntrypointAnalyzer { Task> AnalyzeContainerAsync( string imageReference, CancellationToken cancellationToken = default); Task> GetVulnerableComponentsAsync( string imageDigest, CancellationToken cancellationToken = default); }