Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls.
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled

This commit is contained in:
master
2025-11-20 07:50:52 +02:00
parent 616ec73133
commit 10212d67c0
473 changed files with 316758 additions and 388 deletions

View File

@@ -0,0 +1,120 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Analyzers.Lang;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
/// <summary>
/// Optional harness that executes the emitted Deno runtime shim when an entrypoint is provided via environment variable.
/// This keeps runtime capture opt-in and offline-friendly.
/// </summary>
internal static class DenoRuntimeTraceRunner
{
private const string EntrypointEnvVar = "STELLA_DENO_ENTRYPOINT";
private const string BinaryEnvVar = "STELLA_DENO_BINARY";
private const string RuntimeFileName = "deno-runtime.ndjson";
public static async Task<bool> TryExecuteAsync(
LanguageAnalyzerContext context,
ILogger? logger,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
var entrypoint = Environment.GetEnvironmentVariable(EntrypointEnvVar);
if (string.IsNullOrWhiteSpace(entrypoint))
{
logger?.LogDebug("Deno runtime trace skipped: {EnvVar} not set", EntrypointEnvVar);
return false;
}
var entrypointPath = Path.GetFullPath(Path.Combine(context.RootPath, entrypoint));
if (!File.Exists(entrypointPath))
{
logger?.LogWarning("Deno runtime trace skipped: entrypoint '{Entrypoint}' missing", entrypointPath);
return false;
}
var shimPath = Path.Combine(context.RootPath, DenoRuntimeShim.FileName);
if (!File.Exists(shimPath))
{
await DenoRuntimeShim.WriteAsync(context.RootPath, cancellationToken).ConfigureAwait(false);
}
var binary = Environment.GetEnvironmentVariable(BinaryEnvVar);
if (string.IsNullOrWhiteSpace(binary))
{
binary = "deno";
}
var startInfo = new ProcessStartInfo
{
FileName = binary,
WorkingDirectory = context.RootPath,
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
};
startInfo.ArgumentList.Add("run");
startInfo.ArgumentList.Add("--cached-only");
startInfo.ArgumentList.Add("--allow-read");
startInfo.ArgumentList.Add("--allow-env");
startInfo.ArgumentList.Add("--quiet");
startInfo.ArgumentList.Add(shimPath);
startInfo.Environment[EntrypointEnvVar] = entrypointPath;
try
{
using var process = Process.Start(startInfo);
if (process is null)
{
logger?.LogWarning("Deno runtime trace skipped: failed to start 'deno' process");
return false;
}
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (process.ExitCode != 0)
{
var stderr = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
logger?.LogWarning(
"Deno runtime trace failed with exit code {ExitCode}. stderr: {Error}",
process.ExitCode,
Truncate(stderr));
return false;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Deno runtime trace skipped: {Message}", ex.Message);
return false;
}
var runtimePath = Path.Combine(context.RootPath, RuntimeFileName);
if (!File.Exists(runtimePath))
{
logger?.LogWarning(
"Deno runtime trace finished but did not emit {RuntimeFile}",
RuntimeFileName);
return false;
}
return true;
}
private static string Truncate(string? value, int maxLength = 400)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
return value.Length <= maxLength ? value : value[..maxLength];
}
}

View File

@@ -0,0 +1,33 @@
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
internal sealed record NodeEntrypoint(
string Path,
string? BinName,
string? MainField,
string? ModuleField,
string ConditionSet)
{
public static NodeEntrypoint Create(string path, string? binName, string? mainField, string? moduleField, IEnumerable<string>? conditions)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
var conditionSet = NormalizeConditions(conditions);
return new NodeEntrypoint(path, binName, mainField, moduleField, conditionSet);
}
private static string NormalizeConditions(IEnumerable<string>? conditions)
{
if (conditions is null)
{
return string.Empty;
}
var distinct = conditions
.Where(static c => !string.IsNullOrWhiteSpace(c))
.Select(static c => c.Trim())
.Distinct(StringComparer.Ordinal)
.OrderBy(static c => c, StringComparer.Ordinal);
return string.Join(',', distinct);
}
}

View File

