feat: Implement IsolatedReplayContext for deterministic audit replay

- Added IsolatedReplayContext class to provide an isolated environment for replaying audit bundles without external calls.
- Introduced methods for initializing the context, verifying input digests, and extracting inputs for policy evaluation.
- Created supporting interfaces and options for context configuration.

feat: Create ReplayExecutor for executing policy re-evaluation and verdict comparison

- Developed ReplayExecutor class to handle the execution of replay processes, including input verification and verdict comparison.
- Implemented detailed drift detection and error handling during replay execution.
- Added interfaces for policy evaluation and replay execution options.

feat: Add ScanSnapshotFetcher for fetching scan data and snapshots

- Introduced ScanSnapshotFetcher class to retrieve necessary scan data and snapshots for audit bundle creation.
- Implemented methods to fetch scan metadata, advisory feeds, policy snapshots, and VEX statements.
- Created supporting interfaces for scan data, feed snapshots, and policy snapshots.
This commit is contained in:
StellaOps Bot
2025-12-23 07:46:34 +02:00
parent e47627cfff
commit 7e384ab610
77 changed files with 153346 additions and 209 deletions

View File

@@ -217,7 +217,7 @@ public sealed class NodeCallGraphExtractor : ICallGraphExtractor
IsEntrypoint: false,
EntrypointType: null,
IsSink: true,
SinkCategory: sink.Category));
SinkCategory: MapSinkCategory(sink.Category)));
// Add edge from caller to sink
var callerNodeId = CallGraphNodeIds.Compute(sink.Caller);
@@ -299,10 +299,15 @@ public sealed class NodeCallGraphExtractor : ICallGraphExtractor
"file_read" or "path_traversal" => SinkCategory.PathTraversal,
"weak_crypto" or "crypto_weak" => SinkCategory.CryptoWeak,
"ldap_injection" => SinkCategory.LdapInjection,
"nosql_injection" or "nosql" => SinkCategory.NoSqlInjection,
"nosql_injection" or "nosql" => SinkCategory.SqlRaw, // Map to SQL as closest category
"xss" or "template_injection" => SinkCategory.TemplateInjection,
"log_injection" or "log_forging" => SinkCategory.LogForging,
"regex_dos" or "redos" => SinkCategory.ReDos,
"log_injection" or "log_forging" => SinkCategory.LogInjection,
"regex_dos" or "redos" => SinkCategory.CodeInjection, // Map to code injection as closest
"code_injection" or "eval" => SinkCategory.CodeInjection,
"xxe" => SinkCategory.XxeInjection,
"xpath_injection" => SinkCategory.XPathInjection,
"open_redirect" => SinkCategory.OpenRedirect,
"reflection" => SinkCategory.Reflection,
_ => null
};

View File

@@ -0,0 +1,137 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) StellaOps
using System.Collections.Immutable;
using StellaOps.Scanner.Reachability.Stack;
namespace StellaOps.Scanner.Reachability.Layer1;
/// <summary>
/// Layer 1 analyzer: Static call graph reachability.
/// Determines if vulnerable symbols are reachable from application entrypoints
/// via static code analysis.
/// </summary>
public interface ILayer1Analyzer
{
/// <summary>
/// Analyzes static reachability of a vulnerable symbol.
/// </summary>
/// <param name="symbol">The vulnerable symbol to check</param>
/// <param name="graph">The call graph to analyze</param>
/// <param name="entrypoints">Known application entrypoints</param>
/// <param name="ct">Cancellation token</param>
/// <returns>Layer 1 reachability analysis result</returns>
Task<ReachabilityLayer1> AnalyzeAsync(
VulnerableSymbol symbol,
CallGraph graph,
ImmutableArray<Entrypoint> entrypoints,
CancellationToken ct = default);
}
/// <summary>
/// A call graph representing method/function calls in the application.
/// </summary>
public sealed record CallGraph
{
/// <summary>Unique identifier for this call graph</summary>
public required string Id { get; init; }
/// <summary>When this call graph was generated</summary>
public required DateTimeOffset GeneratedAt { get; init; }
/// <summary>All nodes in the graph</summary>
public ImmutableArray<CallGraphNode> Nodes { get; init; } = [];
/// <summary>All edges (calls) in the graph</summary>
public ImmutableArray<CallGraphEdge> Edges { get; init; } = [];
/// <summary>Source of this call graph</summary>
public required CallGraphSource Source { get; init; }
/// <summary>Language/platform this graph represents</summary>
public required string Language { get; init; }
}
/// <summary>
/// A node in the call graph (method/function).
/// </summary>
public sealed record CallGraphNode(
string Id,
string Name,
string? ClassName,
string? Namespace,
string? FileName,
int? LineNumber,
bool IsEntrypoint,
bool IsExternal
);
/// <summary>
/// An edge in the call graph (call from one method to another).
/// </summary>
public sealed record CallGraphEdge(
string FromNodeId,
string ToNodeId,
CallSiteType CallType,
int? LineNumber,
bool IsConditional
);
/// <summary>
/// Source of a call graph.
/// </summary>
public enum CallGraphSource
{
/// <summary>Roslyn/ILSpy analysis for .NET</summary>
DotNetAnalysis,
/// <summary>TypeScript/JavaScript AST analysis</summary>
NodeAnalysis,
/// <summary>javap/ASM analysis for Java</summary>
JavaAnalysis,
/// <summary>go/analysis for Go</summary>
GoAnalysis,
/// <summary>Python AST analysis</summary>
PythonAnalysis,
/// <summary>Binary disassembly</summary>
BinaryAnalysis,
/// <summary>Combined from multiple sources</summary>
Composite
}
/// <summary>
/// Input for Layer 1 analysis.
/// </summary>
public sealed record Layer1AnalysisInput
{
public required VulnerableSymbol Symbol { get; init; }
public required CallGraph Graph { get; init; }
public ImmutableArray<Entrypoint> Entrypoints { get; init; } = [];
public Layer1AnalysisOptions? Options { get; init; }
}
/// <summary>
/// Options for Layer 1 analysis.
/// </summary>
public sealed record Layer1AnalysisOptions
{
/// <summary>Maximum call path depth to explore</summary>
public int MaxPathDepth { get; init; } = 100;
/// <summary>Maximum number of paths to return</summary>
public int MaxPaths { get; init; } = 10;
/// <summary>Include paths through external libraries</summary>
public bool IncludeExternalPaths { get; init; } = true;
/// <summary>Consider reflection calls as potential paths</summary>
public bool ConsiderReflection { get; init; } = true;
/// <summary>Consider dynamic dispatch as potential paths</summary>
public bool ConsiderDynamicDispatch { get; init; } = true;
}

