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