nuget reorganization
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Globalization;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Observations;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno;
|
||||
@@ -18,6 +19,8 @@ public sealed class DenoLanguageAnalyzer : ILanguageAnalyzer
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
|
||||
await TryWriteRuntimeShimAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var workspace = await DenoWorkspaceNormalizer.NormalizeAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
var moduleGraph = DenoModuleGraphResolver.Resolve(workspace, cancellationToken);
|
||||
var compatibility = DenoNpmCompatibilityAdapter.Analyze(workspace, moduleGraph, cancellationToken);
|
||||
@@ -62,6 +65,8 @@ public sealed class DenoLanguageAnalyzer : ILanguageAnalyzer
|
||||
metadata: observationMetadata,
|
||||
evidence: observationEvidence);
|
||||
|
||||
TryIngestRuntimeTrace(context);
|
||||
|
||||
// Task 5+ will convert moduleGraph + compatibility and bundle insights into SBOM components and evidence records.
|
||||
GC.KeepAlive(moduleGraph);
|
||||
GC.KeepAlive(compatibility);
|
||||
@@ -70,6 +75,18 @@ public sealed class DenoLanguageAnalyzer : ILanguageAnalyzer
|
||||
GC.KeepAlive(observationDocument);
|
||||
}
|
||||
|
||||
private static async ValueTask TryWriteRuntimeShimAsync(LanguageAnalyzerContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DenoRuntimeShim.WriteAsync(context.RootPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Shim is best-effort; failure should not block static analysis.
|
||||
}
|
||||
}
|
||||
|
||||
private void TryPersistObservation(
|
||||
LanguageAnalyzerContext context,
|
||||
byte[] observationBytes,
|
||||
@@ -111,4 +128,59 @@ public sealed class DenoLanguageAnalyzer : ILanguageAnalyzer
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private void TryIngestRuntimeTrace(LanguageAnalyzerContext context)
|
||||
{
|
||||
if (context.AnalysisStore is not { } analysisStore)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tracePath = Path.Combine(context.RootPath, "deno-runtime.ndjson");
|
||||
if (!File.Exists(tracePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] content;
|
||||
try
|
||||
{
|
||||
content = File.ReadAllBytes(tracePath);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (metadata, hash) = DenoRuntimeTraceProbe.Analyze(content);
|
||||
var runtimeMeta = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["deno.runtime.hash"] = hash,
|
||||
["deno.runtime.event_count"] = metadata.EventCount.ToString(CultureInfo.InvariantCulture),
|
||||
["deno.runtime.permission_uses"] = metadata.PermissionUses.ToString(CultureInfo.InvariantCulture),
|
||||
["deno.runtime.module_loads"] = metadata.ModuleLoads.ToString(CultureInfo.InvariantCulture),
|
||||
["deno.runtime.remote_origins"] = string.Join(',', metadata.RemoteOrigins),
|
||||
["deno.runtime.permissions"] = string.Join(',', metadata.UniquePermissions),
|
||||
["deno.runtime.npm_resolutions"] = metadata.NpmResolutions.ToString(CultureInfo.InvariantCulture),
|
||||
["deno.runtime.wasm_loads"] = metadata.WasmLoads.ToString(CultureInfo.InvariantCulture),
|
||||
["deno.runtime.dynamic_imports"] = metadata.DynamicImports.ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
var payload = new AnalyzerObservationPayload(
|
||||
analyzerId: Id,
|
||||
kind: "deno.runtime.v1",
|
||||
mediaType: "application/x-ndjson",
|
||||
content: content,
|
||||
metadata: runtimeMeta,
|
||||
view: "runtime");
|
||||
|
||||
analysisStore.Set("deno.runtime", payload);
|
||||
|
||||
// Also emit policy signals into AnalysisStore for downstream consumption.
|
||||
var signals = DenoPolicySignalEmitter.FromTrace(hash, metadata);
|
||||
foreach (var signal in signals)
|
||||
{
|
||||
analysisStore.Set(signal.Key, signal.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the TypeScript runtime shim that captures Deno runtime events into NDJSON.
|
||||
/// This shim is written to disk alongside the analyzer to be invoked by the worker/CLI.
|
||||
/// </summary>
|
||||
internal static class DenoRuntimeShim
|
||||
{
|
||||
private const string ShimFileName = "trace-shim.ts";
|
||||
|
||||
public static string FileName => ShimFileName;
|
||||
|
||||
public static async Task<string> WriteAsync(string directory, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(directory);
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var path = Path.Combine(directory, ShimFileName);
|
||||
await File.WriteAllTextAsync(path, ShimSource, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
|
||||
return path;
|
||||
}
|
||||
|
||||
// NOTE: This shim is intentionally self contained and avoids network calls.
|
||||
private const string ShimSource = """
|
||||
// deno-runtime trace shim (offline, deterministic)
|
||||
// Emits module load, permission use, npm resolution, and wasm load events.
|
||||
const events: Array<Record<string, unknown>> = [];
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function addEvent(evt: Record<string, unknown>) {
|
||||
// Deterministic key order via stringify on object literal insertion order.
|
||||
events.push(evt);
|
||||
}
|
||||
|
||||
function hashPath(input: string): string {
|
||||
const data = new TextEncoder().encode(input);
|
||||
const hash = crypto.subtle.digestSync("SHA-256", data);
|
||||
return Array.from(new Uint8Array(hash))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function relPath(abs: string): { normalized: string; path_sha256: string } {
|
||||
const cwd = Deno.cwd();
|
||||
const rel = abs.startsWith(cwd) ? abs.slice(cwd.length + 1) : abs;
|
||||
const normalized = rel.replaceAll("\\", "/");
|
||||
return { normalized, path_sha256: hashPath(normalized) };
|
||||
}
|
||||
|
||||
// Wrap permission requests
|
||||
const originalPermissions = Deno.permissions;
|
||||
Deno.permissions = {
|
||||
...originalPermissions,
|
||||
request: async (...args: Parameters<typeof originalPermissions.request>) => {
|
||||
const res = await originalPermissions.request(...args);
|
||||
const name = args[0]?.name ?? "unknown";
|
||||
const module = relPath(import.meta.url);
|
||||
addEvent({
|
||||
type: "deno.permission.use",
|
||||
ts: nowIso(),
|
||||
permission: name,
|
||||
module,
|
||||
details: "permissions.request",
|
||||
});
|
||||
return res;
|
||||
},
|
||||
query: (...args: Parameters<typeof originalPermissions.query>) =>
|
||||
originalPermissions.query(...args),
|
||||
revoke: (...args: Parameters<typeof originalPermissions.revoke>) =>
|
||||
originalPermissions.revoke(...args),
|
||||
};
|
||||
|
||||
// Hook dynamic import calls by wrapping import()
|
||||
const originalImport = globalThis.import ?? ((specifier: string) => import(specifier));
|
||||
globalThis.import = async (specifier: string) => {
|
||||
const mod = typeof specifier === "string" ? specifier : String(specifier);
|
||||
addEvent({
|
||||
type: "deno.module.load",
|
||||
ts: nowIso(),
|
||||
module: relPath(mod),
|
||||
reason: "dynamic-import",
|
||||
permissions: [],
|
||||
origin: mod.startsWith("http") ? mod : undefined,
|
||||
});
|
||||
return originalImport(specifier);
|
||||
};
|
||||
|
||||
// Hook WebAssembly loads
|
||||
const originalInstantiate = WebAssembly.instantiate;
|
||||
WebAssembly.instantiate = async (
|
||||
bufferSource: BufferSource | WebAssembly.Module,
|
||||
importObject?: WebAssembly.Imports,
|
||||
) => {
|
||||
addEvent({
|
||||
type: "deno.wasm.load",
|
||||
ts: nowIso(),
|
||||
module: relPath("wasm://buffer"),
|
||||
importer: relPath(import.meta.url).normalized,
|
||||
reason: "instantiate",
|
||||
});
|
||||
return originalInstantiate(bufferSource, importObject);
|
||||
};
|
||||
|
||||
// Capture npm resolution hints from env when present
|
||||
const npmMeta = Deno.env.get("STELLA_NPM_SPECIFIER");
|
||||
if (npmMeta) {
|
||||
addEvent({
|
||||
type: "deno.npm.resolution",
|
||||
ts: nowIso(),
|
||||
specifier: npmMeta,
|
||||
package: npmMeta,
|
||||
version: "",
|
||||
resolved: "file://$DENO_DIR/npm",
|
||||
exists: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Write NDJSON on exit
|
||||
function flush() {
|
||||
const sorted = events.sort((a, b) => {
|
||||
const at = String(a.ts);
|
||||
const bt = String(b.ts);
|
||||
if (at === bt) return String(a.type).localeCompare(String(b.type));
|
||||
return at.localeCompare(bt);
|
||||
});
|
||||
const data = sorted.map((e) => JSON.stringify(e)).join("\\n") + "\\n";
|
||||
Deno.writeTextFileSync("deno-runtime.ndjson", data);
|
||||
}
|
||||
|
||||
addEvent({
|
||||
type: "deno.runtime.start",
|
||||
ts: nowIso(),
|
||||
module: relPath(import.meta.url),
|
||||
reason: "shim-start",
|
||||
});
|
||||
|
||||
globalThis.addEventListener("unload", () => {
|
||||
flush();
|
||||
});
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
|
||||
|
||||
internal static class DenoRuntimeTraceProbe
|
||||
{
|
||||
public static (DenoRuntimeTraceMetadata Metadata, string Sha256) Analyze(byte[] content)
|
||||
{
|
||||
var metadata = ComputeMetadata(content);
|
||||
var sha = ComputeSha256(content);
|
||||
return (metadata, sha);
|
||||
}
|
||||
|
||||
private static DenoRuntimeTraceMetadata ComputeMetadata(byte[] content)
|
||||
{
|
||||
var moduleLoads = 0;
|
||||
var permissionUses = 0;
|
||||
var origins = new HashSet<string>(StringComparer.Ordinal);
|
||||
var permissions = new HashSet<string>(StringComparer.Ordinal);
|
||||
var npmResolutions = 0;
|
||||
var wasmLoads = 0;
|
||||
var dynamicImports = 0;
|
||||
var events = 0;
|
||||
|
||||
if (content is null || content.Length == 0)
|
||||
{
|
||||
return new DenoRuntimeTraceMetadata(0, 0, 0, Array.Empty<string>(), Array.Empty<string>(), 0, 0, 0);
|
||||
}
|
||||
|
||||
var span = new ReadOnlySpan<byte>(content);
|
||||
var start = 0;
|
||||
|
||||
while (start < span.Length)
|
||||
{
|
||||
var end = span.Slice(start).IndexOf((byte)'\n');
|
||||
var lineLength = end >= 0 ? end : span.Length - start;
|
||||
var line = span.Slice(start, lineLength);
|
||||
start += lineLength + 1;
|
||||
|
||||
if (line.IsEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(line);
|
||||
if (!document.RootElement.TryGetProperty("type", out var typeProp))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var type = typeProp.GetString() ?? string.Empty;
|
||||
events++;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "deno.module.load":
|
||||
moduleLoads++;
|
||||
if (document.RootElement.TryGetProperty("origin", out var originProp)
|
||||
&& originProp.ValueKind == JsonValueKind.String
|
||||
&& !string.IsNullOrWhiteSpace(originProp.GetString()))
|
||||
{
|
||||
origins.Add(originProp.GetString()!);
|
||||
}
|
||||
|
||||
if (document.RootElement.TryGetProperty("reason", out var reasonProp)
|
||||
&& string.Equals(reasonProp.GetString(), "dynamic-import", StringComparison.Ordinal))
|
||||
{
|
||||
dynamicImports++;
|
||||
}
|
||||
|
||||
if (document.RootElement.TryGetProperty("permissions", out var permsProp)
|
||||
&& permsProp.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var perm in permsProp.EnumerateArray())
|
||||
{
|
||||
if (perm.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(perm.GetString()))
|
||||
{
|
||||
permissions.Add(perm.GetString()!.Trim().ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "deno.permission.use":
|
||||
permissionUses++;
|
||||
if (document.RootElement.TryGetProperty("permission", out var permProp)
|
||||
&& permProp.ValueKind == JsonValueKind.String
|
||||
&& !string.IsNullOrWhiteSpace(permProp.GetString()))
|
||||
{
|
||||
permissions.Add(permProp.GetString()!.Trim().ToLowerInvariant());
|
||||
}
|
||||
break;
|
||||
|
||||
case "deno.npm.resolution":
|
||||
npmResolutions++;
|
||||
break;
|
||||
|
||||
case "deno.wasm.load":
|
||||
wasmLoads++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return new DenoRuntimeTraceMetadata(
|
||||
EventCount: events,
|
||||
ModuleLoads: moduleLoads,
|
||||
PermissionUses: permissionUses,
|
||||
RemoteOrigins: origins.OrderBy(o => o, StringComparer.Ordinal).ToArray(),
|
||||
UniquePermissions: permissions.OrderBy(p => p, StringComparer.Ordinal).ToArray(),
|
||||
NpmResolutions: npmResolutions,
|
||||
WasmLoads: wasmLoads,
|
||||
DynamicImports: dynamicImports);
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] content)
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
var hash = sha.ComputeHash(content ?? Array.Empty<byte>());
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user