View File

@@ -0,0 +1,193 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) StellaOps
using System.Collections.Immutable;
using StellaOps.Scanner.Reachability.Stack;
namespace StellaOps.Scanner.Reachability.Layer2;
/// <summary>
/// Layer 2 analyzer: Binary/loader resolution.
/// Determines if the dynamic loader actually links the vulnerable symbol at runtime.
/// </summary>
public interface ILayer2Analyzer
{
/// <summary>
/// Analyzes whether a vulnerable symbol is actually resolved by the loader.
/// </summary>
/// <param name="symbol">The vulnerable symbol to check</param>
/// <param name="binary">The binary artifact to analyze</param>
/// <param name="context">Loader context (paths, preloads, etc.)</param>
/// <param name="ct">Cancellation token</param>
/// <returns>Layer 2 resolution analysis result</returns>
Task<ReachabilityLayer2> AnalyzeAsync(
VulnerableSymbol symbol,
BinaryArtifact binary,
LoaderContext context,
CancellationToken ct = default);
}
/// <summary>
/// A binary artifact (executable, shared library, etc.).
/// </summary>
public sealed record BinaryArtifact
{
/// <summary>Path to the binary file</summary>
public required string Path { get; init; }
/// <summary>Binary format</summary>
public required BinaryFormat Format { get; init; }
/// <summary>Architecture (x86_64, arm64, etc.)</summary>
public required string Architecture { get; init; }
/// <summary>Direct library dependencies (NEEDED/imports)</summary>
public ImmutableArray<LibraryDependency> Dependencies { get; init; } = [];
/// <summary>Imported symbols</summary>
public ImmutableArray<ImportedSymbol> ImportedSymbols { get; init; } = [];
/// <summary>Exported symbols</summary>
public ImmutableArray<ExportedSymbol> ExportedSymbols { get; init; } = [];
/// <summary>RPATH entries (ELF)</summary>
public ImmutableArray<string> Rpath { get; init; } = [];
/// <summary>RUNPATH entries (ELF)</summary>
public ImmutableArray<string> RunPath { get; init; } = [];
/// <summary>Whether the binary has ASLR/PIE</summary>
public bool HasPie { get; init; }
/// <summary>Whether the binary is stripped</summary>
public bool IsStripped { get; init; }
}
/// <summary>
/// Binary format.
/// </summary>
public enum BinaryFormat
{
/// <summary>ELF (Linux/Unix)</summary>
Elf,
/// <summary>PE (Windows)</summary>
Pe,
/// <summary>Mach-O (macOS)</summary>
MachO,
/// <summary>.NET assembly</summary>
DotNetAssembly,
/// <summary>Java class/JAR</summary>
JavaClass,
/// <summary>WebAssembly</summary>
Wasm
}
/// <summary>
/// A library dependency.
/// </summary>
public sealed record LibraryDependency(
string Name,
string? Version,
bool IsDelayLoad,
bool IsOptional
);
/// <summary>
/// An imported symbol.
/// </summary>
public sealed record ImportedSymbol(
string Name,
string? Library,
string? SymbolVersion,
bool IsWeak
);
/// <summary>
/// An exported symbol.
/// </summary>
public sealed record ExportedSymbol(
string Name,
string? SymbolVersion,
ulong? Address,
bool IsDefault
);
/// <summary>
/// Loader context - environment affecting symbol resolution.
/// </summary>
public sealed record LoaderContext
{
/// <summary>LD_LIBRARY_PATH or equivalent</summary>
public ImmutableArray<string> LibraryPath { get; init; } = [];
/// <summary>LD_PRELOAD or equivalent</summary>
public ImmutableArray<string> Preloads { get; init; } = [];
/// <summary>System library directories</summary>
public ImmutableArray<string> SystemPaths { get; init; } = [];
/// <summary>Available libraries in the environment</summary>
public ImmutableArray<AvailableLibrary> AvailableLibraries { get; init; } = [];
/// <summary>Whether to consider LD_PRELOAD interposition</summary>
public bool ConsiderPreloadInterposition { get; init; } = true;
/// <summary>Operating system</summary>
public required OperatingSystemType OS { get; init; }
}
/// <summary>
/// Operating system type.
/// </summary>
public enum OperatingSystemType
{
Linux,
Windows,
MacOS,
FreeBSD,
Unknown
}
/// <summary>
/// A library available in the loader context.
/// </summary>
public sealed record AvailableLibrary(
string Name,
string Path,
string? Version,
ImmutableArray<ExportedSymbol> Exports
);
/// <summary>
/// Input for Layer 2 analysis.
/// </summary>
public sealed record Layer2AnalysisInput
{
public required VulnerableSymbol Symbol { get; init; }
public required BinaryArtifact Binary { get; init; }
public required LoaderContext Context { get; init; }
public Layer2AnalysisOptions? Options { get; init; }
}
/// <summary>
/// Options for Layer 2 analysis.
/// </summary>
public sealed record Layer2AnalysisOptions
{
/// <summary>Consider symbol versioning (e.g., GLIBC_2.17)</summary>
public bool ConsiderSymbolVersioning { get; init; } = true;
/// <summary>Consider delay-load DLLs (PE)</summary>
public bool ConsiderDelayLoad { get; init; } = true;
/// <summary>Consider weak symbols</summary>
public bool ConsiderWeakSymbols { get; init; } = true;
/// <summary>Consider side-by-side manifests (Windows)</summary>
public bool ConsiderSxsManifests { get; init; } = true;
}

