feat: Add Bun language analyzer and related functionality

- Implemented BunPackageNormalizer to deduplicate packages by name and version.
- Created BunProjectDiscoverer to identify Bun project roots in the filesystem.
- Added project files for the Bun analyzer including manifest and project configuration.
- Developed comprehensive tests for Bun language analyzer covering various scenarios.
- Included fixture files for testing standard installs, isolated linker installs, lockfile-only scenarios, and workspaces.
- Established stubs for authentication sessions to facilitate testing in the web application.
This commit is contained in:
StellaOps Bot
2025-12-06 11:20:35 +02:00
parent b978ae399f
commit a7cd10020a
85 changed files with 7414 additions and 42 deletions

View File

@@ -0,0 +1,122 @@
using StellaOps.Scanner.Analyzers.Lang.Bun;
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Lang.Bun.Tests;
public sealed class BunLanguageAnalyzerTests
{
[Fact]
public async Task StandardInstallProducesDeterministicOutputAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "bun", "standard");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new BunLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task IsolatedLinkerInstallIsParsedAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "bun", "isolated");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new BunLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task LockfileOnlyIsParsedAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "bun", "lockfile-only");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new BunLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task BinaryLockfileEmitsRemediationAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "bun", "binary-lockfile");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new BunLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task WorkspacesAreParsedAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "bun", "workspaces");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new BunLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
[Fact]
public async Task SymlinkSafetyIsEnforcedAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "bun", "symlinks");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new BunLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
}
}

View File

@@ -0,0 +1,22 @@
[
{
"analyzerId": "bun",
"componentKey": "remediation::bun-binary-lockfile::.",
"name": "Bun Binary Lockfile",
"type": "bun-remediation",
"usedByEntrypoint": false,
"metadata": {
"remediation": "Run \u0027bun install --save-text-lockfile\u0027 to generate bun.lock, then remove bun.lockb.",
"severity": "info",
"type": "unsupported-artifact"
},
"evidence": [
{
"kind": "metadata",
"source": "bun.lockb",
"locator": ".",
"value": "Binary lockfile detected; text lockfile required for SCA."
}
]
}
]

View File

@@ -0,0 +1,7 @@
{
"name": "bun-binary-lockfile-fixture",
"version": "1.0.0",
"dependencies": {
"debug": "^4.3.4"
}
}

View File

@@ -0,0 +1,7 @@
{
"lockfileVersion": 1,
"packages": {
"is-odd@3.0.1": ["https://registry.npmjs.org/is-odd/-/is-odd-3.0.1.tgz", "sha512-CQpnWPrDwmP1+SMHXvTXAoSEu2mCPgMU0VKt1WcA7D8VXCo4HfVNlUbD1k8Tg0BVDX/LhyRaZqKqiS4vI6tTHg=="],
"is-number@6.0.0": ["https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz", "sha512-Wu1VZAVuL1snqOnHLxJ0l2p3pjlzLnMcJ8gJhaTZVfP7VFKN7fSJ8X/gR0qFCLwfFJ0Rqd3IxfS+TY/Lc1Q7Pw=="]
}
}

View File

@@ -0,0 +1,72 @@
[
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/is-number@6.0.0",
"purl": "pkg:npm/is-number@6.0.0",
"name": "is-number",
"version": "6.0.0",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"integrity": "sha512-Wu1VZAVuL1snqOnHLxJ0l2p3pjlzLnMcJ8gJhaTZVfP7VFKN7fSJ8X/gR0qFCLwfFJ0Rqd3IxfS+TY/Lc1Q7Pw==",
"packageManager": "bun",
"path": "node_modules/.bun/is-number@6.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/.bun/is-number@6.0.0/package.json"
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-Wu1VZAVuL1snqOnHLxJ0l2p3pjlzLnMcJ8gJhaTZVfP7VFKN7fSJ8X/gR0qFCLwfFJ0Rqd3IxfS+TY/Lc1Q7Pw=="
}
]
},
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/is-odd@3.0.1",
"purl": "pkg:npm/is-odd@3.0.1",
"name": "is-odd",
"version": "3.0.1",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"integrity": "sha512-CQpnWPrDwmP1+SMHXvTXAoSEu2mCPgMU0VKt1WcA7D8VXCo4HfVNlUbD1k8Tg0BVDX/LhyRaZqKqiS4vI6tTHg==",
"packageManager": "bun",
"path": "node_modules/.bun/is-odd@3.0.1",
"resolved": "https://registry.npmjs.org/is-odd/-/is-odd-3.0.1.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/.bun/is-odd@3.0.1/package.json"
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/is-odd/-/is-odd-3.0.1.tgz"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-CQpnWPrDwmP1+SMHXvTXAoSEu2mCPgMU0VKt1WcA7D8VXCo4HfVNlUbD1k8Tg0BVDX/LhyRaZqKqiS4vI6tTHg=="
}
]
}
]