@@ -0,0 +1,10 @@
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
internal sealed record NodeImportEdge(
string SourceFile,
string TargetSpecifier,
string Kind,
string Evidence)
{
public string ComparisonKey => string.Concat(SourceFile, "|", TargetSpecifier, "|", Kind);
}

View File

@@ -0,0 +1,91 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Esprima;
using Esprima.Ast;
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
internal static class NodeImportWalker
{
public static IReadOnlyList<NodeImportEdge> AnalyzeImports(string sourcePath, string content)
{
ArgumentException.ThrowIfNullOrWhiteSpace(sourcePath);
if (content is null)
{
return Array.Empty<NodeImportEdge>();
}
Script script;
try
{
script = new JavaScriptParser(content, new ParserOptions
{
Tolerant = true,
AdaptRegexp = true,
Source = sourcePath
}).ParseScript();
}
catch (ParserException)
{
return Array.Empty<NodeImportEdge>();
}
var edges = new List<NodeImportEdge>();
Walk(script, sourcePath, edges);
return edges.Count == 0
? Array.Empty<NodeImportEdge>()
: edges.OrderBy(e => e.ComparisonKey, StringComparer.Ordinal).ToArray();
}
private static void Walk(Node node, string sourcePath, List<NodeImportEdge> edges)
{
switch (node)
{
case ImportDeclaration importDecl when !string.IsNullOrWhiteSpace(importDecl.Source?.StringValue):
edges.Add(new NodeImportEdge(sourcePath, importDecl.Source.StringValue!, "import", BuildEvidence(importDecl.Loc)));
break;
case CallExpression call when IsRequire(call) && call.Arguments.FirstOrDefault() is Literal { Value: string target }:
edges.Add(new NodeImportEdge(sourcePath, target, "require", BuildEvidence(call.Loc)));
break;
case ImportExpression importExp when importExp.Source is Literal { Value: string importTarget }:
edges.Add(new NodeImportEdge(sourcePath, importTarget, "import()", BuildEvidence(importExp.Loc)));
break;
}
foreach (var child in node.ChildNodes)
{
Walk(child, sourcePath, edges);
}
}
private static bool IsRequire(CallExpression call)
{
return call.Callee is Identifier id && string.Equals(id.Name, "require", StringComparison.Ordinal)
&& call.Arguments.Count == 1 && call.Arguments[0] is Literal { Value: string };
}
private static string BuildEvidence(Location? loc)
{
if (loc is null)
{
return string.Empty;
}
var json = new JsonObject
{
["start"] = BuildPosition(loc.Start),
["end"] = BuildPosition(loc.End)
};
return json.ToJsonString(new JsonSerializerOptions { WriteIndented = false });
}
private static JsonObject BuildPosition(Position pos)
{
return new JsonObject
{
["line"] = pos.Line,
["column"] = pos.Column
};
}
}

View File