View File

@@ -0,0 +1,205 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) StellaOps
using System.Collections.Immutable;
using StellaOps.Scanner.Reachability.Stack;
namespace StellaOps.Scanner.Reachability.Layer3;
/// <summary>
/// Layer 3 analyzer: Runtime gating detection.
/// Determines if any feature flag, configuration, or environment condition
/// blocks execution of the vulnerable code path.
/// </summary>
public interface ILayer3Analyzer
{
/// <summary>
/// Analyzes whether runtime conditions gate (block) execution of a call path.
/// </summary>
/// <param name="path">The call path to analyze for gating conditions</param>
/// <param name="context">Runtime context (config, env vars, etc.)</param>
/// <param name="ct">Cancellation token</param>
/// <returns>Layer 3 gating analysis result</returns>
Task<ReachabilityLayer3> AnalyzeAsync(
CallPath path,
RuntimeContext context,
CancellationToken ct = default);
/// <summary>
/// Analyzes gating for multiple paths and aggregates results.
/// </summary>
/// <param name="paths">Call paths to analyze</param>
/// <param name="context">Runtime context</param>
/// <param name="ct">Cancellation token</param>
/// <returns>Aggregated Layer 3 result</returns>
Task<ReachabilityLayer3> AnalyzeMultipleAsync(
ImmutableArray<CallPath> paths,
RuntimeContext context,
CancellationToken ct = default);
}
/// <summary>
/// Runtime context - configuration and environment affecting execution.
/// </summary>
public sealed record RuntimeContext
{
/// <summary>Environment variables</summary>
public ImmutableDictionary<string, string> EnvironmentVariables { get; init; } =
ImmutableDictionary<string, string>.Empty;
/// <summary>Configuration values from files/services</summary>
public ImmutableDictionary<string, ConfigValue> Configuration { get; init; } =
ImmutableDictionary<string, ConfigValue>.Empty;
/// <summary>Feature flags and their states</summary>
public ImmutableDictionary<string, FeatureFlag> FeatureFlags { get; init; } =
ImmutableDictionary<string, FeatureFlag>.Empty;
/// <summary>Build/compile-time configuration</summary>
public BuildConfiguration? BuildConfig { get; init; }
/// <summary>Platform information</summary>
public PlatformInfo? Platform { get; init; }
/// <summary>Process capabilities/privileges</summary>
public ImmutableArray<string> Capabilities { get; init; } = [];
}
/// <summary>
/// A configuration value.
/// </summary>
public sealed record ConfigValue(
string Key,
string? Value,
ConfigValueSource Source,
bool IsSecret
);
/// <summary>
/// Source of a configuration value.
/// </summary>
public enum ConfigValueSource
{
EnvironmentVariable,
ConfigFile,
CommandLine,
RemoteService,
Default,
Unknown
}
/// <summary>
/// A feature flag.
/// </summary>
public sealed record FeatureFlag(
string Name,
bool IsEnabled,
FeatureFlagSource Source,
string? Description
);
/// <summary>
/// Source of a feature flag.
/// </summary>
public enum FeatureFlagSource
{
CompileTime,
ConfigFile,
RemoteService,
EnvironmentVariable,
Default,
Unknown
}
/// <summary>
/// Build/compile-time configuration.
/// </summary>
public sealed record BuildConfiguration
{
/// <summary>Whether this is a debug build</summary>
public bool IsDebugBuild { get; init; }
/// <summary>Defined preprocessor symbols</summary>
public ImmutableArray<string> DefineConstants { get; init; } = [];
/// <summary>Target framework</summary>
public string? TargetFramework { get; init; }
/// <summary>Build mode (Debug, Release, etc.)</summary>
public string? BuildMode { get; init; }
}
/// <summary>
/// Platform information.
/// </summary>
public sealed record PlatformInfo
{
/// <summary>Operating system</summary>
public required string OS { get; init; }
/// <summary>OS version</summary>
public string? OSVersion { get; init; }
/// <summary>Architecture (x64, arm64, etc.)</summary>
public required string Architecture { get; init; }
/// <summary>Whether running in container</summary>
public bool IsContainer { get; init; }
/// <summary>Container runtime if applicable</summary>
public string? ContainerRuntime { get; init; }
}
/// <summary>
/// Input for Layer 3 analysis.
/// </summary>
public sealed record Layer3AnalysisInput
{
public required CallPath Path { get; init; }
public required RuntimeContext Context { get; init; }
public Layer3AnalysisOptions? Options { get; init; }
}
/// <summary>
/// Options for Layer 3 analysis.
/// </summary>
public sealed record Layer3AnalysisOptions
{
/// <summary>Detect feature flag patterns in code</summary>
public bool DetectFeatureFlags { get; init; } = true;
/// <summary>Detect environment variable checks</summary>
public bool DetectEnvVarChecks { get; init; } = true;
/// <summary>Detect configuration value checks</summary>
public bool DetectConfigChecks { get; init; } = true;
/// <summary>Detect platform checks</summary>
public bool DetectPlatformChecks { get; init; } = true;
/// <summary>Detect capability/privilege checks</summary>
public bool DetectCapabilityChecks { get; init; } = true;
/// <summary>Feature flag patterns to detect (regex)</summary>
public ImmutableArray<string> FeatureFlagPatterns { get; init; } = [
@"FeatureFlags?\.",
@"IsFeatureEnabled",
@"Feature\.IsEnabled",
@"LaunchDarkly",
@"Unleash",
@"ConfigCat"
];
/// <summary>Known blocking conditions</summary>
public ImmutableArray<KnownGatingPattern> KnownPatterns { get; init; } = [];
}
/// <summary>
/// A known gating pattern to detect.
/// </summary>
public sealed record KnownGatingPattern(
string Pattern,
GatingType Type,
string Description,
bool IsBlockingByDefault
);