View File

@@ -0,0 +1,7 @@
{
"name": "bun-isolated-fixture",
"version": "1.0.0",
"dependencies": {
"is-odd": "^3.0.1"
}
}

View File

@@ -0,0 +1,6 @@
{
"lockfileVersion": 1,
"packages": {
"ms@2.1.3": ["https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="]
}
}

View File

@@ -0,0 +1,31 @@
[
{
"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",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"source": "bun.lock"
},
"evidence": [
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
]
}
]

View File

@@ -0,0 +1,7 @@
{
"name": "bun-lockfile-only-fixture",
"version": "1.0.0",
"dependencies": {
"ms": "^2.1.3"
}
}

View File

@@ -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=="]
}
}

View File

@@ -0,0 +1,23 @@
[
{
"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": {
"packageManager": "bun",
"path": "node_modules/lodash",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/lodash/package.json"
}
]
}
]

View File

@@ -0,0 +1,7 @@
{
"name": "bun-standard-fixture",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.21"
}
}

View File

@@ -0,0 +1,6 @@
{
"lockfileVersion": 1,
"packages": {
"safe-pkg@1.0.0": ["https://registry.npmjs.org/safe-pkg/-/safe-pkg-1.0.0.tgz", "sha512-abc123"]
}
}

View File

@@ -0,0 +1,37 @@
[
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/safe-pkg@1.0.0",
"purl": "pkg:npm/safe-pkg@1.0.0",
"name": "safe-pkg",
"version": "1.0.0",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"integrity": "sha512-abc123",
"packageManager": "bun",
"path": "node_modules/safe-pkg",
"resolved": "https://registry.npmjs.org/safe-pkg/-/safe-pkg-1.0.0.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/safe-pkg/package.json"
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/safe-pkg/-/safe-pkg-1.0.0.tgz"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-abc123"
}
]
}
]

View File

@@ -0,0 +1,7 @@
{
"name": "bun-symlinks-fixture",
"version": "1.0.0",
"dependencies": {
"safe-pkg": "^1.0.0"
}
}

View File

@@ -0,0 +1,6 @@
{
"lockfileVersion": 1,
"packages": {
"chalk@5.3.0": ["https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="]
}
}

View File

@@ -0,0 +1,37 @@
[
{
"analyzerId": "bun",
"componentKey": "purl::pkg:npm/chalk@5.3.0",
"purl": "pkg:npm/chalk@5.3.0",
"name": "chalk",
"version": "5.3.0",
"type": "npm",
"usedByEntrypoint": false,
"metadata": {
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"packageManager": "bun",
"path": "node_modules/chalk",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"source": "node_modules"
},
"evidence": [
{
"kind": "file",
"source": "node_modules",
"locator": "node_modules/chalk/package.json"
},
{
"kind": "metadata",
"source": "resolved",
"locator": "bun.lock",
"value": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz"
},
{
"kind": "metadata",
"source": "integrity",
"locator": "bun.lock",
"value": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
}
]
}
]

View File

@@ -0,0 +1,8 @@
{
"name": "bun-workspaces-fixture",
"version": "1.0.0",
"workspaces": ["packages/*"],
"dependencies": {
"chalk": "^5.3.0"
}
}

View File

@@ -0,0 +1,55 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
<ConcelierTestingPath></ConcelierTestingPath>
<ConcelierSharedTestsPath></ConcelierSharedTestsPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
<PackageReference Remove="xunit" />
<PackageReference Remove="xunit.runner.visualstudio" />
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Remove="Mongo2Go" />
<PackageReference Remove="coverlet.collector" />
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
<Using Remove="StellaOps.Concelier.Testing" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit.v3" Version="3.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/StellaOps.Scanner.Analyzers.Lang.Bun.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<!-- Exclude OpenSsl shared files since they're already included via Lang.Tests reference -->
<ItemGroup>
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslLegacyShim.cs" />
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslAutoInit.cs" />
</ItemGroup>
</Project>