fix: scope workspace fix commands to the declaring member - #975
Merged
Conversation
Parent-upgrade fix commands were emitted flat at the workspace root (e.g. `pnpm add concurrently@9.2.4`, `npm install eslint@9.0.0`) instead of scoped to the member that declares the package. Running the flat command in a workspace root installs into the wrong place. Affected all four package managers. Root causes, all in buildSuggestedFixCommandPlan: - The three parent-upgrade target sites (upgrade-parent-to-version, recommendedParentUpgrade, and the chain-resolution branch) never attached the parent's workspace membership, so the command builder fell back to a flat root install. - buildPnpmLockfileRefreshCommand emitted `pnpm -C <member>` rather than pnpm's workspace selector `pnpm --filter ./<member>`. - The pnpm install command placed --filter after `add`; the selector is a CLI-level flag that must precede the command (`pnpm --filter ./m add`). - findSuggestedCommandForFinding (which feeds the per-finding runnableFixCommand used by JSON, the HTML per-finding view, and CycloneDX) returned a target's flat explicit command before checking workspaces, so those surfaces stayed flat even after the plan command was scoped. - The raw per-target `command` field (exposed in JSON and embedded in the HTML report data) was left flat; it is now normalized to the scoped form for install targets. Now every command-bearing output (terminal, JSON, HTML report, CycloneDX) scopes parent-upgrade commands to the correct member. SARIF is unaffected (it emits human-readable action text, not an install command).
Adds coverage for the parent-upgrade and within-range-refresh scoping across npm (-w), pnpm (--filter ./), yarn (yarn workspace <name> add), and bun (--filter). Includes the three parent-upgrade code paths, the per-finding runnableFixCommand, and the raw per-target command field, so no flat command survives in any command-bearing output.
Real runnable monorepo examples where a member's dev dependency (concurrently@9.2.1) transitively pulls vulnerable shell-quote@1.8.3, fixable only by upgrading the parent. Used to verify parent-upgrade fix commands are scoped to the declaring workspace member end to end.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #972
Parent-upgrade fix commands were emitted flat at the workspace root (e.g.
pnpm add concurrently@9.2.4,npm install eslint@9.0.0) instead of scoped to the member that declares the package. In a monorepo, running the flat command from the root installs into the wrong place and doesn't fix the member where the vulnerable transitive dep actually lives. This affected all four package managers, and it surfaced from a real enterprise adopter's pnpm + Angular/NestJS workspace.What changed
All in
buildSuggestedFixCommandPlan(src/remediation/fix-commands.ts):pnpm --filter ./<member> add,npm install -w <member>,yarn workspace <member> add, orbun add --filter <member>.buildPnpmLockfileRefreshCommanduses pnpm's workspace selectorpnpm --filter ./<member>instead of the-C <member>directory shim.--filterbeforeadd(it's a CLI-level selector), fixing the flag order.findSuggestedCommandForFinding(which feeds the per-findingrunnableFixCommandused by JSON, the HTML per-finding view, and CycloneDX) now prefers the workspace-scoped command over a target's flat explicit command, while leaving already-scoped parent-update commands untouched.commandfield (exposed in JSON and embedded in the HTML report data) is normalized to the scoped form.SARIF is unaffected - it emits human-readable action text ("Upgrade X to Y"), not an install command.
Verification
Confirmed before/after on real runnable workspace examples for npm (
examples/workspace), pnpm and yarn (two new example fixtures). bun's parent-upgrade path is not reachable through a real scan today (its parser doesn't attribute parent chains for transitive findings - separate limitation), so its scoping is covered by a unit test. Every command-bearing output (terminal, JSON, HTML report, CycloneDX) was checked to contain zero flat commands.In-house fix. Not a contributor pickup (remediation/fix-command internals).