View File

@@ -0,0 +1,364 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) StellaOps
using System.Collections.Immutable;
using StellaOps.Scanner.Explainability.Assumptions;
namespace StellaOps.Scanner.Reachability.Stack;
/// <summary>
/// Composite three-layer reachability model.
/// Exploitability is proven only when ALL THREE layers align.
/// </summary>
public sealed record ReachabilityStack
{
/// <summary>Unique identifier for this reachability assessment</summary>
public required string Id { get; init; }
/// <summary>The finding this reachability assessment applies to</summary>
public required string FindingId { get; init; }
/// <summary>The vulnerable symbol being analyzed</summary>
public required VulnerableSymbol Symbol { get; init; }
/// <summary>Layer 1: Static call graph analysis</summary>
public required ReachabilityLayer1 StaticCallGraph { get; init; }
/// <summary>Layer 2: Binary/loader resolution</summary>
public required ReachabilityLayer2 BinaryResolution { get; init; }
/// <summary>Layer 3: Runtime gating analysis</summary>
public required ReachabilityLayer3 RuntimeGating { get; init; }
/// <summary>Final verdict derived from all three layers</summary>
public required ReachabilityVerdict Verdict { get; init; }
/// <summary>When this assessment was performed</summary>
public required DateTimeOffset AnalyzedAt { get; init; }
/// <summary>Human-readable explanation of the verdict</summary>
public string? Explanation { get; init; }
}
/// <summary>
/// A symbol that may be vulnerable in the target.
/// </summary>
public sealed record VulnerableSymbol(
string Name,
string? Library,
string? Version,
string VulnerabilityId,
SymbolType Type
);
/// <summary>
/// Type of symbol being analyzed.
/// </summary>
public enum SymbolType
{
/// <summary>Native function (C/C++)</summary>
Function,
/// <summary>.NET method</summary>
Method,
/// <summary>Java method</summary>
JavaMethod,
/// <summary>JavaScript/Node function</summary>
JsFunction,
/// <summary>Python function</summary>
PyFunction,
/// <summary>Go function</summary>
GoFunction,
/// <summary>Rust function</summary>
RustFunction
}
/// <summary>
/// Layer 1: Static call graph reachability.
/// Determines if the vulnerable symbol is reachable from any entrypoint via static analysis.
/// </summary>
public sealed record ReachabilityLayer1
{
/// <summary>Whether the symbol is reachable from any entrypoint</summary>
public required bool IsReachable { get; init; }
/// <summary>Call paths from entrypoints to the vulnerable symbol</summary>
public ImmutableArray<CallPath> Paths { get; init; } = [];
/// <summary>Entrypoints that can reach the vulnerable symbol</summary>
public ImmutableArray<Entrypoint> ReachingEntrypoints { get; init; } = [];
/// <summary>Confidence level of this layer's analysis</summary>
public required ConfidenceLevel Confidence { get; init; }
/// <summary>Analysis method used</summary>
public string? AnalysisMethod { get; init; }
/// <summary>Any limitations or caveats</summary>
public ImmutableArray<string> Limitations { get; init; } = [];
}
/// <summary>
/// A call path from entrypoint to vulnerable symbol.
/// </summary>
public sealed record CallPath
{
/// <summary>Sequence of method/function calls</summary>
public required ImmutableArray<CallSite> Sites { get; init; }
/// <summary>The entrypoint this path starts from</summary>
public required Entrypoint Entrypoint { get; init; }
/// <summary>Path confidence score</summary>
public double Confidence { get; init; } = 1.0;
/// <summary>Whether this path has any conditional branches</summary>
public bool HasConditionals { get; init; }
}
/// <summary>
/// A single call site in a call path.
/// </summary>
public sealed record CallSite(
string MethodName,
string? ClassName,
string? FileName,
int? LineNumber,
CallSiteType Type
);
/// <summary>
/// Type of call site.
/// </summary>
public enum CallSiteType
{
Direct,
Virtual,
Interface,
Delegate,
Reflection,
Dynamic
}
/// <summary>
/// An application entrypoint.
/// </summary>
public sealed record Entrypoint(
string Name,
EntrypointType Type,
string? Location,
string? Description
);
/// <summary>
/// Type of entrypoint.
/// </summary>
public enum EntrypointType
{
Main,
HttpEndpoint,
MessageHandler,
Timer,
EventHandler,
Constructor,
StaticInitializer,
TestMethod
}
/// <summary>
/// Layer 2: Binary/loader resolution.
/// Determines if the dynamic loader actually links the vulnerable symbol.
/// </summary>
public sealed record ReachabilityLayer2
{
/// <summary>Whether the symbol is actually resolved/linked at runtime</summary>
public required bool IsResolved { get; init; }
/// <summary>Resolution details if resolved</summary>
public SymbolResolution? Resolution { get; init; }
/// <summary>The loader rule that determined resolution</summary>
public LoaderRule? AppliedRule { get; init; }
/// <summary>Confidence level of this layer's analysis</summary>
public required ConfidenceLevel Confidence { get; init; }
/// <summary>Why the symbol is/isn't resolved</summary>
public string? Reason { get; init; }
/// <summary>Alternative symbols that could be loaded instead</summary>
public ImmutableArray<string> Alternatives { get; init; } = [];
}
/// <summary>
/// Details of how a symbol was resolved.
/// </summary>
public sealed record SymbolResolution(
string SymbolName,
string ResolvedLibrary,
string? ResolvedVersion,
string? SymbolVersion,
ResolutionMethod Method
);
/// <summary>
/// How the symbol was resolved.
/// </summary>
public enum ResolutionMethod
{
DirectLink,
DynamicLoad,
DelayLoad,
WeakSymbol,
Interposition
}
/// <summary>
/// A loader rule that affected resolution.
/// </summary>
public sealed record LoaderRule(
LoaderRuleType Type,
string Value,
string? Source
);
/// <summary>
/// Type of loader rule.
/// </summary>
public enum LoaderRuleType
{
Rpath,
RunPath,
LdLibraryPath,
LdPreload,
SymbolVersion,
ImportTable,
DelayLoadTable,
SxsManifest
}
/// <summary>
/// Layer 3: Runtime gating analysis.
/// Determines if any feature flag, config, or environment blocks execution.
/// </summary>
public sealed record ReachabilityLayer3
{
/// <summary>Whether execution is gated (blocked) by runtime conditions</summary>
public required bool IsGated { get; init; }
/// <summary>Gating conditions found</summary>
public ImmutableArray<GatingCondition> Conditions { get; init; } = [];
/// <summary>Overall gating outcome</summary>
public required GatingOutcome Outcome { get; init; }
/// <summary>Confidence level of this layer's analysis</summary>
public required ConfidenceLevel Confidence { get; init; }
/// <summary>Description of gating analysis</summary>
public string? Description { get; init; }
}
/// <summary>
/// A condition that gates (potentially blocks) execution.
/// </summary>
public sealed record GatingCondition(
GatingType Type,
string Description,
string? ConfigKey,
string? EnvVar,
bool IsBlocking,
GatingStatus Status
);
/// <summary>
/// Type of gating condition.
/// </summary>
public enum GatingType
{
/// <summary>Feature flag check (e.g., if (FeatureFlags.UseNewAuth))</summary>
FeatureFlag,
/// <summary>Environment variable check</summary>
EnvironmentVariable,
/// <summary>Configuration value check</summary>
ConfigurationValue,
/// <summary>Compile-time conditional (#if DEBUG)</summary>
CompileTimeConditional,
/// <summary>Platform check (RuntimeInformation.IsOSPlatform)</summary>
PlatformCheck,
/// <summary>Capability/privilege check</summary>
CapabilityCheck,
/// <summary>License/subscription check</summary>
LicenseCheck,
/// <summary>A/B test or experiment flag</summary>
ExperimentFlag
}
/// <summary>
/// Status of a gating condition.
/// </summary>
public enum GatingStatus
{
/// <summary>Condition is enabled, code path is accessible</summary>
Enabled,
/// <summary>Condition is disabled, code path is blocked</summary>
Disabled,
/// <summary>Condition status is unknown</summary>
Unknown,
/// <summary>Condition is configurable at runtime</summary>
RuntimeConfigurable
}
/// <summary>
/// Overall outcome of gating analysis.
/// </summary>
public enum GatingOutcome
{
/// <summary>No gating detected, path is open</summary>
NotGated,
/// <summary>Gating detected and path is blocked</summary>
Blocked,
/// <summary>Gating detected but path is conditionally open</summary>
Conditional,
/// <summary>Unable to determine gating status</summary>
Unknown
}
/// <summary>
/// Final reachability verdict derived from all three layers.
/// </summary>
public enum ReachabilityVerdict
{
/// <summary>All 3 layers confirm reachable - definitely exploitable</summary>
Exploitable,
/// <summary>L1+L2 confirm, L3 unknown - likely exploitable</summary>
LikelyExploitable,
/// <summary>L1 confirms, L2+L3 unknown - possibly exploitable</summary>
PossiblyExploitable,
/// <summary>Any layer definitively blocks - not exploitable</summary>
Unreachable,
/// <summary>Insufficient data to determine</summary>
Unknown
}

