Skip to content

fix: handle peerDependencies and optionalDependencies consistently ac…#392

Merged
soul2zimate merged 4 commits into
guacsec:mainfrom
soul2zimate:TC-3977
Apr 7, 2026
Merged

fix: handle peerDependencies and optionalDependencies consistently ac…#392
soul2zimate merged 4 commits into
guacsec:mainfrom
soul2zimate:TC-3977

Conversation

@soul2zimate

Copy link
Copy Markdown
Contributor

…ross JS providers

JavaScript providers (npm, pnpm, yarn classic, yarn berry) produced inconsistent dependency analysis results when package.json contains peerDependencies, optionalDependencies, and devDependencies.

  • pnpm: lodash (optionalDependency) was missing because pnpm outputs optional deps under a separate "optionalDependencies" key, but addDependenciesToSbom and getRootDependencies only read the "dependencies" key.
  • Yarn Berry: devDependencies were included in the scan because yarn info has no --prod flag and the processor did not filter at root level.
  • Yarn Classic: lodash was missing because Manifest.loadDependencies only read the "dependencies" key, causing the filter in addDependenciesToSbom to exclude it.
  • All yarn providers: minimist (peerDependency) was missing because yarn does not include peer deps in its output, and no backfill mechanism existed.

Fixes applied (matching trustify-da-javascript-client approach):

  • Manifest: loadDependencies now includes optionalDependencies and peerDependencies names. Added peerDependencies and optionalDependencies as Map<String, String> fields.
  • JavaScriptProvider: addDependenciesToSbom and getRootDependencies now also read the "optionalDependencies" key from dep tree output. Added ensurePeerAndOptionalDeps to backfill missing peer/optional deps using declared versions from package.json.
  • YarnBerryProcessor: addDependenciesToSbom now filters root node dependencies to only include production deps (those in manifest.dependencies).

Fixes: #391

Jira issue: https://redhat.atlassian.net/browse/TC-3977

…ross JS providers

JavaScript providers (npm, pnpm, yarn classic, yarn berry) produced inconsistent
dependency analysis results when package.json contains peerDependencies,
optionalDependencies, and devDependencies.

- pnpm: lodash (optionalDependency) was missing because pnpm outputs optional
  deps under a separate "optionalDependencies" key, but addDependenciesToSbom
  and getRootDependencies only read the "dependencies" key.
- Yarn Berry: devDependencies were included in the scan because yarn info has
  no --prod flag and the processor did not filter at root level.
- Yarn Classic: lodash was missing because Manifest.loadDependencies only read
  the "dependencies" key, causing the filter in addDependenciesToSbom to exclude it.
- All yarn providers: minimist (peerDependency) was missing because yarn does not
  include peer deps in its output, and no backfill mechanism existed.

Fixes applied (matching trustify-da-javascript-client approach):
- Manifest: loadDependencies now includes optionalDependencies and peerDependencies
  names. Added peerDependencies and optionalDependencies as Map<String, String> fields.
- JavaScriptProvider: addDependenciesToSbom and getRootDependencies now also read
  the "optionalDependencies" key from dep tree output. Added ensurePeerAndOptionalDeps
  to backfill missing peer/optional deps using declared versions from package.json.
- YarnBerryProcessor: addDependenciesToSbom now filters root node dependencies to
  only include production deps (those in manifest.dependencies).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Fix inconsistent handling of peer and optional dependencies across JavaScript providers

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Handle peerDependencies and optionalDependencies consistently across all JavaScript providers
  (npm, pnpm, yarn classic, yarn berry)
• Add backfill mechanism to ensure peer and optional dependencies are included in SBOM even when
  missing from provider output
• Filter root-level devDependencies in Yarn Berry to only include production dependencies
• Extend Manifest to track peerDependencies and optionalDependencies as separate maps
Diagram
flowchart LR
  A["Manifest"] -->|loads peerDeps & optionalDeps| B["Manifest Maps"]
  C["JavaScriptProvider"] -->|reads optionalDependencies key| D["addDependenciesToSbom"]
  C -->|backfills missing deps| E["ensurePeerAndOptionalDeps"]
  E -->|uses declared versions| B
  F["YarnBerryProcessor"] -->|filters root deps| G["Production only"]
  D --> H["Consistent SBOM"]
  E --> H
  G --> H
