Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls.
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user