From e170cce1f06c28afcee0d19a023e18942ae4ffc6 Mon Sep 17 00:00:00 2001 From: nkgotcode <58643031+nkgotcode@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:01:36 -0500 Subject: [PATCH 1/2] test fixture remediation scans --- examples/yarn-within-range/package.json | 10 ++ examples/yarn-within-range/yarn.lock | 14 ++ tests/fixture-scan.test.ts | 174 ++++++++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 examples/yarn-within-range/package.json create mode 100644 examples/yarn-within-range/yarn.lock create mode 100644 tests/fixture-scan.test.ts diff --git a/examples/yarn-within-range/package.json b/examples/yarn-within-range/package.json new file mode 100644 index 0000000..f46ddc0 --- /dev/null +++ b/examples/yarn-within-range/package.json @@ -0,0 +1,10 @@ +{ + "name": "cve-lite-example-yarn-within-range", + "version": "1.0.0", + "private": true, + "description": "Intentionally vulnerable Yarn Classic example where a transitive dependency can be refreshed within the parent range.", + "license": "MIT", + "dependencies": { + "@aws-amplify/core": "6.16.1" + } +} diff --git a/examples/yarn-within-range/yarn.lock b/examples/yarn-within-range/yarn.lock new file mode 100644 index 0000000..84c345d --- /dev/null +++ b/examples/yarn-within-range/yarn.lock @@ -0,0 +1,14 @@ +# yarn lockfile v1 + + +"@aws-amplify/core@6.16.1": + version "6.16.1" + resolved "https://registry.yarnpkg.com/@aws-amplify/core/-/core-6.16.1.tgz#example" + integrity sha512-example + dependencies: + js-cookie "^3.0.5" + +js-cookie@^3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.6.tgz#example" + integrity sha512-example diff --git a/tests/fixture-scan.test.ts b/tests/fixture-scan.test.ts new file mode 100644 index 0000000..fa1d624 --- /dev/null +++ b/tests/fixture-scan.test.ts @@ -0,0 +1,174 @@ +import path from "node:path"; +import { loadPackages } from "../src/parsers/index.js"; +import { buildSuggestedFixCommandPlan } from "../src/remediation/fix-commands.js"; +import type { Finding, PackageRef, ScanInput } from "../src/types.js"; + +const examplesDir = path.join(process.cwd(), "examples"); + +function loadFixture(name: string): ScanInput { + return loadPackages(path.join(examplesDir, name), false, 3); +} + +function requirePackage(scanInput: ScanInput, name: string): PackageRef { + const pkg = scanInput.packages.find(item => item.name === name); + if (!pkg) { + throw new Error(`Expected fixture to include ${name}`); + } + return pkg; +} + +function findingFor( + scanInput: ScanInput, + packageName: string, + overrides: Partial, +): Finding { + const pkg = requirePackage(scanInput, packageName); + return { + pkg, + vulnerabilities: [{ id: `OSV-${packageName}` }], + severity: "high", + cveAliases: [], + dependencyPaths: pkg.paths ?? [["project", packageName]], + relationship: "direct", + firstFixedVersion: null, + ...overrides, + }; +} + +describe("fixture remediation scans", () => { + it("suggests a package-lock refresh for wrong-parent instead of upgrading the parent", () => { + const scanInput = loadFixture("wrong-parent"); + const finding = findingFor(scanInput, "js-cookie", { + relationship: "transitive", + firstFixedVersion: "3.0.7", + recommendedNpmTransitiveRemediation: { + kind: "update-parent-within-range", + package: "js-cookie", + currentVersion: "3.0.6", + targetChildVersion: "3.0.7", + viaPath: ["project", "@aws-amplify/core", "js-cookie"], + reason: "js-cookie can be refreshed within @aws-amplify/core's declared range.", + }, + }); + + const plan = buildSuggestedFixCommandPlan([finding], scanInput); + + expect(plan?.packageManager).toBe("npm"); + expect(plan?.command).toBe("npm update js-cookie"); + expect(plan?.targets).toEqual([ + expect.objectContaining({ + package: "js-cookie", + kind: "parent-update", + displayTargetVersion: "lockfile refresh", + }), + ]); + expect(plan?.targets).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ package: "@aws-amplify/core", kind: "parent-upgrade" }), + ]), + ); + }); + + it("suggests a yarn lockfile refresh for yarn-within-range instead of upgrading the parent", () => { + const scanInput = loadFixture("yarn-within-range"); + const finding = findingFor(scanInput, "js-cookie", { + relationship: "transitive", + firstFixedVersion: "3.0.7", + dependencyPaths: [["project", "@aws-amplify/core", "js-cookie"]], + recommendedNpmTransitiveRemediation: { + kind: "update-parent-within-range", + package: "@aws-amplify/core", + currentVersion: "6.16.1", + targetChildVersion: "3.0.7", + viaPath: ["project", "@aws-amplify/core", "js-cookie"], + reason: "js-cookie can be refreshed within @aws-amplify/core's declared range.", + }, + }); + + const plan = buildSuggestedFixCommandPlan([finding], scanInput); + + expect(plan?.packageManager).toBe("yarn"); + expect(plan?.command).toBe("yarn upgrade js-cookie"); + expect(plan?.targets).toEqual([ + expect.objectContaining({ + package: "js-cookie", + kind: "parent-update", + displayTargetVersion: "lockfile refresh", + }), + ]); + expect(plan?.targets).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ package: "@aws-amplify/core", kind: "parent-upgrade" }), + ]), + ); + }); + + it("builds a direct dependency target for direct-fixable", () => { + const scanInput = loadFixture("direct-fixable"); + const finding = findingFor(scanInput, "axios", { + relationship: "direct", + firstFixedVersion: "0.21.2", + validatedFirstFixedVersion: "0.21.2", + }); + + const plan = buildSuggestedFixCommandPlan([finding], scanInput); + + expect(plan?.command).toBe("npm install axios@0.21.2"); + expect(plan?.sections).toEqual([ + expect.objectContaining({ + kind: "urgent", + command: "npm install axios@0.21.2", + }), + ]); + expect(plan?.targets).toEqual([ + expect.objectContaining({ package: "axios", kind: "direct" }), + ]); + }); + + it("keeps transitive-only findings out of direct fix targets", () => { + const scanInput = loadFixture("transitive-only"); + const finding = findingFor(scanInput, "string-width", { + relationship: "transitive", + firstFixedVersion: "4.2.3", + dependencyPaths: [["project", "lint-staged", "string-width"]], + recommendedParentUpgrade: { + package: "lint-staged", + currentVersion: "15.2.0", + targetVersion: "15.2.1", + viaPath: ["project", "lint-staged", "string-width"], + vulnerablePackage: "string-width", + confidence: "exact-direct-child", + reason: "Upgrade lint-staged to pick up a safe string-width release.", + }, + }); + + const plan = buildSuggestedFixCommandPlan([finding], scanInput); + + expect(plan?.targets).toEqual([ + expect.objectContaining({ + package: "lint-staged", + kind: "parent-upgrade", + }), + ]); + expect(plan?.targets).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ package: "string-width", kind: "direct" }), + ]), + ); + }); + + it("loads no-findings with no mocked findings and produces no fix command", () => { + const scanInput = loadFixture("no-findings"); + + expect(scanInput.packages.length).toBeGreaterThan(0); + expect(buildSuggestedFixCommandPlan([], scanInput)).toEqual( + expect.objectContaining({ + command: null, + sections: [], + targets: [], + skipped: [], + totalFindingCount: 0, + }), + ); + }); +}); From e04834c4f2eccaf0d5cbd4ff35274bbfacf08e84 Mon Sep 17 00:00:00 2001 From: Khanh Le Date: Thu, 4 Jun 2026 20:13:25 -0500 Subject: [PATCH 2/2] Address fixture scan review feedback --- examples/yarn-within-range/package.json | 10 ---- examples/yarn-within-range/yarn.lock | 14 ----- tests/fixture-scan.test.ts | 80 ++++++++++++++----------- 3 files changed, 45 insertions(+), 59 deletions(-) delete mode 100644 examples/yarn-within-range/package.json delete mode 100644 examples/yarn-within-range/yarn.lock diff --git a/examples/yarn-within-range/package.json b/examples/yarn-within-range/package.json deleted file mode 100644 index f46ddc0..0000000 --- a/examples/yarn-within-range/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "cve-lite-example-yarn-within-range", - "version": "1.0.0", - "private": true, - "description": "Intentionally vulnerable Yarn Classic example where a transitive dependency can be refreshed within the parent range.", - "license": "MIT", - "dependencies": { - "@aws-amplify/core": "6.16.1" - } -} diff --git a/examples/yarn-within-range/yarn.lock b/examples/yarn-within-range/yarn.lock deleted file mode 100644 index 84c345d..0000000 --- a/examples/yarn-within-range/yarn.lock +++ /dev/null @@ -1,14 +0,0 @@ -# yarn lockfile v1 - - -"@aws-amplify/core@6.16.1": - version "6.16.1" - resolved "https://registry.yarnpkg.com/@aws-amplify/core/-/core-6.16.1.tgz#example" - integrity sha512-example - dependencies: - js-cookie "^3.0.5" - -js-cookie@^3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.6.tgz#example" - integrity sha512-example diff --git a/tests/fixture-scan.test.ts b/tests/fixture-scan.test.ts index fa1d624..9ae22de 100644 --- a/tests/fixture-scan.test.ts +++ b/tests/fixture-scan.test.ts @@ -1,3 +1,4 @@ +import fs from "node:fs"; import path from "node:path"; import { loadPackages } from "../src/parsers/index.js"; import { buildSuggestedFixCommandPlan } from "../src/remediation/fix-commands.js"; @@ -6,7 +7,12 @@ import type { Finding, PackageRef, ScanInput } from "../src/types.js"; const examplesDir = path.join(process.cwd(), "examples"); function loadFixture(name: string): ScanInput { - return loadPackages(path.join(examplesDir, name), false, 3); + return loadPackages(path.join(examplesDir, name), false, 4); +} + +function itWithFixture(name: string, testName: string, testFn: () => void): void { + const fixtureTest = fs.existsSync(path.join(examplesDir, name)) ? it : it.skip; + fixtureTest(testName, testFn); } function requirePackage(scanInput: ScanInput, name: string): PackageRef { @@ -69,39 +75,43 @@ describe("fixture remediation scans", () => { ); }); - it("suggests a yarn lockfile refresh for yarn-within-range instead of upgrading the parent", () => { - const scanInput = loadFixture("yarn-within-range"); - const finding = findingFor(scanInput, "js-cookie", { - relationship: "transitive", - firstFixedVersion: "3.0.7", - dependencyPaths: [["project", "@aws-amplify/core", "js-cookie"]], - recommendedNpmTransitiveRemediation: { - kind: "update-parent-within-range", - package: "@aws-amplify/core", - currentVersion: "6.16.1", - targetChildVersion: "3.0.7", - viaPath: ["project", "@aws-amplify/core", "js-cookie"], - reason: "js-cookie can be refreshed within @aws-amplify/core's declared range.", - }, - }); - - const plan = buildSuggestedFixCommandPlan([finding], scanInput); - - expect(plan?.packageManager).toBe("yarn"); - expect(plan?.command).toBe("yarn upgrade js-cookie"); - expect(plan?.targets).toEqual([ - expect.objectContaining({ - package: "js-cookie", - kind: "parent-update", - displayTargetVersion: "lockfile refresh", - }), - ]); - expect(plan?.targets).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ package: "@aws-amplify/core", kind: "parent-upgrade" }), - ]), - ); - }); + itWithFixture( + "yarn-within-range", + "suggests a yarn lockfile refresh for yarn-within-range instead of upgrading the parent", + () => { + const scanInput = loadFixture("yarn-within-range"); + const finding = findingFor(scanInput, "js-cookie", { + relationship: "transitive", + firstFixedVersion: "3.0.7", + dependencyPaths: [["project", "@aws-amplify/core", "js-cookie"]], + recommendedNpmTransitiveRemediation: { + kind: "update-parent-within-range", + package: "@aws-amplify/core", + currentVersion: "6.16.1", + targetChildVersion: "3.0.7", + viaPath: ["project", "@aws-amplify/core", "js-cookie"], + reason: "js-cookie can be refreshed within @aws-amplify/core's declared range.", + }, + }); + + const plan = buildSuggestedFixCommandPlan([finding], scanInput); + + expect(plan?.packageManager).toBe("yarn"); + expect(plan?.command).toBe("yarn upgrade js-cookie"); + expect(plan?.targets).toEqual([ + expect.objectContaining({ + package: "js-cookie", + kind: "parent-update", + displayTargetVersion: "lockfile refresh", + }), + ]); + expect(plan?.targets).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ package: "@aws-amplify/core", kind: "parent-upgrade" }), + ]), + ); + }, + ); it("builds a direct dependency target for direct-fixable", () => { const scanInput = loadFixture("direct-fixable"); @@ -129,7 +139,7 @@ describe("fixture remediation scans", () => { const scanInput = loadFixture("transitive-only"); const finding = findingFor(scanInput, "string-width", { relationship: "transitive", - firstFixedVersion: "4.2.3", + firstFixedVersion: "7.2.1", dependencyPaths: [["project", "lint-staged", "string-width"]], recommendedParentUpgrade: { package: "lint-staged",