Skip to content

Commit 7a06dfc

Browse files
authored
fix: collect all dependencies from workspace packages in scanner (#24942)
### What does this PR do? Fixes #23688 ### How did you verify your code works? Another test
1 parent 9ed5328 commit 7a06dfc

File tree

2 files changed

+311
-1
lines changed

2 files changed

+311
-1
lines changed

src/install/PackageManager/security_scanner.zig

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ const PackageCollector = struct {
383383
const root_pkg_id: PackageID = 0;
384384
const root_deps = pkg_dependencies[root_pkg_id];
385385

386+
// collect all npm deps from the root package
386387
for (root_deps.begin()..root_deps.end()) |_dep_id| {
387388
const dep_id: DependencyID = @intCast(_dep_id);
388389
const dep_pkg_id = this.manager.lockfile.buffers.resolutions.items[dep_id];
@@ -408,6 +409,39 @@ const PackageCollector = struct {
408409
.dep_path = dep_path_buf,
409410
});
410411
}
412+
413+
// and collect npm deps from workspace packages
414+
for (0..pkgs.len) |pkg_idx| {
415+
const pkg_id: PackageID = @intCast(pkg_idx);
416+
if (pkg_resolutions[pkg_id].tag != .workspace) continue;
417+
418+
const workspace_deps = pkg_dependencies[pkg_id];
419+
for (workspace_deps.begin()..workspace_deps.end()) |_dep_id| {
420+
const dep_id: DependencyID = @intCast(_dep_id);
421+
const dep_pkg_id = this.manager.lockfile.buffers.resolutions.items[dep_id];
422+
423+
if (dep_pkg_id == invalid_package_id) continue;
424+
425+
const dep_res = pkg_resolutions[dep_pkg_id];
426+
if (dep_res.tag != .npm) continue;
427+
428+
if ((try this.dedupe.getOrPut(dep_pkg_id)).found_existing) continue;
429+
430+
var pkg_path_buf = std.array_list.Managed(PackageID).init(this.manager.allocator);
431+
try pkg_path_buf.append(pkg_id);
432+
try pkg_path_buf.append(dep_pkg_id);
433+
434+
var dep_path_buf = std.array_list.Managed(DependencyID).init(this.manager.allocator);
435+
try dep_path_buf.append(dep_id);
436+
437+
try this.queue.writeItem(.{
438+
.pkg_id = dep_pkg_id,
439+
.dep_id = dep_id,
440+
.pkg_path = pkg_path_buf,
441+
.dep_path = dep_path_buf,
442+
});
443+
}
444+
}
411445
}
412446

