using System.Collections.Immutable; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; namespace StellaOps.Scanner.Reachability; /// /// Semantic attribute keys for richgraph-v1 nodes. /// /// /// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 19). /// These attributes extend RichGraphNode to include semantic analysis data. /// public static class RichGraphSemanticAttributes { /// Application intent (WebServer, Worker, CliTool, etc.). public const string Intent = "semantic_intent"; /// Comma-separated capability flags. public const string Capabilities = "semantic_capabilities"; /// Threat vector types (comma-separated). public const string ThreatVectors = "semantic_threats"; /// Risk score (0.0-1.0). public const string RiskScore = "semantic_risk_score"; /// Confidence score (0.0-1.0). public const string Confidence = "semantic_confidence"; /// Confidence tier (Unknown, Low, Medium, High, Definitive). public const string ConfidenceTier = "semantic_confidence_tier"; /// Framework name. public const string Framework = "semantic_framework"; /// Framework version. public const string FrameworkVersion = "semantic_framework_version"; /// Whether this is an entrypoint node. public const string IsEntrypoint = "is_entrypoint"; /// Data flow boundaries (JSON array). public const string DataBoundaries = "semantic_boundaries"; /// OWASP category if applicable. public const string OwaspCategory = "owasp_category"; /// CWE ID if applicable. public const string CweId = "cwe_id"; // Sprint: SPRINT_20260112_004_SCANNER_reachability_trace_runtime_evidence // Runtime evidence overlay attributes (do not alter lattice precedence) /// Reachability score (0.0-1.0) - computed from path confidence. public const string ReachabilityScore = "reachability_score"; /// Whether this node/edge was confirmed at runtime ("true"/"false"). public const string RuntimeConfirmed = "runtime_confirmed"; /// Number of runtime observations for this node/edge. public const string RuntimeObservationCount = "runtime_observation_count"; /// Timestamp of first runtime observation (ISO 8601). public const string RuntimeFirstObserved = "runtime_first_observed"; /// Timestamp of last runtime observation (ISO 8601). public const string RuntimeLastObserved = "runtime_last_observed"; /// Runtime evidence URI reference. public const string RuntimeEvidenceUri = "runtime_evidence_uri"; /// Runtime confirmation type (confirmed/partial/none). public const string RuntimeConfirmationType = "runtime_confirmation_type"; } /// /// Extension methods for accessing semantic data on RichGraph nodes. /// public static class RichGraphSemanticExtensions { /// Gets the application intent from node attributes. public static string? GetIntent(this RichGraphNode node) { return node.Attributes?.TryGetValue(RichGraphSemanticAttributes.Intent, out var value) == true ? value : null; } /// Gets the capabilities as a list. public static IReadOnlyList GetCapabilities(this RichGraphNode node) { if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.Capabilities, out var value) != true || string.IsNullOrWhiteSpace(value)) { return Array.Empty(); } return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } /// Gets the threat vectors as a list. public static IReadOnlyList GetThreatVectors(this RichGraphNode node) { if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.ThreatVectors, out var value) != true || string.IsNullOrWhiteSpace(value)) { return Array.Empty(); } return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } /// Gets the risk score. public static double? GetRiskScore(this RichGraphNode node) { if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.RiskScore, out var value) != true || string.IsNullOrWhiteSpace(value)) { return null; } return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var score) ? score : null; } /// Gets the confidence score. public static double? GetConfidence(this RichGraphNode node) { if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.Confidence, out var value) != true || string.IsNullOrWhiteSpace(value)) { return null; } return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var score) ? score : null; } /// Checks if this node is an entrypoint. public static bool IsEntrypoint(this RichGraphNode node) { if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.IsEntrypoint, out var value) != true || string.IsNullOrWhiteSpace(value)) { return false; } return bool.TryParse(value, out var result) && result; } /// Checks if node has semantic data. public static bool HasSemanticData(this RichGraphNode node) { return node.Attributes?.ContainsKey(RichGraphSemanticAttributes.Intent) == true || node.Attributes?.ContainsKey(RichGraphSemanticAttributes.Capabilities) == true; } /// Gets the framework name. public static string? GetFramework(this RichGraphNode node) { return node.Attributes?.TryGetValue(RichGraphSemanticAttributes.Framework, out var value) == true ? value : null; } /// Gets all entrypoint nodes from the graph. public static IReadOnlyList GetEntrypointNodes(this RichGraph graph) { return graph.Nodes.Where(n => n.IsEntrypoint()).ToList(); } /// Gets all nodes with semantic data. public static IReadOnlyList GetNodesWithSemantics(this RichGraph graph) { return graph.Nodes.Where(n => n.HasSemanticData()).ToList(); } /// Calculates overall risk score for the graph. public static double CalculateOverallRiskScore(this RichGraph graph) { var riskScores = graph.Nodes .Select(n => n.GetRiskScore()) .Where(s => s.HasValue) .Select(s => s!.Value) .ToList(); if (riskScores.Count == 0) return 0.0; // Use max risk score as overall return riskScores.Max(); } // Sprint: SPRINT_20260112_004_SCANNER_reachability_trace_runtime_evidence // Extension methods for runtime evidence overlay attributes /// Gets the reachability score (0.0-1.0). public static double? GetReachabilityScore(this RichGraphNode node) { if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.ReachabilityScore, out var value) != true || string.IsNullOrWhiteSpace(value)) { return null; } return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var score) ? score : null; } /// Gets whether this node was confirmed at runtime. public static bool? GetRuntimeConfirmed(this RichGraphNode node) { if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.RuntimeConfirmed, out var value) != true || string.IsNullOrWhiteSpace(value)) { return null; } return bool.TryParse(value, out var result) ? result : null; } /// Gets the runtime observation count. public static ulong? GetRuntimeObservationCount(this RichGraphNode node) { if (node.Attributes?.TryGetValue(RichGraphSemanticAttributes.RuntimeObservationCount, out var value) != true || string.IsNullOrWhiteSpace(value)) { return null; } return ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count) ? count : null; } /// Gets the runtime confirmation type (confirmed/partial/none). public static string? GetRuntimeConfirmationType(this RichGraphNode node) { return node.Attributes?.TryGetValue(RichGraphSemanticAttributes.RuntimeConfirmationType, out var value) == true ? value : null; } /// Gets the runtime evidence URI. public static string? GetRuntimeEvidenceUri(this RichGraphNode node) { return node.Attributes?.TryGetValue(RichGraphSemanticAttributes.RuntimeEvidenceUri, out var value) == true ? value : null; } /// Gets nodes with runtime confirmation. public static IReadOnlyList GetRuntimeConfirmedNodes(this RichGraph graph) { return graph.Nodes.Where(n => n.GetRuntimeConfirmed() == true).ToList(); } /// Calculates the graph-level runtime coverage percentage. public static double CalculateRuntimeCoverage(this RichGraph graph) { if (graph.Nodes.Count == 0) return 0.0; var confirmedCount = graph.Nodes.Count(n => n.GetRuntimeConfirmed() == true); return (double)confirmedCount / graph.Nodes.Count * 100.0; } /// Gets the average reachability score for the graph. public static double? CalculateAverageReachabilityScore(this RichGraph graph) { var scores = graph.Nodes .Select(n => n.GetReachabilityScore()) .Where(s => s.HasValue) .Select(s => s!.Value) .ToList(); if (scores.Count == 0) return null; return scores.Average(); } } /// /// Builder for creating RichGraphNode with semantic attributes. /// public sealed class RichGraphNodeSemanticBuilder { private readonly Dictionary _attributes = new(StringComparer.Ordinal); public RichGraphNodeSemanticBuilder WithIntent(string intent) { _attributes[RichGraphSemanticAttributes.Intent] = intent; return this; } public RichGraphNodeSemanticBuilder WithCapabilities(IEnumerable capabilities) { _attributes[RichGraphSemanticAttributes.Capabilities] = string.Join(",", capabilities); return this; } public RichGraphNodeSemanticBuilder WithThreatVectors(IEnumerable threats) { _attributes[RichGraphSemanticAttributes.ThreatVectors] = string.Join(",", threats); return this; } public RichGraphNodeSemanticBuilder WithRiskScore(double score) { _attributes[RichGraphSemanticAttributes.RiskScore] = score.ToString("F3", CultureInfo.InvariantCulture); return this; } public RichGraphNodeSemanticBuilder WithConfidence(double score, string tier) { _attributes[RichGraphSemanticAttributes.Confidence] = score.ToString("F3", CultureInfo.InvariantCulture); _attributes[RichGraphSemanticAttributes.ConfidenceTier] = tier; return this; } public RichGraphNodeSemanticBuilder WithFramework(string framework, string? version = null) { _attributes[RichGraphSemanticAttributes.Framework] = framework; if (version is not null) { _attributes[RichGraphSemanticAttributes.FrameworkVersion] = version; } return this; } public RichGraphNodeSemanticBuilder AsEntrypoint() { _attributes[RichGraphSemanticAttributes.IsEntrypoint] = "true"; return this; } public RichGraphNodeSemanticBuilder WithOwaspCategory(string category) { _attributes[RichGraphSemanticAttributes.OwaspCategory] = category; return this; } public RichGraphNodeSemanticBuilder WithCweId(int cweId) { _attributes[RichGraphSemanticAttributes.CweId] = cweId.ToString(CultureInfo.InvariantCulture); return this; } // Sprint: SPRINT_20260112_004_SCANNER_reachability_trace_runtime_evidence // Builder methods for runtime evidence overlay attributes /// Sets the reachability score (0.0-1.0). public RichGraphNodeSemanticBuilder WithReachabilityScore(double score) { _attributes[RichGraphSemanticAttributes.ReachabilityScore] = Math.Clamp(score, 0.0, 1.0).ToString("F3", CultureInfo.InvariantCulture); return this; } /// Sets the runtime confirmed flag. public RichGraphNodeSemanticBuilder WithRuntimeConfirmed(bool confirmed) { _attributes[RichGraphSemanticAttributes.RuntimeConfirmed] = confirmed.ToString().ToLowerInvariant(); return this; } /// Sets the runtime observation count. public RichGraphNodeSemanticBuilder WithRuntimeObservationCount(ulong count) { _attributes[RichGraphSemanticAttributes.RuntimeObservationCount] = count.ToString(CultureInfo.InvariantCulture); return this; } /// Sets the runtime observation timestamps. public RichGraphNodeSemanticBuilder WithRuntimeObservationTimes(DateTimeOffset firstObserved, DateTimeOffset lastObserved) { _attributes[RichGraphSemanticAttributes.RuntimeFirstObserved] = firstObserved.ToString("O", CultureInfo.InvariantCulture); _attributes[RichGraphSemanticAttributes.RuntimeLastObserved] = lastObserved.ToString("O", CultureInfo.InvariantCulture); return this; } /// Sets the runtime evidence URI. public RichGraphNodeSemanticBuilder WithRuntimeEvidenceUri(string uri) { _attributes[RichGraphSemanticAttributes.RuntimeEvidenceUri] = uri; return this; } /// Sets the runtime confirmation type (confirmed/partial/none). public RichGraphNodeSemanticBuilder WithRuntimeConfirmationType(string confirmationType) { _attributes[RichGraphSemanticAttributes.RuntimeConfirmationType] = confirmationType; return this; } /// Builds the attributes dictionary. public IReadOnlyDictionary Build() { return _attributes.ToImmutableDictionary(); } /// Merges semantic attributes with existing node attributes. public IReadOnlyDictionary MergeWith(IReadOnlyDictionary? existing) { var merged = new Dictionary(StringComparer.Ordinal); if (existing is not null) { foreach (var pair in existing) { merged[pair.Key] = pair.Value; } } foreach (var pair in _attributes) { merged[pair.Key] = pair.Value; } return merged.ToImmutableDictionary(); } /// Creates a new RichGraphNode with semantic attributes. public RichGraphNode ApplyTo(RichGraphNode node) { return node with { Attributes = MergeWith(node.Attributes) }; } }