Loading

Grey Divider

File Changes

1. src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java 🐞 Bug fix +35/-5

Add peer and optional dependency handling

src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java


2. src/main/java/io/github/guacsec/trustifyda/providers/YarnBerryProcessor.java 🐞 Bug fix +17/-1

Filter root dependencies to production only

src/main/java/io/github/guacsec/trustifyda/providers/YarnBerryProcessor.java


3. src/main/java/io/github/guacsec/trustifyda/providers/javascript/model/Manifest.java ✨ Enhancement +25/-2

Add peer and optional dependency maps

src/main/java/io/github/guacsec/trustifyda/providers/javascript/model/Manifest.java


View more (20)
4. src/test/java/io/github/guacsec/trustifyda/providers/Javascript_Provider_Test.java 🧪 Tests +1/-1

Add mixed dependency types test case

src/test/java/io/github/guacsec/trustifyda/providers/Javascript_Provider_Test.java


5. src/test/java/io/github/guacsec/trustifyda/providers/ManifestTest.java 🧪 Tests +47/-0

New test for manifest loading

src/test/java/io/github/guacsec/trustifyda/providers/ManifestTest.java


6. src/test/resources/tst_manifests/npm/deps_with_mixed_dep_types/package.json 🧪 Tests +21/-0

Test package with mixed dependency types

src/test/resources/tst_manifests/npm/deps_with_mixed_dep_types/package.json


7. src/test/resources/tst_manifests/npm/deps_with_mixed_dep_types/npm-ls-component.json 🧪 Tests +26/-0

Expected npm component output

src/test/resources/tst_manifests/npm/deps_with_mixed_dep_types/npm-ls-component.json


8. src/test/resources/tst_manifests/npm/deps_with_mixed_dep_types/npm-ls-stack.json 🧪 Tests +40/-0

Expected npm stack output

src/test/resources/tst_manifests/npm/deps_with_mixed_dep_types/npm-ls-stack.json


9. src/test/resources/tst_manifests/npm/common/deps_with_mixed_dep_types/expected_component_sbom.json 🧪 Tests +72/-0

Expected component SBOM output

src/test/resources/tst_manifests/npm/common/deps_with_mixed_dep_types/expected_component_sbom.json


10. src/test/resources/tst_manifests/npm/deps_with_mixed_dep_types/expected_stack_sbom.json 🧪 Tests +98/-0

Expected stack SBOM output

src/test/resources/tst_manifests/npm/deps_with_mixed_dep_types/expected_stack_sbom.json


11. src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/package.json 🧪 Tests +21/-0

Test package for pnpm provider

src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/package.json


12. src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/pnpm-ls-component.json 🧪 Tests +30/-0

Expected pnpm component output

src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/pnpm-ls-component.json


13. src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/pnpm-ls-stack.json 🧪 Tests +44/-0

Expected pnpm stack output

src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/pnpm-ls-stack.json


14. src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/expected_stack_sbom.json 🧪 Tests +98/-0

Expected pnpm SBOM output

src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/expected_stack_sbom.json


15. src/test/resources/tst_manifests/yarn-berry/deps_with_mixed_dep_types/package.json 🧪 Tests +21/-0

Test package for yarn berry provider

src/test/resources/tst_manifests/yarn-berry/deps_with_mixed_dep_types/package.json


16. src/test/resources/tst_manifests/yarn-berry/deps_with_mixed_dep_types/yarn-berry-ls-component.json 🧪 Tests +6/-0

Expected yarn berry component output

src/test/resources/tst_manifests/yarn-berry/deps_with_mixed_dep_types/yarn-berry-ls-component.json


17. src/test/resources/tst_manifests/yarn-berry/deps_with_mixed_dep_types/yarn-berry-ls-stack.json 🧪 Tests +8/-0

Expected yarn berry stack output

src/test/resources/tst_manifests/yarn-berry/deps_with_mixed_dep_types/yarn-berry-ls-stack.json


18. src/test/resources/tst_manifests/yarn-berry/deps_with_mixed_dep_types/expected_stack_sbom.json 🧪 Tests +98/-0

Expected yarn berry SBOM output

src/test/resources/tst_manifests/yarn-berry/deps_with_mixed_dep_types/expected_stack_sbom.json