413447
pub fn collectUpdatePackages(this: *PackageCollector) !void {
@@ -427,7 +461,7 @@ const PackageCollector = struct {
427461
for (0..pkgs.len) |_pkg_id| update_dep_id: {
428462
const pkg_id: PackageID = @intCast(_pkg_id);
429463
const pkg_res = pkg_resolutions[pkg_id];
430-
if (pkg_res.tag != .root) continue;
464+
if (pkg_res.tag != .root and pkg_res.tag != .workspace) continue;
431465

432466
const pkg_deps = pkg_dependencies[pkg_id];
433467
for (pkg_deps.begin()..pkg_deps.end()) |_dep_id| {
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
2+
import { join } from "node:path";
3+
import { getRegistry, startRegistry, stopRegistry } from "./simple-dummy-registry";
4+
5+
test("security scanner receives packages from workspace dependencies", async () => {
6+
const registryUrl = await startRegistry(false);
7+
8+
try {
9+
const registry = getRegistry();
10+
if (!registry) {
11+
throw new Error("Registry not found");
12+
}
13+
14+
registry.clearRequestLog();
15+
registry.setScannerBehavior("none");
16+
17+
// Create a workspace setup with root package and multiple workspace packages
18+
const files = {
19+
"package.json": JSON.stringify(
20+
{
21+
name: "workspace-root",
22+
private: true,
23+
workspaces: ["packages/*"],
24+
},
25+
null,
26+
2,
27+
),
28+
"packages/app1/package.json": JSON.stringify(
29+
{
30+
name: "app1",
31+
dependencies: {
32+
"left-pad": "1.3.0",
33+
},
34+
},
35+
null,
36+
2,
37+
),
38+
"packages/app2/package.json": JSON.stringify(
39+
{
40+
name: "app2",
41+
dependencies: {
42+
"is-even": "1.0.0",
43+
},
44+
},
45+
null,
46+
2,
47+
),
48+
"packages/lib1/package.json": JSON.stringify(
49+
{
50+
name: "lib1",
51+
dependencies: {
52+
"is-odd": "1.0.0",
53+
},
54+
},
55+
null,
56+
2,
57+
),
58+
"scanner.js": `export const scanner = {
59+
version: "1",
60+
scan: async function(payload) {
61+
console.error("SCANNER_RAN: " + payload.packages.length + " packages");
62+
return [];
63+
}
64+
}`,
65+
};
66+
67+
const dir = tempDirWithFiles("scanner-workspaces", files);
68+
69+
await Bun.write(
70+
join(dir, "bunfig.toml"),
71+
`[install]
72+
cache.disable = true
73+
registry = "${registryUrl}/"
74+
75+
[install.security]
76+
scanner = "./scanner.js"`,
77+
);
78+
79+
const { stdout, stderr } = Bun.spawn({
80+
cmd: [bunExe(), "install"],
81+
cwd: dir,
82+
stdout: "pipe",
83+
stderr: "pipe",
84+
env: bunEnv,
85+
});
86+
87+
const output = (await stdout.text()) + (await stderr.text());
88+
89+
// The scanner should receive packages from all workspace dependencies
90+
expect(output).toContain("SCANNER_RAN:");
91+
92+
// Extract the number of packages from the output
93+
const match = output.match(/SCANNER_RAN: (\d+) packages/);
94+
expect(match).toBeTruthy();
95+
96+
const packagesScanned = parseInt(match![1], 10);
97+
// Exact package count: left-pad, is-even, is-odd (is-even <-> is-odd have circular deps)
98+
expect(packagesScanned).toBe(3);
99+
} finally {
100+
stopRegistry();
101+
}
102+
});
103+
104+
test("security scanner receives packages from workspace dependencies with hoisted linker", async () => {
105+
const registryUrl = await startRegistry(false);
106+
107+
try {
108+
const registry = getRegistry();
109+
if (!registry) {
110+
throw new Error("Registry not found");
111+
}
112+
113+
registry.clearRequestLog();
114+
registry.setScannerBehavior("none");
115+
116+
const files = {
117+
"package.json": JSON.stringify(
118+
{
119+
name: "workspace-root",
120+
private: true,
121+
workspaces: ["packages/*"],
122+
},
123+
null,
124+
2,
125+
),
126+
"packages/app1/package.json": JSON.stringify(
127+
{
128+
name: "app1",
129+
dependencies: {
130+
"left-pad": "1.3.0",
131+
},
132+
},
133+
null,
134+
2,
135+
),
136+
"packages/app2/package.json": JSON.stringify(
137+
{
138+
name: "app2",
139+
dependencies: {
140+
"is-even": "1.0.0",
141+
},
142+
},
143+
null,
144+
2,
145+
),
146+
"scanner.js": `export const scanner = {
147+
version: "1",
148+
scan: async function(payload) {
149+
console.error("SCANNER_RAN: " + payload.packages.length + " packages");
150+
return [];
151+
}
152+
}`,
153+
};
154+
155+
const dir = tempDirWithFiles("scanner-workspaces-hoisted", files);
156+
157+
await Bun.write(
158+
join(dir, "bunfig.toml"),
159+
`[install]
160+
cache.disable = true
161+
linker = "hoisted"
162+
registry = "${registryUrl}/"
163+
164+
[install.security]
165+
scanner = "./scanner.js"`,
166+
);
167+
168+
const { stdout, stderr } = Bun.spawn({
169+
cmd: [bunExe(), "install"],
170+
cwd: dir,
171+
stdout: "pipe",
172+
stderr: "pipe",
173+
env: bunEnv,
174+
});
175+
176+
const output = (await stdout.text()) + (await stderr.text());
177+
178+
expect(output).toContain("SCANNER_RAN:");
179+
180+
const match = output.match(/SCANNER_RAN: (\d+) packages/);
181+
expect(match).toBeTruthy();
182+
183+
const packagesScanned = parseInt(match![1], 10);
184+
// Exact package count: left-pad, is-even, is-odd (is-even <-> is-odd have circular deps)
185+
expect(packagesScanned).toBe(3);
186+
} finally {
187+
stopRegistry();
188+
}
189+
});
190+
191+
test("security scanner receives packages from workspace dependencies with isolated linker", async () => {
192+
const registryUrl = await startRegistry(false);
193+
194+
try {
195+
const registry = getRegistry();
196+
if (!registry) {
197+
throw new Error("Registry not found");
198+
}
199+
200+
registry.clearRequestLog();
201+
registry.setScannerBehavior("none");
202+
203+
const files = {
204+
"package.json": JSON.stringify(
205+
{
206+
name: "workspace-root",
207+
private: true,
208+
workspaces: ["packages/*"],
209+
},
210+
null,
211+
2,
212+
),
213+
"packages/app1/package.json": JSON.stringify(
214+
{
215+
name: "app1",
216+
dependencies: {
217+
"left-pad": "1.3.0",
218+
},
219+
},
220+
null,
221+
2,
222+
),
223+
"packages/app2/package.json": JSON.stringify(
224+
{
225+
name: "app2",
226+
dependencies: {
227+
"is-even": "1.0.0",
228+
},
229+
},
230+
null,
231+
2,
232+
),
233+
"scanner.js": `export const scanner = {
234+
version: "1",
235+
scan: async function(payload) {
236+
console.error("SCANNER_RAN: " + payload.packages.length + " packages");
237+
return [];
238+
}
239+
}`,
240+
};
241+
242+
const dir = tempDirWithFiles("scanner-workspaces-isolated", files);
243+
244+
await Bun.write(
245+
join(dir, "bunfig.toml"),
246+
`[install]
247+
cache.disable = true
248+
linker = "isolated"
249+
registry = "${registryUrl}/"
250+
251+
[install.security]
252+
scanner = "./scanner.js"`,
253+
);
254+
255+
const { stdout, stderr } = Bun.spawn({
256+
cmd: [bunExe(), "install"],
257+
cwd: dir,
258+
stdout: "pipe",
259+
stderr: "pipe",
260+
env: bunEnv,
261+
});
262+
263+
const output = (await stdout.text()) + (await stderr.text());
264+
265+
expect(output).toContain("SCANNER_RAN:");
266+
267+
const match = output.match(/SCANNER_RAN: (\d+) packages/);
268+
expect(match).toBeTruthy();
269+
270+
const packagesScanned = parseInt(match![1], 10);
271+
// Exact package count: left-pad, is-even, is-odd (is-even <-> is-odd have circular deps)
272+
expect(packagesScanned).toBe(3);
273+
} finally {
274+
stopRegistry();
275+
}
276+
});

0 commit comments

Comments
 (0)