View File

@@ -0,0 +1,210 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) StellaOps
using System.Text;
using StellaOps.Scanner.Explainability.Assumptions;
namespace StellaOps.Scanner.Reachability.Stack;
/// <summary>
/// Evaluates three-layer reachability to produce a final verdict.
/// </summary>
public interface IReachabilityStackEvaluator
{
/// <summary>
/// Evaluates the three layers and produces a complete ReachabilityStack with verdict.
/// </summary>
ReachabilityStack Evaluate(
string findingId,
VulnerableSymbol symbol,
ReachabilityLayer1 layer1,
ReachabilityLayer2 layer2,
ReachabilityLayer3 layer3);
/// <summary>
/// Derives the verdict from three layers.
/// </summary>
ReachabilityVerdict DeriveVerdict(
ReachabilityLayer1 layer1,
ReachabilityLayer2 layer2,
ReachabilityLayer3 layer3);
}
/// <summary>
/// Default implementation of <see cref="IReachabilityStackEvaluator"/>.
/// </summary>
/// <remarks>
/// Verdict Truth Table:
/// | L1 Reachable | L2 Resolved | L3 Gated | Verdict |
/// |--------------|-------------|----------|---------|
/// | Yes | Yes | No | Exploitable |
/// | Yes | Yes | Unknown | LikelyExploitable |
/// | Yes | Yes | Yes | Unreachable |
/// | Yes | Unknown | Unknown | PossiblyExploitable |
/// | Yes | No | * | Unreachable |
/// | No | * | * | Unreachable |
/// | Unknown | * | * | Unknown |
/// </remarks>
public sealed class ReachabilityStackEvaluator : IReachabilityStackEvaluator
{
/// <inheritdoc />
public ReachabilityStack Evaluate(
string findingId,
VulnerableSymbol symbol,
ReachabilityLayer1 layer1,
ReachabilityLayer2 layer2,
ReachabilityLayer3 layer3)
{
var verdict = DeriveVerdict(layer1, layer2, layer3);
var explanation = GenerateExplanation(layer1, layer2, layer3, verdict);
return new ReachabilityStack
{
Id = Guid.NewGuid().ToString("N"),
FindingId = findingId,
Symbol = symbol,
StaticCallGraph = layer1,
BinaryResolution = layer2,
RuntimeGating = layer3,
Verdict = verdict,
AnalyzedAt = DateTimeOffset.UtcNow,
Explanation = explanation
};
}
/// <inheritdoc />
public ReachabilityVerdict DeriveVerdict(
ReachabilityLayer1 layer1,
ReachabilityLayer2 layer2,
ReachabilityLayer3 layer3)
{
// Check for unknown L1 - can't determine anything
if (layer1.Confidence == ConfidenceLevel.Low && !layer1.IsReachable && layer1.Paths.Length == 0)
{
return ReachabilityVerdict.Unknown;
}
// L1 definitively blocks (not reachable via static analysis)
if (!layer1.IsReachable && layer1.Confidence >= ConfidenceLevel.Medium)
{
return ReachabilityVerdict.Unreachable;
}
// L2 definitively blocks (symbol not linked)
if (!layer2.IsResolved && layer2.Confidence >= ConfidenceLevel.Medium)
{
return ReachabilityVerdict.Unreachable;
}
// L3 definitively blocks (gating prevents execution)
if (layer3.IsGated && layer3.Outcome == GatingOutcome.Blocked && layer3.Confidence >= ConfidenceLevel.Medium)
{
return ReachabilityVerdict.Unreachable;
}
// All three confirm reachable
if (layer1.IsReachable &&
layer2.IsResolved &&
!layer3.IsGated &&
layer3.Outcome == GatingOutcome.NotGated)
{
return ReachabilityVerdict.Exploitable;
}
// L1 + L2 confirm, but L3 blocked with low confidence (can't trust the block)
// Treat as if L3 analysis is inconclusive - still exploitable since we can't rely on the gate
if (layer1.IsReachable &&
layer2.IsResolved &&
layer3.Outcome == GatingOutcome.Blocked &&
layer3.Confidence < ConfidenceLevel.Medium)
{
return ReachabilityVerdict.Exploitable;
}
// L1 + L2 confirm, L3 unknown/conditional
if (layer1.IsReachable &&
layer2.IsResolved &&
(layer3.Outcome == GatingOutcome.Unknown || layer3.Outcome == GatingOutcome.Conditional))
{
return ReachabilityVerdict.LikelyExploitable;
}
// L1 confirms, L2/L3 unknown
if (layer1.IsReachable &&
(layer2.Confidence == ConfidenceLevel.Low || !layer2.IsResolved))
{
return ReachabilityVerdict.PossiblyExploitable;
}
// Default to unknown if we can't determine
return ReachabilityVerdict.Unknown;
}
private static string GenerateExplanation(
ReachabilityLayer1 layer1,
ReachabilityLayer2 layer2,
ReachabilityLayer3 layer3,
ReachabilityVerdict verdict)
{
var sb = new StringBuilder();
// Verdict summary
sb.AppendLine(verdict switch
{
ReachabilityVerdict.Exploitable =>
"All three reachability layers confirm the vulnerability is exploitable.",
ReachabilityVerdict.LikelyExploitable =>
"Static and binary analysis confirm reachability. Runtime gating status is unclear.",
ReachabilityVerdict.PossiblyExploitable =>
"Static analysis shows reachability, but binary resolution or runtime gating is uncertain.",
ReachabilityVerdict.Unreachable =>
"At least one reachability layer definitively blocks exploitation.",
ReachabilityVerdict.Unknown =>
"Insufficient evidence to determine reachability.",
_ => "Verdict determination failed."
});
sb.AppendLine();
// Layer 1 details
sb.AppendLine($"**Layer 1 (Static Call Graph)**: {(layer1.IsReachable ? "Reachable" : "Not reachable")} [{layer1.Confidence}]");
if (layer1.Paths.Length > 0)
{
sb.AppendLine($" - {layer1.Paths.Length} call path(s) found");
sb.AppendLine($" - {layer1.ReachingEntrypoints.Length} entrypoint(s) can reach vulnerable code");
}
if (layer1.AnalysisMethod is not null)
{
sb.AppendLine($" - Analysis method: {layer1.AnalysisMethod}");
}
// Layer 2 details
sb.AppendLine($"**Layer 2 (Binary Resolution)**: {(layer2.IsResolved ? "Resolved" : "Not resolved")} [{layer2.Confidence}]");
if (layer2.Resolution is not null)
{
sb.AppendLine($" - Symbol: {layer2.Resolution.SymbolName}");
sb.AppendLine($" - Library: {layer2.Resolution.ResolvedLibrary}");
if (layer2.Resolution.SymbolVersion is not null)
{
sb.AppendLine($" - Version: {layer2.Resolution.SymbolVersion}");
}
}
if (layer2.Reason is not null)
{
sb.AppendLine($" - Reason: {layer2.Reason}");
}
// Layer 3 details
sb.AppendLine($"**Layer 3 (Runtime Gating)**: {(layer3.IsGated ? "Gated" : "Not gated")} - {layer3.Outcome} [{layer3.Confidence}]");
if (layer3.Conditions.Length > 0)
{
foreach (var condition in layer3.Conditions)
{
var status = condition.IsBlocking ? "BLOCKING" : "non-blocking";
sb.AppendLine($" - [{status}] {condition.Type}: {condition.Description}");
}
}
return sb.ToString();
}
}

