diff --git a/examples/multiple-versions-same-pkg/package-lock.json b/examples/multiple-versions-same-pkg/package-lock.json new file mode 100644 index 0000000..cd3a28a --- /dev/null +++ b/examples/multiple-versions-same-pkg/package-lock.json @@ -0,0 +1,38 @@ +{ + "name": "cve-lite-example-multiple-versions-same-pkg", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cve-lite-example-multiple-versions-same-pkg", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "karma": "1.7.1", + "lodash": "4.17.20" + } + }, + "node_modules/karma": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", + "integrity": "sha512-0TylHLtdl5f1QXi1LQn9f9v9wfqQv5JEoig6/IHDmysp8Z3fSQ8WLskChqdtoMa06XWKLFq2QAzs/Qn4m1XJmw==", + "license": "MIT", + "dependencies": { + "lodash": "^3.8.0" + } + }, + "node_modules/karma/node_modules/lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha512-u2/S9Y9fKF3bTlG2ba7tk9poHw2fmsfa4sQi2ghvHffftq/UcD0YQ8oUoGyz9/ZMEPX7AWHf45kFA7S+8EIYow==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcIlOINaEH7UKfZ0b8VmgphaGm+lh+4pd0s2/KCrJBD6NBkk+KNNMygi0ZWNLOd3gppdUa+dPdY0sHhUseGw==", + "license": "MIT" + } + } +} diff --git a/examples/multiple-versions-same-pkg/package.json b/examples/multiple-versions-same-pkg/package.json new file mode 100644 index 0000000..dd69a9b --- /dev/null +++ b/examples/multiple-versions-same-pkg/package.json @@ -0,0 +1,11 @@ +{ + "name": "cve-lite-example-multiple-versions-same-pkg", + "version": "1.0.0", + "private": true, + "description": "npm regression fixture: lodash@3.10.1 (via karma) and lodash@4.17.20 (direct) — same package at two versions, each must be a separate finding.", + "license": "MIT", + "dependencies": { + "karma": "1.7.1", + "lodash": "4.17.20" + } +} diff --git a/examples/readme.md b/examples/readme.md index 2f091b1..6661439 100644 --- a/examples/readme.md +++ b/examples/readme.md @@ -30,6 +30,7 @@ Small curated projects committed to the repository. Clone the repo and scan imme | `wrong-parent` | npm | 3-level transitive chain where the immediate parent's range already covers the fix — expects `npm update js-cookie`, not a parent bump. | | `no-findings` | npm | Clean project with no known vulnerabilities — demonstrates success output. | | `dev-only-finding` | npm | Vulnerable package that only appears in devDependencies — classified as a direct finding in full scans and excluded by `--prod-only`. | +| `multiple-versions-same-pkg` | npm | Same package at two installed versions (`lodash@3.10.1` via karma, `lodash@4.17.20` direct) — each version must appear as a separate finding. | | `any fixture` + `.cve-lite/baseline.json` | any | Run `cve-lite . --ratchet` on any fixture to establish a baseline. Rescan without the flag to see only new findings. `.cve-lite/` directories should NOT be committed from example fixtures. | | `mal-private-registry` | npm | `node-ipc@9.2.3` with `resolved` pointing to a private registry — demonstrates `Unverifiable (private source)` output for MAL- advisories where the artifact origin cannot be confirmed. | | `pnpm-mal-private-registry` | pnpm v9 | `node-ipc@9.2.3` resolved from a private registry — demonstrates `Unverifiable (private source)` detection for pnpm v9 lockfiles. | @@ -175,6 +176,7 @@ node dist/index.js examples/wrong-parent --verbose node dist/index.js examples/no-findings node dist/index.js examples/dev-only-finding --verbose node dist/index.js examples/dev-only-finding --verbose --prod-only +node dist/index.js examples/multiple-versions-same-pkg --verbose node dist/index.js examples/lima-site --verbose # In-repo snapshot: Astro diff --git a/tests/fixture-scan.test.ts b/tests/fixture-scan.test.ts index a99ff5f..b183f34 100644 --- a/tests/fixture-scan.test.ts +++ b/tests/fixture-scan.test.ts @@ -16,10 +16,13 @@ function itWithFixture(name: string, testName: string, testFn: () => void): void fixtureTest(testName, testFn); } -function requirePackage(scanInput: ScanInput, name: string): PackageRef { - const pkg = scanInput.packages.find(item => item.name === name); +function requirePackage(scanInput: ScanInput, name: string, version?: string): PackageRef { + const pkg = scanInput.packages.find( + item => item.name === name && (version === undefined || item.version === version), + ); if (!pkg) { - throw new Error(`Expected fixture to include ${name}`); + const versionSuffix = version ? `@${version}` : ""; + throw new Error(`Expected fixture to include ${name}${versionSuffix}`); } return pkg; } @@ -183,6 +186,42 @@ describe("fixture remediation scans", () => { ); }); + itWithFixture( + "multiple-versions-same-pkg", + "reports lodash@3 and lodash@4 as separate installed packages with correct relationships", + () => { + const scanInput = loadFixture("multiple-versions-same-pkg"); + const lodash3 = requirePackage(scanInput, "lodash", "3.10.1"); + const lodash4 = requirePackage(scanInput, "lodash", "4.17.20"); + + expect(scanInput.packages.filter(item => item.name === "lodash")).toHaveLength(2); + expect(lodash3.paths?.every(path => path.length > 2)).toBe(true); + expect(lodash4.paths?.some(path => path.length <= 2)).toBe(true); + + const directFinding = findingFor(scanInput, "lodash", { + relationship: "direct", + firstFixedVersion: "4.18.0", + validatedFirstFixedVersion: "4.18.0", + }); + directFinding.pkg = lodash4; + + const transitiveFinding = findingFor(scanInput, "lodash", { + relationship: "transitive", + firstFixedVersion: "4.18.0", + validatedFirstFixedVersion: "4.18.0", + dependencyPaths: lodash3.paths ?? [], + }); + transitiveFinding.pkg = lodash3; + + const directPlan = buildSuggestedFixCommandPlan([directFinding], scanInput); + const transitivePlan = buildSuggestedFixCommandPlan([transitiveFinding], scanInput); + + expect(directPlan?.command).toBe("npm install lodash@4.18.0"); + expect(directPlan?.targets.find(t => t.kind === "direct")?.package).toBe("lodash"); + expect(transitivePlan?.targets.find(t => t.kind === "direct")).toBeUndefined(); + }, + ); + it("mal-private-registry fixture - node-ipc resolvedUrl is from a private registry", () => { const scanInput = loadFixture("mal-private-registry"); const nodeIpc = scanInput.packages.find(p => p.name === "node-ipc");