Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/output/sarif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const SARIF_RULE_NAME = "VulnerableDependency";

const LOCKFILE_NAMES: Record<ScanSource, string> = {
"package-lock": "package-lock.json",
"npm-shrinkwrap": "npm-shrinkwrap.json",
"pnpm-lock": "pnpm-lock.yaml",
"yarn-lock": "yarn.lock",
"bun-lock": "bun.lockb",
Expand Down
34 changes: 32 additions & 2 deletions src/parsers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { loadFromYarnLock } from "./yarn-lock.js";

export function loadPackages(projectRoot: string, prodOnly: boolean, maxDepth: number): ScanInput {
const rootBunLock = path.join(projectRoot, "bun.lock");
const rootShrinkwrap = path.join(projectRoot, "npm-shrinkwrap.json");
const rootPackageLock = path.join(projectRoot, "package-lock.json");
const rootPnpmLock = path.join(projectRoot, "pnpm-lock.yaml");
const rootYarnLock = path.join(projectRoot, "yarn.lock");
Expand All @@ -29,6 +30,21 @@ export function loadPackages(projectRoot: string, prodOnly: boolean, maxDepth: n
};
}

if (fs.existsSync(rootShrinkwrap)) {
return {
mode: "resolved-lockfile",
source: "npm-shrinkwrap",
filePath: rootShrinkwrap,
packages: loadFromPackageLock(rootShrinkwrap, prodOnly),
notes: [
"Scanned resolved dependency versions from npm-shrinkwrap.json.",
"Dependency paths are derived from lockfile package locations."
],
warnings: [],
skippedDependencies: []
};
}

if (fs.existsSync(rootPackageLock)) {
return {
mode: "resolved-lockfile",
Expand Down Expand Up @@ -74,7 +90,7 @@ export function loadPackages(projectRoot: string, prodOnly: boolean, maxDepth: n
};
}

const discoveredLockfiles = findFiles(projectRoot, ["bun.lock", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"], maxDepth);
const discoveredLockfiles = findFiles(projectRoot, ["bun.lock", "npm-shrinkwrap.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"], maxDepth);
if (discoveredLockfiles.length > 0) {
const selected = chooseBestLockfile(discoveredLockfiles);
const selectedName = path.basename(selected);
Expand All @@ -92,6 +108,20 @@ export function loadPackages(projectRoot: string, prodOnly: boolean, maxDepth: n
skippedDependencies: []
};
}
if (selectedName === "npm-shrinkwrap.json") {
return {
mode: "resolved-lockfile",
source: "npm-shrinkwrap",
filePath: selected,
packages: loadFromPackageLock(selected, prodOnly),
notes: [
`Scanned resolved dependency versions from ${relativeOrName(projectRoot, selected)}.`,
"Dependency paths are derived from lockfile package locations."
],
warnings: ["No supported lockfile was found at the repo root, so a nested lockfile was used instead."],
skippedDependencies: []
};
}
if (selectedName === "package-lock.json") {
return {
mode: "resolved-lockfile",
Expand Down Expand Up @@ -180,7 +210,7 @@ export function loadPackages(projectRoot: string, prodOnly: boolean, maxDepth: n
export function buildNoPackagesMessage(projectRoot: string): string {
return [
"No scannable packages were found.",
"Supported inputs: bun.lock, package-lock.json, pnpm-lock.yaml, yarn.lock, or package.json with exact pinned versions.",
"Supported inputs: bun.lock, npm-shrinkwrap.json, package-lock.json, pnpm-lock.yaml, yarn.lock, or package.json with exact pinned versions.",
`Searched under: ${projectRoot}`
].join(" ");
}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export type NpmTransitiveGraph = {
};

export type ScanMode = "resolved-lockfile" | "manifest-fallback";
export type ScanSource = "package-lock" | "pnpm-lock" | "yarn-lock" | "bun-lock" | "package-json" | "unknown";
export type ScanSource = "package-lock" | "npm-shrinkwrap" | "pnpm-lock" | "yarn-lock" | "bun-lock" | "package-json" | "unknown";

export type ScanInput = {
mode: ScanMode;
Expand Down
70 changes: 70 additions & 0 deletions tests/parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,76 @@ packages:
}
});

it("detects npm-shrinkwrap.json at root and reports npm-shrinkwrap source", () => {
const projectDir = createTempProjectDir();

fs.writeFileSync(
path.join(projectDir, "npm-shrinkwrap.json"),
JSON.stringify({
lockfileVersion: 3,
packages: {
"": {},
"node_modules/lodash": { version: "4.17.21" },
},
}),
"utf8",
);

try {
const result = loadPackages(projectDir, false, 4);

expect(result.source).toBe("npm-shrinkwrap");
expect(path.basename(result.filePath ?? "")).toBe("npm-shrinkwrap.json");
expect(result.mode).toBe("resolved-lockfile");
expect(result.warnings).toEqual([]);
expect(result.packages).toEqual(
expect.arrayContaining([expect.objectContaining({ name: "lodash", version: "4.17.21" })]),
);
} finally {
removeDir(projectDir);
}
});

it("prefers npm-shrinkwrap.json over package-lock.json when both exist", () => {
const projectDir = createTempProjectDir();

fs.writeFileSync(
path.join(projectDir, "npm-shrinkwrap.json"),
JSON.stringify({
lockfileVersion: 3,
packages: {
"": {},
"node_modules/lodash": { version: "4.17.21" },
},
}),
"utf8",
);
fs.writeFileSync(
path.join(projectDir, "package-lock.json"),
JSON.stringify({
lockfileVersion: 3,
packages: {
"": {},
"node_modules/express": { version: "4.18.0" },
},
}),
"utf8",
);

try {
const result = loadPackages(projectDir, false, 4);

expect(result.source).toBe("npm-shrinkwrap");
expect(path.basename(result.filePath ?? "")).toBe("npm-shrinkwrap.json");
expect(result.packages).toEqual(
expect.arrayContaining([expect.objectContaining({ name: "lodash" })]),
);
expect(result.packages.map(p => p.name)).not.toContain("express");
} finally {
removeDir(projectDir);
}
});

it("falls back to package.json and surfaces the npmrc package-lock warning", () => {
const projectDir = createTempProjectDir();

Expand Down