View File

@@ -1 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Npgsql" Version="9.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.Explainability\StellaOps.Scanner.Explainability.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.ProofSpine\StellaOps.Scanner.ProofSpine.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.Surface.Env\StellaOps.Scanner.Surface.Env.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.SmartDiff\StellaOps.Scanner.SmartDiff.csproj" />
<ProjectReference Include="..\..\StellaOps.Scanner.Analyzers.Native\StellaOps.Scanner.Analyzers.Native.csproj" />
<ProjectReference Include="..\..\..\Attestor\StellaOps.Attestor\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
<ProjectReference Include="..\..\..\Attestor\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
<ProjectReference Include="..\..\..\Attestor\__Libraries\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>

View File

@@ -127,7 +127,7 @@ public sealed class OciArtifactPusher
return new OciArtifactManifest
{
MediaType = OciMediaTypes.ArtifactManifest,
MediaType = OciMediaTypes.ImageManifest,
ArtifactType = request.ArtifactType,
Config = new OciDescriptor
{
@@ -140,7 +140,7 @@ public sealed class OciArtifactPusher
? null
: new OciDescriptor
{
MediaType = OciMediaTypes.ArtifactManifest,
MediaType = OciMediaTypes.ImageManifest,
Digest = request.SubjectDigest!,
Size = 0
},
@@ -220,7 +220,7 @@ public sealed class OciArtifactPusher
Content = new ByteArrayContent(manifestBytes)
};
request.Content.Headers.ContentType = new MediaTypeHeaderValue(OciMediaTypes.ArtifactManifest);
request.Content.Headers.ContentType = new MediaTypeHeaderValue(OciMediaTypes.ImageManifest);
auth.ApplyTo(request);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);

