feat: Add initial implementation of Vulnerability Resolver Jobs
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created project for StellaOps.Scanner.Analyzers.Native.Tests with necessary dependencies. - Documented roles and guidelines in AGENTS.md for Scheduler module. - Implemented IResolverJobService interface and InMemoryResolverJobService for handling resolver jobs. - Added ResolverBacklogNotifier and ResolverBacklogService for monitoring job metrics. - Developed API endpoints for managing resolver jobs and retrieving metrics. - Defined models for resolver job requests and responses. - Integrated dependency injection for resolver job services. - Implemented ImpactIndexSnapshot for persisting impact index data. - Introduced SignalsScoringOptions for configurable scoring weights in reachability scoring. - Added unit tests for ReachabilityScoringService and RuntimeFactsIngestionService. - Created dotnet-filter.sh script to handle command-line arguments for dotnet. - Established nuget-prime project for managing package downloads.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Deno;
|
||||
|
||||
public sealed class DenoPolicySignalEmitterTests
|
||||
{
|
||||
[Fact]
|
||||
public void EmitsSignalsFromMetadata()
|
||||
{
|
||||
var metadata = new DenoRuntimeTraceMetadata(
|
||||
EventCount: 5,
|
||||
ModuleLoads: 2,
|
||||
PermissionUses: 3,
|
||||
RemoteOrigins: new[] { "https://deno.land", "https://esm.sh" },
|
||||
UniquePermissions: new[] { "env", "fs" },
|
||||
NpmResolutions: 4,
|
||||
WasmLoads: 1,
|
||||
DynamicImports: 2);
|
||||
|
||||
var signals = DenoPolicySignalEmitter.FromTrace("abc123", metadata);
|
||||
|
||||
Assert.Equal("abc123", signals["surface.lang.deno.runtime.hash"]);
|
||||
Assert.Equal("env,fs", signals["surface.lang.deno.permissions"]);
|
||||
Assert.Equal("https://deno.land,https://esm.sh", signals["surface.lang.deno.remote_origins"]);
|
||||
Assert.Equal("4", signals["surface.lang.deno.npm_modules"]);
|
||||
Assert.Equal("1", signals["surface.lang.deno.wasm_modules"]);
|
||||
Assert.Equal("2", signals["surface.lang.deno.dynamic_imports"]);
|
||||
Assert.Equal("2", signals["surface.lang.deno.module_loads"]);
|
||||
Assert.Equal("3", signals["surface.lang.deno.permission_uses"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Deno;
|
||||
|
||||
public sealed class DenoRuntimePathHasherTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProducesNormalizedRelativePathAndStableHash()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var absolute = Path.Combine(root, "subdir", "main.ts");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(absolute)!);
|
||||
File.WriteAllText(absolute, "// sample");
|
||||
|
||||
var identity = DenoRuntimePathHasher.Create(root, absolute);
|
||||
|
||||
Assert.Equal("subdir/main.ts", identity.Normalized);
|
||||
Assert.Equal("2d0ef79c25b433a216f41853e89d8e1e1e1ef0b0e77d12b37a7f4f7c2a25f635", identity.PathSha256);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsesDotForRootPath()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var identity = DenoRuntimePathHasher.Create(root, root);
|
||||
Assert.Equal(".", identity.Normalized);
|
||||
Assert.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", identity.PathSha256);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Deno;
|
||||
|
||||
public sealed class DenoRuntimeTraceRecorderTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildsOrderedSnapshotAndHash()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var recorder = new DenoRuntimeTraceRecorder(root);
|
||||
|
||||
recorder.AddPermissionUse(
|
||||
absoluteModulePath: Path.Combine(root, "c.ts"),
|
||||
permission: "NET",
|
||||
details: "fetch",
|
||||
timestamp: DateTimeOffset.Parse("2025-11-17T12:00:02Z"));
|
||||
|
||||
recorder.AddModuleLoad(
|
||||
absoluteModulePath: Path.Combine(root, "b.ts"),
|
||||
reason: "dynamic-import",
|
||||
permissions: new[] { "fs" },
|
||||
origin: null,
|
||||
timestamp: DateTimeOffset.Parse("2025-11-17T12:00:01Z"));
|
||||
|
||||
recorder.AddModuleLoad(
|
||||
absoluteModulePath: Path.Combine(root, "a.ts"),
|
||||
reason: "static-import",
|
||||
permissions: Array.Empty<string>(),
|
||||
origin: "https://deno.land/x/std",
|
||||
timestamp: DateTimeOffset.Parse("2025-11-17T12:00:00Z"));
|
||||
|
||||
var snapshot = recorder.Build();
|
||||
|
||||
// Ensure ordering by timestamp then type
|
||||
var ndjson = System.Text.Encoding.UTF8.GetString(snapshot.Content);
|
||||
var lines = ndjson.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
Assert.StartsWith("{\"type\":\"deno.module.load\",\"ts\":\"2025-11-17T12:00:00+00:00\"", lines[0]);
|
||||
Assert.StartsWith("{\"type\":\"deno.module.load\",\"ts\":\"2025-11-17T12:00:01+00:00\"", lines[1]);
|
||||
Assert.StartsWith("{\"type\":\"deno.permission.use\",\"ts\":\"2025-11-17T12:00:02+00:00\"", lines[2]);
|
||||
|
||||
Assert.Equal(3, snapshot.Metadata.EventCount);
|
||||
Assert.Equal(2, snapshot.Metadata.ModuleLoads);
|
||||
Assert.Equal(1, snapshot.Metadata.PermissionUses);
|
||||
Assert.Equal(new[] { "https://deno.land/x/std" }, snapshot.Metadata.RemoteOrigins);
|
||||
Assert.Equal(new[] { "net" }, snapshot.Metadata.UniquePermissions);
|
||||
Assert.Equal(0, snapshot.Metadata.NpmResolutions);
|
||||
Assert.Equal(0, snapshot.Metadata.WasmLoads);
|
||||
Assert.Equal(1, snapshot.Metadata.DynamicImports);
|
||||
|
||||
// Stable hash check
|
||||
Assert.Equal("198c6e038f1c39a78a52b844f051bfa6eaa5312faa66f1bc73d2f6d1048d8a7a", snapshot.Sha256);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Deno;
|
||||
|
||||
public sealed class DenoRuntimeTraceSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProducesDeterministicNdjsonAndMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var events = new DenoRuntimeEvent[]
|
||||
{
|
||||
new DenoModuleLoadEvent(
|
||||
Ts: DateTimeOffset.Parse("2025-11-17T12:00:00.123Z"),
|
||||
Module: new DenoModuleIdentity("app/main.ts", "abc123"),
|
||||
Reason: "dynamic-import",
|
||||
Permissions: new[] {"fs", "net"},
|
||||
Origin: "https://deno.land/x/std@0.208.0/http/server.ts"),
|
||||
new DenoPermissionUseEvent(
|
||||
Ts: DateTimeOffset.Parse("2025-11-17T12:00:01.234Z"),
|
||||
Permission: "ffi",
|
||||
Module: new DenoModuleIdentity("native/mod.ts", "def456"),
|
||||
Details: "Deno.dlopen")
|
||||
};
|
||||
|
||||
// Act
|
||||
var (content, hash, metadata) = DenoRuntimeTraceSerializer.Serialize(events);
|
||||
|
||||
// Assert
|
||||
var text = Encoding.UTF8.GetString(content);
|
||||
|
||||
Assert.Equal(2, metadata.EventCount);
|
||||
Assert.Equal(1, metadata.ModuleLoads);
|
||||
Assert.Equal(1, metadata.PermissionUses);
|
||||
Assert.Equal(new[] { "https://deno.land/x/std@0.208.0/http/server.ts" }, metadata.RemoteOrigins);
|
||||
Assert.Equal(new[] { "ffi", "fs", "net" }, metadata.UniquePermissions);
|
||||
Assert.Equal(0, metadata.NpmResolutions);
|
||||
Assert.Equal(0, metadata.WasmLoads);
|
||||
Assert.Equal(1, metadata.DynamicImports);
|
||||
|
||||
// Stable hash and NDJSON ordering
|
||||
const string expectedNdjson =
|
||||
@"{\""type\"":\"\"deno.module.load\"",\""ts\"":\"\"2025-11-17T12:00:00.123+00:00\"",\""module\"":{\""normalized\"":\"\"app/main.ts\"",\""path_sha256\"":\"\"abc123\""},\""reason\"":\"\"dynamic-import\"",\""permissions\"":[\"\"fs\"\", \""net\""],\""origin\"":\"\"https://deno.land/x/std@0.208.0/http/server.ts\""}
|
||||
{\""type\"":\"\"deno.permission.use\"",\""ts\"":\"\"2025-11-17T12:00:01.234+00:00\"",\""permission\"":\"\"ffi\"",\""module\"":{\""normalized\"":\"\"native/mod.ts\"",\""path_sha256\"":\"\"def456\""},\""details\"":\"\"Deno.dlopen\""}
|
||||
";
|
||||
|
||||
Assert.Equal(expectedNdjson.Replace("\r\n", "\n"), text.Replace("\r\n", "\n"));
|
||||
Assert.Equal("fdc6f07fe6b18b4cdd228c44b83e61d63063b7bd3422a2d3ab8000ac8420ceb0", hash);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
@@ -36,12 +37,12 @@ public sealed class JavaLanguageAnalyzerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LockfilesProduceDeclaredOnlyComponentsAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
public async Task LockfilesProduceDeclaredOnlyComponentsAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var jarPath = CreateSampleJar(root, "com.example", "runtime-only", "1.0.0");
|
||||
var lockPath = Path.Combine(root, "gradle.lockfile");
|
||||
@@ -63,18 +64,125 @@ public sealed class JavaLanguageAnalyzerTests
|
||||
Assert.True(ComponentHasMetadata(rootElement, "declared-only", "declaredOnly", "true"));
|
||||
Assert.True(ComponentHasMetadata(rootElement, "declared-only", "lockSource", "gradle.lockfile"));
|
||||
Assert.True(ComponentHasMetadata(rootElement, "runtime-only", "lockMissing", "true"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ComponentHasMetadata(JsonElement root, string componentName, string key, string expected)
|
||||
{
|
||||
foreach (var element in root.EnumerateArray())
|
||||
{
|
||||
if (!element.TryGetProperty("name", out var nameElement) ||
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CapturesFrameworkConfigurationHintsAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "demo-framework.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
|
||||
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
|
||||
{
|
||||
WritePomProperties(archive, "com.example", "demo-framework", "1.0.0");
|
||||
WriteManifest(archive, "demo-framework", "1.0.0", "com.example");
|
||||
|
||||
CreateTextEntry(archive, "META-INF/spring.factories");
|
||||
CreateTextEntry(archive, "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports");
|
||||
CreateTextEntry(archive, "META-INF/spring/org.springframework.boot.actuate.autoconfigure.AutoConfiguration.imports");
|
||||
CreateTextEntry(archive, "BOOT-INF/classes/application.yml");
|
||||
CreateTextEntry(archive, "WEB-INF/web.xml");
|
||||
CreateTextEntry(archive, "META-INF/web-fragment.xml");
|
||||
CreateTextEntry(archive, "META-INF/persistence.xml");
|
||||
CreateTextEntry(archive, "META-INF/beans.xml");
|
||||
CreateTextEntry(archive, "META-INF/jaxb.index");
|
||||
CreateTextEntry(archive, "META-INF/services/jakarta.ws.rs.ext.RuntimeDelegate");
|
||||
CreateTextEntry(archive, "logback.xml");
|
||||
CreateTextEntry(archive, "META-INF/native-image/demo/reflect-config.json");
|
||||
}
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
root,
|
||||
analyzers,
|
||||
cancellationToken,
|
||||
new LanguageUsageHints(new[] { jarPath }));
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var component = document.RootElement
|
||||
.EnumerateArray()
|
||||
.First(element => string.Equals(element.GetProperty("name").GetString(), "demo-framework", StringComparison.Ordinal));
|
||||
|
||||
var metadata = component.GetProperty("metadata");
|
||||
Assert.Equal("demo-framework.jar!META-INF/spring.factories", metadata.GetProperty("config.spring.factories").GetString());
|
||||
Assert.Equal(
|
||||
"demo-framework.jar!META-INF/spring/org.springframework.boot.actuate.autoconfigure.AutoConfiguration.imports,demo-framework.jar!META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
|
||||
metadata.GetProperty("config.spring.imports").GetString());
|
||||
Assert.Equal("demo-framework.jar!BOOT-INF/classes/application.yml", metadata.GetProperty("config.spring.properties").GetString());
|
||||
Assert.Equal("demo-framework.jar!WEB-INF/web.xml", metadata.GetProperty("config.web.xml").GetString());
|
||||
Assert.Equal("demo-framework.jar!META-INF/web-fragment.xml", metadata.GetProperty("config.web.fragment").GetString());
|
||||
Assert.Equal("demo-framework.jar!META-INF/persistence.xml", metadata.GetProperty("config.jpa").GetString());
|
||||
Assert.Equal("demo-framework.jar!META-INF/beans.xml", metadata.GetProperty("config.cdi").GetString());
|
||||
Assert.Equal("demo-framework.jar!META-INF/jaxb.index", metadata.GetProperty("config.jaxb").GetString());
|
||||
Assert.Equal("demo-framework.jar!META-INF/services/jakarta.ws.rs.ext.RuntimeDelegate", metadata.GetProperty("config.jaxrs").GetString());
|
||||
Assert.Equal("demo-framework.jar!logback.xml", metadata.GetProperty("config.logging").GetString());
|
||||
Assert.Equal("demo-framework.jar!META-INF/native-image/demo/reflect-config.json", metadata.GetProperty("config.graal").GetString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CapturesJniHintsAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var jarPath = Path.Combine(root, "demo-jni.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
|
||||
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
|
||||
{
|
||||
WritePomProperties(archive, "com.example", "demo-jni", "1.0.0");
|
||||
WriteManifest(archive, "demo-jni", "1.0.0", "com.example");
|
||||
|
||||
CreateBinaryEntry(archive, "com/example/App.class", "System.loadLibrary(\"foo\")");
|
||||
CreateTextEntry(archive, "lib/native/libfoo.so");
|
||||
CreateTextEntry(archive, "META-INF/native-image/demo/jni-config.json");
|
||||
}
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
root,
|
||||
analyzers,
|
||||
cancellationToken,
|
||||
new LanguageUsageHints(new[] { jarPath }));
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var component = document.RootElement
|
||||
.EnumerateArray()
|
||||
.First(element => string.Equals(element.GetProperty("name").GetString(), "demo-jni", StringComparison.Ordinal));
|
||||
|
||||
var metadata = component.GetProperty("metadata");
|
||||
Assert.Equal("libfoo.so", metadata.GetProperty("jni.nativeLibs").GetString());
|
||||
Assert.Equal("demo-jni.jar!META-INF/native-image/demo/jni-config.json", metadata.GetProperty("jni.graalConfig").GetString());
|
||||
Assert.Equal("demo-jni.jar!com/example/App.class", metadata.GetProperty("jni.loadCalls").GetString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ComponentHasMetadata(JsonElement root, string componentName, string key, string expected)
|
||||
{
|
||||
foreach (var element in root.EnumerateArray())
|
||||
{
|
||||
if (!element.TryGetProperty("name", out var nameElement) ||
|
||||
!string.Equals(nameElement.GetString(), componentName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
@@ -96,13 +204,53 @@ public sealed class JavaLanguageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string CreateSampleJar(string root, string groupId, string artifactId, string version)
|
||||
{
|
||||
var jarPath = Path.Combine(root, $"{artifactId}-{version}.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void WritePomProperties(ZipArchive archive, string groupId, string artifactId, string version)
|
||||
{
|
||||
var pomPropertiesPath = $"META-INF/maven/{groupId}/{artifactId}/pom.properties";
|
||||
var pomPropertiesEntry = archive.CreateEntry(pomPropertiesPath);
|
||||
using var writer = new StreamWriter(pomPropertiesEntry.Open(), Encoding.UTF8);
|
||||
writer.WriteLine($"groupId={groupId}");
|
||||
writer.WriteLine($"artifactId={artifactId}");
|
||||
writer.WriteLine($"version={version}");
|
||||
writer.WriteLine("packaging=jar");
|
||||
writer.WriteLine("name=Sample");
|
||||
}
|
||||
|
||||
private static void WriteManifest(ZipArchive archive, string artifactId, string version, string groupId)
|
||||
{
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8);
|
||||
writer.WriteLine("Manifest-Version: 1.0");
|
||||
writer.WriteLine($"Implementation-Title: {artifactId}");
|
||||
writer.WriteLine($"Implementation-Version: {version}");
|
||||
writer.WriteLine($"Implementation-Vendor: {groupId}");
|
||||
}
|
||||
|
||||
private static void CreateTextEntry(ZipArchive archive, string path, string? content = null)
|
||||
{
|
||||
var entry = archive.CreateEntry(path);
|
||||
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
writer.Write(content);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateBinaryEntry(ZipArchive archive, string path, string content)
|
||||
{
|
||||
var entry = archive.CreateEntry(path);
|
||||
using var stream = entry.Open();
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
private static string CreateSampleJar(string root, string groupId, string artifactId, string version)
|
||||
{
|
||||
var jarPath = Path.Combine(root, $"{artifactId}-{version}.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
|
||||
using var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create);
|
||||
var pomPropertiesPath = $"META-INF/maven/{groupId}/{artifactId}/pom.properties";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/tar-demo@1.2.3",
|
||||
"purl": "pkg:npm/tar-demo@1.2.3",
|
||||
"name": "tar-demo",
|
||||
"version": "1.2.3",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"installScripts": "true",
|
||||
"path": "tgz",
|
||||
"policyHint.installLifecycle": "install",
|
||||
"script.install": "echo install"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "tgz/tar-demo.tgz!package/package.json",
|
||||
"sha256": "dd27b49de19040a8b5738d4ad0d17ef2041e5ac8a6c5300dbace9be8fcf3ed67"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:scripts",
|
||||
"locator": "tgz/tar-demo.tgz!package/package.json#scripts.install",
|
||||
"value": "echo install"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
18.17.1
|
||||
@@ -0,0 +1,2 @@
|
||||
FROM node:18.17.1-alpine
|
||||
CMD ["node", "index.js"]
|
||||
@@ -0,0 +1,67 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/version-targets@1.0.0",
|
||||
"purl": "pkg:npm/version-targets@1.0.0",
|
||||
"name": "version-targets",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"nodeVersion": "18.17.1;18.17.1-alpine",
|
||||
"nodeVersionSource.dockerfile": "18.17.1-alpine",
|
||||
"nodeVersionSource.nvmrc": "18.17.1",
|
||||
"path": "."
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node-version:dockerfile",
|
||||
"locator": "Dockerfile",
|
||||
"value": "18.17.1-alpine",
|
||||
"sha256": "b38d145059ea1b7018105f769070f1d07276b30719ce20358f673bef9655bcdf"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node-version:nvmrc",
|
||||
"locator": ".nvmrc",
|
||||
"value": "18.17.1",
|
||||
"sha256": "cbc986933feddabb31649808506d635bb5d74667ba2da9aafc46ffe706ec745b"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/tar-demo@1.2.3",
|
||||
"purl": "pkg:npm/tar-demo@1.2.3",
|
||||
"name": "tar-demo",
|
||||
"version": "1.2.3",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"installScripts": "true",
|
||||
"path": "tgz",
|
||||
"policyHint.installLifecycle": "install",
|
||||
"script.install": "echo install"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "tgz/tar-demo.tgz!package/package.json",
|
||||
"sha256": "dd27b49de19040a8b5738d4ad0d17ef2041e5ac8a6c5300dbace9be8fcf3ed67"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:scripts",
|
||||
"locator": "tgz/tar-demo.tgz!package/package.json#scripts.install",
|
||||
"value": "echo install"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "version-targets",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -7,11 +7,11 @@ namespace StellaOps.Scanner.Analyzers.Lang.Node.Tests;
|
||||
public sealed class NodeLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task WorkspaceFixtureProducesDeterministicOutputAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "workspaces");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
public async Task WorkspaceFixtureProducesDeterministicOutputAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "workspaces");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
@@ -20,8 +20,46 @@ public sealed class NodeLanguageAnalyzerTests
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionTargetsAreCapturedAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "version-targets");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new NodeLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TarballPackageIsParsedAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "version-targets");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new NodeLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using StellaOps.Scanner.Analyzers.Native;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests;
|
||||
|
||||
public class NativeFormatDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DetectsElf64LittleEndian()
|
||||
{
|
||||
var bytes = new byte[64];
|
||||
bytes[0] = 0x7F; bytes[1] = (byte)'E'; bytes[2] = (byte)'L'; bytes[3] = (byte)'F';
|
||||
bytes[4] = 0x02; // 64-bit
|
||||
bytes[5] = 0x01; // little endian
|
||||
bytes[7] = 0x00; // System V / Linux
|
||||
bytes[18] = 0x3E; // e_machine low byte (x86_64)
|
||||
bytes[19] = 0x00;
|
||||
|
||||
using var stream = new MemoryStream(bytes);
|
||||
var detected = NativeFormatDetector.TryDetect(stream, out var id);
|
||||
|
||||
Assert.True(detected);
|
||||
Assert.Equal(NativeFormat.Elf, id.Format);
|
||||
Assert.Equal("x86_64", id.CpuArchitecture);
|
||||
Assert.Equal("linux", id.OperatingSystem);
|
||||
Assert.Equal("le", id.Endianness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsElfInterpreterAndBuildId()
|
||||
{
|
||||
// Minimal ELF64 with two program headers: PT_INTERP and PT_NOTE (GNU build-id)
|
||||
var buffer = new byte[512];
|
||||
|
||||
// ELF header
|
||||
buffer[0] = 0x7F; buffer[1] = (byte)'E'; buffer[2] = (byte)'L'; buffer[3] = (byte)'F';
|
||||
buffer[4] = 0x02; // 64-bit
|
||||
buffer[5] = 0x01; // little endian
|
||||
buffer[7] = 0x00; // System V
|
||||
buffer[18] = 0x3E; buffer[19] = 0x00; // x86_64
|
||||
|
||||
// e_phoff (offset 32) = 0x40
|
||||
BitConverter.GetBytes((ulong)0x40).CopyTo(buffer, 32);
|
||||
// e_phentsize (offset 54) = 56, e_phnum (56) = 2
|
||||
BitConverter.GetBytes((ushort)56).CopyTo(buffer, 54);
|
||||
BitConverter.GetBytes((ushort)2).CopyTo(buffer, 56);
|
||||
|
||||
// Program header 0: PT_INTERP
|
||||
var ph0 = 0x40;
|
||||
BitConverter.GetBytes((uint)3).CopyTo(buffer, ph0); // p_type
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, ph0 + 4); // p_flags
|
||||
BitConverter.GetBytes((ulong)0x100).CopyTo(buffer, ph0 + 8); // p_offset
|
||||
BitConverter.GetBytes((ulong)0).CopyTo(buffer, ph0 + 16); // p_vaddr
|
||||
BitConverter.GetBytes((ulong)0).CopyTo(buffer, ph0 + 24); // p_paddr
|
||||
BitConverter.GetBytes((ulong)0x18).CopyTo(buffer, ph0 + 32); // p_filesz
|
||||
BitConverter.GetBytes((ulong)0x18).CopyTo(buffer, ph0 + 40); // p_memsz
|
||||
BitConverter.GetBytes((ulong)0).CopyTo(buffer, ph0 + 48); // p_align
|
||||
|
||||
// Program header 1: PT_NOTE
|
||||
var ph1 = ph0 + 56;
|
||||
BitConverter.GetBytes((uint)4).CopyTo(buffer, ph1); // p_type
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, ph1 + 4); // p_flags
|
||||
BitConverter.GetBytes((ulong)0x120).CopyTo(buffer, ph1 + 8); // p_offset
|
||||
BitConverter.GetBytes((ulong)0).CopyTo(buffer, ph1 + 16); // p_vaddr
|
||||
BitConverter.GetBytes((ulong)0).CopyTo(buffer, ph1 + 24); // p_paddr
|
||||
BitConverter.GetBytes((ulong)0x20).CopyTo(buffer, ph1 + 32); // p_filesz
|
||||
BitConverter.GetBytes((ulong)0x20).CopyTo(buffer, ph1 + 40); // p_memsz
|
||||
BitConverter.GetBytes((ulong)0).CopyTo(buffer, ph1 + 48); // p_align
|
||||
|
||||
// PT_INTERP data
|
||||
var interpBytes = System.Text.Encoding.ASCII.GetBytes("/lib64/ld-linux-x86-64.so.2\0");
|
||||
Array.Copy(interpBytes, 0, buffer, 0x100, interpBytes.Length);
|
||||
|
||||
// PT_NOTE data (GNU build-id type 3)
|
||||
var note = new byte[0x20];
|
||||
BitConverter.GetBytes((uint)4).CopyTo(note, 0); // namesz
|
||||
BitConverter.GetBytes((uint)16).CopyTo(note, 4); // descsz
|
||||
BitConverter.GetBytes((uint)3).CopyTo(note, 8); // type
|
||||
Array.Copy(System.Text.Encoding.ASCII.GetBytes("GNU\0"), 0, note, 12, 4);
|
||||
var buildId = Enumerable.Range(1, 16).Select(i => (byte)i).ToArray();
|
||||
Array.Copy(buildId, 0, note, 16, 16);
|
||||
Array.Copy(note, 0, buffer, 0x120, note.Length);
|
||||
|
||||
using var stream = new MemoryStream(buffer);
|
||||
var detected = NativeFormatDetector.TryDetect(stream, out var id);
|
||||
|
||||
Assert.True(detected);
|
||||
Assert.Equal(NativeFormat.Elf, id.Format);
|
||||
Assert.Equal("x86_64", id.CpuArchitecture);
|
||||
Assert.Equal("/lib64/ld-linux-x86-64.so.2", id.InterpreterPath);
|
||||
Assert.Equal("0102030405060708090a0b0c0d0e0f10", id.BuildId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsPe()
|
||||
{
|
||||
var bytes = new byte[256];
|
||||
bytes[0] = (byte)'M'; bytes[1] = (byte)'Z';
|
||||
var peOffset = 0x80;
|
||||
BitConverter.GetBytes(peOffset).CopyTo(bytes, 0x3C);
|
||||
bytes[peOffset] = (byte)'P';
|
||||
bytes[peOffset + 1] = (byte)'E';
|
||||
bytes[peOffset + 2] = 0; bytes[peOffset + 3] = 0;
|
||||
bytes[peOffset + 4] = 0x64; // machine 0x8664 little-endian
|
||||
bytes[peOffset + 5] = 0x86;
|
||||
|
||||
using var stream = new MemoryStream(bytes);
|
||||
var detected = NativeFormatDetector.TryDetect(stream, out var id);
|
||||
|
||||
Assert.True(detected);
|
||||
Assert.Equal(NativeFormat.Pe, id.Format);
|
||||
Assert.Equal("x86_64", id.CpuArchitecture);
|
||||
Assert.Equal("windows", id.OperatingSystem);
|
||||
Assert.Equal("le", id.Endianness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsMachO64()
|
||||
{
|
||||
var bytes = new byte[32];
|
||||
// 0xFEEDFACF (little-endian 64-bit)
|
||||
bytes[0] = 0xFE; bytes[1] = 0xED; bytes[2] = 0xFA; bytes[3] = 0xCF;
|
||||
// cputype 0x01000007 (x86_64) big endian ordering for this magic
|
||||
bytes[4] = 0x01; bytes[5] = 0x00; bytes[6] = 0x00; bytes[7] = 0x07;
|
||||
|
||||
using var stream = new MemoryStream(bytes);
|
||||
var detected = NativeFormatDetector.TryDetect(stream, out var id);
|
||||
|
||||
Assert.True(detected);
|
||||
Assert.Equal(NativeFormat.MachO, id.Format);
|
||||
Assert.Equal("x86_64", id.CpuArchitecture);
|
||||
Assert.Equal("darwin", id.OperatingSystem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractsMachOUuid()
|
||||
{
|
||||
var buffer = new byte[128];
|
||||
// Mach-O 64 little endian magic 0xCFFAEDFE
|
||||
buffer[0] = 0xCF; buffer[1] = 0xFA; buffer[2] = 0xED; buffer[3] = 0xFE;
|
||||
// cputype (little endian path) write 0x01000007 at bytes 4-7
|
||||
buffer[4] = 0x07; buffer[5] = 0x00; buffer[6] = 0x00; buffer[7] = 0x01;
|
||||
// ncmds at offset 16 (little endian)
|
||||
BitConverter.GetBytes((uint)1).CopyTo(buffer, 16);
|
||||
// sizeofcmds at offset 20
|
||||
BitConverter.GetBytes((uint)32).CopyTo(buffer, 20);
|
||||
// load command starts at 32
|
||||
var cmdOffset = 32;
|
||||
BitConverter.GetBytes((uint)0x1B).CopyTo(buffer, cmdOffset); // LC_UUID
|
||||
BitConverter.GetBytes((uint)32).CopyTo(buffer, cmdOffset + 4); // cmdsize
|
||||
var uuid = Guid.NewGuid();
|
||||
uuid.ToByteArray().CopyTo(buffer, cmdOffset + 8);
|
||||
|
||||
using var stream = new MemoryStream(buffer);
|
||||
var detected = NativeFormatDetector.TryDetect(stream, out var id);
|
||||
|
||||
Assert.True(detected);
|
||||
Assert.Equal(NativeFormat.MachO, id.Format);
|
||||
Assert.Equal(uuid.ToString(), id.Uuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsUnknownForUnsupported()
|
||||
{
|
||||
var bytes = new byte[] { 0x00, 0x01, 0x02, 0x03 };
|
||||
using var stream = new MemoryStream(bytes);
|
||||
|
||||
var detected = NativeFormatDetector.TryDetect(stream, out var id);
|
||||
|
||||
Assert.False(detected);
|
||||
Assert.Equal(NativeFormat.Unknown, id.Format);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user