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);
}