From 09a636b841cfb5df31083cc6f8f72393adc900c5 Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Wed, 22 Apr 2026 13:29:52 +0100 Subject: [PATCH 1/4] fix(yarn-berry): identify workspace member as root in dependency tree The `#isRoot` method in `Yarn_berry_processor` only matched the workspace root package (`@workspace:.`), causing workspace member analysis to fail to find the member's dependency subtree. Changed to match by manifest name prefix so members like `member-a@workspace:packages/member-a` are correctly identified as the root when analyzing from a workspace member's manifest. Co-Authored-By: Claude Opus 4.6 --- .../processors/yarn_berry_processor.js | 2 +- test/providers/javascript.test.js | 26 ++++++++++ .../component_expected_sbom.json | 36 ++++++++++++++ .../workspace_member/listing_component.json | 24 +++++++++ .../workspace_member/listing_stack.json | 42 ++++++++++++++++ .../yarn-berry/workspace_member/package.json | 5 ++ .../packages/member-a/package.json | 7 +++ .../workspace_member/stack_expected_sbom.json | 49 +++++++++++++++++++ .../yarn-berry/workspace_member/yarn.lock | 2 + 9 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 test/providers/tst_manifests/yarn-berry/workspace_member/component_expected_sbom.json create mode 100644 test/providers/tst_manifests/yarn-berry/workspace_member/listing_component.json create mode 100644 test/providers/tst_manifests/yarn-berry/workspace_member/listing_stack.json create mode 100644 test/providers/tst_manifests/yarn-berry/workspace_member/package.json create mode 100644 test/providers/tst_manifests/yarn-berry/workspace_member/packages/member-a/package.json create mode 100644 test/providers/tst_manifests/yarn-berry/workspace_member/stack_expected_sbom.json create mode 100644 test/providers/tst_manifests/yarn-berry/workspace_member/yarn.lock diff --git a/src/providers/processors/yarn_berry_processor.js b/src/providers/processors/yarn_berry_processor.js index 7da52689..c6fda3ad 100644 --- a/src/providers/processors/yarn_berry_processor.js +++ b/src/providers/processors/yarn_berry_processor.js @@ -80,7 +80,7 @@ export default class Yarn_berry_processor extends Yarn_processor { if (!name) { return false; } - return name.endsWith("@workspace:."); + return name.startsWith(`${this._manifest.name}@workspace:`); } /** diff --git a/test/providers/javascript.test.js b/test/providers/javascript.test.js index 5101d80f..d9270755 100644 --- a/test/providers/javascript.test.js +++ b/test/providers/javascript.test.js @@ -106,6 +106,32 @@ suite('testing the javascript-npm data provider', async () => { }); + [ + { providerName: 'yarn-berry', testCase: 'workspace_member' }, + ].forEach(({ providerName, testCase }) => { + test(`verify workspace member data for ${providerName} - stack analysis`, async () => { + let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/${providerName}/${testCase}/stack_expected_sbom.json`).toString(); + let listing = fs.readFileSync(`test/providers/tst_manifests/${providerName}/${testCase}/listing_stack.json`).toString(); + + const provider = await createMockProvider(providerName, listing); + const manifestPath = `test/providers/tst_manifests/${providerName}/${testCase}/packages/member-a/package.json`; + let providedDataForStack = provider.provideStack(manifestPath); + + compareSboms(providedDataForStack.content, expectedSbom); + }).timeout(process.env.GITHUB_ACTIONS ? 30000 : 10000); + + test(`verify workspace member data for ${providerName} - component analysis`, async () => { + let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/${providerName}/${testCase}/component_expected_sbom.json`).toString(); + let listing = fs.readFileSync(`test/providers/tst_manifests/${providerName}/${testCase}/listing_component.json`).toString(); + + const provider = await createMockProvider(providerName, listing); + const manifestPath = `test/providers/tst_manifests/${providerName}/${testCase}/packages/member-a/package.json`; + let providedDataForComponent = provider.provideComponent(manifestPath); + + compareSboms(providedDataForComponent.content, expectedSbom); + }).timeout(process.env.GITHUB_ACTIONS ? 15000 : 10000); + }); + test('loads a valid manifest with ignored dependencies', () => { const testCase = 'package_json_deps_with_exhortignore_object'; const manifestPath = `test/providers/tst_manifests/npm/${testCase}/package.json`; diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/component_expected_sbom.json b/test/providers/tst_manifests/yarn-berry/workspace_member/component_expected_sbom.json new file mode 100644 index 00000000..b1033c7e --- /dev/null +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/component_expected_sbom.json @@ -0,0 +1,36 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-08-07T00:00:00.000Z", + "component": { + "name": "member-a", + "version": "1.0.0", + "purl": "pkg:npm/member-a@1.0.0", + "type": "application", + "bom-ref": "pkg:npm/member-a@1.0.0" + } + }, + "components": [ + { + "name": "axios", + "version": "0.21.1", + "purl": "pkg:npm/axios@0.21.1", + "type": "library", + "bom-ref": "pkg:npm/axios@0.21.1" + } + ], + "dependencies": [ + { + "ref": "pkg:npm/member-a@1.0.0", + "dependsOn": [ + "pkg:npm/axios@0.21.1" + ] + }, + { + "ref": "pkg:npm/axios@0.21.1", + "dependsOn": [] + } + ] +} diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/listing_component.json b/test/providers/tst_manifests/yarn-berry/workspace_member/listing_component.json new file mode 100644 index 00000000..2144c73a --- /dev/null +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/listing_component.json @@ -0,0 +1,24 @@ +{ + "value": "axios@npm:0.21.1", + "children": { + "Version": "0.21.1" + } +} +{ + "value": "follow-redirects@npm:1.15.6", + "children": { + "Version": "1.15.6" + } +} +{ + "value": "test-workspace-root@workspace:.", + "children": { + "Version": "0.0.0" + } +} +{ + "value": "member-a@workspace:packages/member-a", + "children": { + "Version": "1.0.0" + } +} diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/listing_stack.json b/test/providers/tst_manifests/yarn-berry/workspace_member/listing_stack.json new file mode 100644 index 00000000..b86c2bb8 --- /dev/null +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/listing_stack.json @@ -0,0 +1,42 @@ +{ + "value": "axios@npm:0.21.1", + "children": { + "Version": "0.21.1", + "Dependencies": [ + { + "descriptor": "follow-redirects@npm:^1.14.0", + "locator": "follow-redirects@npm:1.15.6" + } + ] + } +} +{ + "value": "follow-redirects@npm:1.15.6", + "children": { + "Version": "1.15.6" + } +} +{ + "value": "test-workspace-root@workspace:.", + "children": { + "Version": "0.0.0", + "Dependencies": [ + { + "descriptor": "member-a@workspace:packages/member-a", + "locator": "member-a@workspace:packages/member-a" + } + ] + } +} +{ + "value": "member-a@workspace:packages/member-a", + "children": { + "Version": "1.0.0", + "Dependencies": [ + { + "descriptor": "axios@npm:^0.21.1", + "locator": "axios@npm:0.21.1" + } + ] + } +} diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/package.json b/test/providers/tst_manifests/yarn-berry/workspace_member/package.json new file mode 100644 index 00000000..98101a90 --- /dev/null +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/package.json @@ -0,0 +1,5 @@ +{ + "name": "test-workspace-root", + "private": true, + "workspaces": ["packages/*"] +} diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/packages/member-a/package.json b/test/providers/tst_manifests/yarn-berry/workspace_member/packages/member-a/package.json new file mode 100644 index 00000000..fba8d2af --- /dev/null +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/packages/member-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "member-a", + "version": "1.0.0", + "dependencies": { + "axios": "0.21.1" + } +} diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/stack_expected_sbom.json b/test/providers/tst_manifests/yarn-berry/workspace_member/stack_expected_sbom.json new file mode 100644 index 00000000..d2166788 --- /dev/null +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/stack_expected_sbom.json @@ -0,0 +1,49 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2023-08-07T00:00:00.000Z", + "component": { + "name": "member-a", + "version": "1.0.0", + "purl": "pkg:npm/member-a@1.0.0", + "type": "application", + "bom-ref": "pkg:npm/member-a@1.0.0" + } + }, + "components": [ + { + "name": "axios", + "version": "0.21.1", + "purl": "pkg:npm/axios@0.21.1", + "type": "library", + "bom-ref": "pkg:npm/axios@0.21.1" + }, + { + "name": "follow-redirects", + "version": "1.15.6", + "purl": "pkg:npm/follow-redirects@1.15.6", + "type": "library", + "bom-ref": "pkg:npm/follow-redirects@1.15.6" + } + ], + "dependencies": [ + { + "ref": "pkg:npm/member-a@1.0.0", + "dependsOn": [ + "pkg:npm/axios@0.21.1" + ] + }, + { + "ref": "pkg:npm/axios@0.21.1", + "dependsOn": [ + "pkg:npm/follow-redirects@1.15.6" + ] + }, + { + "ref": "pkg:npm/follow-redirects@1.15.6", + "dependsOn": [] + } + ] +} diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/yarn.lock b/test/providers/tst_manifests/yarn-berry/workspace_member/yarn.lock new file mode 100644 index 00000000..19d1db70 --- /dev/null +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/yarn.lock @@ -0,0 +1,2 @@ +__metadata: + version: 8 From 8b1967846d069f601c24e6d1857a2981c955b09b Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Wed, 22 Apr 2026 14:45:01 +0100 Subject: [PATCH 2/4] docs: add explanatory comment to yarn-berry #isRoot fix Co-Authored-By: Claude Opus 4.6 --- src/providers/processors/yarn_berry_processor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/providers/processors/yarn_berry_processor.js b/src/providers/processors/yarn_berry_processor.js index c6fda3ad..57f550a7 100644 --- a/src/providers/processors/yarn_berry_processor.js +++ b/src/providers/processors/yarn_berry_processor.js @@ -80,6 +80,7 @@ export default class Yarn_berry_processor extends Yarn_processor { if (!name) { return false; } + // Workspace members use paths like "member-a@workspace:packages/member-a", not just "@workspace:." return name.startsWith(`${this._manifest.name}@workspace:`); } From 2e1f281d69a58446ad2ec3ac5cf820b775ea53b4 Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Wed, 22 Apr 2026 15:31:50 +0100 Subject: [PATCH 3/4] fix(yarn-berry): add --all flag and regenerate listings from real CLI output yarn info --recursive without --all only returns the workspace root, omitting workspace member dependencies entirely. Adding --all includes packages from all workspace members in the output. Test fixtures regenerated from actual yarn info output, including real virtual locators and follow-redirects resolving to 1.16.0. Co-Authored-By: Claude Opus 4.6 --- .../processors/yarn_berry_processor.js | 5 +++- .../workspace_member/listing_component.json | 24 ++++++++++------- .../workspace_member/listing_stack.json | 27 ++++++++----------- .../workspace_member/stack_expected_sbom.json | 10 +++---- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/providers/processors/yarn_berry_processor.js b/src/providers/processors/yarn_berry_processor.js index 57f550a7..6dfd2915 100644 --- a/src/providers/processors/yarn_berry_processor.js +++ b/src/providers/processors/yarn_berry_processor.js @@ -20,7 +20,10 @@ export default class Yarn_berry_processor extends Yarn_processor { * @returns {string[]} Command arguments for listing dependencies */ listCmdArgs(includeTransitive) { - return ['info', includeTransitive ? '--recursive' : '--all', '--json']; + // --all is needed to include workspace members in the output + return includeTransitive + ? ['info', '--recursive', '--all', '--json'] + : ['info', '--all', '--json']; } /** diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/listing_component.json b/test/providers/tst_manifests/yarn-berry/workspace_member/listing_component.json index 2144c73a..3b49b82a 100644 --- a/test/providers/tst_manifests/yarn-berry/workspace_member/listing_component.json +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/listing_component.json @@ -1,13 +1,25 @@ { "value": "axios@npm:0.21.1", "children": { - "Version": "0.21.1" + "Version": "0.21.1", + "Dependencies": [ + { + "descriptor": "follow-redirects@virtual:d192f6b3b31cd5d11a443145a3883a70c04cbd7c813c53085dbaf50263735f1162f10fdbddd53c24e162ec3bc37b90966413084323739b7cf942b8bfb4da8831#npm:^1.10.0", + "locator": "follow-redirects@virtual:d192f6b3b31cd5d11a443145a3883a70c04cbd7c813c53085dbaf50263735f1162f10fdbddd53c24e162ec3bc37b90966413084323739b7cf942b8bfb4da8831#npm:1.16.0" + } + ] } } { - "value": "follow-redirects@npm:1.15.6", + "value": "member-a@workspace:packages/member-a", "children": { - "Version": "1.15.6" + "Version": "1.0.0", + "Dependencies": [ + { + "descriptor": "axios@npm:0.21.1", + "locator": "axios@npm:0.21.1" + } + ] } } { @@ -16,9 +28,3 @@ "Version": "0.0.0" } } -{ - "value": "member-a@workspace:packages/member-a", - "children": { - "Version": "1.0.0" - } -} diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/listing_stack.json b/test/providers/tst_manifests/yarn-berry/workspace_member/listing_stack.json index b86c2bb8..e515b2a8 100644 --- a/test/providers/tst_manifests/yarn-berry/workspace_member/listing_stack.json +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/listing_stack.json @@ -4,39 +4,34 @@ "Version": "0.21.1", "Dependencies": [ { - "descriptor": "follow-redirects@npm:^1.14.0", - "locator": "follow-redirects@npm:1.15.6" + "descriptor": "follow-redirects@virtual:d192f6b3b31cd5d11a443145a3883a70c04cbd7c813c53085dbaf50263735f1162f10fdbddd53c24e162ec3bc37b90966413084323739b7cf942b8bfb4da8831#npm:^1.10.0", + "locator": "follow-redirects@virtual:d192f6b3b31cd5d11a443145a3883a70c04cbd7c813c53085dbaf50263735f1162f10fdbddd53c24e162ec3bc37b90966413084323739b7cf942b8bfb4da8831#npm:1.16.0" } ] } } { - "value": "follow-redirects@npm:1.15.6", + "value": "follow-redirects@npm:1.16.0", "children": { - "Version": "1.15.6" + "Instances": 1, + "Version": "1.16.0" } } { - "value": "test-workspace-root@workspace:.", + "value": "member-a@workspace:packages/member-a", "children": { - "Version": "0.0.0", + "Version": "1.0.0", "Dependencies": [ { - "descriptor": "member-a@workspace:packages/member-a", - "locator": "member-a@workspace:packages/member-a" + "descriptor": "axios@npm:0.21.1", + "locator": "axios@npm:0.21.1" } ] } } { - "value": "member-a@workspace:packages/member-a", + "value": "test-workspace-root@workspace:.", "children": { - "Version": "1.0.0", - "Dependencies": [ - { - "descriptor": "axios@npm:^0.21.1", - "locator": "axios@npm:0.21.1" - } - ] + "Version": "0.0.0" } } diff --git a/test/providers/tst_manifests/yarn-berry/workspace_member/stack_expected_sbom.json b/test/providers/tst_manifests/yarn-berry/workspace_member/stack_expected_sbom.json index d2166788..28da3d7a 100644 --- a/test/providers/tst_manifests/yarn-berry/workspace_member/stack_expected_sbom.json +++ b/test/providers/tst_manifests/yarn-berry/workspace_member/stack_expected_sbom.json @@ -22,10 +22,10 @@ }, { "name": "follow-redirects", - "version": "1.15.6", - "purl": "pkg:npm/follow-redirects@1.15.6", + "version": "1.16.0", + "purl": "pkg:npm/follow-redirects@1.16.0", "type": "library", - "bom-ref": "pkg:npm/follow-redirects@1.15.6" + "bom-ref": "pkg:npm/follow-redirects@1.16.0" } ], "dependencies": [ @@ -38,11 +38,11 @@ { "ref": "pkg:npm/axios@0.21.1", "dependsOn": [ - "pkg:npm/follow-redirects@1.15.6" + "pkg:npm/follow-redirects@1.16.0" ] }, { - "ref": "pkg:npm/follow-redirects@1.15.6", + "ref": "pkg:npm/follow-redirects@1.16.0", "dependsOn": [] } ] From 2542dd0984f5ed38b2780ab66ad7bf96aeeedd7f Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Thu, 23 Apr 2026 11:36:07 +0100 Subject: [PATCH 4/4] chore(test): remove GITHUB_ACTIONS timeout ternaries Use the larger timeout value unconditionally instead of branching on the CI environment variable. Co-Authored-By: Claude Opus 4.6 --- test/providers/javascript.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/providers/javascript.test.js b/test/providers/javascript.test.js index d9270755..c782dc45 100644 --- a/test/providers/javascript.test.js +++ b/test/providers/javascript.test.js @@ -90,7 +90,7 @@ suite('testing the javascript-npm data provider', async () => { compareSboms(providedDataForStack.content, expectedSbom); - }).timeout(process.env.GITHUB_ACTIONS ? 30000 : 10000); + }).timeout(30000); test(`verify package.json data provided for ${providerName} - component analysis - ${scenario}`, async () => { // load the expected list for the scenario let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/js-common/${testCase}/component_expected_sbom.json`,).toString().trim() @@ -102,7 +102,7 @@ suite('testing the javascript-npm data provider', async () => { let providedDataForComponent = provider.provideComponent(manifestPath); compareSboms(providedDataForComponent.content, expectedSbom); - }).timeout(process.env.GITHUB_ACTIONS ? 15000 : 10000) + }).timeout(15000) }); @@ -118,7 +118,7 @@ suite('testing the javascript-npm data provider', async () => { let providedDataForStack = provider.provideStack(manifestPath); compareSboms(providedDataForStack.content, expectedSbom); - }).timeout(process.env.GITHUB_ACTIONS ? 30000 : 10000); + }).timeout(30000); test(`verify workspace member data for ${providerName} - component analysis`, async () => { let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/${providerName}/${testCase}/component_expected_sbom.json`).toString(); @@ -129,7 +129,7 @@ suite('testing the javascript-npm data provider', async () => { let providedDataForComponent = provider.provideComponent(manifestPath); compareSboms(providedDataForComponent.content, expectedSbom); - }).timeout(process.env.GITHUB_ACTIONS ? 15000 : 10000); + }).timeout(15000); }); test('loads a valid manifest with ignored dependencies', () => {