19. src/test/resources/tst_manifests/yarn-classic/deps_with_mixed_dep_types/package.json 🧪 Tests +21/-0

Test package for yarn classic provider

src/test/resources/tst_manifests/yarn-classic/deps_with_mixed_dep_types/package.json


20. src/test/resources/tst_manifests/yarn-classic/deps_with_mixed_dep_types/yarn-classic-ls-component.json 🧪 Tests +1/-0

Expected yarn classic component output

src/test/resources/tst_manifests/yarn-classic/deps_with_mixed_dep_types/yarn-classic-ls-component.json


21. src/test/resources/tst_manifests/yarn-classic/deps_with_mixed_dep_types/yarn-classic-ls-stack.json 🧪 Tests +1/-0

Expected yarn classic stack output

src/test/resources/tst_manifests/yarn-classic/deps_with_mixed_dep_types/yarn-classic-ls-stack.json


22. src/test/resources/tst_manifests/yarn-classic/deps_with_mixed_dep_types/expected_stack_sbom.json 🧪 Tests +98/-0

Expected yarn classic SBOM output

src/test/resources/tst_manifests/yarn-classic/deps_with_mixed_dep_types/expected_stack_sbom.json


23. src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/pnpm-lock.yaml Additional files +0/-0

...

src/test/resources/tst_manifests/pnpm/deps_with_mixed_dep_types/pnpm-lock.yaml


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Dependency-exists check broken🐞
Description
JavaScriptProvider.ensurePeerAndOptionalDeps relies on Sbom.checkIfPackageInsideDependsOnList to
avoid re-adding already-present deps, but CycloneDXSbom.checkIfPackageInsideDependsOnList never
populates the list it checks and effectively always returns false. As a result, peer/optional deps
are always treated as missing and added again, risking duplicate root dependsOn entries and
inconsistent SBOM output (especially for scoped packages).
Code

src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java[R229-240]