@@ -1,4 +1,6 @@
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
using System.Globalization;
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
internal sealed class NodePackage
{
@@ -80,6 +82,12 @@ internal sealed class NodePackage
public bool IsYarnPnp { get; }
private readonly List<NodeEntrypoint> _entrypoints = new();
private readonly List<NodeImportEdge> _imports = new();
public IReadOnlyList<NodeEntrypoint> Entrypoints => _entrypoints;
public IReadOnlyList<NodeImportEdge> Imports => _imports;
public string RelativePathNormalized => string.IsNullOrEmpty(RelativePath) ? string.Empty : RelativePath.Replace(Path.DirectorySeparatorChar, '/');
public string ComponentKey => $"purl::{Purl}";
@@ -113,10 +121,43 @@ internal sealed class NodePackage
LanguageEvidenceKind.Metadata,
"package.json:scripts",
locator,
script.Command,
script.Sha256));
}
script.Command,
script.Sha256));
}
foreach (var entrypoint in _entrypoints)
{
var locator = string.IsNullOrEmpty(PackageJsonLocator)
? "package.json#entrypoint"
: $"{PackageJsonLocator}#entrypoint";
var content = string.Join(';', new[]
{
entrypoint.Path,
entrypoint.BinName,
entrypoint.MainField,
entrypoint.ModuleField,
entrypoint.ConditionSet
}.Where(static v => !string.IsNullOrWhiteSpace(v)));
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.Metadata,
"package.json:entrypoint",
locator,
content,
sha256: null));
}
foreach (var importEdge in _imports.OrderBy(static e => e.ComparisonKey, StringComparer.Ordinal))
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.Source,
"node.import",
importEdge.SourceFile,
importEdge.TargetSpecifier,
sha256: null));
}
return evidence
.OrderBy(static e => e.ComparisonKey, StringComparer.Ordinal)
.ToArray();
@@ -186,6 +227,33 @@ internal sealed class NodePackage
}
}
if (_entrypoints.Count > 0)
{
var paths = _entrypoints
.Select(static ep => ep.Path)
.OrderBy(static p => p, StringComparer.Ordinal)
.ToArray();
entries.Add(new KeyValuePair<string, string?>("entrypoint", string.Join(';', paths)));
var conditionSets = _entrypoints
.Select(static ep => ep.ConditionSet)
.Where(static cs => !string.IsNullOrWhiteSpace(cs))
.Distinct(StringComparer.Ordinal)
.OrderBy(static cs => cs, StringComparer.Ordinal)
.ToArray();
if (conditionSets.Length > 0)
{
entries.Add(new KeyValuePair<string, string?>("entrypoint.conditions", string.Join(';', conditionSets)));
}
}
if (_imports.Count > 0)
{
entries.Add(new KeyValuePair<string, string?>("imports", _imports.Count.ToString(CultureInfo.InvariantCulture)));
}
if (HasInstallScripts)
{
entries.Add(new KeyValuePair<string, string?>("installScripts", "true"));
@@ -230,6 +298,48 @@ internal sealed class NodePackage
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToArray();
}
public void AddEntrypoint(string path, string conditionSet, string? binName, string? mainField, string? moduleField)
{
if (string.IsNullOrWhiteSpace(path))
{
return;
}
var entry = NodeEntrypoint.Create(path.Replace(Path.DirectorySeparatorChar, '/'), binName, mainField, moduleField, ParseConditionSet(conditionSet));
if (_entrypoints.Any(ep => string.Equals(ep.Path, entry.Path, StringComparison.Ordinal)))
{
return;
}
_entrypoints.Add(entry);
}
public void AddImport(string sourceFile, string targetSpecifier, string kind, string evidence)
{
if (string.IsNullOrWhiteSpace(sourceFile) || string.IsNullOrWhiteSpace(targetSpecifier))
{
return;
}
var edge = new NodeImportEdge(sourceFile.Replace(Path.DirectorySeparatorChar, '/'), targetSpecifier.Trim(), kind.Trim(), evidence);
if (_imports.Any(e => string.Equals(e.ComparisonKey, edge.ComparisonKey, StringComparison.Ordinal)))
{
return;
}
_imports.Add(edge);
}
private static IEnumerable<string> ParseConditionSet(string conditionSet)
{
if (string.IsNullOrWhiteSpace(conditionSet))
{
return Array.Empty<string>();
}
return conditionSet.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
private static string BuildPurl(string name, string version)
{

View File

@@ -61,9 +61,65 @@ internal static class NodePackageCollector
AppendDeclaredPackages(packages, lockData);
AttachImports(context, packages, cancellationToken);
return packages;
}
private static void AttachImports(LanguageAnalyzerContext context, List<NodePackage> packages, CancellationToken cancellationToken)
{
foreach (var package in packages)
{
cancellationToken.ThrowIfCancellationRequested();
var packageRoot = string.IsNullOrEmpty(package.RelativePathNormalized)
? context.RootPath
: Path.Combine(context.RootPath, package.RelativePathNormalized.Replace('/', Path.DirectorySeparatorChar));
if (!Directory.Exists(packageRoot))
{
continue;
}
foreach (var file in EnumerateSourceFiles(packageRoot))
{
cancellationToken.ThrowIfCancellationRequested();
string content;
try
{
content = File.ReadAllText(file);
}
catch (IOException)
{
continue;
}
var imports = NodeImportWalker.AnalyzeImports(context.GetRelativePath(file).Replace(Path.DirectorySeparatorChar, '/'), content);
foreach (var edge in imports)
{
package.AddImport(edge.SourceFile, edge.TargetSpecifier, edge.Kind, edge.Evidence);
}
}
}
}
private static IEnumerable<string> EnumerateSourceFiles(string root)
{
foreach (var extension in new[] { ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx" })
{
foreach (var file in Directory.EnumerateFiles(root, "*" + extension, new EnumerationOptions
{
RecurseSubdirectories = true,
MatchCasing = MatchCasing.CaseInsensitive,
IgnoreInaccessible = true
}))
{
yield return file;
}
}
}
private static void TraverseDirectory(
LanguageAnalyzerContext context,
string directory,

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace StellaOps.Scanner.Emit.Reachability;
public enum ReachabilityState
{
Unknown = 0,
Conditional = 1,
Reachable = 2,
Unreachable = 3
}
public enum ReachabilityEvidenceKind
{
StaticPath,
RuntimeHit,
RuntimeSinkHit,
Guard,
Mitigation
}
public readonly record struct ReachabilityEvidence(
ReachabilityEvidenceKind Kind,
string? Reference = null);
public sealed record ReachabilityLatticeResult(
ReachabilityState State,
double Score);
public static class ReachabilityLattice
{
public static ReachabilityLatticeResult Evaluate(IEnumerable<ReachabilityEvidence> rawEvidence)
{
var evidence = rawEvidence
.Where(e => Enum.IsDefined(typeof(ReachabilityEvidenceKind), e.Kind))
.OrderBy(e => e.Kind)
.ThenBy(e => e.Reference ?? string.Empty, StringComparer.Ordinal)
.ToList();
var hasRuntimeSinkHit = evidence.Any(e => e.Kind is ReachabilityEvidenceKind.RuntimeSinkHit);
var hasRuntimeHit = evidence.Any(e => e.Kind is ReachabilityEvidenceKind.RuntimeHit or ReachabilityEvidenceKind.RuntimeSinkHit);
var hasStaticPath = evidence.Any(e => e.Kind is ReachabilityEvidenceKind.StaticPath);
var guardCount = evidence.Count(e => e.Kind is ReachabilityEvidenceKind.Guard);
var mitigationCount = evidence.Count(e => e.Kind is ReachabilityEvidenceKind.Mitigation);
var score = 0.0;
var state = ReachabilityState.Unknown;
if (hasStaticPath)
{
state = ReachabilityState.Conditional;
score += 0.50;
}
if (hasRuntimeHit)
{
state = ReachabilityState.Reachable;
score += 0.30;
if (hasRuntimeSinkHit)
{
score += 0.10;
}
}
if (!hasRuntimeHit && guardCount > 0)
{
state = state switch
{
ReachabilityState.Reachable => ReachabilityState.Conditional,
ReachabilityState.Conditional => ReachabilityState.Unknown,
_ => state
};
score = Math.Max(score - 0.20 * guardCount, 0);
}
if (!hasRuntimeHit && mitigationCount > 0)
{
state = ReachabilityState.Unreachable;
score = Math.Max(score - 0.30 * mitigationCount, 0);
}
if (state == ReachabilityState.Unknown && score <= 0 && evidence.Count == 0)
{
return new ReachabilityLatticeResult(ReachabilityState.Unknown, 0);
}
var capped = Math.Clamp(score, 0, 1);
var rounded = Math.Round(capped, 2, MidpointRounding.AwayFromZero);
return new ReachabilityLatticeResult(state, rounded);
}
}

View File

@@ -0,0 +1,52 @@
using System.Text;
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Deno;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Deno;
public sealed class DenoLanguageAnalyzerRuntimeTests
{
[Fact]
public async Task IngestsRuntimeTraceAndEmitsSignals()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var runtimePath = Path.Combine(root, "deno-runtime.ndjson");
var ndjson = new StringBuilder()
.AppendLine("{\"type\":\"deno.module.load\",\"ts\":\"2025-11-18T00:00:00Z\",\"module\":{\"normalized\":\"app/main.ts\",\"path_sha256\":\"abc\"},\"reason\":\"dynamic-import\",\"permissions\":[\"fs\"],\"origin\":\"https://deno.land/x/std@0.208.0/http/server.ts\"}")
.AppendLine("{\"type\":\"deno.permission.use\",\"ts\":\"2025-11-18T00:00:01Z\",\"permission\":\"net\",\"module\":{\"normalized\":\"app/net.ts\",\"path_sha256\":\"def\"},\"details\":\"permissions.request\"}")
.AppendLine("{\"type\":\"deno.wasm.load\",\"ts\":\"2025-11-18T00:00:02Z\",\"module\":{\"normalized\":\"pkg/module.wasm\",\"path_sha256\":\"ghi\"},\"importer\":\"app/main.ts\",\"reason\":\"instantiate\"}")
.AppendLine("{\"type\":\"deno.npm.resolution\",\"ts\":\"2025-11-18T00:00:03Z\",\"specifier\":\"npm:chalk@5\",\"package\":\"chalk\",\"version\":\"5.3.0\",\"resolved\":\"file:///cache/chalk\",\"exists\":true}")
.ToString();
await File.WriteAllTextAsync(runtimePath, ndjson);
var store = new ScanAnalysisStore();
var context = new LanguageAnalyzerContext(root, TimeProvider.System, usageHints: null, services: null, analysisStore: store);
var analyzer = new DenoLanguageAnalyzer();
var engine = new LanguageAnalyzerEngine(new[] { analyzer });
await engine.AnalyzeAsync(context, CancellationToken.None);
Assert.True(store.TryGet(ScanAnalysisKeys.DenoRuntimePayload, out AnalyzerObservationPayload runtimePayload));
Assert.Equal("deno.runtime.v1", runtimePayload.Kind);
Assert.Equal("application/x-ndjson", runtimePayload.MediaType);
Assert.True(store.TryGet("surface.lang.deno.permissions", out string? permissions));
Assert.Equal("fs,net", permissions);
Assert.True(store.TryGet("surface.lang.deno.remote_origins", out string? origins));
Assert.Equal("https://deno.land/x/std@0.208.0/http/server.ts", origins);
Assert.True(store.TryGet("surface.lang.deno.wasm_modules", out string? wasm));
Assert.Equal("1", wasm);
Assert.True(store.TryGet("surface.lang.deno.npm_modules", out string? npm));
Assert.Equal("1", npm);
}
finally
{
TestPaths.SafeDelete(root);
}
}
}

View File

@@ -0,0 +1,93 @@
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Deno;
public sealed class DenoRuntimeTraceRunnerTests
{
[Fact]
public async Task ReturnsFalse_WhenEntrypointEnvMissing()
{
using var env = new EnvironmentVariableScope("STELLA_DENO_ENTRYPOINT", null);
var root = TestPaths.CreateTemporaryDirectory();
try
{
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var result = await DenoRuntimeTraceRunner.TryExecuteAsync(context, logger: null, CancellationToken.None);
Assert.False(result);
Assert.False(File.Exists(Path.Combine(root, "deno-runtime.ndjson")));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ReturnsFalse_WhenEntrypointMissing()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
using var env = new EnvironmentVariableScope("STELLA_DENO_ENTRYPOINT", "app/main.ts");
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var result = await DenoRuntimeTraceRunner.TryExecuteAsync(context, logger: null, CancellationToken.None);
Assert.False(result);
Assert.False(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName)));
Assert.False(File.Exists(Path.Combine(root, "deno-runtime.ndjson")));
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public async Task ReturnsFalse_WhenDenoBinaryUnavailable()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var entrypoint = Path.Combine(root, "main.ts");
await File.WriteAllTextAsync(entrypoint, "console.log('hi')");
using var entryEnv = new EnvironmentVariableScope("STELLA_DENO_ENTRYPOINT", entrypoint);
using var binaryEnv = new EnvironmentVariableScope("STELLA_DENO_BINARY", Guid.NewGuid().ToString("N"));
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var result = await DenoRuntimeTraceRunner.TryExecuteAsync(context, logger: null, CancellationToken.None);
Assert.False(result);
Assert.True(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName)));
}
finally
{
TestPaths.SafeDelete(root);
}
}
private sealed class EnvironmentVariableScope : IDisposable
{
private readonly string _name;
private readonly string? _original;
public EnvironmentVariableScope(string name, string? value)
{
_name = name;
_original = Environment.GetEnvironmentVariable(name);
Environment.SetEnvironmentVariable(name, value);
}
public void Dispose()
{
Environment.SetEnvironmentVariable(_name, _original);
}
}
}

