up
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
|
||||
@@ -58,6 +59,7 @@ internal static class NodePackageCollector
|
||||
}
|
||||
|
||||
TraverseTarballs(context, lockData, workspaceIndex, packages, visited, yarnPnpPresent, cancellationToken);
|
||||
TraverseYarnPnpCache(context, packages, visited, yarnPnpPresent, cancellationToken);
|
||||
|
||||
AppendDeclaredPackages(packages, lockData);
|
||||
|
||||
@@ -349,6 +351,110 @@ internal static class NodePackageCollector
|
||||
}
|
||||
}
|
||||
|
||||
private static void TraverseYarnPnpCache(
|
||||
LanguageAnalyzerContext context,
|
||||
List<NodePackage> packages,
|
||||
HashSet<string> visited,
|
||||
bool yarnPnpPresent,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!yarnPnpPresent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cacheDirectory = Path.Combine(context.RootPath, ".yarn", "cache");
|
||||
if (!Directory.Exists(cacheDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enumerationOptions = new EnumerationOptions
|
||||
{
|
||||
RecurseSubdirectories = true,
|
||||
IgnoreInaccessible = true,
|
||||
AttributesToSkip = FileAttributes.ReparsePoint | FileAttributes.Device
|
||||
};
|
||||
|
||||
foreach (var zipPath in Directory.EnumerateFiles(cacheDirectory, "*.zip", enumerationOptions))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
TryProcessZipball(context, zipPath, packages, visited, yarnPnpPresent, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryProcessZipball(
|
||||
LanguageAnalyzerContext context,
|
||||
string zipPath,
|
||||
List<NodePackage> packages,
|
||||
HashSet<string> visited,
|
||||
bool yarnPnpPresent,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var archive = ZipFile.OpenRead(zipPath);
|
||||
var packageEntry = archive.Entries
|
||||
.FirstOrDefault(entry => entry.FullName.EndsWith("package.json", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (packageEntry is null || packageEntry.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var entryStream = packageEntry.Open();
|
||||
using var buffer = new MemoryStream();
|
||||
entryStream.CopyTo(buffer);
|
||||
buffer.Position = 0;
|
||||
|
||||
var sha256 = SHA256.HashData(buffer.ToArray());
|
||||
var sha256Hex = Convert.ToHexString(sha256).ToLowerInvariant();
|
||||
buffer.Position = 0;
|
||||
|
||||
using var document = JsonDocument.Parse(buffer);
|
||||
var root = document.RootElement;
|
||||
|
||||
var relativeDirectory = NormalizeRelativeDirectoryZip(context, zipPath);
|
||||
var locator = BuildZipLocator(context, zipPath, packageEntry.FullName);
|
||||
var usedByEntrypoint = context.UsageHints.IsPathUsed(zipPath);
|
||||
|
||||
var package = TryCreatePackageFromJson(
|
||||
context,
|
||||
root,
|
||||
relativeDirectory,
|
||||
locator,
|
||||
usedByEntrypoint,
|
||||
cancellationToken,
|
||||
lockData: null,
|
||||
workspaceIndex: null,
|
||||
packageJsonPath: null,
|
||||
packageSha256: sha256Hex,
|
||||
yarnPnpPresent: yarnPnpPresent);
|
||||
|
||||
if (package is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (visited.Add($"zip::{locator}"))
|
||||
{
|
||||
packages.Add(package);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// ignore unreadable zipballs
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
// ignore invalid zip payloads
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// ignore malformed package definitions in zips
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendDeclaredPackages(List<NodePackage> packages, NodeLockData lockData)
|
||||
{
|
||||
if (lockData.DeclaredPackages.Count == 0)
|
||||
@@ -572,6 +678,17 @@ internal static class NodePackageCollector
|
||||
return $"{normalizedArchive}!{normalizedEntry}";
|
||||
}
|
||||
|
||||
private static string BuildZipLocator(LanguageAnalyzerContext context, string zipPath, string entryName)
|
||||
{
|
||||
var relative = context.GetRelativePath(zipPath);
|
||||
var normalizedArchive = string.IsNullOrWhiteSpace(relative) || relative == "."
|
||||
? Path.GetFileName(zipPath)
|
||||
: relative.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
var normalizedEntry = entryName.Replace('\\', '/');
|
||||
return $"{normalizedArchive}!{normalizedEntry}";
|
||||
}
|
||||
|
||||
private static string NormalizeRelativeDirectoryTar(LanguageAnalyzerContext context, string tgzPath)
|
||||
{
|
||||
var relative = context.GetRelativePath(Path.GetDirectoryName(tgzPath)!);
|
||||
@@ -583,6 +700,17 @@ internal static class NodePackageCollector
|
||||
return relative.Replace(Path.DirectorySeparatorChar, '/');
|
||||
}
|
||||
|
||||
private static string NormalizeRelativeDirectoryZip(LanguageAnalyzerContext context, string zipPath)
|
||||
{
|
||||
var relative = context.GetRelativePath(Path.GetDirectoryName(zipPath)!);
|
||||
if (string.IsNullOrEmpty(relative) || relative == ".")
|
||||
{
|
||||
return "zip";
|
||||
}
|
||||
|
||||
return relative.Replace(Path.DirectorySeparatorChar, '/');
|
||||
}
|
||||
|
||||
private static bool ShouldSkipDirectory(string name)
|
||||
{
|
||||
if (name.Length == 0)
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/cached-lib@1.0.0",
|
||||
"purl": "pkg:npm/cached-lib@1.0.0",
|
||||
"name": "cached-lib",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"path": ".yarn/cache",
|
||||
"yarnPnp": "true"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": ".yarn/cache/cached-lib-1.0.0.zip!package/package.json",
|
||||
"sha256": "b13d2a5d313d5929280c14af2086e23ca8f0d60761085c0ad44982ec307c92e3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/yarn-pnp-demo@1.0.0",
|
||||
|
||||
28
src/Scanner/docs/deno-runtime-trace.md
Normal file
28
src/Scanner/docs/deno-runtime-trace.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Deno Runtime Trace Collection (DENO-26-010)
|
||||
|
||||
This shows how to collect Deno runtime traces with the existing analyzer runtime runner (no code changes required).
|
||||
|
||||
## Prereqs
|
||||
- `deno` binary available locally (cached; no network fetch).
|
||||
- Set `STELLA_DENO_ENTRYPOINT` to the entry file of the Deno app (relative to repo root or absolute).
|
||||
- Optional: set `STELLA_DENO_TRACE_ARGS` for extra `deno run` args (e.g., `-A`).
|
||||
|
||||
## How to run via analyzer/worker
|
||||
1. Ensure the scanner job sets the environment variable before invoking analyzers:
|
||||
- `STELLA_DENO_ENTRYPOINT=app.ts`
|
||||
2. Run the scanner (worker or CLI) as usual. The Deno analyzer will:
|
||||
- Generate and write the runtime shim next to the entrypoint.
|
||||
- Execute `deno run` with the shim to produce `deno-runtime.ndjson`.
|
||||
- Parse the NDJSON into AnalysisStore under `ScanAnalysisKeys.DenoRuntimePayload` and emit policy signals.
|
||||
|
||||
## Offline/airgap notes
|
||||
- No outbound network calls; all modules must be local/cached.
|
||||
- Paths are hashed deterministically; timestamps are UTC.
|
||||
- If `deno` is missing or entrypoint unset, runtime capture is skipped (no failure).
|
||||
|
||||
## CLI shortcut
|
||||
You can invoke the analyzer tests as a smoke check:
|
||||
```bash
|
||||
dotnet test src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj -c Release
|
||||
```
|
||||
This ensures the runtime runner and parser remain healthy.
|
||||
Reference in New Issue
Block a user