up
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests;
|
||||
|
||||
internal static class LanguageAnalyzerSmokeHarness
|
||||
{
|
||||
public static async Task AssertDeterministicAsync(string fixturePath, string goldenPath, ILanguageAnalyzer analyzer, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fixturePath)) throw new ArgumentException("fixturePath required", nameof(fixturePath));
|
||||
if (string.IsNullOrWhiteSpace(goldenPath)) throw new ArgumentException("goldenPath required", nameof(goldenPath));
|
||||
|
||||
var engine = new LanguageAnalyzerEngine(new[] { analyzer });
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
var result = await engine.AnalyzeAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
var actual = Normalize(result.ToJson(indent: true));
|
||||
var expected = Normalize(await File.ReadAllTextAsync(goldenPath, cancellationToken).ConfigureAwait(false));
|
||||
|
||||
if (!string.Equals(actual, expected, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = goldenPath + ".actual";
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
|
||||
await File.WriteAllTextAsync(actualPath, actual, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
private static string Normalize(string value)
|
||||
{
|
||||
return value.Replace("\r\n", "\n", StringComparison.Ordinal).TrimEnd();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.IO;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Node;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests;
|
||||
|
||||
public class Phase22SmokeTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Phase22_Fixture_Matches_Golden()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = Path.GetFullPath(Path.Combine("..", "StellaOps.Scanner.Analyzers.Lang.Node.Tests", "Fixtures", "lang", "node", "phase22"));
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
await LanguageAnalyzerSmokeHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
new NodeLanguageAnalyzer(),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<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>
|
||||
<!-- Stay scoped: disable implicit restore sources beyond local nugets -->
|
||||
<RestoreSources>$(StellaOpsLocalNuGetSource)</RestoreSources>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../StellaOps.Scanner.Analyzers.Lang.Node.Tests/Fixtures/lang/node/phase22/**" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
FROM node:22-alpine
|
||||
ENV NODE_OPTIONS="--require ./bootstrap.js"
|
||||
@@ -0,0 +1,25 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "warning:node-options:Dockerfile#2",
|
||||
"purl": null,
|
||||
"name": "NODE_OPTIONS warning",
|
||||
"version": null,
|
||||
"type": "node:warning",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"locator": "Dockerfile#2",
|
||||
"reason": "NODE_OPTIONS",
|
||||
"source": "Dockerfile",
|
||||
"value": "--require ./bootstrap.js"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "node.env",
|
||||
"locator": "Dockerfile#2",
|
||||
"value": "--require ./bootstrap.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "container-env",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/layer-lib@0.1.0",
|
||||
"purl": "pkg:npm/layer-lib@0.1.0",
|
||||
"name": "layer-lib",
|
||||
"version": "0.1.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"entrypoint": "layers/layer1/node_modules/layer-lib/index.js",
|
||||
"path": "layers/layer1/node_modules/layer-lib"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "layers/layer1/node_modules/layer-lib/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "layers/layer1/node_modules/layer-lib/package.json#entrypoint",
|
||||
"value": "layers/layer1/node_modules/layer-lib/index.js;index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "observation::node-phase22",
|
||||
"name": "Node Observation (Phase 22)",
|
||||
"type": "node-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"node.observation.components": "1",
|
||||
"node.observation.edges": "0",
|
||||
"node.observation.entrypoints": "1"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "node.observation",
|
||||
"locator": "phase22.ndjson",
|
||||
"value": "{\u0022type\u0022:\u0022component\u0022,\u0022componentType\u0022:\u0022pkg\u0022,\u0022path\u0022:\u0022/original.ts\u0022,\u0022format\u0022:\u0022esm\u0022,\u0022fromBundle\u0022:true,\u0022reason\u0022:\u0022source-map\u0022,\u0022confidence\u0022:0.87,\u0022resolverTrace\u0022:[\u0022bundle:/src/index.js\u0022,\u0022map:/src/index.js.map\u0022,\u0022source:/original.ts\u0022]}\n{\u0022type\u0022:\u0022entrypoint\u0022,\u0022path\u0022:\u0022/src/index.js\u0022,\u0022format\u0022:\u0022esm\u0022,\u0022reason\u0022:\u0022bundle-entrypoint\u0022,\u0022confidence\u0022:0.88,\u0022resolverTrace\u0022:[\u0022bundle:/src/index.js\u0022,\u0022map:/src/index.js.map\u0022]}",
|
||||
"sha256": "b2d6ac4c2b422ab26943dab38c2a7b8e8fa2979122e0c2674adb5a48f9cdd2fb"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/dynamic-imports@1.0.0",
|
||||
"purl": "pkg:npm/dynamic-imports@1.0.0",
|
||||
"name": "dynamic-imports",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"entrypoint": "src/index.js",
|
||||
"path": "."
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "src/index.js;src/index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "dynamic-imports",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import staticDep from './lib/static.js';
|
||||
const concat = require('./lib/' + 'concat.js');
|
||||
|
||||
async function load(name) {
|
||||
const mod = await import(`./lib/${name}/entry.js`);
|
||||
return mod;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"file": "index.js",
|
||||
"sources": ["original.ts"],
|
||||
"sourcesContent": ["import mapped from './lib/sourcemap.js';"],
|
||||
"mappings": ""
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = 'concat';
|
||||
@@ -0,0 +1 @@
|
||||
export default 'runtime-entry';
|
||||
@@ -0,0 +1 @@
|
||||
export const mapped = true;
|
||||
@@ -0,0 +1 @@
|
||||
export default 'static';
|
||||
@@ -0,0 +1,27 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "observation::node-phase22",
|
||||
"purl": null,
|
||||
"name": "Node Observation (Phase 22)",
|
||||
"version": null,
|
||||
"type": "node-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"node.observation.components": "3",
|
||||
"node.observation.edges": "3",
|
||||
"node.observation.entrypoints": "1",
|
||||
"node.observation.native": "1",
|
||||
"node.observation.wasm": "1"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "node.observation",
|
||||
"locator": "phase22.ndjson",
|
||||
"value": "{\"type\":\"component\",\"componentType\":\"native\",\"path\":\"/native/addon.node\",\"reason\":\"native-addon-file\",\"confidence\":0.82,\"resolverTrace\":[\"file:/native/addon.node\"],\"arch\":\"x86_64\",\"platform\":\"linux\"}\n{\"type\":\"component\",\"componentType\":\"wasm\",\"path\":\"/pkg/pkg.wasm\",\"reason\":\"wasm-file\",\"confidence\":0.8,\"resolverTrace\":[\"file:/pkg/pkg.wasm\"]}\n{\"type\":\"component\",\"componentType\":\"pkg\",\"path\":\"/src/app.js\",\"format\":\"esm\",\"fromBundle\":true,\"reason\":\"source-map\",\"confidence\":0.87,\"resolverTrace\":[\"bundle:/dist/main.js\",\"map:/dist/main.js.map\",\"source:/src/app.js\"]}\n{\"type\":\"edge\",\"edgeType\":\"native-addon\",\"from\":\"/dist/main.js\",\"to\":\"/native/addon.node\",\"reason\":\"native-dlopen-string\",\"confidence\":0.76,\"resolverTrace\":[\"source:/dist/main.js\",\"call:process.dlopen('../native/addon.node')\"]}\n{\"type\":\"edge\",\"edgeType\":\"wasm\",\"from\":\"/dist/main.js\",\"to\":\"/pkg/pkg.wasm\",\"reason\":\"wasm-import\",\"confidence\":0.74,\"resolverTrace\":[\"source:/dist/main.js\",\"call:WebAssembly.instantiate('../pkg/pkg.wasm')\"]}\n{\"type\":\"edge\",\"edgeType\":\"capability\",\"from\":\"/dist/main.js\",\"to\":\"child_process.execFile\",\"reason\":\"capability-child-process\",\"confidence\":0.7,\"resolverTrace\":[\"source:/dist/main.js\",\"call:child_process.execFile\"]}\n{\"type\":\"entrypoint\",\"path\":\"/dist/main.js\",\"format\":\"esm\",\"reason\":\"bundle-entrypoint\",\"confidence\":0.88,\"resolverTrace\":[\"bundle:/dist/main.js\",\"map:/dist/main.js.map\"]}",
|
||||
"sha256": "7e99e8fbd63eb2f29717ce6b03dc148d969b203e10a072d1bcd6ff0c5fe424bb"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
import childProcess from 'child_process';
|
||||
export function start() {
|
||||
childProcess.execFile('ls');
|
||||
return WebAssembly.instantiateStreaming(fetch('./pkg/pkg.wasm'));
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/pkg@1.2.3",
|
||||
"purl": "pkg:npm/pkg@1.2.3",
|
||||
"name": "pkg",
|
||||
"version": "1.2.3",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"entrypoint": "node_modules/.pnpm/pkg@1.2.3/node_modules/pkg/index.js",
|
||||
"path": "node_modules/.pnpm/pkg@1.2.3/node_modules/pkg"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "node_modules/.pnpm/pkg@1.2.3/node_modules/pkg/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "node_modules/.pnpm/pkg@1.2.3/node_modules/pkg/package.json#entrypoint",
|
||||
"value": "node_modules/.pnpm/pkg@1.2.3/node_modules/pkg/index.js;index.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/pnpm-demo@0.0.1",
|
||||
"purl": "pkg:npm/pnpm-demo@0.0.1",
|
||||
"name": "pnpm-demo",
|
||||
"version": "0.0.1",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"path": "."
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "package.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "pnpm-demo",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"pkg": "1.2.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node-runtime",
|
||||
"componentKey": "runtime-edge:src/index.js->./lib/runtime.js",
|
||||
"purl": null,
|
||||
"name": "runtime-edge",
|
||||
"version": null,
|
||||
"type": "node:runtime-edge",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"from": "src/index.js",
|
||||
"loaderId.sha256": "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589",
|
||||
"reason": "runtime-require",
|
||||
"to": "./lib/runtime.js"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "node.runtime",
|
||||
"locator": "runtime-require|src/index.js|./lib/runtime.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node-runtime",
|
||||
"componentKey": "/layers/app/node_modules/native/addon.node",
|
||||
"purl": null,
|
||||
"name": "addon.node",
|
||||
"version": null,
|
||||
"type": "node:runtime-component",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"loaderId.sha256": "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589",
|
||||
"path": "/layers/app/node_modules/native/addon.node",
|
||||
"reason": "runtime-load"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "node.runtime",
|
||||
"locator": "runtime-load"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type":"edge","from":"src/index.js","to":"./lib/runtime.js","reason":"runtime-require","loaderId":"abcd"}
|
||||
{"type":"component","path":"/layers/app/node_modules/native/addon.node","reason":"runtime-load","loaderId":"abcd"}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "runtime-evidence",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
console.log('runtime evidence fixture');
|
||||
@@ -26,4 +26,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -40,4 +40,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -138,4 +138,103 @@ public sealed class NodeLanguageAnalyzerTests
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Phase22BundleNativeWasmObservationAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "phase22");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new NodeLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContainerLayersAreScannedAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "container-layers");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[] { new NodeLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DynamicImportsEmitEvidenceAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "imports-dynamic");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[] { new NodeLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PnpmVirtualStoreIsParsedAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "pnpm-store");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[] { new NodeLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RuntimeEvidenceIsIngestedAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "runtime-evidence");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[] { new NodeLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DockerfileNodeOptionsWarningIsEmittedAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "container-env");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[] { new NodeLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +72,11 @@ public sealed class ComponentGraphBuilderTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DeterministicOrdering()
|
||||
{
|
||||
var fragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:layer1", new[]
|
||||
public void Build_DeterministicOrdering()
|
||||
{
|
||||
var fragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:layer1", new[]
|
||||
{
|
||||
new ComponentRecord
|
||||
{
|
||||
@@ -93,7 +93,27 @@ public sealed class ComponentGraphBuilderTests
|
||||
|
||||
var graph1 = ComponentGraphBuilder.Build(fragments);
|
||||
var graph2 = ComponentGraphBuilder.Build(fragments);
|
||||
|
||||
Assert.Equal(graph1.Components.Select(c => c.Identity.Key), graph2.Components.Select(c => c.Identity.Key));
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(graph1.Components.Select(c => c.Identity.Key), graph2.Components.Select(c => c.Identity.Key));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_SortsLayersByDigest()
|
||||
{
|
||||
var fragments = new[]
|
||||
{
|
||||
LayerComponentFragment.Create("sha256:zzzz", Array.Empty<ComponentRecord>()),
|
||||
LayerComponentFragment.Create("sha256:aaaa", Array.Empty<ComponentRecord>()),
|
||||
LayerComponentFragment.Create("sha256:mmmm", Array.Empty<ComponentRecord>()),
|
||||
};
|
||||
|
||||
var graph = ComponentGraphBuilder.Build(fragments);
|
||||
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"sha256:aaaa",
|
||||
"sha256:mmmm",
|
||||
"sha256:zzzz"
|
||||
}, graph.Layers.Select(layer => layer.LayerDigest));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,16 +75,22 @@ public sealed class CycloneDxComposerTests
|
||||
var first = composer.Compose(request);
|
||||
var second = composer.Compose(request);
|
||||
|
||||
Assert.Equal(first.Inventory.JsonSha256, second.Inventory.JsonSha256);
|
||||
Assert.Equal(first.Inventory.ProtobufSha256, second.Inventory.ProtobufSha256);
|
||||
Assert.Equal(first.Inventory.SerialNumber, second.Inventory.SerialNumber);
|
||||
|
||||
Assert.NotNull(first.Usage);
|
||||
Assert.NotNull(second.Usage);
|
||||
Assert.Equal(first.Usage!.JsonSha256, second.Usage!.JsonSha256);
|
||||
Assert.Equal(first.Usage.ProtobufSha256, second.Usage.ProtobufSha256);
|
||||
Assert.Equal(first.Usage.SerialNumber, second.Usage.SerialNumber);
|
||||
}
|
||||
Assert.Equal(first.Inventory.JsonSha256, second.Inventory.JsonSha256);
|
||||
Assert.Equal(first.Inventory.ContentHash, first.Inventory.JsonSha256);
|
||||
Assert.Equal(first.Inventory.ProtobufSha256, second.Inventory.ProtobufSha256);
|
||||
Assert.Equal(first.Inventory.SerialNumber, second.Inventory.SerialNumber);
|
||||
Assert.Null(first.Inventory.MerkleRoot);
|
||||
Assert.Null(first.Inventory.CompositionUri);
|
||||
|
||||
Assert.NotNull(first.Usage);
|
||||
Assert.NotNull(second.Usage);
|
||||
Assert.Equal(first.Usage!.JsonSha256, second.Usage!.JsonSha256);
|
||||
Assert.Equal(first.Usage.ContentHash, first.Usage.JsonSha256);
|
||||
Assert.Equal(first.Usage.ProtobufSha256, second.Usage.ProtobufSha256);
|
||||
Assert.Equal(first.Usage.SerialNumber, second.Usage.SerialNumber);
|
||||
Assert.Null(first.Usage.MerkleRoot);
|
||||
Assert.Null(first.Usage.CompositionUri);
|
||||
}
|
||||
|
||||
private static SbomCompositionRequest BuildRequest()
|
||||
{
|
||||
|
||||
@@ -654,6 +654,140 @@ public sealed class EntryTraceAnalyzerTests
|
||||
Assert.Equal(EntryTraceTerminalType.Native, terminal.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_PropagatesUserSwitchWrapper()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
fs.AddBinaryFile("/usr/bin/sudo", CreateGoBinary(), executable: true);
|
||||
fs.AddBinaryFile("/usr/bin/python", CreateGoBinary(), executable: true);
|
||||
fs.AddFile("/srv/app.py", "print('hi')\n", executable: false);
|
||||
|
||||
var context = new EntryTraceContext(
|
||||
fs,
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
ImmutableArray.Create("/usr/bin"),
|
||||
"/",
|
||||
"root",
|
||||
"sha256:user-switch",
|
||||
"scan-sudo",
|
||||
NullLogger.Instance);
|
||||
|
||||
var spec = EntrypointSpecification.FromExecForm(
|
||||
new[] { "sudo", "-u", "app", "python", "/srv/app.py" },
|
||||
null);
|
||||
|
||||
var analyzer = CreateAnalyzer();
|
||||
var result = await analyzer.ResolveAsync(spec, context);
|
||||
|
||||
var terminal = Assert.Single(result.Terminals);
|
||||
Assert.Equal("/usr/bin/python", terminal.Path);
|
||||
Assert.Equal("app", terminal.User);
|
||||
Assert.Contains("/srv/app.py", terminal.Arguments);
|
||||
|
||||
var edge = Assert.Single(result.Edges.Where(e => e.Relationship == "wrapper"));
|
||||
Assert.Equal("true", edge.Metadata?["guarded"]);
|
||||
Assert.Equal("user", edge.Metadata?["state-change"]);
|
||||
Assert.Equal("app", edge.Metadata?["user"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_PropagatesEnvWrapperIntoPlan()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
fs.AddBinaryFile("/usr/bin/env", CreateGoBinary(), executable: true);
|
||||
fs.AddBinaryFile("/usr/bin/python", CreateGoBinary(), executable: true);
|
||||
fs.AddFile("/srv/app.py", "print('env')\n", executable: false);
|
||||
|
||||
var context = new EntryTraceContext(
|
||||
fs,
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
ImmutableArray.Create("/usr/bin"),
|
||||
"/",
|
||||
"root",
|
||||
"sha256:env-wrapper",
|
||||
"scan-env",
|
||||
NullLogger.Instance);
|
||||
|
||||
var spec = EntrypointSpecification.FromExecForm(
|
||||
new[] { "env", "FOO=bar", "python", "/srv/app.py" },
|
||||
null);
|
||||
|
||||
var analyzer = CreateAnalyzer();
|
||||
var result = await analyzer.ResolveAsync(spec, context);
|
||||
|
||||
var plan = Assert.Single(result.Plans);
|
||||
Assert.True(plan.Environment.TryGetValue("FOO", out var value) && value == "bar");
|
||||
var terminal = Assert.Single(result.Terminals);
|
||||
Assert.Equal("/usr/bin/python", terminal.Path);
|
||||
|
||||
var edge = Assert.Single(result.Edges.Where(e => e.Relationship == "wrapper"));
|
||||
Assert.Equal("env", edge.Metadata?["state-change"]);
|
||||
Assert.Equal("true", edge.Metadata?["guarded"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_AccumulatesWorkingDirectoryFromShellCd()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
fs.AddBinaryFile("/bin/sh", CreateGoBinary(), executable: true);
|
||||
fs.AddBinaryFile("/usr/bin/python", CreateGoBinary(), executable: true);
|
||||
fs.AddFile("/entry.sh", "#!/bin/sh\ncd /service\nexec python /srv/service.py\n", executable: true);
|
||||
fs.AddFile("/srv/service.py", "print('svc')\n", executable: false);
|
||||
|
||||
var context = new EntryTraceContext(
|
||||
fs,
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
ImmutableArray.Create("/bin", "/usr/bin"),
|
||||
"/",
|
||||
"root",
|
||||
"sha256:cd-trace",
|
||||
"scan-cd",
|
||||
NullLogger.Instance);
|
||||
|
||||
var spec = EntrypointSpecification.FromExecForm(
|
||||
new[] { "/entry.sh" },
|
||||
null);
|
||||
|
||||
var analyzer = CreateAnalyzer();
|
||||
var result = await analyzer.ResolveAsync(spec, context);
|
||||
|
||||
var terminal = Assert.Single(result.Terminals);
|
||||
Assert.Equal("/usr/bin/python", terminal.Path);
|
||||
Assert.Equal("/service", terminal.WorkingDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_HandlesInitShimAndGuardsEdge()
|
||||
{
|
||||
var fs = new TestRootFileSystem();
|
||||
fs.AddBinaryFile("/sbin/tini", CreateGoBinary(), executable: true);
|
||||
fs.AddBinaryFile("/usr/bin/python", CreateGoBinary(), executable: true);
|
||||
fs.AddFile("/srv/app.py", "print('shim')\n", executable: false);
|
||||
|
||||
var context = new EntryTraceContext(
|
||||
fs,
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
ImmutableArray.Create("/sbin", "/usr/bin"),
|
||||
"/",
|
||||
"root",
|
||||
"sha256:init-shim",
|
||||
"scan-tini",
|
||||
NullLogger.Instance);
|
||||
|
||||
var spec = EntrypointSpecification.FromExecForm(
|
||||
new[] { "/sbin/tini", "--", "python", "/srv/app.py" },
|
||||
null);
|
||||
|
||||
var analyzer = CreateAnalyzer();
|
||||
var result = await analyzer.ResolveAsync(spec, context);
|
||||
|
||||
var terminal = Assert.Single(result.Terminals);
|
||||
Assert.Equal("/usr/bin/python", terminal.Path);
|
||||
var edge = Assert.Single(result.Edges.Where(e => e.Relationship == "wrapper"));
|
||||
Assert.Equal("true", edge.Metadata?["guarded"]);
|
||||
Assert.Equal("init", edge.Metadata?["shim"]);
|
||||
}
|
||||
|
||||
private static byte[] CreateGoBinary()
|
||||
{
|
||||
var buffer = new byte[256];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.EntryTrace;
|
||||
@@ -29,6 +30,26 @@ public sealed class EntryTraceRuntimeReconcilerTests
|
||||
Assert.Contains(reconciled.Diagnostics, d => d.Reason == EntryTraceUnknownReason.RuntimeMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reconcile_EmitsRuntimeChain_InDiagnostics()
|
||||
{
|
||||
var reconciler = new EntryTraceRuntimeReconciler();
|
||||
var graph = CreateGraph("/usr/local/bin/app");
|
||||
|
||||
var procGraph = ProcGraphBuilder.Build(new FakeProvider(new[]
|
||||
{
|
||||
CreateProcess(1, 0, "/sbin/tini", "tini", 100),
|
||||
CreateProcess(5, 1, "/usr/local/bin/app", "app", 200),
|
||||
}));
|
||||
|
||||
var reconciled = reconciler.Reconcile(graph, procGraph);
|
||||
|
||||
var diag = Assert.Single(reconciled.Diagnostics, d => d.Reason == EntryTraceUnknownReason.RuntimeMatch);
|
||||
Assert.Contains("tini", diag.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("/usr/local/bin/app", diag.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("->", diag.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reconcile_FlagsMismatch_WhenDifferentExecutable()
|
||||
{
|
||||
|
||||
@@ -246,13 +246,34 @@ public sealed class SurfaceManifestStageExecutorTests
|
||||
Span: null,
|
||||
Metadata: null);
|
||||
|
||||
var plan = new EntryTracePlan(
|
||||
ImmutableArray.Create("/bin/entry"),
|
||||
ImmutableDictionary<string, string>.Empty,
|
||||
WorkingDirectory: "/",
|
||||
User: "root",
|
||||
TerminalPath: "/bin/entry",
|
||||
Type: EntryTraceTerminalType.Native,
|
||||
Runtime: "native",
|
||||
Confidence: 92.5,
|
||||
Evidence: ImmutableDictionary<string, string>.Empty);
|
||||
|
||||
var terminal = new EntryTraceTerminal(
|
||||
Path: "/bin/entry",
|
||||
Type: EntryTraceTerminalType.Native,
|
||||
Runtime: "native",
|
||||
Confidence: 92.5,
|
||||
Evidence: ImmutableDictionary<string, string>.Empty,
|
||||
User: "root",
|
||||
WorkingDirectory: "/",
|
||||
Arguments: ImmutableArray.Create("/bin/entry"));
|
||||
|
||||
var graph = new EntryTraceGraph(
|
||||
Outcome: EntryTraceOutcome.Resolved,
|
||||
Nodes: ImmutableArray.Create(node),
|
||||
Edges: ImmutableArray<EntryTraceEdge>.Empty,
|
||||
Diagnostics: ImmutableArray<EntryTraceDiagnostic>.Empty,
|
||||
Plans: ImmutableArray<EntryTracePlan>.Empty,
|
||||
Terminals: ImmutableArray<EntryTraceTerminal>.Empty);
|
||||
Plans: ImmutableArray.Create(plan),
|
||||
Terminals: ImmutableArray.Create(terminal));
|
||||
|
||||
context.Analysis.Set(ScanAnalysisKeys.EntryTraceGraph, graph);
|
||||
|
||||
@@ -301,6 +322,44 @@ public sealed class SurfaceManifestStageExecutorTests
|
||||
Assert.NotEmpty(packageStore.LastInventory!.Packages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_AddsBestEntryTraceMetadata()
|
||||
{
|
||||
var metrics = new ScannerWorkerMetrics();
|
||||
var publisher = new TestSurfaceManifestPublisher("tenant-a");
|
||||
var cache = new RecordingSurfaceCache();
|
||||
var environment = new TestSurfaceEnvironment("tenant-a");
|
||||
var hash = CreateCryptoHash();
|
||||
var packageStore = new RecordingRubyPackageStore();
|
||||
|
||||
var executor = new SurfaceManifestStageExecutor(
|
||||
publisher,
|
||||
cache,
|
||||
environment,
|
||||
metrics,
|
||||
NullLogger<SurfaceManifestStageExecutor>.Instance,
|
||||
hash,
|
||||
packageStore);
|
||||
|
||||
var context = CreateContext();
|
||||
PopulateAnalysis(context);
|
||||
|
||||
await executor.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
var entrytracePayloads = publisher.LastRequest!.Payloads
|
||||
.Where(p => p.Kind.StartsWith("entrytrace", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
Assert.NotEmpty(entrytracePayloads);
|
||||
foreach (var payload in entrytracePayloads)
|
||||
{
|
||||
Assert.Equal("/bin/entry", payload.Metadata!["best_terminal"]);
|
||||
Assert.Equal("92.5000", payload.Metadata!["best_confidence"]);
|
||||
Assert.Equal("root", payload.Metadata!["best_user"]);
|
||||
Assert.Equal("/", payload.Metadata!["best_workdir"]);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task PopulateRubyAnalyzerResultsAsync(ScanJobContext context)
|
||||
{
|
||||
var fixturePath = Path.Combine(
|
||||
|
||||
Reference in New Issue
Block a user