View File

@@ -2,7 +2,16 @@
public static class OciMediaTypes
{
/// <summary>
/// OCI 1.1 image manifest (used for all manifests including artifacts).
/// </summary>
public const string ImageManifest = "application/vnd.oci.image.manifest.v1+json";
/// <summary>
/// Deprecated artifact manifest type (kept for compatibility, prefer ImageManifest).
/// </summary>
public const string ArtifactManifest = "application/vnd.oci.artifact.manifest.v1+json";
public const string EmptyConfig = "application/vnd.oci.empty.v1+json";
public const string OctetStream = "application/octet-stream";
@@ -26,4 +35,30 @@ public static class OciMediaTypes
/// Config media type for verdict attestation artifacts.
/// </summary>
public const string VerdictConfig = "application/vnd.stellaops.verdict.config.v1+json";
// Sprint: SPRINT_5200_0001_0001 - Policy Pack Distribution
/// <summary>
/// Media type for policy pack artifacts.
/// </summary>
public const string PolicyPack = "application/vnd.stellaops.policy-pack.v1+json";
/// <summary>
/// Config media type for policy pack artifacts.
/// </summary>
public const string PolicyPackConfig = "application/vnd.stellaops.policy-pack.config.v1+json";
/// <summary>
/// Media type for policy pack attestation (DSSE envelope).
/// </summary>
public const string PolicyPackAttestation = "application/vnd.stellaops.policy-pack.attestation.v1+json";
/// <summary>
/// Media type for policy pack YAML layer.
/// </summary>
public const string PolicyPackYaml = "application/vnd.stellaops.policy-pack.yaml.v1";
/// <summary>
/// Media type for policy pack override layer.
/// </summary>
public const string PolicyPackOverride = "application/vnd.stellaops.policy-pack.override.v1+json";
}

View File

@@ -26,7 +26,7 @@ public sealed record OciArtifactManifest
public int SchemaVersion { get; init; } = 2;
[JsonPropertyName("mediaType")]
public string MediaType { get; init; } = OciMediaTypes.ArtifactManifest;
public string MediaType { get; init; } = OciMediaTypes.ImageManifest;
[JsonPropertyName("artifactType")]
public string? ArtifactType { get; init; }