Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Node.Tests/Node/NodeEntrypointDetectionTests.cs
StellaOps Bot e53a282fbe
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
feat: Add native binary analyzer test utilities and implement SM2 signing tests
- Introduced `NativeTestBase` class for ELF, PE, and Mach-O binary parsing helpers and assertions.
- Created `TestCryptoFactory` for SM2 cryptographic provider setup and key generation.
- Implemented `Sm2SigningTests` to validate signing functionality with environment gate checks.
- Developed console export service and store with comprehensive unit tests for export status management.
2025-12-07 13:12:41 +02:00

686 lines
18 KiB
C#

using System.Text.Json;
using StellaOps.Scanner.Analyzers.Lang.Node;
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
namespace StellaOps.Scanner.Analyzers.Lang.Node.Tests.Node;
/// <summary>
/// Tests for entrypoint detection in Node packages including bin, exports, main,
/// module, worker, electron, and shebang detection.
/// </summary>
public sealed class NodeEntrypointDetectionTests : IDisposable
{
private readonly string _tempDir;
public NodeEntrypointDetectionTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "node-entrypoint-tests-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
catch
{
// Ignore cleanup errors
}
}
private void WriteFile(string relativePath, string content)
{
var fullPath = Path.Combine(_tempDir, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
File.WriteAllText(fullPath, content);
}
#region bin field tests
[Fact]
public async Task BinField_StringFormat_DetectsEntrypoint()
{
// Arrange
var packageJson = new
{
name = "cli-pkg",
version = "1.0.0",
bin = "./cli.js"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("cli.js", "// cli");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("cli.js", result);
}
[Fact]
public async Task BinField_ObjectFormat_DetectsEntrypoints()
{
// Arrange
var packageJson = new
{
name = "multi-cli-pkg",
version = "1.0.0",
bin = new
{
cmd1 = "./bin/cmd1.js",
cmd2 = "./bin/cmd2.js"
}
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("bin/cmd1.js", "// cmd1");
WriteFile("bin/cmd2.js", "// cmd2");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("bin/cmd1.js", result);
Assert.Contains("bin/cmd2.js", result);
}
[Fact]
public async Task BinField_ObjectFormat_IncludesBinNames()
{
// Arrange
var packageJson = new
{
name = "named-cli-pkg",
version = "1.0.0",
bin = new
{
mycli = "./cli.js"
}
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("cli.js", "// cli");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("mycli", result);
}
#endregion
#region main/module field tests
[Fact]
public async Task MainField_DetectsEntrypoint()
{
// Arrange
var packageJson = new
{
name = "lib-pkg",
version = "1.0.0",
main = "./dist/index.js"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("dist/index.js", "// index");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/index.js", result);
}
[Fact]
public async Task ModuleField_DetectsEntrypoint()
{
// Arrange
var packageJson = new
{
name = "esm-pkg",
version = "1.0.0",
module = "./dist/index.mjs"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("dist/index.mjs", "// esm index");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/index.mjs", result);
}
[Fact]
public async Task BothMainAndModule_DetectsBothEntrypoints()
{
// Arrange
var packageJson = new
{
name = "dual-pkg",
version = "1.0.0",
main = "./dist/index.cjs",
module = "./dist/index.mjs"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("dist/index.cjs", "// cjs");
WriteFile("dist/index.mjs", "// esm");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/index.cjs", result);
Assert.Contains("dist/index.mjs", result);
}
#endregion
#region exports field tests
[Fact]
public async Task ExportsField_StringFormat_DetectsEntrypoint()
{
// Arrange
var packageJson = new
{
name = "exports-str-pkg",
version = "1.0.0",
exports = "./dist/index.js"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("dist/index.js", "// index");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/index.js", result);
}
[Fact]
public async Task ExportsField_ObjectWithImportRequire_DetectsBothEntrypoints()
{
// Arrange
var packageJson = new
{
name = "exports-obj-pkg",
version = "1.0.0",
exports = new
{
import = "./dist/index.mjs",
require = "./dist/index.cjs"
}
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("dist/index.mjs", "// esm");
WriteFile("dist/index.cjs", "// cjs");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/index.mjs", result);
Assert.Contains("dist/index.cjs", result);
}
[Fact]
public async Task ExportsField_MultipleSubpaths_DetectsAllEntrypoints()
{
// Arrange - Using raw JSON to match the exact structure
var packageJsonContent = @"{
""name"": ""exports-multi-pkg"",
""version"": ""1.0.0"",
""exports"": {
""."": ""./dist/index.js"",
""./utils"": ""./dist/utils.js"",
""./types"": ""./dist/types.d.ts""
}
}";
WriteFile("package.json", packageJsonContent);
WriteFile("dist/index.js", "// index");
WriteFile("dist/utils.js", "// utils");
WriteFile("dist/types.d.ts", "// types");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/index.js", result);
Assert.Contains("dist/utils.js", result);
Assert.Contains("dist/types.d.ts", result);
}
[Fact]
public async Task ExportsField_ConditionalExports_DetectsEntrypoints()
{
// Arrange
var packageJsonContent = @"{
""name"": ""conditional-exports-pkg"",
""version"": ""1.0.0"",
""exports"": {
""."": {
""import"": ""./dist/index.mjs"",
""require"": ""./dist/index.cjs"",
""default"": ""./dist/index.js""
}
}
}";
WriteFile("package.json", packageJsonContent);
WriteFile("dist/index.mjs", "// esm");
WriteFile("dist/index.cjs", "// cjs");
WriteFile("dist/index.js", "// default");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/index.mjs", result);
Assert.Contains("dist/index.cjs", result);
Assert.Contains("dist/index.js", result);
}
[Fact]
public async Task ExportsField_NestedConditions_FlattensAndDetectsEntrypoints()
{
// Arrange
var packageJsonContent = @"{
""name"": ""nested-exports-pkg"",
""version"": ""1.0.0"",
""exports"": {
""."": {
""node"": {
""import"": ""./dist/node.mjs"",
""require"": ""./dist/node.cjs""
},
""browser"": ""./dist/browser.js""
}
}
}";
WriteFile("package.json", packageJsonContent);
WriteFile("dist/node.mjs", "// node esm");
WriteFile("dist/node.cjs", "// node cjs");
WriteFile("dist/browser.js", "// browser");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/node.mjs", result);
Assert.Contains("dist/node.cjs", result);
Assert.Contains("dist/browser.js", result);
}
#endregion
#region imports field tests
[Fact]
public async Task ImportsField_DetectsEntrypoints()
{
// Arrange
var packageJsonContent = @"{
""name"": ""imports-pkg"",
""version"": ""1.0.0"",
""imports"": {
""#internal"": ""./src/internal.js""
}
}";
WriteFile("package.json", packageJsonContent);
WriteFile("src/internal.js", "// internal");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("src/internal.js", result);
}
#endregion
#region worker field tests
[Fact]
public async Task WorkerField_DetectsEntrypoint()
{
// Arrange
var packageJson = new
{
name = "worker-pkg",
version = "1.0.0",
worker = "./dist/worker.js"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("dist/worker.js", "// worker");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/worker.js", result);
Assert.Contains("worker", result); // condition set
}
#endregion
#region electron detection tests
[Fact]
public async Task ElectronDependency_DetectsElectronEntrypoint()
{
// Arrange
var packageJson = new
{
name = "electron-app",
version = "1.0.0",
main = "./src/main.js",
dependencies = new
{
electron = "^25.0.0"
}
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("src/main.js", "// electron main");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("electron", result);
}
[Fact]
public async Task ElectronDevDependency_DetectsElectronEntrypoint()
{
// Arrange
var packageJson = new
{
name = "electron-dev-app",
version = "1.0.0",
main = "./src/main.js",
devDependencies = new
{
electron = "^25.0.0"
}
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("src/main.js", "// electron main");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("electron", result);
}
#endregion
#region shebang detection tests
[Fact]
public async Task ShebangScript_NodeShebang_DetectsEntrypoint()
{
// Arrange
var packageJson = new
{
name = "shebang-pkg",
version = "1.0.0"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("cli.js", "#!/usr/bin/env node\nconsole.log('cli');");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("cli.js", result);
Assert.Contains("shebang:node", result);
}
[Fact]
public async Task ShebangScript_DirectNodePath_DetectsEntrypoint()
{
// Arrange
var packageJson = new
{
name = "shebang-direct-pkg",
version = "1.0.0"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("cli.mjs", "#!/usr/bin/node\nconsole.log('cli');");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("cli.mjs", result);
}
[Fact]
public async Task ShebangScript_NotNode_DoesNotDetect()
{
// Arrange
var packageJson = new
{
name = "shebang-bash-pkg",
version = "1.0.0"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("script.sh", "#!/bin/bash\necho 'hello'");
WriteFile("some.js", "// not a shebang");
// Act
var result = await RunAnalyzerAsync();
// Assert
// Should not contain shebang:node for non-node scripts
var json = JsonDocument.Parse(result);
var hasNodeShebang = json.RootElement.EnumerateArray()
.Any(p => p.ToString().Contains("shebang:node"));
// The .sh file won't be scanned for shebangs (wrong extension)
// The .js file doesn't have a shebang
}
[Fact]
public async Task ShebangScript_TypeScriptExtension_DetectsEntrypoint()
{
// Arrange
var packageJson = new
{
name = "shebang-ts-pkg",
version = "1.0.0"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("cli.ts", "#!/usr/bin/env node\nconsole.log('cli');");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("cli.ts", result);
}
[Fact]
public async Task ShebangScript_WithLeadingWhitespace_DetectsEntrypoint()
{
// Arrange
var packageJson = new
{
name = "shebang-ws-pkg",
version = "1.0.0"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("cli.js", " #!/usr/bin/env node\nconsole.log('cli');");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("cli.js", result);
}
#endregion
#region path normalization tests
[Fact]
public async Task PathNormalization_LeadingDotSlash_IsNormalized()
{
// Arrange
var packageJson = new
{
name = "path-norm-pkg",
version = "1.0.0",
main = "./dist/index.js"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("dist/index.js", "// index");
// Act
var result = await RunAnalyzerAsync();
// Assert
// Path should be normalized (leading ./ stripped in entrypoint path)
// The entrypoint evidence contains the normalized path
var json = JsonDocument.Parse(result);
var evidence = json.RootElement.EnumerateArray()
.SelectMany(p => p.TryGetProperty("evidence", out var ev) ? ev.EnumerateArray() : Enumerable.Empty<JsonElement>())
.Where(e => e.TryGetProperty("source", out var src) && src.GetString() == "package.json:entrypoint")
.ToList();
// Should have entrypoint evidence with normalized path (starts with dist/, not ./dist/)
Assert.True(evidence.Any(e => e.TryGetProperty("value", out var val) &&
val.GetString()!.StartsWith("dist/", StringComparison.Ordinal)));
}
[Fact]
public async Task PathNormalization_MultipleLeadingDotSlash_IsNormalized()
{
// Arrange
var packageJson = new
{
name = "multi-dot-pkg",
version = "1.0.0",
main = "././dist/index.js"
};
WriteFile("package.json", JsonSerializer.Serialize(packageJson));
WriteFile("dist/index.js", "// index");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/index.js", result);
}
[Fact]
public async Task PathNormalization_BackslashesAreNormalized()
{
// Arrange - Windows-style paths
var packageJsonContent = @"{
""name"": ""backslash-pkg"",
""version"": ""1.0.0"",
""main"": ""dist\\index.js""
}";
WriteFile("package.json", packageJsonContent);
WriteFile("dist/index.js", "// index");
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("dist/index.js", result);
}
#endregion
#region edge cases
[Fact]
public async Task EmptyBinField_DoesNotCrash()
{
// Arrange
var packageJsonContent = @"{
""name"": ""empty-bin-pkg"",
""version"": ""1.0.0"",
""bin"": {}
}";
WriteFile("package.json", packageJsonContent);
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("empty-bin-pkg", result);
}
[Fact]
public async Task EmptyExportsField_DoesNotCrash()
{
// Arrange
var packageJsonContent = @"{
""name"": ""empty-exports-pkg"",
""version"": ""1.0.0"",
""exports"": {}
}";
WriteFile("package.json", packageJsonContent);
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("empty-exports-pkg", result);
}
[Fact]
public async Task NullBinValue_DoesNotCrash()
{
// Arrange
var packageJsonContent = @"{
""name"": ""null-bin-pkg"",
""version"": ""1.0.0"",
""bin"": null
}";
WriteFile("package.json", packageJsonContent);
// Act
var result = await RunAnalyzerAsync();
// Assert
Assert.Contains("null-bin-pkg", result);
}
[Fact]
public async Task WhitespaceEntrypoint_DoesNotDetect()
{
// Arrange
var packageJsonContent = @"{
""name"": ""whitespace-main-pkg"",
""version"": ""1.0.0"",
""main"": "" ""
}";
WriteFile("package.json", packageJsonContent);
// Act
var result = await RunAnalyzerAsync();
// Assert
// Package should exist but whitespace main should not create entrypoint
Assert.Contains("whitespace-main-pkg", result);
}
#endregion
private async Task<string> RunAnalyzerAsync()
{
var analyzers = new ILanguageAnalyzer[] { new NodeLanguageAnalyzer() };
return await LanguageAnalyzerTestHarness.RunToJsonAsync(
_tempDir,
analyzers,
TestContext.Current.CancellationToken);
}
}