release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -1,86 +1,4 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for <see cref="ReachabilityAnalyzer"/>.
|
||||
/// Defines limits and ordering rules for deterministic path output.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sprint: SPRINT_3700_0001_0001 (WIT-007A, WIT-007B)
|
||||
/// Contract: ReachabilityAnalyzer → PathWitnessBuilder output contract
|
||||
///
|
||||
/// Determinism guarantees:
|
||||
/// - Paths are ordered by (SinkId ASC, EntrypointId ASC, PathLength ASC)
|
||||
/// - Node IDs within paths are ordered from entrypoint to sink (caller → callee)
|
||||
/// - Maximum caps prevent unbounded output
|
||||
/// </remarks>
|
||||
public sealed record ReachabilityAnalysisOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options with sensible limits.
|
||||
/// </summary>
|
||||
public static ReachabilityAnalysisOptions Default { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Maximum depth for BFS traversal (0 = unlimited, default = 256).
|
||||
/// Prevents infinite loops in cyclic graphs.
|
||||
/// </summary>
|
||||
public int MaxDepth { get; init; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of paths to return per sink (default = 10).
|
||||
/// Limits witness explosion when many entrypoints reach the same sink.
|
||||
/// </summary>
|
||||
public int MaxPathsPerSink { get; init; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum total paths to return (default = 100).
|
||||
/// Hard cap to prevent memory issues with highly connected graphs.
|
||||
/// </summary>
|
||||
public int MaxTotalPaths { get; init; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include node metadata in path reconstruction (default = true).
|
||||
/// When false, paths only contain node IDs without additional context.
|
||||
/// </summary>
|
||||
public bool IncludeNodeMetadata { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Explicit list of sink node IDs to target (default = null, meaning use snapshot.SinkIds).
|
||||
/// When set, analysis will only find paths to these specific sinks.
|
||||
/// This enables targeted witness generation for specific vulnerabilities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sprint: SPRINT_3700_0001_0001 (WIT-007B)
|
||||
/// Enables: PathWitnessBuilder can request paths to specific trigger methods.
|
||||
/// </remarks>
|
||||
public ImmutableArray<string>? ExplicitSinks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates options and returns sanitized values.
|
||||
/// </summary>
|
||||
public ReachabilityAnalysisOptions Validated()
|
||||
{
|
||||
// Normalize explicit sinks: trim, dedupe, order
|
||||
ImmutableArray<string>? normalizedSinks = null;
|
||||
if (ExplicitSinks.HasValue && !ExplicitSinks.Value.IsDefaultOrEmpty)
|
||||
{
|
||||
normalizedSinks = ExplicitSinks.Value
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Select(s => s.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(s => s, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
return new ReachabilityAnalysisOptions
|
||||
{
|
||||
MaxDepth = MaxDepth <= 0 ? 256 : Math.Min(MaxDepth, 1024),
|
||||
MaxPathsPerSink = MaxPathsPerSink <= 0 ? 10 : Math.Min(MaxPathsPerSink, 100),
|
||||
MaxTotalPaths = MaxTotalPaths <= 0 ? 100 : Math.Min(MaxTotalPaths, 1000),
|
||||
IncludeNodeMetadata = IncludeNodeMetadata,
|
||||
ExplicitSinks = normalizedSinks
|
||||
};
|
||||
}
|
||||
}
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
// ReachabilityAnalysisOptions is now defined in StellaOps.Scanner.Contracts.
|
||||
// This file exists only for file system tracking - the type is imported via global using.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.CallGraph.Binary.Analysis;
|
||||
using StellaOps.Scanner.CallGraph.Binary.Disassembly;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Binary;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Classifies binary symbols as entrypoints based on naming patterns.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Binary;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Bun;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Classifies Bun functions as entrypoints based on framework patterns.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Bun;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Matches Bun/JS function calls to security sink categories.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Bun;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Deno;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Classifies Deno functions as entrypoints based on framework patterns.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Deno;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Matches Deno function calls to security sink categories.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Deno;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.MSBuild;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.DotNet;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Go;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Classifies Go functions as entrypoints based on framework patterns.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Go;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Matches Go function calls to known security sinks.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Go;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Compression;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Java;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Classifies Java methods as entrypoints based on framework annotations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Java;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Matches Java method calls to known security sinks.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Java;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.JavaScript;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Classifies JavaScript/TypeScript functions as entrypoints.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.JavaScript;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Matches JavaScript/TypeScript function calls to known security sinks.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.JavaScript;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Node;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Php;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Classifies PHP functions as entrypoints based on framework patterns.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Php;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Matches PHP function calls to known security sinks.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Php;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Python;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Classifies Python functions as entrypoints based on framework patterns.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Python;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Matches Python function calls to known security sinks.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Python;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Ruby;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Classifies Ruby methods as entrypoints based on framework patterns.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Ruby;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Description: Matches Ruby method calls to known security sinks.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph.Ruby;
|
||||
|
||||
|
||||
@@ -1,516 +1,13 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scanner.CallGraph.Serialization;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
// Re-exports types from Contracts for backward compatibility.
|
||||
|
||||
namespace StellaOps.Scanner.CallGraph;
|
||||
// All call graph models are now defined in StellaOps.Scanner.Contracts.
|
||||
// This file provides type aliases for backward compatibility with code
|
||||
// that uses the StellaOps.Scanner.CallGraph namespace.
|
||||
|
||||
public sealed record CallGraphSnapshot(
|
||||
[property: JsonPropertyName("scanId")] string ScanId,
|
||||
[property: JsonPropertyName("graphDigest")] string GraphDigest,
|
||||
[property: JsonPropertyName("language")] string Language,
|
||||
[property: JsonPropertyName("extractedAt")] DateTimeOffset ExtractedAt,
|
||||
[property: JsonPropertyName("nodes")]
|
||||
[property: JsonConverter(typeof(ImmutableArrayJsonConverter<CallGraphNode>))]
|
||||
ImmutableArray<CallGraphNode> Nodes,
|
||||
[property: JsonPropertyName("edges")]
|
||||
[property: JsonConverter(typeof(ImmutableArrayJsonConverter<CallGraphEdge>))]
|
||||
ImmutableArray<CallGraphEdge> Edges,
|
||||
[property: JsonPropertyName("entrypointIds")]
|
||||
[property: JsonConverter(typeof(ImmutableArrayJsonConverter<string>))]
|
||||
ImmutableArray<string> EntrypointIds,
|
||||
[property: JsonPropertyName("sinkIds")]
|
||||
[property: JsonConverter(typeof(ImmutableArrayJsonConverter<string>))]
|
||||
ImmutableArray<string> SinkIds)
|
||||
{
|
||||
public CallGraphSnapshot Trimmed()
|
||||
{
|
||||
var nodes = (Nodes.IsDefault ? ImmutableArray<CallGraphNode>.Empty : Nodes)
|
||||
.Where(n => !string.IsNullOrWhiteSpace(n.NodeId))
|
||||
.Select(n => n.Trimmed())
|
||||
.OrderBy(n => n.NodeId, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
global using StellaOps.Scanner.Contracts;
|
||||
|
||||
var edges = (Edges.IsDefault ? ImmutableArray<CallGraphEdge>.Empty : Edges)
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e.SourceId) && !string.IsNullOrWhiteSpace(e.TargetId))
|
||||
.Select(e => e.Trimmed())
|
||||
.OrderBy(e => e.SourceId, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.TargetId, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.CallKind.ToString(), StringComparer.Ordinal)
|
||||
.ThenBy(e => e.CallSite ?? string.Empty, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var entrypoints = (EntrypointIds.IsDefault ? ImmutableArray<string>.Empty : EntrypointIds)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var sinks = (SinkIds.IsDefault ? ImmutableArray<string>.Empty : SinkIds)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return this with
|
||||
{
|
||||
ScanId = ScanId?.Trim() ?? string.Empty,
|
||||
GraphDigest = GraphDigest?.Trim() ?? string.Empty,
|
||||
Language = Language?.Trim() ?? string.Empty,
|
||||
Nodes = nodes,
|
||||
Edges = edges,
|
||||
EntrypointIds = entrypoints,
|
||||
SinkIds = sinks
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CallGraphNode(
|
||||
[property: JsonPropertyName("nodeId")] string NodeId,
|
||||
[property: JsonPropertyName("symbol")] string Symbol,
|
||||
[property: JsonPropertyName("file")] string File,
|
||||
[property: JsonPropertyName("line")] int Line,
|
||||
[property: JsonPropertyName("package")] string Package,
|
||||
[property: JsonPropertyName("visibility")] Visibility Visibility,
|
||||
[property: JsonPropertyName("isEntrypoint")] bool IsEntrypoint,
|
||||
[property: JsonPropertyName("entrypointType")] EntrypointType? EntrypointType,
|
||||
[property: JsonPropertyName("isSink")] bool IsSink,
|
||||
[property: JsonPropertyName("sinkCategory")] SinkCategory? SinkCategory)
|
||||
{
|
||||
public CallGraphNode Trimmed()
|
||||
=> this with
|
||||
{
|
||||
NodeId = NodeId?.Trim() ?? string.Empty,
|
||||
Symbol = Symbol?.Trim() ?? string.Empty,
|
||||
File = File?.Trim() ?? string.Empty,
|
||||
Package = Package?.Trim() ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record CallGraphEdge(
|
||||
[property: JsonPropertyName("sourceId")] string SourceId,
|
||||
[property: JsonPropertyName("targetId")] string TargetId,
|
||||
[property: JsonPropertyName("callKind")] CallKind CallKind,
|
||||
[property: JsonPropertyName("callSite")] string? CallSite = null,
|
||||
[property: JsonPropertyName("explanation")] CallEdgeExplanation? Explanation = null)
|
||||
{
|
||||
public CallGraphEdge Trimmed()
|
||||
=> this with
|
||||
{
|
||||
SourceId = SourceId?.Trim() ?? string.Empty,
|
||||
TargetId = TargetId?.Trim() ?? string.Empty,
|
||||
CallSite = string.IsNullOrWhiteSpace(CallSite) ? null : CallSite.Trim(),
|
||||
Explanation = Explanation?.Trimmed()
|
||||
};
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<Visibility>))]
|
||||
public enum Visibility
|
||||
{
|
||||
Public,
|
||||
Internal,
|
||||
Protected,
|
||||
Private
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<CallKind>))]
|
||||
public enum CallKind
|
||||
{
|
||||
Direct,
|
||||
Virtual,
|
||||
Delegate,
|
||||
Reflection,
|
||||
Dynamic,
|
||||
Plt,
|
||||
Iat
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explanation type for call graph edges.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<CallEdgeExplanationType>))]
|
||||
public enum CallEdgeExplanationType
|
||||
{
|
||||
/// <summary>Static import (ES6 import, Python import, using directive).</summary>
|
||||
Import,
|
||||
|
||||
/// <summary>Dynamic load (require(), dlopen, LoadLibrary).</summary>
|
||||
DynamicLoad,
|
||||
|
||||
/// <summary>Reflection invocation (Class.forName, Type.GetType).</summary>
|
||||
Reflection,
|
||||
|
||||
/// <summary>Foreign function interface (JNI, P/Invoke, ctypes).</summary>
|
||||
Ffi,
|
||||
|
||||
/// <summary>Environment variable guard (process.env.X, os.environ.get).</summary>
|
||||
EnvGuard,
|
||||
|
||||
/// <summary>Feature flag check (LaunchDarkly, unleash, custom flags).</summary>
|
||||
FeatureFlag,
|
||||
|
||||
/// <summary>Platform/architecture guard (process.platform, runtime.GOOS).</summary>
|
||||
PlatformArch,
|
||||
|
||||
/// <summary>Taint gate (sanitization, validation).</summary>
|
||||
TaintGate,
|
||||
|
||||
/// <summary>Loader rule (PLT/IAT/GOT entry).</summary>
|
||||
LoaderRule,
|
||||
|
||||
/// <summary>Direct call (static, virtual, delegate).</summary>
|
||||
DirectCall,
|
||||
|
||||
/// <summary>Cannot determine explanation type.</summary>
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explanation for why an edge exists in the call graph.
|
||||
/// </summary>
|
||||
public sealed record CallEdgeExplanation(
|
||||
[property: JsonPropertyName("type")] CallEdgeExplanationType Type,
|
||||
[property: JsonPropertyName("confidence")] double Confidence,
|
||||
[property: JsonPropertyName("guard")] string? Guard = null,
|
||||
[property: JsonPropertyName("metadata")] ImmutableDictionary<string, string>? Metadata = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a simple direct call explanation with full confidence.
|
||||
/// </summary>
|
||||
public static CallEdgeExplanation DirectCall() =>
|
||||
new(CallEdgeExplanationType.DirectCall, 1.0);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an import explanation with full confidence.
|
||||
/// </summary>
|
||||
public static CallEdgeExplanation Import(string? location = null) =>
|
||||
new(CallEdgeExplanationType.Import, 1.0);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a dynamic load explanation with medium confidence.
|
||||
/// </summary>
|
||||
public static CallEdgeExplanation DynamicLoad(double confidence = 0.5) =>
|
||||
new(CallEdgeExplanationType.DynamicLoad, confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an environment guard explanation.
|
||||
/// </summary>
|
||||
public static CallEdgeExplanation EnvGuard(string guard, double confidence = 0.9) =>
|
||||
new(CallEdgeExplanationType.EnvGuard, confidence, guard);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a feature flag explanation.
|
||||
/// </summary>
|
||||
public static CallEdgeExplanation FeatureFlag(string flag, double confidence = 0.85) =>
|
||||
new(CallEdgeExplanationType.FeatureFlag, confidence, flag);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a platform/architecture guard explanation.
|
||||
/// </summary>
|
||||
public static CallEdgeExplanation PlatformArch(string platform, double confidence = 0.95) =>
|
||||
new(CallEdgeExplanationType.PlatformArch, confidence, $"platform={platform}");
|
||||
|
||||
/// <summary>
|
||||
/// Creates a reflection explanation.
|
||||
/// </summary>
|
||||
public static CallEdgeExplanation ReflectionCall(double confidence = 0.5) =>
|
||||
new(CallEdgeExplanationType.Reflection, confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a loader rule explanation (PLT/IAT/GOT).
|
||||
/// </summary>
|
||||
public static CallEdgeExplanation LoaderRule(string loaderType, ImmutableDictionary<string, string>? metadata = null) =>
|
||||
new(CallEdgeExplanationType.LoaderRule, 0.8, null, metadata ?? ImmutableDictionary<string, string>.Empty.Add("loader", loaderType));
|
||||
|
||||
public CallEdgeExplanation Trimmed() =>
|
||||
this with
|
||||
{
|
||||
Guard = string.IsNullOrWhiteSpace(Guard) ? null : Guard.Trim()
|
||||
};
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<EntrypointType>))]
|
||||
public enum EntrypointType
|
||||
{
|
||||
HttpHandler,
|
||||
GrpcMethod,
|
||||
CliCommand,
|
||||
BackgroundJob,
|
||||
ScheduledJob,
|
||||
MessageHandler,
|
||||
EventSubscriber,
|
||||
WebSocketHandler,
|
||||
EventHandler,
|
||||
Lambda,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public static class CallGraphDigests
|
||||
{
|
||||
private static readonly JsonWriterOptions CanonicalJsonOptions = new()
|
||||
{
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
Indented = false,
|
||||
SkipValidation = false
|
||||
};
|
||||
|
||||
public static string ComputeGraphDigest(CallGraphSnapshot snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
var trimmed = snapshot.Trimmed();
|
||||
|
||||
using var buffer = new MemoryStream(capacity: 64 * 1024);
|
||||
using (var writer = new Utf8JsonWriter(buffer, CanonicalJsonOptions))
|
||||
{
|
||||
WriteDigestPayload(writer, trimmed);
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
var hash = SHA256.HashData(buffer.ToArray());
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
public static string ComputeResultDigest(ReachabilityAnalysisResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
var trimmed = result.Trimmed();
|
||||
|
||||
using var buffer = new MemoryStream(capacity: 64 * 1024);
|
||||
using (var writer = new Utf8JsonWriter(buffer, CanonicalJsonOptions))
|
||||
{
|
||||
WriteDigestPayload(writer, trimmed);
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
var hash = SHA256.HashData(buffer.ToArray());
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static void WriteDigestPayload(Utf8JsonWriter writer, CallGraphSnapshot snapshot)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("schema", "stellaops.callgraph@v1");
|
||||
writer.WriteString("language", snapshot.Language);
|
||||
|
||||
writer.WritePropertyName("nodes");
|
||||
writer.WriteStartArray();
|
||||
foreach (var node in snapshot.Nodes)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("nodeId", node.NodeId);
|
||||
writer.WriteString("symbol", node.Symbol);
|
||||
writer.WriteString("file", node.File);
|
||||
writer.WriteNumber("line", node.Line);
|
||||
writer.WriteString("package", node.Package);
|
||||
writer.WriteString("visibility", node.Visibility.ToString());
|
||||
writer.WriteBoolean("isEntrypoint", node.IsEntrypoint);
|
||||
if (node.EntrypointType is not null)
|
||||
{
|
||||
writer.WriteString("entrypointType", node.EntrypointType.Value.ToString());
|
||||
}
|
||||
writer.WriteBoolean("isSink", node.IsSink);
|
||||
if (node.SinkCategory is not null)
|
||||
{
|
||||
writer.WriteString("sinkCategory", node.SinkCategory.Value.ToString());
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName("edges");
|
||||
writer.WriteStartArray();
|
||||
foreach (var edge in snapshot.Edges)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("sourceId", edge.SourceId);
|
||||
writer.WriteString("targetId", edge.TargetId);
|
||||
writer.WriteString("callKind", edge.CallKind.ToString());
|
||||
if (!string.IsNullOrWhiteSpace(edge.CallSite))
|
||||
{
|
||||
writer.WriteString("callSite", edge.CallSite);
|
||||
}
|
||||
if (edge.Explanation is not null)
|
||||
{
|
||||
writer.WritePropertyName("explanation");
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", edge.Explanation.Type.ToString());
|
||||
writer.WriteNumber("confidence", edge.Explanation.Confidence);
|
||||
if (!string.IsNullOrWhiteSpace(edge.Explanation.Guard))
|
||||
{
|
||||
writer.WriteString("guard", edge.Explanation.Guard);
|
||||
}
|
||||
if (edge.Explanation.Metadata is { Count: > 0 })
|
||||
{
|
||||
writer.WritePropertyName("metadata");
|
||||
writer.WriteStartObject();
|
||||
foreach (var kv in edge.Explanation.Metadata.OrderBy(kv => kv.Key, StringComparer.Ordinal))
|
||||
{
|
||||
writer.WriteString(kv.Key, kv.Value);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName("entrypointIds");
|
||||
writer.WriteStartArray();
|
||||
foreach (var id in snapshot.EntrypointIds)
|
||||
{
|
||||
writer.WriteStringValue(id);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName("sinkIds");
|
||||
writer.WriteStartArray();
|
||||
foreach (var id in snapshot.SinkIds)
|
||||
{
|
||||
writer.WriteStringValue(id);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static void WriteDigestPayload(Utf8JsonWriter writer, ReachabilityAnalysisResult result)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("schema", "stellaops.reachability@v1");
|
||||
writer.WriteString("graphDigest", result.GraphDigest);
|
||||
writer.WriteString("language", result.Language);
|
||||
|
||||
writer.WritePropertyName("reachableNodeIds");
|
||||
writer.WriteStartArray();
|
||||
foreach (var id in result.ReachableNodeIds)
|
||||
{
|
||||
writer.WriteStringValue(id);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName("reachableSinkIds");
|
||||
writer.WriteStartArray();
|
||||
foreach (var id in result.ReachableSinkIds)
|
||||
{
|
||||
writer.WriteStringValue(id);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyName("paths");
|
||||
writer.WriteStartArray();
|
||||
foreach (var path in result.Paths)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("entrypointId", path.EntrypointId);
|
||||
writer.WriteString("sinkId", path.SinkId);
|
||||
writer.WritePropertyName("nodeIds");
|
||||
writer.WriteStartArray();
|
||||
foreach (var nodeId in path.NodeIds)
|
||||
{
|
||||
writer.WriteStringValue(nodeId);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ReachabilityPath(
|
||||
[property: JsonPropertyName("entrypointId")] string EntrypointId,
|
||||
[property: JsonPropertyName("sinkId")] string SinkId,
|
||||
[property: JsonPropertyName("nodeIds")]
|
||||
[property: JsonConverter(typeof(ImmutableArrayJsonConverter<string>))]
|
||||
ImmutableArray<string> NodeIds)
|
||||
{
|
||||
public ReachabilityPath Trimmed()
|
||||
{
|
||||
var nodes = (NodeIds.IsDefault ? ImmutableArray<string>.Empty : NodeIds)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id.Trim())
|
||||
.ToImmutableArray();
|
||||
|
||||
return this with
|
||||
{
|
||||
EntrypointId = EntrypointId?.Trim() ?? string.Empty,
|
||||
SinkId = SinkId?.Trim() ?? string.Empty,
|
||||
NodeIds = nodes
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ReachabilityAnalysisResult(
|
||||
[property: JsonPropertyName("scanId")] string ScanId,
|
||||
[property: JsonPropertyName("graphDigest")] string GraphDigest,
|
||||
[property: JsonPropertyName("language")] string Language,
|
||||
[property: JsonPropertyName("computedAt")] DateTimeOffset ComputedAt,
|
||||
[property: JsonPropertyName("reachableNodeIds")]
|
||||
[property: JsonConverter(typeof(ImmutableArrayJsonConverter<string>))]
|
||||
ImmutableArray<string> ReachableNodeIds,
|
||||
[property: JsonPropertyName("reachableSinkIds")]
|
||||
[property: JsonConverter(typeof(ImmutableArrayJsonConverter<string>))]
|
||||
ImmutableArray<string> ReachableSinkIds,
|
||||
[property: JsonPropertyName("paths")]
|
||||
[property: JsonConverter(typeof(ImmutableArrayJsonConverter<ReachabilityPath>))]
|
||||
ImmutableArray<ReachabilityPath> Paths,
|
||||
[property: JsonPropertyName("resultDigest")] string ResultDigest)
|
||||
{
|
||||
public ReachabilityAnalysisResult Trimmed()
|
||||
{
|
||||
var reachableNodes = (ReachableNodeIds.IsDefault ? ImmutableArray<string>.Empty : ReachableNodeIds)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var reachableSinks = (ReachableSinkIds.IsDefault ? ImmutableArray<string>.Empty : ReachableSinkIds)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
var paths = (Paths.IsDefault ? ImmutableArray<ReachabilityPath>.Empty : Paths)
|
||||
.Select(p => p.Trimmed())
|
||||
.OrderBy(p => p.SinkId, StringComparer.Ordinal)
|
||||
.ThenBy(p => p.EntrypointId, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return this with
|
||||
{
|
||||
ScanId = ScanId?.Trim() ?? string.Empty,
|
||||
GraphDigest = GraphDigest?.Trim() ?? string.Empty,
|
||||
Language = Language?.Trim() ?? string.Empty,
|
||||
ResultDigest = ResultDigest?.Trim() ?? string.Empty,
|
||||
ReachableNodeIds = reachableNodes,
|
||||
ReachableSinkIds = reachableSinks,
|
||||
Paths = paths
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class CallGraphNodeIds
|
||||
{
|
||||
public static string Compute(string stableSymbolId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stableSymbolId))
|
||||
{
|
||||
throw new ArgumentException("Symbol id must be provided.", nameof(stableSymbolId));
|
||||
}
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(stableSymbolId.Trim()));
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
public static string StableSymbolId(string language, string symbol)
|
||||
=> $"{language.Trim().ToLowerInvariant()}:{symbol.Trim()}";
|
||||
}
|
||||
// Type aliases for backward compatibility.
|
||||
// Code using StellaOps.Scanner.CallGraph.CallGraphSnapshot etc. will continue to work
|
||||
// because the global using imports all types from Contracts into this namespace.
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Scanner.Evidence\\StellaOps.Scanner.Evidence.csproj" />
|
||||
<ProjectReference Include="..\\StellaOps.Scanner.Reachability\\StellaOps.Scanner.Reachability.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Contracts\StellaOps.Scanner.Contracts.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Evidence\StellaOps.Scanner.Evidence.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user