+  private void ensurePeerAndOptionalDeps(Sbom sbom) {
+    var rootComponent = sbom.getRoot();
+    var depSources = new Map[] {manifest.peerDependencies, manifest.optionalDependencies};
+    for (var source : depSources) {
+      @SuppressWarnings("unchecked")
+      Map<String, String> deps = source;
+      deps.forEach(
+          (name, version) -> {
+            if (!sbom.checkIfPackageInsideDependsOnList(rootComponent, name)) {
+              sbom.addDependency(manifest.root, toPurl(name, version), null);
+            }
+          });
Evidence
ensurePeerAndOptionalDeps gates additions on checkIfPackageInsideDependsOnList, but
CycloneDXSbom.checkIfPackageInsideDependsOnList initializes allDirectDeps to an empty list and
discards the collected dependencies, so it always returns false. Additionally, it compares only
PackageURL.getName() (no namespace) to the provided name string, which will not match scoped names
like "@scope/pkg" even if the list assignment bug is fixed.

src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java[229-241]
src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java[328-354]
src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java[262-299]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`JavaScriptProvider.ensurePeerAndOptionalDeps` relies on `Sbom.checkIfPackageInsideDependsOnList` to decide whether a dependency is already present under the root. The CycloneDX implementation is currently incorrect (it never assigns the collected deps list, and it compares only `PackageURL.getName()`), so the check always fails and the backfill code always attempts to add peer/optional deps.

## Issue Context
This can cause redundant/duplicate dependency edges in the generated SBOM and makes the backfill logic behave differently than intended.

## Fix Focus Areas
- src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java[328-354]
- src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java[229-241]

## Implementation notes
- In `CycloneDXSbom.checkIfPackageInsideDependsOnList`, actually assign the collected list to `allDirectDeps`.
- Compare against a normalized full package name (namespace + "/" + name when namespace is present) OR compare using coordinates/bom-ref rather than `getName()`.
- Optionally, simplify `ensurePeerAndOptionalDeps` to rely on SBOM-level deduplication only if that is guaranteed (otherwise keep the fixed check).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Optional dep version null crash🐞
Description
JavaScriptProvider.addDependenciesFromKey reads e.getValue().get("version").asText() without a
null-check while now also iterating the dependency tree’s optionalDependencies object. Optional
dependency nodes can legitimately omit version metadata (the code already treats missing versions
as “ignore optional”), so this can throw a NullPointerException during SBOM generation.
Code

src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java[R172-176]

+  private void addDependenciesFromKey(Sbom sbom, JsonNode depTree, String key) {
+    var deps = depTree.get(key);
    if (deps == null) {
      return;
    }
Evidence
The existing traversal for nested deps (addDependenciesOf) explicitly skips entries where
version is missing and documents this as an optional-dependency case. The newly introduced
root-level parsing helper (addDependenciesFromKey) does not perform the same null-guard and will
dereference null when version is absent.

src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java[126-143]
src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java[167-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`addDependenciesFromKey` assumes every dependency entry under the selected key has a non-null `version` field and calls `.asText()` unconditionally. For optional deps, missing `version` is already treated as a valid condition elsewhere, so this helper should be equally defensive.

## Issue Context
This helper is now used for the `optionalDependencies` root key, increasing the likelihood of encountering optional entries without `version`.

## Fix Focus Areas
- src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java[167-185]

## Implementation notes
- Change to:
 - `JsonNode versionNode = e.getValue().get("version");`
 - `if (versionNode == null || versionNode.isNull()) return;`
- Consider mirroring the same skip semantics used in `addDependenciesOf` for consistency.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

1. CycloneDXSbom.checkIfPackageInsideDependsOnList: the stream result
   was never assigned back to allDirectDeps, so the check always returned
   false. Also changed getName() comparison to use full namespace/name to
   handle scoped packages (e.g. @babel/core).

2. JavaScriptProvider.addDependenciesFromKey: added null check for version
   node before calling asText(), matching the defensive pattern already
   used in addDependenciesOf.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@soul2zimate

Copy link
Copy Markdown
Contributor Author

/review

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

391 - Partially compliant

Compliant requirements:

  • Ensure all four JS providers (npm, pnpm, yarn classic, yarn berry) produce consistent dependency analysis results.
  • Include dependencies in the scan results.
  • Include peerDependencies in the scan results.
  • Include optionalDependencies in the scan results.
  • Exclude devDependencies from the scan results.
  • Use npm as baseline: for the provided sample, scan exactly 4 direct deps (express, axios, minimist, lodash) and match this across providers.

Non-compliant requirements:

  • Include bundledDependencies in the scan results (as a subset of dependencies).

Requires further human verification:

  • Validate on real projects (not only fixtures) that npm/pnpm/yarn classic/yarn berry now all yield identical direct-dependency sets when peerDependencies/optionalDependencies are present.
  • Confirm behavior for bundledDependencies is correct and consistent across providers (fixtures do not appear to assert bundled-specific handling beyond being present in dependencies).
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Resource Leak

loadManifest opens an InputStream in a try-with-resources but then reads the file using a second Files.newInputStream(manifestPath) that is not closed. Consider reading from the already-open stream (is) to avoid leaking file descriptors.

private JsonNode loadManifest(Path manifestPath) throws IOException {
  try (var is = Files.newInputStream(manifestPath)) {
    if (is == null) {
      throw new IllegalArgumentException("Unable to read required manifestPath: " + manifestPath);
    }
    return new ObjectMapper().readTree(Files.newInputStream(manifestPath));
  }
Type Safety

ensurePeerAndOptionalDeps uses a raw Map[] plus an unchecked cast/suppression. This is brittle and can hide type issues; prefer iterating explicitly over the two typed maps (or List<Map<String,String>>) without casts.

private void ensurePeerAndOptionalDeps(Sbom sbom) {
  var rootComponent = sbom.getRoot();
  var depSources = new Map[] {manifest.peerDependencies, manifest.optionalDependencies};
  for (var source : depSources) {
    @SuppressWarnings("unchecked")
    Map<String, String> deps = source;
    deps.forEach(
        (name, version) -> {
          if (!sbom.checkIfPackageInsideDependsOnList(rootComponent, name)) {
            sbom.addDependency(manifest.root, toPurl(name, version), null);
          }
        });
  }
}
Version Semantics

Backfilling peer/optional deps uses declared versions from package.json and directly builds PURLs. If versions are ranges/tags (common in peerDependencies, sometimes in optionalDependencies), generated PURLs may be invalid or misleading. Consider validating/normalizing versions or skipping/routing such cases differently.

private void ensurePeerAndOptionalDeps(Sbom sbom) {
  var rootComponent = sbom.getRoot();
  var depSources = new Map[] {manifest.peerDependencies, manifest.optionalDependencies};
  for (var source : depSources) {
    @SuppressWarnings("unchecked")
    Map<String, String> deps = source;
    deps.forEach(
        (name, version) -> {
          if (!sbom.checkIfPackageInsideDependsOnList(rootComponent, name)) {
            sbom.addDependency(manifest.root, toPurl(name, version), null);
          }
        });
  }
}

@soul2zimate
soul2zimate requested a review from ruromero April 3, 2026 04:12
Comment thread src/main/java/io/github/guacsec/trustifyda/providers/YarnBerryProcessor.java Outdated
…S reachability

The previous implementation only filtered devDependencies at the root edge,
but still processed all non-root nodes unconditionally. This meant transitive
dependencies of devDeps (e.g., jest -> some-test-util) would leak into the SBOM.

Uses BFS from root production deps to compute the reachable set, then skips
unreachable nodes and edges during SBOM emission. Also adds a test fixture
with a transitive devDep to expose the issue.

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
@soul2zimate
soul2zimate requested a review from ruromero April 7, 2026 01:46
@ruromero

ruromero commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator

/review

@qodo-code-review

qodo-code-review Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 226279c

@ruromero ruromero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I triggered the Qodo review but from the functionality point of view, LGTM

Comment thread src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java
@soul2zimate

Copy link
Copy Markdown
Contributor Author

Verification Report for TC-3977 (commit 226279c)

Check Result Details
Review Feedback PASS 2 code change requests (qodo bot + ruromero), both addressed by subsequent commits. No sub-tasks needed.
Root-Cause Investigation N/A No sub-tasks created
Scope Containment PASS All 27 changed files are relevant to the fix (4 source, 2 test, 21 test resources)
Diff Size PASS 921+/23- across 27 files; bulk is test resource JSON fixtures — proportionate to scope
Commit Traceability FAIL None of the 3 commits reference TC-3977 in message or body
Sensitive Patterns PASS No sensitive patterns detected
CI Status WARN 4 integration test failures are infrastructure-related: 3x rhtpa backend 504 timeout (gradle-groovy on all OSes), 1x pnpm/windows missing 'scanned' key. Unit tests and all other integration tests pass.
Acceptance Criteria PASS 6/6 criteria met
Verification Commands N/A No verification commands in task

Overall: WARN

Issues requiring attention:

  1. Commit Traceability (FAIL): Commit messages should reference TC-3977 (e.g., in a footer like Implements TC-3977). The branch name matches but commit messages lack the reference.
  2. CI (WARN): Integration test failures appear infrastructure-related (504 timeouts from staging backend), not caused by code changes. The pnpm/windows failure ("missing 'scanned' key") may also be transient but warrants monitoring.

Acceptance Criteria Details:

  • ✓ pnpm scans optionalDependencies via addDependenciesFromKey
  • ✓ yarn-classic scans peerDependencies and optionalDependencies via updated Manifest.loadDependencies
  • ✓ yarn-berry excludes devDependencies using BFS reachability in YarnBerryProcessor.addDependenciesToSbom
  • ✓ All JS providers backfill peer/optional deps via ensurePeerAndOptionalDeps
  • ✓ npm behavior unchanged (no overrides, uses --omit=dev as before)
  • ✓ Tests cover mixed dep type scenarios for all 4 providers

PR has 1 approval from @ruromero ("LGTM from functionality point of view").


This comment was AI-generated by sdlc-workflow/verify-pr v0.5.5.

…eDependsOnList

Match against both leaf name (dep.getName()) and full namespace/name to
support Go callers passing only the leaf name while preserving the new
scoped-package matching for JavaScript. Update Go golden files where
ignore-by-name filtering now correctly removes previously leaked deps.

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
@soul2zimate
soul2zimate merged commit 14fce6d into guacsec:main Apr 7, 2026
35 of 38 checks passed
@soul2zimate
soul2zimate deleted the TC-3977 branch April 7, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JavaScript providers produce inconsistent results for peerDependencies and optionalDependencies

2 participants