View File

@@ -0,0 +1,30 @@
using StellaOps.Scanner.Analyzers.Lang.Node.Internal;
namespace StellaOps.Scanner.Analyzers.Lang.Node.Tests.Node;
public sealed class NodeEntrypointTests
{
[Fact]
public void Create_NormalizesConditions_DedupesAndSorts()
{
var entry = NodeEntrypoint.Create(
path: "src/index.js",
binName: "cli",
mainField: "index.js",
moduleField: null,
conditions: new[] { "node", "browser", "node" });
Assert.Equal("browser,node", entry.ConditionSet);
Assert.Equal("src/index.js", entry.Path);
Assert.Equal("cli", entry.BinName);
Assert.Equal("index.js", entry.MainField);
Assert.Null(entry.ModuleField);
}
[Fact]
public void Create_AllowsEmptyConditions()
{
var entry = NodeEntrypoint.Create("src/app.js", null, null, null, Array.Empty<string>());
Assert.Equal(string.Empty, entry.ConditionSet);
}
}

View File

@@ -0,0 +1,88 @@
using FluentAssertions;
using StellaOps.Scanner.Emit.Reachability;
using Xunit;
namespace StellaOps.Scanner.Emit.Tests.Reachability;
public class ReachabilityLatticeTests
{
[Fact]
public void StaticPath_YieldsConditional()
{
var result = ReachabilityLattice.Evaluate(new[]
{
new ReachabilityEvidence(ReachabilityEvidenceKind.StaticPath, "path1")
});
result.State.Should().Be(ReachabilityState.Conditional);
result.Score.Should().Be(0.5);
}
[Fact]
public void RuntimeHit_PromotesReachableAndAddsBonus()
{
var result = ReachabilityLattice.Evaluate(new[]
{
new ReachabilityEvidence(ReachabilityEvidenceKind.StaticPath),
new ReachabilityEvidence(ReachabilityEvidenceKind.RuntimeHit)
});
result.State.Should().Be(ReachabilityState.Reachable);
result.Score.Should().Be(0.8);
}
[Fact]
public void RuntimeSinkHit_AddsAdditionalBonus()
{
var result = ReachabilityLattice.Evaluate(new[]
{
new ReachabilityEvidence(ReachabilityEvidenceKind.RuntimeHit),
new ReachabilityEvidence(ReachabilityEvidenceKind.RuntimeSinkHit)
});
result.State.Should().Be(ReachabilityState.Reachable);
result.Score.Should().Be(1.0);
}
[Fact]
public void Guard_DemotesWhenNoRuntimeEvidence()
{
var result = ReachabilityLattice.Evaluate(new[]
{
new ReachabilityEvidence(ReachabilityEvidenceKind.StaticPath),
new ReachabilityEvidence(ReachabilityEvidenceKind.Guard)
});
result.State.Should().Be(ReachabilityState.Unknown);
result.Score.Should().Be(0.3);
}
[Fact]
public void Mitigation_SetsUnreachableWhenNoRuntime()
{
var result = ReachabilityLattice.Evaluate(new[]
{
new ReachabilityEvidence(ReachabilityEvidenceKind.StaticPath),
new ReachabilityEvidence(ReachabilityEvidenceKind.Mitigation)
});
result.State.Should().Be(ReachabilityState.Unreachable);
result.Score.Should().Be(0.2);
}
[Fact]
public void OrderIndependentAndRounded()
{
var shuffled = new[]
{
new ReachabilityEvidence(ReachabilityEvidenceKind.Guard),
new ReachabilityEvidence(ReachabilityEvidenceKind.StaticPath),
new ReachabilityEvidence(ReachabilityEvidenceKind.RuntimeHit),
};
var result = ReachabilityLattice.Evaluate(shuffled);
result.State.Should().Be(ReachabilityState.Reachable);
result.Score.Should().Be(0.8);
}
}