Context
scanPackageLockPatch in src/review/lockfile-tamper.ts (function starts line 114) is a heuristic line-based scanner for package-lock.json diffs that flags the classic supply-chain tell: a resolved/integrity value changed without that same lockfile entry's own "version" field genuinely changing (see the module's header comment for the full rationale).
It tracks "which package entry am I currently inside" with a single currentEntryKey/currentPackageName pair and a sawPackagesEntry flag. The state machine resets currentEntryKey/currentPackageName to null in two places:
- Line 150-151: when an object-header line's key is neither a
node_modules/... path nor (once sawPackagesEntry is true) a recognized container key.
- Line 156: unconditionally, on any closing-brace line (
} or },).
The bug: a real package-lock.json entry frequently contains its own nested "dependencies" / "devDependencies" / "optionalDependencies" sub-object (this repo's own package-lock.json has many such entries, e.g. apps/loopover-miner-ui/node_modules/@asamuzakjp/css-color). Once inside the "packages" tree (sawPackagesEntry === true), hitting that nested "dependencies": { header falls into the else branch at line 150 (because npmPackageFromNodeModulesPath("dependencies") returns null and CONTAINER_KEYS.has("dependencies") is only checked when !sawPackagesEntry), resetting currentEntryKey/currentPackageName to null without ever restoring them when the nested object closes. Any resolved/integrity/version line for that same outer entry appearing after the nested sub-object in the diff is then silently skipped by the if (!currentEntryKey || !currentPackageName || line.sign === " ") continue; guard (line 158-159) — exactly the fields this module exists to police.
Today's real npm output happens to always order version/resolved/integrity before dependencies within an entry (confirmed against this repo's own package-lock.json), so legitimate diffs don't trip this. But nothing in the parser enforces that ordering, and an attacker crafting a hand-edited tamper diff specifically to evade this exact check would simply reorder the entry's keys so dependencies precedes resolved/integrity — defeating the tamper detector entirely for that entry. test/unit/lockfile-tamper.test.ts has no case with a node_modules/... entry containing a nested dependencies sub-object positioned before its own resolved/integrity/version fields, so this branch is untested today.
Requirements
- Fix
scanPackageLockPatch so that a nested "dependencies"/"devDependencies"/"optionalDependencies" sub-object inside a node_modules/... entry does not permanently lose track of that entry — subsequent resolved/integrity/version lines belonging to the same outer entry (appearing after the nested object closes) must still be attributed to it, regardless of key order within the entry.
- The fix must be a parsing/state-tracking correction only — it must not change
LockfileTamperCandidate's shape, the module's public API (scanPackageLockPatch's exported callers), or how a detected candidate is turned into a finding/blocker (isConfiguredGateBlocker / gate-mode wiring stays untouched).
- Preserve the existing behavior for the legacy lockfileVersion 1 bare-name
"dependencies" container tree (the !sawPackagesEntry branch) exactly as documented in the module's header comment — this fix is scoped to the node_modules/...-keyed lockfileVersion 2/3 path.
- Must not regress the existing entry-keying fix from
#2692/#2563 (full node_modules/... path as the map key, not bare package name) — the fix should track entry nesting depth, not just presence/absence of a current key.
Deliverables
Test Coverage Requirements
Aim for 99%+ Codecov patch coverage (100% including the new branch) on the touched lines in src/review/lockfile-tamper.ts (src/** is in coverage.include). This is a security-relevant regression fix — a dedicated test for the exact bypass scenario (reordered keys defeating detection) is required, not just incidental line coverage.
Expected Outcome
A package-lock.json diff where a tampered entry's resolved/integrity/version lines appear after that entry's own nested dependencies object is now still correctly flagged by the lockfile-tamper check — closing a state-tracking gap that could otherwise let a reordered-key tamper diff slip past this advisory/gate signal undetected.
Links & Resources
src/review/lockfile-tamper.ts (scanPackageLockPatch, function starting line 114; the reset bug at lines 145-158; entryFor, lines 120-134; CONTAINER_KEYS, near the top of the file)
test/unit/lockfile-tamper.test.ts (existing test suite)
- Prior related fix:
#2563 (original lockfile-tamper-risk gate check) and its #2692 gate-review follow-up (full-path entry keying, documented in this same function's header comment)
Context
scanPackageLockPatchinsrc/review/lockfile-tamper.ts(function starts line 114) is a heuristic line-based scanner forpackage-lock.jsondiffs that flags the classic supply-chain tell: aresolved/integrityvalue changed without that same lockfile entry's own"version"field genuinely changing (see the module's header comment for the full rationale).It tracks "which package entry am I currently inside" with a single
currentEntryKey/currentPackageNamepair and asawPackagesEntryflag. The state machine resetscurrentEntryKey/currentPackageNametonullin two places:node_modules/...path nor (oncesawPackagesEntryis true) a recognized container key.}or},).The bug: a real
package-lock.jsonentry frequently contains its own nested"dependencies"/"devDependencies"/"optionalDependencies"sub-object (this repo's ownpackage-lock.jsonhas many such entries, e.g.apps/loopover-miner-ui/node_modules/@asamuzakjp/css-color). Once inside the"packages"tree (sawPackagesEntry === true), hitting that nested"dependencies": {header falls into theelsebranch at line 150 (becausenpmPackageFromNodeModulesPath("dependencies")returnsnullandCONTAINER_KEYS.has("dependencies")is only checked when!sawPackagesEntry), resettingcurrentEntryKey/currentPackageNametonullwithout ever restoring them when the nested object closes. Anyresolved/integrity/versionline for that same outer entry appearing after the nested sub-object in the diff is then silently skipped by theif (!currentEntryKey || !currentPackageName || line.sign === " ") continue;guard (line 158-159) — exactly the fields this module exists to police.Today's real npm output happens to always order
version/resolved/integritybeforedependencieswithin an entry (confirmed against this repo's ownpackage-lock.json), so legitimate diffs don't trip this. But nothing in the parser enforces that ordering, and an attacker crafting a hand-edited tamper diff specifically to evade this exact check would simply reorder the entry's keys sodependenciesprecedesresolved/integrity— defeating the tamper detector entirely for that entry.test/unit/lockfile-tamper.test.tshas no case with anode_modules/...entry containing a nesteddependenciessub-object positioned before its ownresolved/integrity/versionfields, so this branch is untested today.Requirements
scanPackageLockPatchso that a nested"dependencies"/"devDependencies"/"optionalDependencies"sub-object inside anode_modules/...entry does not permanently lose track of that entry — subsequentresolved/integrity/versionlines belonging to the same outer entry (appearing after the nested object closes) must still be attributed to it, regardless of key order within the entry.LockfileTamperCandidate's shape, the module's public API (scanPackageLockPatch's exported callers), or how a detected candidate is turned into a finding/blocker (isConfiguredGateBlocker/ gate-mode wiring stays untouched)."dependencies"container tree (the!sawPackagesEntrybranch) exactly as documented in the module's header comment — this fix is scoped to thenode_modules/...-keyed lockfileVersion 2/3 path.#2692/#2563(fullnode_modules/...path as the map key, not bare package name) — the fix should track entry nesting depth, not just presence/absence of a current key.Deliverables
scanPackageLockPatchcorrectly attributesresolved/integrity/versionlines to their owning entry even when a nesteddependencies/devDependencies/optionalDependenciessub-object appears earlier in that same entry's diff hunk.test/unit/lockfile-tamper.test.tswith a synthetic diff where anode_modules/fooentry'sdependenciessub-object appears before itsresolved/integrity/versionlines (key order reversed from real npm output), asserting the tamper signal (resolvedOrIntegrityChangedwithout a matchingversionChanged) is still detected.node_modules/fooandnode_modules/bar/node_modules/foo), each also carrying its owndependenciessub-object, to confirm the entry-keying fix from#2692and this fix compose correctly.Test Coverage Requirements
Aim for 99%+ Codecov patch coverage (100% including the new branch) on the touched lines in
src/review/lockfile-tamper.ts(src/**is incoverage.include). This is a security-relevant regression fix — a dedicated test for the exact bypass scenario (reordered keys defeating detection) is required, not just incidental line coverage.Expected Outcome
A
package-lock.jsondiff where a tampered entry'sresolved/integrity/versionlines appear after that entry's own nesteddependenciesobject is now still correctly flagged by the lockfile-tamper check — closing a state-tracking gap that could otherwise let a reordered-key tamper diff slip past this advisory/gate signal undetected.Links & Resources
src/review/lockfile-tamper.ts(scanPackageLockPatch, function starting line 114; the reset bug at lines 145-158;entryFor, lines 120-134;CONTAINER_KEYS, near the top of the file)test/unit/lockfile-tamper.test.ts(existing test suite)#2563(original lockfile-tamper-risk gate check) and its#2692gate-review follow-up (full-path entry keying, documented in this same function's header comment)