Add comprehensive tests for Go and Python version conflict detection and licensing normalization
- Implemented GoVersionConflictDetectorTests to validate pseudo-version detection, conflict analysis, and conflict retrieval for Go modules. - Created VersionConflictDetectorTests for Python to assess conflict detection across various version scenarios, including major, minor, and patch differences. - Added SpdxLicenseNormalizerTests to ensure accurate normalization of SPDX license strings and classifiers. - Developed VendoredPackageDetectorTests to identify vendored packages and extract embedded packages from Python packages, including handling of vendor directories and known vendored packages.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"@company/internal-pkg@1.0.0": ["https://npm.company.com/@company/internal-pkg/-/internal-pkg-1.0.0.tgz", "sha512-customhash123=="]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[install.scopes]
|
||||
"@company" = "https://npm.company.com/"
|
||||
@@ -0,0 +1,39 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "bun",
|
||||
"componentKey": "purl::pkg:npm/%40company/internal-pkg@1.0.0",
|
||||
"purl": "pkg:npm/%40company/internal-pkg@1.0.0",
|
||||
"name": "@company/internal-pkg",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"customRegistry": "https://npm.company.com/",
|
||||
"direct": "true",
|
||||
"integrity": "sha512-customhash123==",
|
||||
"packageManager": "bun",
|
||||
"path": "node_modules/@company/internal-pkg",
|
||||
"resolved": "https://npm.company.com/@company/internal-pkg/-/internal-pkg-1.0.0.tgz",
|
||||
"source": "node_modules"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node_modules",
|
||||
"locator": "node_modules/@company/internal-pkg/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "integrity",
|
||||
"locator": "bun.lock",
|
||||
"value": "sha512-customhash123=="
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "resolved",
|
||||
"locator": "bun.lock",
|
||||
"value": "https://npm.company.com/@company/internal-pkg/-/internal-pkg-1.0.0.tgz"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "custom-registry-fixture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@company/internal-pkg": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"debug@4.3.4": ["https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", {"ms": "^2.1.2"}],
|
||||
"ms@2.1.3": ["https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "bun",
|
||||
"componentKey": "purl::pkg:npm/debug@4.3.4",
|
||||
"purl": "pkg:npm/debug@4.3.4",
|
||||
"name": "debug",
|
||||
"version": "4.3.4",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"direct": "true",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"packageManager": "bun",
|
||||
"path": "node_modules/debug",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"source": "node_modules"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node_modules",
|
||||
"locator": "node_modules/debug/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "integrity",
|
||||
"locator": "bun.lock",
|
||||
"value": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "resolved",
|
||||
"locator": "bun.lock",
|
||||
"value": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "bun",
|
||||
"componentKey": "purl::pkg:npm/ms@2.1.3",
|
||||
"purl": "pkg:npm/ms@2.1.3",
|
||||
"name": "ms",
|
||||
"version": "2.1.3",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"packageManager": "bun",
|
||||
"path": "node_modules/ms",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"source": "node_modules"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node_modules",
|
||||
"locator": "node_modules/ms/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "integrity",
|
||||
"locator": "bun.lock",
|
||||
"value": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "resolved",
|
||||
"locator": "bun.lock",
|
||||
"value": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "deep-tree-fixture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"my-git-pkg@1.0.0": ["git+https://github.com/user/my-git-pkg.git#abc123def456", null]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "bun",
|
||||
"componentKey": "purl::pkg:npm/my-git-pkg@1.0.0",
|
||||
"purl": "pkg:npm/my-git-pkg@1.0.0",
|
||||
"name": "my-git-pkg",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"direct": "true",
|
||||
"gitCommit": "abc123def456",
|
||||
"packageManager": "bun",
|
||||
"path": "node_modules/my-git-pkg",
|
||||
"resolved": "git+https://github.com/user/my-git-pkg.git#abc123def456",
|
||||
"source": "node_modules",
|
||||
"sourceType": "git",
|
||||
"specifier": "git+https://github.com/user/my-git-pkg.git#abc123def456"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node_modules",
|
||||
"locator": "node_modules/my-git-pkg/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "resolved",
|
||||
"locator": "bun.lock",
|
||||
"value": "git+https://github.com/user/my-git-pkg.git#abc123def456"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "git-dependencies-fixture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"my-git-pkg": "github:user/my-git-pkg#v1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vz1kAmtILi+8fm9nJMg7b0GN8sMEJz2mxG/S7mNxhWQ7+D9bF8Q=="]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "bun",
|
||||
"componentKey": "purl::pkg:npm/lodash@4.17.21",
|
||||
"purl": "pkg:npm/lodash@4.17.21",
|
||||
"name": "lodash",
|
||||
"version": "4.17.21",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"direct": "true",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vz1kAmtILi+8fm9nJMg7b0GN8sMEJz2mxG/S7mNxhWQ7+D9bF8Q==",
|
||||
"packageManager": "bun",
|
||||
"patchFile": "patches/lodash@4.17.21.patch",
|
||||
"patched": "true",
|
||||
"path": "node_modules/lodash",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"source": "node_modules"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node_modules",
|
||||
"locator": "node_modules/lodash/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "integrity",
|
||||
"locator": "bun.lock",
|
||||
"value": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vz1kAmtILi+8fm9nJMg7b0GN8sMEJz2mxG/S7mNxhWQ7+D9bF8Q=="
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "resolved",
|
||||
"locator": "bun.lock",
|
||||
"value": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "patched-packages-fixture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"lodash@4.17.21": "patches/lodash@4.17.21.patch"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -1 +1 @@
|
||||
-module.exports = require('./lodash');
|
||||
+module.exports = require('./lodash-patched');
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"@babel/core@7.24.0": ["https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw=="],
|
||||
"@types/node@20.11.0": ["https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxXjVJa7b8XWCF/wPH2E/0Vz9e+V1B3eXX0WCw+INcAobvUag=="]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "bun",
|
||||
"componentKey": "purl::pkg:npm/%40babel/core@7.24.0",
|
||||
"purl": "pkg:npm/%40babel/core@7.24.0",
|
||||
"name": "@babel/core",
|
||||
"version": "7.24.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"direct": "true",
|
||||
"integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
|
||||
"packageManager": "bun",
|
||||
"path": "node_modules/@babel/core",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
|
||||
"source": "node_modules"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node_modules",
|
||||
"locator": "node_modules/@babel/core/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "integrity",
|
||||
"locator": "bun.lock",
|
||||
"value": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw=="
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "resolved",
|
||||
"locator": "bun.lock",
|
||||
"value": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "bun",
|
||||
"componentKey": "purl::pkg:npm/%40types/node@20.11.0",
|
||||
"purl": "pkg:npm/%40types/node@20.11.0",
|
||||
"name": "@types/node",
|
||||
"version": "20.11.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"direct": "true",
|
||||
"integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxXjVJa7b8XWCF/wPH2E/0Vz9e+V1B3eXX0WCw+INcAobvUag==",
|
||||
"packageManager": "bun",
|
||||
"path": "node_modules/@types/node",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
|
||||
"source": "node_modules"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node_modules",
|
||||
"locator": "node_modules/@types/node/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "integrity",
|
||||
"locator": "bun.lock",
|
||||
"value": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxXjVJa7b8XWCF/wPH2E/0Vz9e+V1B3eXX0WCw+INcAobvUag=="
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "resolved",
|
||||
"locator": "bun.lock",
|
||||
"value": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "scoped-packages-fixture",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@types/node": "^20.11.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Bun.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Bun.Tests.Parsers;
|
||||
|
||||
public sealed class BunConfigHelperTests
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public BunConfigHelperTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"bun-config-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
#region ParseConfig Tests
|
||||
|
||||
[Fact]
|
||||
public void ParseConfig_MissingFile_ReturnsEmpty()
|
||||
{
|
||||
var result = BunConfigHelper.ParseConfig(_tempDir);
|
||||
|
||||
Assert.Null(result.DefaultRegistry);
|
||||
Assert.Empty(result.ScopeRegistries);
|
||||
Assert.False(result.HasCustomRegistry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseConfig_DefaultRegistry_ReturnsUrl()
|
||||
{
|
||||
var bunfig = """
|
||||
[install]
|
||||
registry = "https://npm.company.com/"
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
|
||||
|
||||
var result = BunConfigHelper.ParseConfig(_tempDir);
|
||||
|
||||
Assert.Equal("https://npm.company.com/", result.DefaultRegistry);
|
||||
Assert.True(result.HasCustomRegistry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseConfig_ScopedRegistries_ReturnsMappings()
|
||||
{
|
||||
var bunfig = """
|
||||
[install.scopes]
|
||||
"@company" = "https://npm.company.com/"
|
||||
"@internal" = "https://internal.registry.com/"
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
|
||||
|
||||
var result = BunConfigHelper.ParseConfig(_tempDir);
|
||||
|
||||
Assert.Equal(2, result.ScopeRegistries.Count);
|
||||
Assert.Equal("https://npm.company.com/", result.ScopeRegistries["@company"]);
|
||||
Assert.Equal("https://internal.registry.com/", result.ScopeRegistries["@internal"]);
|
||||
Assert.True(result.HasCustomRegistry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseConfig_InlineTableFormat_ExtractsUrl()
|
||||
{
|
||||
var bunfig = """
|
||||
[install.scopes]
|
||||
"@company" = { url = "https://npm.company.com/" }
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
|
||||
|
||||
var result = BunConfigHelper.ParseConfig(_tempDir);
|
||||
|
||||
Assert.Single(result.ScopeRegistries);
|
||||
Assert.Equal("https://npm.company.com/", result.ScopeRegistries["@company"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseConfig_Comments_IgnoresComments()
|
||||
{
|
||||
var bunfig = """
|
||||
# This is a comment
|
||||
[install]
|
||||
# registry for npm packages
|
||||
registry = "https://npm.company.com/"
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
|
||||
|
||||
var result = BunConfigHelper.ParseConfig(_tempDir);
|
||||
|
||||
Assert.Equal("https://npm.company.com/", result.DefaultRegistry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseConfig_EmptyFile_ReturnsEmpty()
|
||||
{
|
||||
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), "");
|
||||
|
||||
var result = BunConfigHelper.ParseConfig(_tempDir);
|
||||
|
||||
Assert.Null(result.DefaultRegistry);
|
||||
Assert.Empty(result.ScopeRegistries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseConfig_BothDefaultAndScoped_ReturnsBoth()
|
||||
{
|
||||
var bunfig = """
|
||||
[install]
|
||||
registry = "https://default.registry.com/"
|
||||
|
||||
[install.scopes]
|
||||
"@company" = "https://npm.company.com/"
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "bunfig.toml"), bunfig);
|
||||
|
||||
var result = BunConfigHelper.ParseConfig(_tempDir);
|
||||
|
||||
Assert.Equal("https://default.registry.com/", result.DefaultRegistry);
|
||||
Assert.Single(result.ScopeRegistries);
|
||||
Assert.Equal("https://npm.company.com/", result.ScopeRegistries["@company"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StripQuotes Tests
|
||||
|
||||
[Fact]
|
||||
public void StripQuotes_DoubleQuotes_RemovesQuotes()
|
||||
{
|
||||
var result = BunConfigHelper.StripQuotes("\"hello world\"");
|
||||
|
||||
Assert.Equal("hello world", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripQuotes_SingleQuotes_RemovesQuotes()
|
||||
{
|
||||
var result = BunConfigHelper.StripQuotes("'hello world'");
|
||||
|
||||
Assert.Equal("hello world", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripQuotes_NoQuotes_ReturnsUnchanged()
|
||||
{
|
||||
var result = BunConfigHelper.StripQuotes("hello world");
|
||||
|
||||
Assert.Equal("hello world", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripQuotes_MismatchedQuotes_ReturnsUnchanged()
|
||||
{
|
||||
var result = BunConfigHelper.StripQuotes("\"hello world'");
|
||||
|
||||
Assert.Equal("\"hello world'", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripQuotes_EmptyString_ReturnsEmpty()
|
||||
{
|
||||
var result = BunConfigHelper.StripQuotes("");
|
||||
|
||||
Assert.Equal("", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripQuotes_SingleCharacter_ReturnsUnchanged()
|
||||
{
|
||||
var result = BunConfigHelper.StripQuotes("a");
|
||||
|
||||
Assert.Equal("a", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ExtractRegistryUrl Tests
|
||||
|
||||
[Fact]
|
||||
public void ExtractRegistryUrl_DirectUrl_ReturnsUrl()
|
||||
{
|
||||
var result = BunConfigHelper.ExtractRegistryUrl("https://npm.company.com/");
|
||||
|
||||
Assert.Equal("https://npm.company.com/", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractRegistryUrl_InlineTable_ExtractsUrl()
|
||||
{
|
||||
var result = BunConfigHelper.ExtractRegistryUrl("{ url = \"https://npm.company.com/\" }");
|
||||
|
||||
Assert.Equal("https://npm.company.com/", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractRegistryUrl_InlineTableSingleQuotes_ExtractsUrl()
|
||||
{
|
||||
var result = BunConfigHelper.ExtractRegistryUrl("{ url = 'https://npm.company.com/' }");
|
||||
|
||||
Assert.Equal("https://npm.company.com/", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractRegistryUrl_InvalidFormat_ReturnsNull()
|
||||
{
|
||||
var result = BunConfigHelper.ExtractRegistryUrl("not-a-url");
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractRegistryUrl_HttpUrl_ReturnsUrl()
|
||||
{
|
||||
var result = BunConfigHelper.ExtractRegistryUrl("http://internal.registry.local/");
|
||||
|
||||
Assert.Equal("http://internal.registry.local/", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Bun.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Bun.Tests.Parsers;
|
||||
|
||||
public sealed class BunLockParserTests
|
||||
{
|
||||
#region ParsePackageKey Tests
|
||||
|
||||
[Fact]
|
||||
public void ParsePackageKey_ScopedPackage_ReturnsCorrectNameAndVersion()
|
||||
{
|
||||
var (name, version) = BunLockParser.ParsePackageKey("@babel/core@7.24.0");
|
||||
|
||||
Assert.Equal("@babel/core", name);
|
||||
Assert.Equal("7.24.0", version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePackageKey_UnscopedPackage_ReturnsCorrectNameAndVersion()
|
||||
{
|
||||
var (name, version) = BunLockParser.ParsePackageKey("lodash@4.17.21");
|
||||
|
||||
Assert.Equal("lodash", name);
|
||||
Assert.Equal("4.17.21", version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePackageKey_InvalidFormat_NoAtSymbol_ReturnsEmpty()
|
||||
{
|
||||
var (name, version) = BunLockParser.ParsePackageKey("lodash");
|
||||
|
||||
Assert.Empty(name);
|
||||
Assert.Empty(version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePackageKey_InvalidFormat_OnlyScope_ReturnsEmpty()
|
||||
{
|
||||
var (name, version) = BunLockParser.ParsePackageKey("@babel");
|
||||
|
||||
Assert.Empty(name);
|
||||
Assert.Empty(version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePackageKey_ScopedPackageWithComplexVersion_ReturnsCorrectParts()
|
||||
{
|
||||
var (name, version) = BunLockParser.ParsePackageKey("@types/node@20.11.24");
|
||||
|
||||
Assert.Equal("@types/node", name);
|
||||
Assert.Equal("20.11.24", version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePackageKey_PreReleaseVersion_ReturnsCorrectParts()
|
||||
{
|
||||
var (name, version) = BunLockParser.ParsePackageKey("typescript@5.4.0-beta");
|
||||
|
||||
Assert.Equal("typescript", name);
|
||||
Assert.Equal("5.4.0-beta", version);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ClassifyResolvedUrl Tests
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_GitPlusHttps_ReturnsGit()
|
||||
{
|
||||
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("git+https://github.com/user/repo.git#abc123");
|
||||
|
||||
Assert.Equal("git", sourceType);
|
||||
Assert.Equal("abc123", gitCommit);
|
||||
Assert.Equal("git+https://github.com/user/repo.git#abc123", specifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_GitPlusSsh_ReturnsGit()
|
||||
{
|
||||
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("git+ssh://git@github.com/user/repo.git#v1.0.0");
|
||||
|
||||
Assert.Equal("git", sourceType);
|
||||
Assert.Equal("v1.0.0", gitCommit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_GithubShorthand_ReturnsGit()
|
||||
{
|
||||
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("github:user/repo#main");
|
||||
|
||||
Assert.Equal("git", sourceType);
|
||||
Assert.Equal("main", gitCommit);
|
||||
Assert.Equal("github:user/repo#main", specifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_GitlabShorthand_ReturnsGit()
|
||||
{
|
||||
var (sourceType, _, _) = BunLockParser.ClassifyResolvedUrl("gitlab:user/repo#v2.0.0");
|
||||
|
||||
Assert.Equal("git", sourceType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_BitbucketShorthand_ReturnsGit()
|
||||
{
|
||||
var (sourceType, _, _) = BunLockParser.ClassifyResolvedUrl("bitbucket:user/repo#feature");
|
||||
|
||||
Assert.Equal("git", sourceType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_TarballUrl_ReturnsTarball()
|
||||
{
|
||||
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("https://example.com/pkg-1.0.0.tgz");
|
||||
|
||||
Assert.Equal("tarball", sourceType);
|
||||
Assert.Null(gitCommit);
|
||||
Assert.Equal("https://example.com/pkg-1.0.0.tgz", specifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_TarGzUrl_ReturnsTarball()
|
||||
{
|
||||
var (sourceType, _, _) = BunLockParser.ClassifyResolvedUrl("https://example.com/pkg-1.0.0.tar.gz");
|
||||
|
||||
Assert.Equal("tarball", sourceType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_NpmRegistryTgz_ReturnsNpm()
|
||||
{
|
||||
var (sourceType, _, _) = BunLockParser.ClassifyResolvedUrl("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz");
|
||||
|
||||
Assert.Equal("npm", sourceType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_FileProtocol_ReturnsFile()
|
||||
{
|
||||
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("file:./local-pkg");
|
||||
|
||||
Assert.Equal("file", sourceType);
|
||||
Assert.Null(gitCommit);
|
||||
Assert.Equal("file:./local-pkg", specifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_LinkProtocol_ReturnsLink()
|
||||
{
|
||||
var (sourceType, _, specifier) = BunLockParser.ClassifyResolvedUrl("link:../packages/shared");
|
||||
|
||||
Assert.Equal("link", sourceType);
|
||||
Assert.Equal("link:../packages/shared", specifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_WorkspaceProtocol_ReturnsWorkspace()
|
||||
{
|
||||
var (sourceType, _, specifier) = BunLockParser.ClassifyResolvedUrl("workspace:*");
|
||||
|
||||
Assert.Equal("workspace", sourceType);
|
||||
Assert.Equal("workspace:*", specifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_NpmRegistry_ReturnsNpm()
|
||||
{
|
||||
var (sourceType, gitCommit, specifier) = BunLockParser.ClassifyResolvedUrl("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz");
|
||||
|
||||
Assert.Equal("npm", sourceType);
|
||||
Assert.Null(gitCommit);
|
||||
Assert.Null(specifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyResolvedUrl_NullOrEmpty_ReturnsNpm()
|
||||
{
|
||||
var (sourceType1, _, _) = BunLockParser.ClassifyResolvedUrl(null);
|
||||
var (sourceType2, _, _) = BunLockParser.ClassifyResolvedUrl("");
|
||||
|
||||
Assert.Equal("npm", sourceType1);
|
||||
Assert.Equal("npm", sourceType2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ExtractGitCommit Tests
|
||||
|
||||
[Fact]
|
||||
public void ExtractGitCommit_HashFragment_ReturnsCommit()
|
||||
{
|
||||
var commit = BunLockParser.ExtractGitCommit("git+https://github.com/user/repo.git#abc123def");
|
||||
|
||||
Assert.Equal("abc123def", commit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractGitCommit_NoFragment_ReturnsNull()
|
||||
{
|
||||
var commit = BunLockParser.ExtractGitCommit("git+https://github.com/user/repo.git");
|
||||
|
||||
Assert.Null(commit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractGitCommit_EmptyFragment_ReturnsNull()
|
||||
{
|
||||
var commit = BunLockParser.ExtractGitCommit("github:user/repo#");
|
||||
|
||||
Assert.Null(commit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractGitCommit_TagName_ReturnsTag()
|
||||
{
|
||||
var commit = BunLockParser.ExtractGitCommit("github:user/repo#v1.2.3");
|
||||
|
||||
Assert.Equal("v1.2.3", commit);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parse Tests
|
||||
|
||||
[Fact]
|
||||
public void Parse_EmptyContent_ReturnsEmptyData()
|
||||
{
|
||||
var result = BunLockParser.Parse("");
|
||||
|
||||
Assert.Empty(result.Entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_WhitespaceContent_ReturnsEmptyData()
|
||||
{
|
||||
var result = BunLockParser.Parse(" \n\t ");
|
||||
|
||||
Assert.Empty(result.Entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MalformedJson_ReturnsEmptyData()
|
||||
{
|
||||
var result = BunLockParser.Parse("{ invalid json }");
|
||||
|
||||
Assert.Empty(result.Entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_JsoncComments_IgnoresCommentsAndParses()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
// This is a comment
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-abc"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Entries);
|
||||
Assert.Equal("lodash", result.Entries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_TrailingCommas_ParsesSuccessfully()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-abc"],
|
||||
},
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArrayFormat_ExtractsResolvedAndIntegrity()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"ms@2.1.3": ["https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "sha512-6FlzubTLZG3J2a"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Entries);
|
||||
var entry = result.Entries[0];
|
||||
Assert.Equal("ms", entry.Name);
|
||||
Assert.Equal("2.1.3", entry.Version);
|
||||
Assert.Equal("https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", entry.Resolved);
|
||||
Assert.Equal("sha512-6FlzubTLZG3J2a", entry.Integrity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ArrayFormat_ExtractsDependencies()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"debug@4.3.4": ["https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "sha512-abc", {"ms": "^2.1.3"}]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Entries);
|
||||
var entry = result.Entries[0];
|
||||
Assert.Single(entry.Dependencies);
|
||||
Assert.Contains("ms", entry.Dependencies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ObjectFormat_ExtractsDevOptionalPeer()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"typescript@5.4.0": {
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.0.tgz",
|
||||
"integrity": "sha512-abc",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Entries);
|
||||
var entry = result.Entries[0];
|
||||
Assert.Equal("typescript", entry.Name);
|
||||
Assert.True(entry.IsDev);
|
||||
Assert.True(entry.IsOptional);
|
||||
Assert.True(entry.IsPeer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_StringFormat_ExtractsResolved()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"lodash@4.17.21": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Entries);
|
||||
var entry = result.Entries[0];
|
||||
Assert.Equal("lodash", entry.Name);
|
||||
Assert.Equal("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", entry.Resolved);
|
||||
Assert.Null(entry.Integrity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SkipsRootProjectEntry()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"": {},
|
||||
".": {},
|
||||
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-abc"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Entries);
|
||||
Assert.Equal("lodash", result.Entries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MultiplePackages_ReturnsAll()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"lodash@4.17.21": ["https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "sha512-lodash"],
|
||||
"ms@2.1.3": ["https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "sha512-ms"],
|
||||
"@babel/core@7.24.0": ["https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "sha512-babel"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Equal(3, result.Entries.Count);
|
||||
Assert.Contains(result.Entries, e => e.Name == "lodash");
|
||||
Assert.Contains(result.Entries, e => e.Name == "ms");
|
||||
Assert.Contains(result.Entries, e => e.Name == "@babel/core");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_GitDependency_ClassifiesCorrectly()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"my-pkg@1.0.0": ["git+https://github.com/user/my-pkg.git#abc123", null]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Entries);
|
||||
var entry = result.Entries[0];
|
||||
Assert.Equal("git", entry.SourceType);
|
||||
Assert.Equal("abc123", entry.GitCommit);
|
||||
Assert.Equal("git+https://github.com/user/my-pkg.git#abc123", entry.Specifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_NoPackagesProperty_ReturnsEmpty()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
""";
|
||||
|
||||
var result = BunLockParser.Parse(content);
|
||||
|
||||
Assert.Empty(result.Entries);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Bun.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Bun.Tests.Parsers;
|
||||
|
||||
public sealed class BunPackageTests
|
||||
{
|
||||
#region Purl Generation Tests
|
||||
|
||||
[Fact]
|
||||
public void FromPackageJson_UnscopedPackage_GeneratesCorrectPurl()
|
||||
{
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules/lodash",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: null);
|
||||
|
||||
Assert.Equal("pkg:npm/lodash@4.17.21", package.Purl);
|
||||
Assert.Equal("purl::pkg:npm/lodash@4.17.21", package.ComponentKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromPackageJson_ScopedPackage_EncodesAtSymbol()
|
||||
{
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "@babel/core",
|
||||
version: "7.24.0",
|
||||
logicalPath: "node_modules/@babel/core",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: null);
|
||||
|
||||
Assert.Equal("pkg:npm/%40babel/core@7.24.0", package.Purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromPackageJson_ScopedPackageWithSlash_EncodesCorrectly()
|
||||
{
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "@types/node",
|
||||
version: "20.11.0",
|
||||
logicalPath: "node_modules/@types/node",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: null);
|
||||
|
||||
Assert.Equal("pkg:npm/%40types/node@20.11.0", package.Purl);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateMetadata Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_BasicPackage_ReturnsRequiredKeys()
|
||||
{
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules/lodash",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: null);
|
||||
|
||||
var metadata = package.CreateMetadata().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
Assert.True(metadata.ContainsKey("packageManager"));
|
||||
Assert.Equal("bun", metadata["packageManager"]);
|
||||
Assert.True(metadata.ContainsKey("source"));
|
||||
Assert.Equal("node_modules", metadata["source"]);
|
||||
Assert.True(metadata.ContainsKey("path"));
|
||||
Assert.Equal("node_modules/lodash", metadata["path"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_AllFieldsSet_ReturnsAllKeys()
|
||||
{
|
||||
var lockEntry = new BunLockEntry
|
||||
{
|
||||
Name = "lodash",
|
||||
Version = "4.17.21",
|
||||
Resolved = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
Integrity = "sha512-abc",
|
||||
IsDev = true,
|
||||
IsOptional = true,
|
||||
IsPeer = true,
|
||||
SourceType = "git",
|
||||
GitCommit = "abc123",
|
||||
Specifier = "github:lodash/lodash#abc123"
|
||||
};
|
||||
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules/lodash",
|
||||
realPath: "node_modules/.bun/lodash@4.17.21",
|
||||
isPrivate: true,
|
||||
lockEntry: lockEntry);
|
||||
|
||||
package.IsDirect = true;
|
||||
package.IsPatched = true;
|
||||
package.PatchFile = "patches/lodash.patch";
|
||||
package.CustomRegistry = "https://npm.company.com/";
|
||||
|
||||
var metadata = package.CreateMetadata().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
Assert.Equal("true", metadata["dev"]);
|
||||
Assert.Equal("true", metadata["direct"]);
|
||||
Assert.Equal("true", metadata["optional"]);
|
||||
Assert.Equal("true", metadata["peer"]);
|
||||
Assert.Equal("true", metadata["private"]);
|
||||
Assert.Equal("true", metadata["patched"]);
|
||||
Assert.Equal("patches/lodash.patch", metadata["patchFile"]);
|
||||
Assert.Equal("https://npm.company.com/", metadata["customRegistry"]);
|
||||
Assert.Equal("abc123", metadata["gitCommit"]);
|
||||
Assert.Equal("git", metadata["sourceType"]);
|
||||
Assert.Equal("github:lodash/lodash#abc123", metadata["specifier"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_SortedAlphabetically()
|
||||
{
|
||||
var lockEntry = new BunLockEntry
|
||||
{
|
||||
Name = "lodash",
|
||||
Version = "4.17.21",
|
||||
Resolved = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
Integrity = "sha512-abc",
|
||||
IsDev = true
|
||||
};
|
||||
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules/lodash",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: lockEntry);
|
||||
|
||||
package.IsDirect = true;
|
||||
|
||||
var keys = package.CreateMetadata().Select(kvp => kvp.Key).ToList();
|
||||
|
||||
// Verify keys are sorted alphabetically
|
||||
var sortedKeys = keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
|
||||
Assert.Equal(sortedKeys, keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_NormalizesPathSeparators()
|
||||
{
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules\\lodash",
|
||||
realPath: "node_modules\\.bun\\lodash@4.17.21",
|
||||
isPrivate: false,
|
||||
lockEntry: null);
|
||||
|
||||
var metadata = package.CreateMetadata().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
Assert.Equal("node_modules/lodash", metadata["path"]);
|
||||
Assert.Equal("node_modules/.bun/lodash@4.17.21", metadata["realPath"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateMetadata_MultipleOccurrences_JoinsWithSemicolon()
|
||||
{
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules/lodash",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: null);
|
||||
|
||||
package.AddOccurrence("node_modules/lodash");
|
||||
package.AddOccurrence("packages/app/node_modules/lodash");
|
||||
|
||||
var metadata = package.CreateMetadata().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
Assert.True(metadata.ContainsKey("occurrences"));
|
||||
Assert.Contains(";", metadata["occurrences"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CreateEvidence Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateEvidence_WithResolvedAndIntegrity_ReturnsAll()
|
||||
{
|
||||
var lockEntry = new BunLockEntry
|
||||
{
|
||||
Name = "lodash",
|
||||
Version = "4.17.21",
|
||||
Resolved = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
Integrity = "sha512-abc123"
|
||||
};
|
||||
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules/lodash",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: lockEntry);
|
||||
|
||||
var evidence = package.CreateEvidence().ToList();
|
||||
|
||||
Assert.Equal(3, evidence.Count);
|
||||
|
||||
// File evidence
|
||||
var fileEvidence = evidence.FirstOrDefault(e => e.Kind == LanguageEvidenceKind.File);
|
||||
Assert.NotNull(fileEvidence);
|
||||
Assert.Equal("node_modules", fileEvidence.Source);
|
||||
Assert.Equal("node_modules/lodash/package.json", fileEvidence.Locator);
|
||||
|
||||
// Resolved evidence
|
||||
var resolvedEvidence = evidence.FirstOrDefault(e => e.Source == "resolved");
|
||||
Assert.NotNull(resolvedEvidence);
|
||||
Assert.Equal(LanguageEvidenceKind.Metadata, resolvedEvidence.Kind);
|
||||
Assert.Equal("bun.lock", resolvedEvidence.Locator);
|
||||
Assert.Equal("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", resolvedEvidence.Value);
|
||||
|
||||
// Integrity evidence
|
||||
var integrityEvidence = evidence.FirstOrDefault(e => e.Source == "integrity");
|
||||
Assert.NotNull(integrityEvidence);
|
||||
Assert.Equal("sha512-abc123", integrityEvidence.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateEvidence_NoLockEntry_ReturnsOnlyFileEvidence()
|
||||
{
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules/lodash",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: null);
|
||||
|
||||
var evidence = package.CreateEvidence().ToList();
|
||||
|
||||
Assert.Single(evidence);
|
||||
Assert.Equal(LanguageEvidenceKind.File, evidence[0].Kind);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FromLockEntry Tests
|
||||
|
||||
[Fact]
|
||||
public void FromLockEntry_CreatesPackageWithAllProperties()
|
||||
{
|
||||
var lockEntry = new BunLockEntry
|
||||
{
|
||||
Name = "ms",
|
||||
Version = "2.1.3",
|
||||
Resolved = "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
Integrity = "sha512-6FlzubTLZG3J2a",
|
||||
IsDev = true,
|
||||
IsOptional = false,
|
||||
IsPeer = false,
|
||||
SourceType = "npm",
|
||||
Dependencies = new List<string> { "debug" }
|
||||
};
|
||||
|
||||
var package = BunPackage.FromLockEntry(lockEntry, "bun.lock");
|
||||
|
||||
Assert.Equal("ms", package.Name);
|
||||
Assert.Equal("2.1.3", package.Version);
|
||||
Assert.Equal("pkg:npm/ms@2.1.3", package.Purl);
|
||||
Assert.Equal("bun.lock", package.Source);
|
||||
Assert.Equal("https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", package.Resolved);
|
||||
Assert.Equal("sha512-6FlzubTLZG3J2a", package.Integrity);
|
||||
Assert.True(package.IsDev);
|
||||
Assert.False(package.IsOptional);
|
||||
Assert.Equal("npm", package.SourceType);
|
||||
Assert.Contains("debug", package.Dependencies);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AddOccurrence Tests
|
||||
|
||||
[Fact]
|
||||
public void AddOccurrence_AddsDuplicatePath_DoesNotDuplicate()
|
||||
{
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules/lodash",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: null);
|
||||
|
||||
package.AddOccurrence("node_modules/lodash");
|
||||
package.AddOccurrence("node_modules/lodash");
|
||||
|
||||
Assert.Single(package.OccurrencePaths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOccurrence_AddsMultiplePaths_StoresAll()
|
||||
{
|
||||
var package = BunPackage.FromPackageJson(
|
||||
name: "lodash",
|
||||
version: "4.17.21",
|
||||
logicalPath: "node_modules/lodash",
|
||||
realPath: null,
|
||||
isPrivate: false,
|
||||
lockEntry: null);
|
||||
|
||||
package.AddOccurrence("node_modules/lodash");
|
||||
package.AddOccurrence("packages/app/node_modules/lodash");
|
||||
package.AddOccurrence("packages/lib/node_modules/lodash");
|
||||
|
||||
Assert.Equal(3, package.OccurrencePaths.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Bun.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Bun.Tests.Parsers;
|
||||
|
||||
public sealed class BunWorkspaceHelperTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public BunWorkspaceHelperTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"bun-workspace-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
#region ParseWorkspaceInfo Tests
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_MissingPackageJson_ReturnsEmpty()
|
||||
{
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Empty(result.WorkspacePatterns);
|
||||
Assert.Empty(result.WorkspacePaths);
|
||||
Assert.Empty(result.DirectDependencies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_NoWorkspaces_ReturnsEmptyPatterns()
|
||||
{
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "my-project",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Empty(result.WorkspacePatterns);
|
||||
Assert.Single(result.DirectDependencies);
|
||||
Assert.True(result.DirectDependencies.ContainsKey("lodash"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_ArrayFormatWorkspaces_ReturnsPatterns()
|
||||
{
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "my-project",
|
||||
"workspaces": ["packages/*", "apps/*"]
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Equal(2, result.WorkspacePatterns.Count);
|
||||
Assert.Contains("packages/*", result.WorkspacePatterns);
|
||||
Assert.Contains("apps/*", result.WorkspacePatterns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_ObjectFormatWorkspaces_ReturnsPatterns()
|
||||
{
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "my-project",
|
||||
"workspaces": {
|
||||
"packages": ["packages/*", "apps/*"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Equal(2, result.WorkspacePatterns.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_ResolvesWorkspacePaths()
|
||||
{
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "my-project",
|
||||
"workspaces": ["packages/*"]
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
|
||||
|
||||
// Create workspace packages
|
||||
var pkgADir = Path.Combine(_tempDir, "packages", "pkg-a");
|
||||
Directory.CreateDirectory(pkgADir);
|
||||
File.WriteAllText(Path.Combine(pkgADir, "package.json"), """{"name": "@my/pkg-a"}""");
|
||||
|
||||
var pkgBDir = Path.Combine(_tempDir, "packages", "pkg-b");
|
||||
Directory.CreateDirectory(pkgBDir);
|
||||
File.WriteAllText(Path.Combine(pkgBDir, "package.json"), """{"name": "@my/pkg-b"}""");
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Equal(2, result.WorkspacePaths.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_ParsesAllDependencyTypes()
|
||||
{
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "my-project",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Equal(4, result.DirectDependencies.Count);
|
||||
|
||||
Assert.True(result.DirectDependencies.ContainsKey("lodash"));
|
||||
Assert.Equal(BunWorkspaceHelper.DependencyType.Production, result.DirectDependencies["lodash"]);
|
||||
|
||||
Assert.True(result.DirectDependencies.ContainsKey("typescript"));
|
||||
Assert.Equal(BunWorkspaceHelper.DependencyType.Dev, result.DirectDependencies["typescript"]);
|
||||
|
||||
Assert.True(result.DirectDependencies.ContainsKey("fsevents"));
|
||||
Assert.Equal(BunWorkspaceHelper.DependencyType.Optional, result.DirectDependencies["fsevents"]);
|
||||
|
||||
Assert.True(result.DirectDependencies.ContainsKey("react"));
|
||||
Assert.Equal(BunWorkspaceHelper.DependencyType.Peer, result.DirectDependencies["react"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_MergesDependencyFlags()
|
||||
{
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "my-project",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"lodash": "^4.17.0"
|
||||
}
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Single(result.DirectDependencies);
|
||||
var depType = result.DirectDependencies["lodash"];
|
||||
Assert.True((depType & BunWorkspaceHelper.DependencyType.Production) != 0);
|
||||
Assert.True((depType & BunWorkspaceHelper.DependencyType.Peer) != 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_ParsesPatchedDependencies()
|
||||
{
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "my-project",
|
||||
"patchedDependencies": {
|
||||
"lodash@4.17.21": "patches/lodash@4.17.21.patch"
|
||||
}
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Single(result.PatchedDependencies);
|
||||
Assert.True(result.PatchedDependencies.ContainsKey("lodash"));
|
||||
Assert.Equal("patches/lodash@4.17.21.patch", result.PatchedDependencies["lodash"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_ScansPatchesDirectory()
|
||||
{
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "my-project"
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
|
||||
|
||||
// Create patches directory with patch files
|
||||
var patchesDir = Path.Combine(_tempDir, "patches");
|
||||
Directory.CreateDirectory(patchesDir);
|
||||
File.WriteAllText(Path.Combine(patchesDir, "lodash@4.17.21.patch"), "diff content");
|
||||
File.WriteAllText(Path.Combine(patchesDir, "@babel+core@7.24.0.patch"), "diff content");
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Equal(2, result.PatchedDependencies.Count);
|
||||
Assert.True(result.PatchedDependencies.ContainsKey("lodash"));
|
||||
Assert.True(result.PatchedDependencies.ContainsKey("@babel+core"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_ScansBunPatchesDirectory()
|
||||
{
|
||||
var packageJson = """
|
||||
{
|
||||
"name": "my-project"
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), packageJson);
|
||||
|
||||
// Create .patches directory (Bun-specific)
|
||||
var patchesDir = Path.Combine(_tempDir, ".patches");
|
||||
Directory.CreateDirectory(patchesDir);
|
||||
File.WriteAllText(Path.Combine(patchesDir, "ms@2.1.3.patch"), "diff content");
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Single(result.PatchedDependencies);
|
||||
Assert.True(result.PatchedDependencies.ContainsKey("ms"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseWorkspaceInfo_MalformedJson_ReturnsEmpty()
|
||||
{
|
||||
File.WriteAllText(Path.Combine(_tempDir, "package.json"), "{ invalid json }");
|
||||
|
||||
var result = BunWorkspaceHelper.ParseWorkspaceInfo(_tempDir);
|
||||
|
||||
Assert.Empty(result.DirectDependencies);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsDirect Tests
|
||||
|
||||
[Fact]
|
||||
public void IsDirect_DirectDependency_ReturnsTrue()
|
||||
{
|
||||
var deps = new Dictionary<string, BunWorkspaceHelper.DependencyType>
|
||||
{
|
||||
["lodash"] = BunWorkspaceHelper.DependencyType.Production
|
||||
};
|
||||
|
||||
var result = BunWorkspaceHelper.IsDirect("lodash", deps);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsDirect_TransitiveDependency_ReturnsFalse()
|
||||
{
|
||||
var deps = new Dictionary<string, BunWorkspaceHelper.DependencyType>
|
||||
{
|
||||
["lodash"] = BunWorkspaceHelper.DependencyType.Production
|
||||
};
|
||||
|
||||
var result = BunWorkspaceHelper.IsDirect("ms", deps);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Go.Tests.Internal;
|
||||
|
||||
public sealed class GoCgoDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AnalyzeGoFileContent_DetectsCgoImport()
|
||||
{
|
||||
var content = @"
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdio.h>
|
||||
*/
|
||||
import ""C""
|
||||
|
||||
func main() {
|
||||
C.puts(C.CString(""Hello from C""))
|
||||
}
|
||||
";
|
||||
|
||||
var result = GoCgoDetector.AnalyzeGoFileContent(content, "main.go");
|
||||
|
||||
Assert.True(result.HasCgoImport);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeGoFileContent_DetectsCgoDirectives()
|
||||
{
|
||||
var content = @"
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I/usr/local/include
|
||||
#cgo LDFLAGS: -L/usr/local/lib -lpng
|
||||
#cgo pkg-config: gtk+-3.0
|
||||
#include <png.h>
|
||||
*/
|
||||
import ""C""
|
||||
|
||||
func main() {}
|
||||
";
|
||||
|
||||
var result = GoCgoDetector.AnalyzeGoFileContent(content, "main.go");
|
||||
|
||||
Assert.True(result.HasCgoImport);
|
||||
Assert.Equal(3, result.Directives.Count);
|
||||
|
||||
var cflags = result.Directives.FirstOrDefault(d => d.Type == "CFLAGS");
|
||||
Assert.NotNull(cflags);
|
||||
Assert.Equal("-I/usr/local/include", cflags.Value);
|
||||
|
||||
var ldflags = result.Directives.FirstOrDefault(d => d.Type == "LDFLAGS");
|
||||
Assert.NotNull(ldflags);
|
||||
Assert.Equal("-L/usr/local/lib -lpng", ldflags.Value);
|
||||
|
||||
var pkgconfig = result.Directives.FirstOrDefault(d => d.Type == "pkg-config");
|
||||
Assert.NotNull(pkgconfig);
|
||||
Assert.Equal("gtk+-3.0", pkgconfig.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeGoFileContent_DetectsIncludedHeaders()
|
||||
{
|
||||
var content = @"
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include ""custom.h""
|
||||
*/
|
||||
import ""C""
|
||||
|
||||
func main() {}
|
||||
";
|
||||
|
||||
var result = GoCgoDetector.AnalyzeGoFileContent(content, "main.go");
|
||||
|
||||
Assert.True(result.HasCgoImport);
|
||||
Assert.Equal(3, result.Headers.Count);
|
||||
Assert.Contains("stdio.h", result.Headers);
|
||||
Assert.Contains("stdlib.h", result.Headers);
|
||||
Assert.Contains("custom.h", result.Headers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeGoFileContent_DetectsPlatformConstrainedDirectives()
|
||||
{
|
||||
var content = @"
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo linux LDFLAGS: -lm
|
||||
#cgo darwin LDFLAGS: -framework CoreFoundation
|
||||
#cgo windows LDFLAGS: -lkernel32
|
||||
*/
|
||||
import ""C""
|
||||
|
||||
func main() {}
|
||||
";
|
||||
|
||||
var result = GoCgoDetector.AnalyzeGoFileContent(content, "main.go");
|
||||
|
||||
Assert.True(result.HasCgoImport);
|
||||
Assert.Equal(3, result.Directives.Count);
|
||||
|
||||
var linuxLdflags = result.Directives.FirstOrDefault(d => d.Constraint?.Contains("linux") == true);
|
||||
Assert.NotNull(linuxLdflags);
|
||||
Assert.Equal("-lm", linuxLdflags.Value);
|
||||
|
||||
var darwinLdflags = result.Directives.FirstOrDefault(d => d.Constraint?.Contains("darwin") == true);
|
||||
Assert.NotNull(darwinLdflags);
|
||||
Assert.Equal("-framework CoreFoundation", darwinLdflags.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeGoFileContent_NoCgoImport_ReturnsEmpty()
|
||||
{
|
||||
var content = @"
|
||||
package main
|
||||
|
||||
import ""fmt""
|
||||
|
||||
func main() {
|
||||
fmt.Println(""Hello"")
|
||||
}
|
||||
";
|
||||
|
||||
var result = GoCgoDetector.AnalyzeGoFileContent(content, "main.go");
|
||||
|
||||
Assert.False(result.HasCgoImport);
|
||||
Assert.Empty(result.Directives);
|
||||
Assert.Empty(result.Headers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractFromBuildSettings_ExtractsCgoEnabled()
|
||||
{
|
||||
var settings = new List<KeyValuePair<string, string?>>
|
||||
{
|
||||
new("CGO_ENABLED", "1"),
|
||||
new("CGO_CFLAGS", "-I/usr/include"),
|
||||
new("CGO_LDFLAGS", "-L/usr/lib -lssl"),
|
||||
new("CC", "gcc"),
|
||||
new("CXX", "g++"),
|
||||
};
|
||||
|
||||
var result = GoCgoDetector.ExtractFromBuildSettings(settings);
|
||||
|
||||
Assert.True(result.CgoEnabled);
|
||||
Assert.Equal("-I/usr/include", result.CgoFlags);
|
||||
Assert.Equal("-L/usr/lib -lssl", result.CgoLdFlags);
|
||||
Assert.Equal("gcc", result.CCompiler);
|
||||
Assert.Equal("g++", result.CxxCompiler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractFromBuildSettings_CgoDisabled_ReturnsFalse()
|
||||
{
|
||||
var settings = new List<KeyValuePair<string, string?>>
|
||||
{
|
||||
new("CGO_ENABLED", "0"),
|
||||
};
|
||||
|
||||
var result = GoCgoDetector.ExtractFromBuildSettings(settings);
|
||||
|
||||
Assert.False(result.CgoEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractFromBuildSettings_NoSettings_ReturnsEmpty()
|
||||
{
|
||||
var settings = new List<KeyValuePair<string, string?>>();
|
||||
|
||||
var result = GoCgoDetector.ExtractFromBuildSettings(settings);
|
||||
|
||||
Assert.False(result.CgoEnabled);
|
||||
Assert.True(result.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CgoAnalysisResult_GetCFlags_CombinesMultipleDirectives()
|
||||
{
|
||||
var directives = new[]
|
||||
{
|
||||
new GoCgoDetector.CgoDirective("CFLAGS", "-I/usr/include", null, "a.go"),
|
||||
new GoCgoDetector.CgoDirective("CFLAGS", "-I/usr/local/include", null, "b.go"),
|
||||
};
|
||||
|
||||
var result = new GoCgoDetector.CgoAnalysisResult(
|
||||
true,
|
||||
["a.go", "b.go"],
|
||||
[.. directives],
|
||||
[],
|
||||
[]);
|
||||
|
||||
var cflags = result.GetCFlags();
|
||||
|
||||
Assert.NotNull(cflags);
|
||||
Assert.Contains("-I/usr/include", cflags);
|
||||
Assert.Contains("-I/usr/local/include", cflags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Go.Tests.Internal;
|
||||
|
||||
public sealed class GoLicenseDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsMitLicense()
|
||||
{
|
||||
var content = @"
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Example Corp
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the ""Software""), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software...
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("MIT", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.Medium, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsApache2License()
|
||||
{
|
||||
var content = @"
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("Apache-2.0", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.Medium, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsBsd3ClauseLicense()
|
||||
{
|
||||
var content = @"
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2023, Example Corp
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("BSD-3-Clause", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.Medium, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsGpl3License()
|
||||
{
|
||||
var content = @"
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("GPL-3.0-only", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.Medium, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsIscLicense()
|
||||
{
|
||||
var content = @"
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2023, Example Corp
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("ISC", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.Medium, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsUnlicense()
|
||||
{
|
||||
var content = @"
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("Unlicense", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.Medium, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsSpdxIdentifier()
|
||||
{
|
||||
var content = @"
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Some license text here...
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("Apache-2.0", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.High, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsDualLicenseSpdx()
|
||||
{
|
||||
var content = @"
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
Dual licensed under MIT and Apache 2.0
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("MIT OR Apache-2.0", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.High, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsMpl2License()
|
||||
{
|
||||
var content = @"
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("MPL-2.0", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.Medium, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_EmptyContent_ReturnsUnknown()
|
||||
{
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent("");
|
||||
|
||||
Assert.False(result.IsDetected);
|
||||
Assert.Null(result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.None, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_UnrecognizedContent_ReturnsUnknown()
|
||||
{
|
||||
var content = @"
|
||||
This is some custom proprietary license text that doesn't match any known patterns.
|
||||
No redistribution allowed without express written permission.
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.False(result.IsDetected);
|
||||
Assert.Null(result.SpdxIdentifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_KeywordFallback_DetectsMit()
|
||||
{
|
||||
var content = @"
|
||||
Some text mentioning MIT but not in the standard format
|
||||
This project is licensed under MIT terms
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
// Should detect MIT via keyword fallback with low confidence
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("MIT", result.SpdxIdentifier);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.Low, result.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LicenseInfo_Unknown_IsDetectedFalse()
|
||||
{
|
||||
var info = GoLicenseDetector.LicenseInfo.Unknown;
|
||||
|
||||
Assert.False(info.IsDetected);
|
||||
Assert.Null(info.SpdxIdentifier);
|
||||
Assert.Null(info.LicenseFile);
|
||||
Assert.Equal(GoLicenseDetector.LicenseConfidence.None, info.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsCC0License()
|
||||
{
|
||||
var content = @"
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP.
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("CC0-1.0", result.SpdxIdentifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsZlibLicense()
|
||||
{
|
||||
var content = @"
|
||||
zlib License
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("Zlib", result.SpdxIdentifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeLicenseContent_DetectsBoostLicense()
|
||||
{
|
||||
var content = @"
|
||||
Boost Software License - Version 1.0 - August 17th, 2003
|
||||
|
||||
Permission is hereby granted, free of charge, to any person or organization
|
||||
obtaining a copy of the software and accompanying documentation covered by
|
||||
this license (the ""Software"") to use, reproduce, display, distribute,
|
||||
execute, and transmit the Software...
|
||||
";
|
||||
|
||||
var result = GoLicenseDetector.AnalyzeLicenseContent(content);
|
||||
|
||||
Assert.True(result.IsDetected);
|
||||
Assert.Equal("BSL-1.0", result.SpdxIdentifier);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Go.Tests.Internal;
|
||||
|
||||
public sealed class GoVersionConflictDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsPseudoVersion_DetectsPseudoVersions()
|
||||
{
|
||||
// Standard pseudo-version formats
|
||||
Assert.True(GoVersionConflictDetector.IsPseudoVersion("v0.0.0-20210101120000-abcdef123456"));
|
||||
Assert.True(GoVersionConflictDetector.IsPseudoVersion("v1.2.3-0.20210101120000-abcdef123456"));
|
||||
Assert.True(GoVersionConflictDetector.IsPseudoVersion("v0.0.0-20230915143052-deadbeef1234"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPseudoVersion_RejectsRegularVersions()
|
||||
{
|
||||
Assert.False(GoVersionConflictDetector.IsPseudoVersion("v1.0.0"));
|
||||
Assert.False(GoVersionConflictDetector.IsPseudoVersion("v1.2.3"));
|
||||
Assert.False(GoVersionConflictDetector.IsPseudoVersion("v0.1.0-alpha"));
|
||||
Assert.False(GoVersionConflictDetector.IsPseudoVersion("v2.0.0-beta.1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_DetectsPseudoVersionConflict()
|
||||
{
|
||||
var modules = new List<GoSourceInventory.GoSourceModule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Path = "github.com/example/mod",
|
||||
Version = "v0.0.0-20210101120000-abcdef123456",
|
||||
},
|
||||
};
|
||||
|
||||
var result = GoVersionConflictDetector.Analyze(
|
||||
modules,
|
||||
[],
|
||||
[],
|
||||
ImmutableArray<string>.Empty);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Single(result.Conflicts);
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictType.PseudoVersion, result.Conflicts[0].ConflictType);
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictSeverity.Medium, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_DetectsReplaceOverrideConflict()
|
||||
{
|
||||
var modules = new List<GoSourceInventory.GoSourceModule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Path = "github.com/example/mod",
|
||||
Version = "v1.0.0",
|
||||
IsReplaced = true,
|
||||
ReplacementPath = "github.com/fork/mod",
|
||||
ReplacementVersion = "v1.1.0",
|
||||
},
|
||||
};
|
||||
|
||||
var result = GoVersionConflictDetector.Analyze(
|
||||
modules,
|
||||
[],
|
||||
[],
|
||||
ImmutableArray<string>.Empty);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Single(result.Conflicts);
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictType.ReplaceOverride, result.Conflicts[0].ConflictType);
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictSeverity.Low, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_DetectsLocalReplacementAsHighSeverity()
|
||||
{
|
||||
var modules = new List<GoSourceInventory.GoSourceModule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Path = "github.com/example/mod",
|
||||
Version = "v1.0.0",
|
||||
IsReplaced = true,
|
||||
ReplacementPath = "../local/mod",
|
||||
},
|
||||
};
|
||||
|
||||
var result = GoVersionConflictDetector.Analyze(
|
||||
modules,
|
||||
[],
|
||||
[],
|
||||
ImmutableArray<string>.Empty);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Single(result.Conflicts);
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictType.LocalReplacement, result.Conflicts[0].ConflictType);
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictSeverity.High, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_DetectsExcludedVersionConflict()
|
||||
{
|
||||
var modules = new List<GoSourceInventory.GoSourceModule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Path = "github.com/example/mod",
|
||||
Version = "v1.0.0",
|
||||
},
|
||||
};
|
||||
|
||||
var excludes = new List<GoModParser.GoModExclude>
|
||||
{
|
||||
new("github.com/example/mod", "v1.0.0"),
|
||||
};
|
||||
|
||||
var result = GoVersionConflictDetector.Analyze(
|
||||
modules,
|
||||
[],
|
||||
excludes,
|
||||
ImmutableArray<string>.Empty);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Single(result.Conflicts);
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictType.ExcludedVersion, result.Conflicts[0].ConflictType);
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictSeverity.High, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_DetectsMajorVersionMismatch()
|
||||
{
|
||||
var modules = new List<GoSourceInventory.GoSourceModule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Path = "github.com/example/mod",
|
||||
Version = "v1.0.0",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Path = "github.com/example/mod/v2",
|
||||
Version = "v2.0.0",
|
||||
},
|
||||
};
|
||||
|
||||
var result = GoVersionConflictDetector.Analyze(
|
||||
modules,
|
||||
[],
|
||||
[],
|
||||
ImmutableArray<string>.Empty);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(2, result.Conflicts.Length);
|
||||
Assert.All(result.Conflicts, c =>
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictType.MajorVersionMismatch, c.ConflictType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_NoConflicts_ReturnsEmpty()
|
||||
{
|
||||
var modules = new List<GoSourceInventory.GoSourceModule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Path = "github.com/example/mod",
|
||||
Version = "v1.0.0",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Path = "github.com/other/lib",
|
||||
Version = "v2.1.0",
|
||||
},
|
||||
};
|
||||
|
||||
var result = GoVersionConflictDetector.Analyze(
|
||||
modules,
|
||||
[],
|
||||
[],
|
||||
ImmutableArray<string>.Empty);
|
||||
|
||||
Assert.False(result.HasConflicts);
|
||||
Assert.Empty(result.Conflicts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConflict_ReturnsConflictForModule()
|
||||
{
|
||||
var modules = new List<GoSourceInventory.GoSourceModule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Path = "github.com/example/mod",
|
||||
Version = "v0.0.0-20210101120000-abcdef123456",
|
||||
},
|
||||
};
|
||||
|
||||
var result = GoVersionConflictDetector.Analyze(
|
||||
modules,
|
||||
[],
|
||||
[],
|
||||
ImmutableArray<string>.Empty);
|
||||
|
||||
var conflict = result.GetConflict("github.com/example/mod");
|
||||
|
||||
Assert.NotNull(conflict);
|
||||
Assert.Equal("github.com/example/mod", conflict.ModulePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConflict_ReturnsNullForNonConflictingModule()
|
||||
{
|
||||
var modules = new List<GoSourceInventory.GoSourceModule>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Path = "github.com/example/mod",
|
||||
Version = "v1.0.0",
|
||||
},
|
||||
};
|
||||
|
||||
var result = GoVersionConflictDetector.Analyze(
|
||||
modules,
|
||||
[],
|
||||
[],
|
||||
ImmutableArray<string>.Empty);
|
||||
|
||||
var conflict = result.GetConflict("github.com/example/mod");
|
||||
|
||||
Assert.Null(conflict);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeWorkspace_DetectsCrossModuleConflicts()
|
||||
{
|
||||
var inventory1 = new GoSourceInventory.SourceInventoryResult(
|
||||
"github.com/workspace/mod1",
|
||||
"1.21",
|
||||
[
|
||||
new GoSourceInventory.GoSourceModule
|
||||
{
|
||||
Path = "github.com/shared/dep",
|
||||
Version = "v1.0.0",
|
||||
},
|
||||
],
|
||||
ImmutableArray<string>.Empty,
|
||||
GoVersionConflictDetector.GoConflictAnalysis.Empty,
|
||||
GoCgoDetector.CgoAnalysisResult.Empty,
|
||||
null);
|
||||
|
||||
var inventory2 = new GoSourceInventory.SourceInventoryResult(
|
||||
"github.com/workspace/mod2",
|
||||
"1.21",
|
||||
[
|
||||
new GoSourceInventory.GoSourceModule
|
||||
{
|
||||
Path = "github.com/shared/dep",
|
||||
Version = "v1.2.0",
|
||||
},
|
||||
],
|
||||
ImmutableArray<string>.Empty,
|
||||
GoVersionConflictDetector.GoConflictAnalysis.Empty,
|
||||
GoCgoDetector.CgoAnalysisResult.Empty,
|
||||
null);
|
||||
|
||||
var result = GoVersionConflictDetector.AnalyzeWorkspace([inventory1, inventory2]);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Single(result.Conflicts);
|
||||
Assert.Equal(GoVersionConflictDetector.GoConflictType.WorkspaceConflict, result.Conflicts[0].ConflictType);
|
||||
Assert.Contains("v1.0.0", result.Conflicts[0].RequestedVersions);
|
||||
Assert.Contains("v1.2.0", result.Conflicts[0].RequestedVersions);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@
|
||||
"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"
|
||||
"path": "node_modules/.pnpm/pkg@1.2.3/node_modules/pkg",
|
||||
"riskLevel": "production",
|
||||
"scope": "production"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Conflicts;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Packaging;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.Conflicts;
|
||||
|
||||
public class VersionConflictDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Analyze_EmptyList_ReturnsEmpty()
|
||||
{
|
||||
var result = VersionConflictDetector.Analyze([]);
|
||||
|
||||
Assert.False(result.HasConflicts);
|
||||
Assert.Equal(0, result.TotalConflicts);
|
||||
Assert.Equal(ConflictSeverity.None, result.MaxSeverity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_SinglePackage_NoConflict()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("requests", "2.28.0", "/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.False(result.HasConflicts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_SameVersionMultipleLocations_NoConflict()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("requests", "2.28.0", "/env1/site-packages"),
|
||||
CreatePackage("requests", "2.28.0", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.False(result.HasConflicts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_DifferentVersions_DetectsConflict()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("requests", "2.28.0", "/env1/site-packages"),
|
||||
CreatePackage("requests", "2.31.0", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(1, result.TotalConflicts);
|
||||
|
||||
var conflict = result.Conflicts[0];
|
||||
Assert.Equal("requests", conflict.NormalizedName);
|
||||
Assert.Equal(2, conflict.UniqueVersions.Count());
|
||||
Assert.Contains("2.28.0", conflict.UniqueVersions);
|
||||
Assert.Contains("2.31.0", conflict.UniqueVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_MajorVersionDifference_HighSeverity()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("django", "3.2.0", "/env1/site-packages"),
|
||||
CreatePackage("django", "4.1.0", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.High, result.MaxSeverity);
|
||||
Assert.Equal(ConflictSeverity.High, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_MinorVersionDifference_MediumSeverity()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("flask", "2.1.0", "/env1/site-packages"),
|
||||
CreatePackage("flask", "2.3.0", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.Medium, result.MaxSeverity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_PatchVersionDifference_LowSeverity()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("pytest", "7.4.0", "/env1/site-packages"),
|
||||
CreatePackage("pytest", "7.4.3", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.Low, result.MaxSeverity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_EpochDifference_HighSeverity()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("pytz", "2023.3", "/env1/site-packages"),
|
||||
CreatePackage("pytz", "1!2023.3", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.High, result.MaxSeverity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_NormalizesPackageNames()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("My-Package", "1.0.0", "/env1/site-packages"),
|
||||
CreatePackage("my_package", "2.0.0", "/env2/site-packages"),
|
||||
CreatePackage("my.package", "3.0.0", "/env3/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(1, result.TotalConflicts);
|
||||
Assert.Equal(3, result.Conflicts[0].UniqueVersions.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_PreReleaseVersions_Handled()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("numpy", "1.24.0", "/env1/site-packages"),
|
||||
CreatePackage("numpy", "1.25.0rc1", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.Medium, result.MaxSeverity); // Minor difference
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_LocalVersions_Handled()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("mypackage", "1.0.0", "/env1/site-packages"),
|
||||
CreatePackage("mypackage", "1.0.0+local.build", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
// Local versions are different but same base version
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_PostReleaseVersions_Handled()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("setuptools", "68.0.0", "/env1/site-packages"),
|
||||
CreatePackage("setuptools", "68.0.0.post1", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.Low, result.MaxSeverity); // Same micro, just post release
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_MultipleConflicts_AllDetected()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("requests", "2.28.0", "/env1/site-packages"),
|
||||
CreatePackage("requests", "2.31.0", "/env2/site-packages"),
|
||||
CreatePackage("flask", "2.0.0", "/env1/site-packages"),
|
||||
CreatePackage("flask", "3.0.0", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(2, result.TotalConflicts);
|
||||
Assert.Equal(ConflictSeverity.High, result.MaxSeverity); // Flask has major diff
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_PackagesWithoutVersion_Ignored()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("mypackage", null, "/env1/site-packages"),
|
||||
CreatePackage("mypackage", "1.0.0", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.False(result.HasConflicts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConflict_ReturnsSpecificConflict()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("requests", "2.28.0", "/env1/site-packages"),
|
||||
CreatePackage("requests", "2.31.0", "/env2/site-packages"),
|
||||
CreatePackage("flask", "2.0.0", "/env1/site-packages")
|
||||
};
|
||||
|
||||
var conflict = VersionConflictDetector.GetConflict(packages, "requests");
|
||||
|
||||
Assert.NotNull(conflict);
|
||||
Assert.Equal("requests", conflict.NormalizedName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConflict_NoConflict_ReturnsNull()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("flask", "2.0.0", "/env1/site-packages")
|
||||
};
|
||||
|
||||
var conflict = VersionConflictDetector.GetConflict(packages, "flask");
|
||||
|
||||
Assert.Null(conflict);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Conflict_PurlGeneration_Correct()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("my_package", "1.0.0", "/env1/site-packages"),
|
||||
CreatePackage("my_package", "2.0.0", "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
var conflict = result.Conflicts[0];
|
||||
|
||||
Assert.Equal("pkg:pypi/my-package", conflict.Purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HighSeverityConflicts_FiltersCorrectly()
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("django", "3.0.0", "/env1/site-packages"),
|
||||
CreatePackage("django", "4.0.0", "/env2/site-packages"), // High
|
||||
CreatePackage("flask", "2.0.0", "/env1/site-packages"),
|
||||
CreatePackage("flask", "2.0.1", "/env2/site-packages") // Low
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
|
||||
Assert.Equal(2, result.TotalConflicts);
|
||||
Assert.Single(result.HighSeverityConflicts);
|
||||
Assert.Equal("django", result.HighSeverityConflicts[0].NormalizedName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("1.0.0", "2.0.0", 3)] // ConflictSeverity.High
|
||||
[InlineData("1.0.0", "1.1.0", 2)] // ConflictSeverity.Medium
|
||||
[InlineData("1.0.0", "1.0.1", 1)] // ConflictSeverity.Low
|
||||
[InlineData("1!1.0.0", "2!1.0.0", 3)] // ConflictSeverity.High (epoch diff)
|
||||
[InlineData("1.0.0a1", "1.0.0", 1)] // ConflictSeverity.Low
|
||||
public void Analyze_VersionPairs_CorrectSeverity(string v1, string v2, int expectedSeverity)
|
||||
{
|
||||
var packages = new[]
|
||||
{
|
||||
CreatePackage("testpkg", v1, "/env1/site-packages"),
|
||||
CreatePackage("testpkg", v2, "/env2/site-packages")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(packages);
|
||||
var expected = (ConflictSeverity)expectedSeverity;
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(expected, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
private static PythonPackageInfo CreatePackage(string name, string? version, string location)
|
||||
{
|
||||
return new PythonPackageInfo(
|
||||
Name: name,
|
||||
Version: version,
|
||||
Kind: PythonPackageKind.Wheel,
|
||||
Location: location,
|
||||
MetadataPath: $"{location}/{name.ToLowerInvariant()}-{version ?? "0.0.0"}.dist-info",
|
||||
TopLevelModules: [],
|
||||
Dependencies: [],
|
||||
Extras: [],
|
||||
RecordFiles: [],
|
||||
InstallerTool: "pip",
|
||||
EditableTarget: null,
|
||||
IsDirectDependency: true,
|
||||
Confidence: PythonPackageConfidence.High);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Licensing;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.Licensing;
|
||||
|
||||
public class SpdxLicenseNormalizerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("MIT", "MIT")]
|
||||
[InlineData("MIT License", "MIT")]
|
||||
[InlineData("The MIT License", "MIT")]
|
||||
[InlineData("mit", "MIT")]
|
||||
public void NormalizeFromString_MitVariations(string input, string expected)
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString(input);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Apache", "Apache-2.0")]
|
||||
[InlineData("Apache 2.0", "Apache-2.0")]
|
||||
[InlineData("Apache-2.0", "Apache-2.0")]
|
||||
[InlineData("Apache License 2.0", "Apache-2.0")]
|
||||
[InlineData("Apache License, Version 2.0", "Apache-2.0")]
|
||||
[InlineData("Apache Software License", "Apache-2.0")]
|
||||
[InlineData("ASL 2.0", "Apache-2.0")]
|
||||
public void NormalizeFromString_ApacheVariations(string input, string expected)
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString(input);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("BSD", "BSD-3-Clause")]
|
||||
[InlineData("BSD License", "BSD-3-Clause")]
|
||||
[InlineData("BSD-2-Clause", "BSD-2-Clause")]
|
||||
[InlineData("BSD-3-Clause", "BSD-3-Clause")]
|
||||
[InlineData("BSD 2-Clause", "BSD-2-Clause")]
|
||||
[InlineData("BSD 3-Clause", "BSD-3-Clause")]
|
||||
[InlineData("Simplified BSD", "BSD-2-Clause")]
|
||||
[InlineData("New BSD", "BSD-3-Clause")]
|
||||
public void NormalizeFromString_BsdVariations(string input, string expected)
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString(input);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("GPL", "GPL-3.0-only")]
|
||||
[InlineData("GPLv2", "GPL-2.0-only")]
|
||||
[InlineData("GPLv3", "GPL-3.0-only")]
|
||||
[InlineData("GPL-2.0", "GPL-2.0-only")]
|
||||
[InlineData("GPL-3.0", "GPL-3.0-only")]
|
||||
[InlineData("GPL-2.0-only", "GPL-2.0-only")]
|
||||
[InlineData("GPL-3.0-only", "GPL-3.0-only")]
|
||||
[InlineData("GPL-2.0-or-later", "GPL-2.0-or-later")]
|
||||
[InlineData("GPL-3.0-or-later", "GPL-3.0-or-later")]
|
||||
public void NormalizeFromString_GplVariations(string input, string expected)
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString(input);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("LGPL", "LGPL-3.0-only")]
|
||||
[InlineData("LGPLv2", "LGPL-2.0-only")]
|
||||
[InlineData("LGPL-2.0", "LGPL-2.0-only")]
|
||||
[InlineData("LGPL-2.1", "LGPL-2.1-only")]
|
||||
[InlineData("LGPLv3", "LGPL-3.0-only")]
|
||||
[InlineData("LGPL-3.0", "LGPL-3.0-only")]
|
||||
public void NormalizeFromString_LgplVariations(string input, string expected)
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString(input);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("MPL", "MPL-2.0")]
|
||||
[InlineData("MPL-2.0", "MPL-2.0")]
|
||||
[InlineData("Mozilla Public License 2.0", "MPL-2.0")]
|
||||
public void NormalizeFromString_MplVariations(string input, string expected)
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString(input);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ISC", "ISC")]
|
||||
[InlineData("ISC License", "ISC")]
|
||||
[InlineData("Unlicense", "Unlicense")]
|
||||
[InlineData("The Unlicense", "Unlicense")]
|
||||
[InlineData("CC0", "CC0-1.0")]
|
||||
[InlineData("CC0-1.0", "CC0-1.0")]
|
||||
[InlineData("Public Domain", "Unlicense")]
|
||||
[InlineData("Zlib", "Zlib")]
|
||||
[InlineData("PSF", "PSF-2.0")]
|
||||
public void NormalizeFromString_OtherLicenses(string input, string expected)
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString(input);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromClassifiers_MitClassifier()
|
||||
{
|
||||
var classifiers = new[] { "License :: OSI Approved :: MIT License" };
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromClassifiers(classifiers);
|
||||
Assert.Equal("MIT", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromClassifiers_ApacheClassifier()
|
||||
{
|
||||
var classifiers = new[] { "License :: OSI Approved :: Apache Software License" };
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromClassifiers(classifiers);
|
||||
Assert.Equal("Apache-2.0", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromClassifiers_MultipleLicenses_ReturnsOrExpression()
|
||||
{
|
||||
var classifiers = new[]
|
||||
{
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"License :: OSI Approved :: Apache Software License"
|
||||
};
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromClassifiers(classifiers);
|
||||
// Should return "Apache-2.0 OR MIT" (alphabetically sorted)
|
||||
Assert.Equal("Apache-2.0 OR MIT", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromClassifiers_NoLicenseClassifiers_ReturnsNull()
|
||||
{
|
||||
var classifiers = new[]
|
||||
{
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Programming Language :: Python :: 3"
|
||||
};
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromClassifiers(classifiers);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_Pep639Expression_TakesPrecedence()
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.Normalize(
|
||||
license: "MIT",
|
||||
classifiers: new[] { "License :: OSI Approved :: Apache Software License" },
|
||||
licenseExpression: "GPL-3.0-only");
|
||||
|
||||
Assert.Equal("GPL-3.0-only", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_ClassifiersOverLicenseString()
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.Normalize(
|
||||
license: "Some custom license",
|
||||
classifiers: new[] { "License :: OSI Approved :: MIT License" });
|
||||
|
||||
Assert.Equal("MIT", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_FallsBackToLicenseString()
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.Normalize(
|
||||
license: "MIT",
|
||||
classifiers: new[] { "Programming Language :: Python :: 3" });
|
||||
|
||||
Assert.Equal("MIT", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_AllNull_ReturnsNull()
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.Normalize(null, null, null);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "GPL-3.0-only")]
|
||||
[InlineData("License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", "GPL-2.0-or-later")]
|
||||
[InlineData("License :: OSI Approved :: BSD License", "BSD-3-Clause")]
|
||||
public void NormalizeFromClassifiers_GplBsdClassifiers(string classifier, string expected)
|
||||
{
|
||||
var classifiers = new[] { classifier };
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromClassifiers(classifiers);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromString_UnknownLicense_ReturnsLicenseRef()
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString("Custom License v1.0");
|
||||
Assert.StartsWith("LicenseRef-", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromString_Empty_ReturnsNull()
|
||||
{
|
||||
Assert.Null(SpdxLicenseNormalizer.NormalizeFromString(""));
|
||||
Assert.Null(SpdxLicenseNormalizer.NormalizeFromString(" "));
|
||||
Assert.Null(SpdxLicenseNormalizer.NormalizeFromString(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromString_VeryLongText_ReturnsNull()
|
||||
{
|
||||
var longText = new string('x', 200);
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString(longText);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromString_Url_ReturnsNull()
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString("https://opensource.org/licenses/MIT");
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("GPL-2.0-only AND MIT", true)]
|
||||
[InlineData("Apache-2.0 OR MIT", true)]
|
||||
[InlineData("MIT WITH Classpath-exception-2.0", true)]
|
||||
[InlineData("Apache-2.0", true)]
|
||||
public void Normalize_ValidSpdxExpression_AcceptedAsPep639(string expression, bool isValid)
|
||||
{
|
||||
if (isValid)
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.Normalize(null, null, expression);
|
||||
Assert.Equal(expression, result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromClassifiers_DuplicateLicenses_Deduplicated()
|
||||
{
|
||||
var classifiers = new[]
|
||||
{
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"License :: OSI Approved :: MIT License", // duplicate
|
||||
"Programming Language :: Python :: 3"
|
||||
};
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromClassifiers(classifiers);
|
||||
Assert.Equal("MIT", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromString_PatternMatch_GplWithVersion()
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString("GNU General Public License v3");
|
||||
Assert.Equal("GPL-3.0-only", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeFromString_PatternMatch_BsdWithClauses()
|
||||
{
|
||||
var result = SpdxLicenseNormalizer.NormalizeFromString("BSD 2-Clause License");
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("BSD", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Packaging;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Vendoring;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.VirtualFileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.Vendoring;
|
||||
|
||||
public class VendoredPackageDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Analyze_NoVendorDirectory_ReturnsNotVendored()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/mypackage/__init__.py",
|
||||
"/site-packages/mypackage/module.py");
|
||||
|
||||
var package = CreatePackage("mypackage", "1.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsVendored);
|
||||
Assert.Equal(VendoringConfidence.None, result.Confidence);
|
||||
Assert.Empty(result.EmbeddedPackages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Analyze_WithVendorDirectory_DetectsVendoring()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/mypackage/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/urllib3/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/urllib3/connection.py");
|
||||
|
||||
var package = CreatePackage("mypackage", "1.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsVendored);
|
||||
Assert.True(result.Confidence >= VendoringConfidence.Low);
|
||||
Assert.Contains(result.Markers, m => m.StartsWith("vendor-directory:"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Analyze_ExtractsEmbeddedPackages()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/pip/__init__.py",
|
||||
"/site-packages/pip/_vendor/__init__.py",
|
||||
"/site-packages/pip/_vendor/certifi/__init__.py",
|
||||
"/site-packages/pip/_vendor/urllib3/__init__.py",
|
||||
"/site-packages/pip/_vendor/requests/__init__.py");
|
||||
|
||||
var package = CreatePackage("pip", "23.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsVendored);
|
||||
Assert.True(result.EmbeddedCount >= 3);
|
||||
|
||||
var embeddedNames = result.EmbeddedPackages.Select(p => p.Name).ToList();
|
||||
Assert.Contains("certifi", embeddedNames);
|
||||
Assert.Contains("urllib3", embeddedNames);
|
||||
Assert.Contains("requests", embeddedNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Analyze_KnownVendoredPackage_HighConfidence()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/pip/__init__.py",
|
||||
"/site-packages/pip/_vendor/__init__.py",
|
||||
"/site-packages/pip/_vendor/certifi/__init__.py",
|
||||
"/site-packages/pip/_vendor/urllib3/__init__.py",
|
||||
"/site-packages/pip/_vendor/packaging/__init__.py");
|
||||
|
||||
var package = CreatePackage("pip", "23.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsVendored);
|
||||
Assert.Contains("known-vendored-package", result.Markers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Analyze_DetectsThirdPartyPattern()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/mypackage/__init__.py",
|
||||
"/site-packages/mypackage/third_party/__init__.py",
|
||||
"/site-packages/mypackage/third_party/six.py");
|
||||
|
||||
var package = CreatePackage("mypackage", "1.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsVendored);
|
||||
Assert.Contains(result.Markers, m => m.Contains("third_party"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Analyze_DetectsExternPattern()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/mypackage/__init__.py",
|
||||
"/site-packages/mypackage/extern/__init__.py",
|
||||
"/site-packages/mypackage/extern/six.py");
|
||||
|
||||
var package = CreatePackage("mypackage", "1.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsVendored);
|
||||
Assert.Contains(result.Markers, m => m.Contains("extern"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Analyze_SkipsInternalDirectories()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/mypackage/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/urllib3/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/__pycache__/cached.pyc",
|
||||
"/site-packages/mypackage/_vendor/.hidden/__init__.py");
|
||||
|
||||
var package = CreatePackage("mypackage", "1.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
var embeddedNames = result.EmbeddedPackages.Select(p => p.Name).ToList();
|
||||
Assert.DoesNotContain("__pycache__", embeddedNames);
|
||||
Assert.DoesNotContain(".hidden", embeddedNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmbeddedPackage_GeneratesCorrectPurl()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/pip/__init__.py",
|
||||
"/site-packages/pip/_vendor/__init__.py",
|
||||
"/site-packages/pip/_vendor/urllib3/__init__.py");
|
||||
|
||||
var package = CreatePackage("pip", "23.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
var urllib3 = result.EmbeddedPackages.FirstOrDefault(p => p.Name == "urllib3");
|
||||
Assert.NotNull(urllib3);
|
||||
Assert.StartsWith("pkg:pypi/urllib3", urllib3.Purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmbeddedPackage_GeneratesQualifiedName()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/pip/__init__.py",
|
||||
"/site-packages/pip/_vendor/__init__.py",
|
||||
"/site-packages/pip/_vendor/urllib3/__init__.py");
|
||||
|
||||
var package = CreatePackage("pip", "23.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
var urllib3 = result.EmbeddedPackages.FirstOrDefault(p => p.Name == "urllib3");
|
||||
Assert.NotNull(urllib3);
|
||||
Assert.Equal("pip._vendor.urllib3", urllib3.QualifiedName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Analyze_RecordEntriesWithVendor_AddsMarker()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/mypackage/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/six.py");
|
||||
|
||||
var recordFiles = ImmutableArray.Create(
|
||||
new PythonRecordEntry("mypackage/__init__.py", "sha256=abc", 100),
|
||||
new PythonRecordEntry("mypackage/_vendor/__init__.py", "sha256=def", 50),
|
||||
new PythonRecordEntry("mypackage/_vendor/six.py", "sha256=ghi", 500));
|
||||
|
||||
var package = CreatePackageWithRecords("mypackage", "1.0.0", "/site-packages", recordFiles);
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsVendored);
|
||||
Assert.Contains("record-vendor-entries", result.Markers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Analyze_MultipleVendorDirectories_DetectsAll()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/mypackage/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/six.py",
|
||||
"/site-packages/mypackage/extern/__init__.py",
|
||||
"/site-packages/mypackage/extern/toml.py");
|
||||
|
||||
var package = CreatePackage("mypackage", "1.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsVendored);
|
||||
Assert.True(result.VendorPaths.Length >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Analyze_SingleFileModule_Detected()
|
||||
{
|
||||
var vfs = CreateMockVfs(
|
||||
"/site-packages/mypackage/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/__init__.py",
|
||||
"/site-packages/mypackage/_vendor/six.py"); // Single file module
|
||||
|
||||
var package = CreatePackage("mypackage", "1.0.0", "/site-packages");
|
||||
|
||||
var result = await VendoredPackageDetector.AnalyzeAsync(vfs, package, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsVendored);
|
||||
var embeddedNames = result.EmbeddedPackages.Select(p => p.Name).ToList();
|
||||
Assert.Contains("six", embeddedNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotVendored_ReturnsEmptyAnalysis()
|
||||
{
|
||||
var analysis = VendoringAnalysis.NotVendored("testpkg");
|
||||
|
||||
Assert.Equal("testpkg", analysis.PackageName);
|
||||
Assert.False(analysis.IsVendored);
|
||||
Assert.Equal(VendoringConfidence.None, analysis.Confidence);
|
||||
Assert.Empty(analysis.Markers);
|
||||
Assert.Empty(analysis.EmbeddedPackages);
|
||||
Assert.Empty(analysis.VendorPaths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEmbeddedPackageList_FormatsCorrectly()
|
||||
{
|
||||
var analysis = new VendoringAnalysis(
|
||||
"pip",
|
||||
true,
|
||||
VendoringConfidence.High,
|
||||
["vendor-directory:_vendor"],
|
||||
[
|
||||
new EmbeddedPackage("urllib3", "2.0.0", null, "/pip/_vendor/urllib3", "pip"),
|
||||
new EmbeddedPackage("certifi", "2023.7.22", null, "/pip/_vendor/certifi", "pip")
|
||||
],
|
||||
["/pip/_vendor"]);
|
||||
|
||||
var list = analysis.GetEmbeddedPackageList();
|
||||
|
||||
Assert.Contains("certifi@2023.7.22", list);
|
||||
Assert.Contains("urllib3@2.0.0", list);
|
||||
}
|
||||
|
||||
private static PythonVirtualFileSystem CreateMockVfs(params string[] filePaths)
|
||||
{
|
||||
var builder = PythonVirtualFileSystem.CreateBuilder();
|
||||
|
||||
foreach (var path in filePaths)
|
||||
{
|
||||
// Normalize path - remove leading slash for the builder
|
||||
var normalizedPath = path.TrimStart('/');
|
||||
builder.AddFile(
|
||||
normalizedPath,
|
||||
path, // Use original as absolute path for testing
|
||||
PythonFileSource.SitePackages,
|
||||
size: 100);
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private static PythonPackageInfo CreatePackage(string name, string version, string location)
|
||||
{
|
||||
return new PythonPackageInfo(
|
||||
Name: name,
|
||||
Version: version,
|
||||
Kind: PythonPackageKind.Wheel,
|
||||
Location: location.TrimStart('/'),
|
||||
MetadataPath: $"{location.TrimStart('/')}/{name.ToLowerInvariant()}-{version}.dist-info",
|
||||
TopLevelModules: [name.ToLowerInvariant()],
|
||||
Dependencies: [],
|
||||
Extras: [],
|
||||
RecordFiles: [],
|
||||
InstallerTool: "pip",
|
||||
EditableTarget: null,
|
||||
IsDirectDependency: true,
|
||||
Confidence: PythonPackageConfidence.High);
|
||||
}
|
||||
|
||||
private static PythonPackageInfo CreatePackageWithRecords(
|
||||
string name,
|
||||
string version,
|
||||
string location,
|
||||
ImmutableArray<PythonRecordEntry> records)
|
||||
{
|
||||
return new PythonPackageInfo(
|
||||
Name: name,
|
||||
Version: version,
|
||||
Kind: PythonPackageKind.Wheel,
|
||||
Location: location.TrimStart('/'),
|
||||
MetadataPath: $"{location.TrimStart('/')}/{name.ToLowerInvariant()}-{version}.dist-info",
|
||||
TopLevelModules: [name.ToLowerInvariant()],
|
||||
Dependencies: [],
|
||||
Extras: [],
|
||||
RecordFiles: records,
|
||||
InstallerTool: "pip",
|
||||
EditableTarget: null,
|
||||
IsDirectDependency: true,
|
||||
Confidence: PythonPackageConfidence.High);
|
||||
}
|
||||
}
|
||||
@@ -321,4 +321,106 @@ public class ElfDynamicSectionParserTests
|
||||
buffer[offset + bytes.Length] = 0; // null terminator
|
||||
return bytes.Length;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesElfWithVersionNeeds()
|
||||
{
|
||||
// Test that version needs (GLIBC_2.17, etc.) are properly extracted
|
||||
var buffer = new byte[4096];
|
||||
SetupElf64Header(buffer, littleEndian: true);
|
||||
|
||||
// String table at offset 0x400
|
||||
var strtab = 0x400;
|
||||
var libcOffset = 1; // "libc.so.6"
|
||||
var glibc217Offset = libcOffset + WriteString(buffer, strtab + libcOffset, "libc.so.6") + 1;
|
||||
var glibc228Offset = glibc217Offset + WriteString(buffer, strtab + glibc217Offset, "GLIBC_2.17") + 1;
|
||||
var strtabSize = glibc228Offset + WriteString(buffer, strtab + glibc228Offset, "GLIBC_2.28") + 1;
|
||||
|
||||
// Section headers at offset 0x800
|
||||
var shoff = 0x800;
|
||||
var shentsize = 64;
|
||||
var shnum = 3; // null + .dynstr + .gnu.version_r
|
||||
|
||||
BitConverter.GetBytes((ulong)shoff).CopyTo(buffer, 40);
|
||||
BitConverter.GetBytes((ushort)shentsize).CopyTo(buffer, 58);
|
||||
BitConverter.GetBytes((ushort)shnum).CopyTo(buffer, 60);
|
||||
|
||||
// Section header 0: null
|
||||
// Section header 1: .dynstr
|
||||
var sh1 = shoff + shentsize;
|
||||
BitConverter.GetBytes((uint)3).CopyTo(buffer, sh1 + 4); // sh_type = SHT_STRTAB
|
||||
BitConverter.GetBytes((ulong)0x400).CopyTo(buffer, sh1 + 16); // sh_addr
|
||||
BitConverter.GetBytes((ulong)strtab).CopyTo(buffer, sh1 + 24); // sh_offset
|
||||
BitConverter.GetBytes((ulong)strtabSize).CopyTo(buffer, sh1 + 32); // sh_size
|
||||
|
||||
// Section header 2: .gnu.version_r (SHT_GNU_verneed = 0x6ffffffe)
|
||||
var verneedFileOffset = 0x600;
|
||||
var sh2 = shoff + shentsize * 2;
|
||||
BitConverter.GetBytes((uint)0x6ffffffe).CopyTo(buffer, sh2 + 4); // sh_type = SHT_GNU_verneed
|
||||
BitConverter.GetBytes((ulong)0x600).CopyTo(buffer, sh2 + 16); // sh_addr (vaddr)
|
||||
BitConverter.GetBytes((ulong)verneedFileOffset).CopyTo(buffer, sh2 + 24); // sh_offset
|
||||
|
||||
// Version needs section at offset 0x600
|
||||
// Verneed entry for libc.so.6 with two version requirements
|
||||
// Elf64_Verneed: vn_version(2), vn_cnt(2), vn_file(4), vn_aux(4), vn_next(4)
|
||||
var verneedOffset = verneedFileOffset;
|
||||
BitConverter.GetBytes((ushort)1).CopyTo(buffer, verneedOffset); // vn_version = 1
|
||||
BitConverter.GetBytes((ushort)2).CopyTo(buffer, verneedOffset + 2); // vn_cnt = 2 aux entries
|
||||
BitConverter.GetBytes((uint)libcOffset).CopyTo(buffer, verneedOffset + 4); // vn_file -> "libc.so.6"
|
||||
BitConverter.GetBytes((uint)16).CopyTo(buffer, verneedOffset + 8); // vn_aux = 16 (offset to first aux)
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, verneedOffset + 12); // vn_next = 0 (last entry)
|
||||
|
||||
// Vernaux entries
|
||||
// Elf64_Vernaux: vna_hash(4), vna_flags(2), vna_other(2), vna_name(4), vna_next(4)
|
||||
var aux1Offset = verneedOffset + 16;
|
||||
BitConverter.GetBytes((uint)0x0d696910).CopyTo(buffer, aux1Offset); // vna_hash for GLIBC_2.17
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, aux1Offset + 4); // vna_flags
|
||||
BitConverter.GetBytes((ushort)2).CopyTo(buffer, aux1Offset + 6); // vna_other
|
||||
BitConverter.GetBytes((uint)glibc217Offset).CopyTo(buffer, aux1Offset + 8); // vna_name -> "GLIBC_2.17"
|
||||
BitConverter.GetBytes((uint)16).CopyTo(buffer, aux1Offset + 12); // vna_next = 16 (offset to next aux)
|
||||
|
||||
var aux2Offset = aux1Offset + 16;
|
||||
BitConverter.GetBytes((uint)0x09691974).CopyTo(buffer, aux2Offset); // vna_hash for GLIBC_2.28
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, aux2Offset + 4);
|
||||
BitConverter.GetBytes((ushort)3).CopyTo(buffer, aux2Offset + 6);
|
||||
BitConverter.GetBytes((uint)glibc228Offset).CopyTo(buffer, aux2Offset + 8); // vna_name -> "GLIBC_2.28"
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, aux2Offset + 12); // vna_next = 0 (last aux)
|
||||
|
||||
// Dynamic section at offset 0x200
|
||||
var dynOffset = 0x200;
|
||||
var dynEntrySize = 16;
|
||||
var dynIndex = 0;
|
||||
|
||||
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 5, 0x400); // DT_STRTAB
|
||||
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 10, (ulong)strtabSize); // DT_STRSZ
|
||||
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 1, (ulong)libcOffset); // DT_NEEDED -> libc.so.6
|
||||
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 0x6ffffffe, 0x600); // DT_VERNEED (vaddr)
|
||||
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 0x6fffffff, 1); // DT_VERNEEDNUM = 1
|
||||
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex, 0, 0); // DT_NULL
|
||||
|
||||
var dynSize = dynEntrySize * (dynIndex + 1);
|
||||
|
||||
// Program header
|
||||
var phoff = 0x40;
|
||||
var phentsize = 56;
|
||||
var phnum = 1;
|
||||
|
||||
BitConverter.GetBytes((ulong)phoff).CopyTo(buffer, 32);
|
||||
BitConverter.GetBytes((ushort)phentsize).CopyTo(buffer, 54);
|
||||
BitConverter.GetBytes((ushort)phnum).CopyTo(buffer, 56);
|
||||
|
||||
BitConverter.GetBytes((uint)2).CopyTo(buffer, phoff); // PT_DYNAMIC
|
||||
BitConverter.GetBytes((ulong)dynOffset).CopyTo(buffer, phoff + 8);
|
||||
BitConverter.GetBytes((ulong)dynSize).CopyTo(buffer, phoff + 32);
|
||||
|
||||
using var stream = new MemoryStream(buffer);
|
||||
var result = ElfDynamicSectionParser.TryParse(stream, out var info);
|
||||
|
||||
result.Should().BeTrue();
|
||||
info.Dependencies.Should().HaveCount(1);
|
||||
info.Dependencies[0].Soname.Should().Be("libc.so.6");
|
||||
info.Dependencies[0].VersionNeeds.Should().HaveCount(2);
|
||||
info.Dependencies[0].VersionNeeds.Should().Contain(v => v.Version == "GLIBC_2.17");
|
||||
info.Dependencies[0].VersionNeeds.Should().Contain(v => v.Version == "GLIBC_2.28");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,4 +275,226 @@ public class PeImportParserTests
|
||||
""";
|
||||
Encoding.UTF8.GetBytes(manifestXml).CopyTo(buffer, 0x1000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesPe32PlusWithImportThunks()
|
||||
{
|
||||
// Test that 64-bit PE files correctly parse 8-byte import thunks
|
||||
var buffer = new byte[8192];
|
||||
SetupPe32PlusHeaderWithImports(buffer);
|
||||
|
||||
using var stream = new MemoryStream(buffer);
|
||||
var result = PeImportParser.TryParse(stream, out var info);
|
||||
|
||||
result.Should().BeTrue();
|
||||
info.Is64Bit.Should().BeTrue();
|
||||
info.Dependencies.Should().HaveCount(1);
|
||||
info.Dependencies[0].DllName.Should().Be("kernel32.dll");
|
||||
// Verify function names are parsed correctly with 8-byte thunks
|
||||
info.Dependencies[0].ImportedFunctions.Should().Contain("GetProcAddress");
|
||||
info.Dependencies[0].ImportedFunctions.Should().Contain("LoadLibraryA");
|
||||
}
|
||||
|
||||
private static void SetupPe32PlusHeaderWithImports(byte[] buffer)
|
||||
{
|
||||
// DOS header
|
||||
buffer[0] = (byte)'M';
|
||||
buffer[1] = (byte)'Z';
|
||||
BitConverter.GetBytes(0x80).CopyTo(buffer, 0x3C); // e_lfanew
|
||||
|
||||
// PE signature
|
||||
var peOffset = 0x80;
|
||||
buffer[peOffset] = (byte)'P';
|
||||
buffer[peOffset + 1] = (byte)'E';
|
||||
|
||||
// COFF header
|
||||
BitConverter.GetBytes((ushort)0x8664).CopyTo(buffer, peOffset + 4); // Machine = x86_64
|
||||
BitConverter.GetBytes((ushort)2).CopyTo(buffer, peOffset + 6); // NumberOfSections
|
||||
BitConverter.GetBytes((ushort)0xF0).CopyTo(buffer, peOffset + 20); // SizeOfOptionalHeader (PE32+)
|
||||
|
||||
// Optional header (PE32+)
|
||||
var optHeaderOffset = peOffset + 24;
|
||||
BitConverter.GetBytes((ushort)0x20b).CopyTo(buffer, optHeaderOffset); // Magic = PE32+
|
||||
BitConverter.GetBytes((ushort)PeSubsystem.WindowsConsole).CopyTo(buffer, optHeaderOffset + 68); // Subsystem
|
||||
BitConverter.GetBytes((uint)16).CopyTo(buffer, optHeaderOffset + 108); // NumberOfRvaAndSizes
|
||||
|
||||
// Data directory - Import Directory (entry 1)
|
||||
var dataDirOffset = optHeaderOffset + 112;
|
||||
BitConverter.GetBytes((uint)0x2000).CopyTo(buffer, dataDirOffset + 8); // Import Directory RVA
|
||||
BitConverter.GetBytes((uint)40).CopyTo(buffer, dataDirOffset + 12); // Import Directory Size
|
||||
|
||||
// Section headers
|
||||
var sectionOffset = optHeaderOffset + 0xF0;
|
||||
|
||||
// .text section
|
||||
".text\0\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8); // VirtualSize
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 12); // VirtualAddress
|
||||
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 16); // SizeOfRawData
|
||||
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 20); // PointerToRawData
|
||||
|
||||
// .idata section
|
||||
sectionOffset += 40;
|
||||
".idata\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8); // VirtualSize
|
||||
BitConverter.GetBytes((uint)0x2000).CopyTo(buffer, sectionOffset + 12); // VirtualAddress
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 16); // SizeOfRawData
|
||||
BitConverter.GetBytes((uint)0x400).CopyTo(buffer, sectionOffset + 20); // PointerToRawData
|
||||
|
||||
// Import descriptor at file offset 0x400 (RVA 0x2000)
|
||||
var importOffset = 0x400;
|
||||
BitConverter.GetBytes((uint)0x2080).CopyTo(buffer, importOffset); // OriginalFirstThunk RVA
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 4); // TimeDateStamp
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 8); // ForwarderChain
|
||||
BitConverter.GetBytes((uint)0x2100).CopyTo(buffer, importOffset + 12); // Name RVA
|
||||
BitConverter.GetBytes((uint)0x2080).CopyTo(buffer, importOffset + 16); // FirstThunk
|
||||
|
||||
// Null terminator for import directory
|
||||
// (already zero at importOffset + 20)
|
||||
|
||||
// Import Lookup Table (ILT) / Import Name Table at RVA 0x2080 -> file offset 0x480
|
||||
// PE32+ uses 8-byte entries!
|
||||
var iltOffset = 0x480;
|
||||
// Entry 1: Import by name, hint-name RVA = 0x2120
|
||||
BitConverter.GetBytes((ulong)0x2120).CopyTo(buffer, iltOffset);
|
||||
// Entry 2: Import by name, hint-name RVA = 0x2140
|
||||
BitConverter.GetBytes((ulong)0x2140).CopyTo(buffer, iltOffset + 8);
|
||||
// Null terminator (8 bytes of zero)
|
||||
// (already zero)
|
||||
|
||||
// DLL name at RVA 0x2100 -> file offset 0x500
|
||||
"kernel32.dll\0"u8.CopyTo(buffer.AsSpan(0x500));
|
||||
|
||||
// Hint-Name table entries
|
||||
// Entry 1 at RVA 0x2120 -> file offset 0x520
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, 0x520); // Hint
|
||||
"GetProcAddress\0"u8.CopyTo(buffer.AsSpan(0x522));
|
||||
|
||||
// Entry 2 at RVA 0x2140 -> file offset 0x540
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, 0x540); // Hint
|
||||
"LoadLibraryA\0"u8.CopyTo(buffer.AsSpan(0x542));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesPeWithEmbeddedResourceManifest()
|
||||
{
|
||||
// Test that manifest is properly extracted from PE resources
|
||||
var buffer = new byte[16384];
|
||||
SetupPe32HeaderWithResourceManifest(buffer);
|
||||
|
||||
using var stream = new MemoryStream(buffer);
|
||||
var result = PeImportParser.TryParse(stream, out var info);
|
||||
|
||||
result.Should().BeTrue();
|
||||
info.SxsDependencies.Should().HaveCountGreaterOrEqualTo(1);
|
||||
info.SxsDependencies.Should().Contain(d => d.Name == "Microsoft.VC90.CRT");
|
||||
}
|
||||
|
||||
private static void SetupPe32HeaderWithResourceManifest(byte[] buffer)
|
||||
{
|
||||
// DOS header
|
||||
buffer[0] = (byte)'M';
|
||||
buffer[1] = (byte)'Z';
|
||||
BitConverter.GetBytes(0x80).CopyTo(buffer, 0x3C);
|
||||
|
||||
// PE signature
|
||||
var peOffset = 0x80;
|
||||
buffer[peOffset] = (byte)'P';
|
||||
buffer[peOffset + 1] = (byte)'E';
|
||||
|
||||
// COFF header
|
||||
BitConverter.GetBytes((ushort)0x8664).CopyTo(buffer, peOffset + 4);
|
||||
BitConverter.GetBytes((ushort)2).CopyTo(buffer, peOffset + 6); // 2 sections
|
||||
BitConverter.GetBytes((ushort)0xE0).CopyTo(buffer, peOffset + 20);
|
||||
|
||||
// Optional header (PE32)
|
||||
var optHeaderOffset = peOffset + 24;
|
||||
BitConverter.GetBytes((ushort)0x10b).CopyTo(buffer, optHeaderOffset);
|
||||
BitConverter.GetBytes((ushort)PeSubsystem.WindowsConsole).CopyTo(buffer, optHeaderOffset + 68);
|
||||
BitConverter.GetBytes((uint)16).CopyTo(buffer, optHeaderOffset + 92);
|
||||
|
||||
// Data directory - Resource Directory (entry 2)
|
||||
var dataDirOffset = optHeaderOffset + 96;
|
||||
BitConverter.GetBytes((uint)0x3000).CopyTo(buffer, dataDirOffset + 16); // Resource Directory RVA
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, dataDirOffset + 20); // Resource Directory Size
|
||||
|
||||
// Section headers
|
||||
var sectionOffset = optHeaderOffset + 0xE0;
|
||||
|
||||
// .text section
|
||||
".text\0\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8);
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 12);
|
||||
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 16);
|
||||
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 20);
|
||||
|
||||
// .rsrc section
|
||||
sectionOffset += 40;
|
||||
".rsrc\0\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8);
|
||||
BitConverter.GetBytes((uint)0x3000).CopyTo(buffer, sectionOffset + 12); // VirtualAddress
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 16);
|
||||
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 20); // PointerToRawData
|
||||
|
||||
// Resource directory at file offset 0x1000 (RVA 0x3000)
|
||||
var rsrcBase = 0x1000;
|
||||
|
||||
// Root directory (Type level)
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, rsrcBase); // Characteristics
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, rsrcBase + 4); // TimeDateStamp
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, rsrcBase + 8); // MajorVersion
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, rsrcBase + 10); // MinorVersion
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, rsrcBase + 12); // NumberOfNamedEntries
|
||||
BitConverter.GetBytes((ushort)1).CopyTo(buffer, rsrcBase + 14); // NumberOfIdEntries
|
||||
|
||||
// Entry for RT_MANIFEST (ID=24) at offset 16
|
||||
BitConverter.GetBytes((uint)24).CopyTo(buffer, rsrcBase + 16); // ID = RT_MANIFEST
|
||||
BitConverter.GetBytes((uint)(0x80000000 | 0x30)).CopyTo(buffer, rsrcBase + 20); // Offset to subdirectory (high bit set)
|
||||
|
||||
// Name/ID subdirectory at offset 0x30
|
||||
var nameDir = rsrcBase + 0x30;
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, nameDir);
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, nameDir + 4);
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, nameDir + 8);
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, nameDir + 10);
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, nameDir + 12);
|
||||
BitConverter.GetBytes((ushort)1).CopyTo(buffer, nameDir + 14);
|
||||
|
||||
// Entry for ID=1 (application manifest)
|
||||
BitConverter.GetBytes((uint)1).CopyTo(buffer, nameDir + 16);
|
||||
BitConverter.GetBytes((uint)(0x80000000 | 0x50)).CopyTo(buffer, nameDir + 20); // Offset to language subdirectory
|
||||
|
||||
// Language subdirectory at offset 0x50
|
||||
var langDir = rsrcBase + 0x50;
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, langDir);
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, langDir + 4);
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, langDir + 8);
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, langDir + 10);
|
||||
BitConverter.GetBytes((ushort)0).CopyTo(buffer, langDir + 12);
|
||||
BitConverter.GetBytes((ushort)1).CopyTo(buffer, langDir + 14);
|
||||
|
||||
// Entry for language (e.g., 0x409 = English US)
|
||||
BitConverter.GetBytes((uint)0x409).CopyTo(buffer, langDir + 16);
|
||||
BitConverter.GetBytes((uint)0x70).CopyTo(buffer, langDir + 20); // Offset to data entry (no high bit = data entry)
|
||||
|
||||
// Data entry at offset 0x70
|
||||
var dataEntry = rsrcBase + 0x70;
|
||||
BitConverter.GetBytes((uint)0x3100).CopyTo(buffer, dataEntry); // Data RVA
|
||||
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, dataEntry + 4); // Data Size
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, dataEntry + 8); // CodePage
|
||||
BitConverter.GetBytes((uint)0).CopyTo(buffer, dataEntry + 12); // Reserved
|
||||
|
||||
// Manifest data at RVA 0x3100 -> file offset 0x1100
|
||||
var manifestXml = """
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.21022.8" processorArchitecture="amd64" publicKeyToken="1fc8b3b9a1e18e3b"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
""";
|
||||
Encoding.UTF8.GetBytes(manifestXml).CopyTo(buffer, 0x1100);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user