@cyclonedx/cdxgen 12.2.0 → 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +242 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +532 -168
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +276 -68
  33. package/lib/cli/index.poku.js +368 -0
  34. package/lib/helpers/analyzer.js +1052 -5
  35. package/lib/helpers/analyzer.poku.js +301 -0
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/depsUtils.js +16 -0
  48. package/lib/helpers/depsUtils.poku.js +58 -1
  49. package/lib/helpers/display.js +245 -61
  50. package/lib/helpers/display.poku.js +162 -2
  51. package/lib/helpers/exportUtils.js +123 -0
  52. package/lib/helpers/exportUtils.poku.js +60 -0
  53. package/lib/helpers/formulationParsers.js +69 -0
  54. package/lib/helpers/formulationParsers.poku.js +44 -0
  55. package/lib/helpers/gtfobins.js +189 -0
  56. package/lib/helpers/gtfobins.poku.js +49 -0
  57. package/lib/helpers/lolbas.js +267 -0
  58. package/lib/helpers/lolbas.poku.js +39 -0
  59. package/lib/helpers/osqueryTransform.js +84 -0
  60. package/lib/helpers/osqueryTransform.poku.js +49 -0
  61. package/lib/helpers/provenanceUtils.js +193 -0
  62. package/lib/helpers/provenanceUtils.poku.js +145 -0
  63. package/lib/helpers/pylockutils.js +281 -0
  64. package/lib/helpers/pylockutils.poku.js +48 -0
  65. package/lib/helpers/registryProvenance.js +793 -0
  66. package/lib/helpers/registryProvenance.poku.js +452 -0
  67. package/lib/helpers/remote/dependency-track.js +84 -0
  68. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  69. package/lib/helpers/source.js +1267 -0
  70. package/lib/helpers/source.poku.js +771 -0
  71. package/lib/helpers/spdxUtils.js +97 -0
  72. package/lib/helpers/spdxUtils.poku.js +70 -0
  73. package/lib/helpers/table.js +384 -0
  74. package/lib/helpers/table.poku.js +186 -0
  75. package/lib/helpers/unicodeScan.js +147 -0
  76. package/lib/helpers/unicodeScan.poku.js +45 -0
  77. package/lib/helpers/utils.js +882 -136
  78. package/lib/helpers/utils.poku.js +995 -91
  79. package/lib/managers/binary.js +29 -5
  80. package/lib/managers/docker.js +179 -52
  81. package/lib/managers/docker.poku.js +327 -28
  82. package/lib/managers/oci.js +107 -23
  83. package/lib/managers/oci.poku.js +132 -0
  84. package/lib/server/openapi.yaml +50 -0
  85. package/lib/server/server.js +228 -331
  86. package/lib/server/server.poku.js +220 -5
  87. package/lib/stages/postgen/annotator.js +7 -0
  88. package/lib/stages/postgen/annotator.poku.js +40 -0
  89. package/lib/stages/postgen/auditBom.js +20 -5
  90. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  91. package/lib/stages/postgen/postgen.js +40 -0
  92. package/lib/stages/postgen/postgen.poku.js +47 -0
  93. package/lib/stages/postgen/ruleEngine.js +80 -2
  94. package/lib/stages/postgen/spdxConverter.js +796 -0
  95. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  96. package/lib/validator/bomValidator.js +232 -0
  97. package/lib/validator/bomValidator.poku.js +70 -0
  98. package/lib/validator/complianceRules.js +70 -7
  99. package/lib/validator/complianceRules.poku.js +30 -0
  100. package/lib/validator/reporters/annotations.js +2 -2
  101. package/lib/validator/reporters/console.js +13 -2
  102. package/lib/validator/reporters.poku.js +13 -0
  103. package/package.json +10 -8
  104. package/types/bin/audit.d.ts +3 -0
  105. package/types/bin/audit.d.ts.map +1 -0
  106. package/types/bin/convert.d.ts +3 -0
  107. package/types/bin/convert.d.ts.map +1 -0
  108. package/types/bin/repl.d.ts.map +1 -1
  109. package/types/lib/audit/index.d.ts +115 -0
  110. package/types/lib/audit/index.d.ts.map +1 -0
  111. package/types/lib/audit/progress.d.ts +27 -0
  112. package/types/lib/audit/progress.d.ts.map +1 -0
  113. package/types/lib/audit/reporters.d.ts +35 -0
  114. package/types/lib/audit/reporters.d.ts.map +1 -0
  115. package/types/lib/audit/scoring.d.ts +35 -0
  116. package/types/lib/audit/scoring.d.ts.map +1 -0
  117. package/types/lib/audit/targets.d.ts +63 -0
  118. package/types/lib/audit/targets.d.ts.map +1 -0
  119. package/types/lib/cli/index.d.ts +8 -0
  120. package/types/lib/cli/index.d.ts.map +1 -1
  121. package/types/lib/helpers/analyzer.d.ts +13 -0
  122. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  123. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  124. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  125. package/types/lib/helpers/bomUtils.d.ts +5 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  127. package/types/lib/helpers/chromextutils.d.ts +97 -0
  128. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  129. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  130. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  131. package/types/lib/helpers/containerRisk.d.ts +17 -0
  132. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  133. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  134. package/types/lib/helpers/display.d.ts +4 -1
  135. package/types/lib/helpers/display.d.ts.map +1 -1
  136. package/types/lib/helpers/exportUtils.d.ts +40 -0
  137. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  138. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  139. package/types/lib/helpers/gtfobins.d.ts +17 -0
  140. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  141. package/types/lib/helpers/lolbas.d.ts +16 -0
  142. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  143. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  144. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  145. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  146. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  147. package/types/lib/helpers/pylockutils.d.ts +51 -0
  148. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  149. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  150. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  151. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  152. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  153. package/types/lib/helpers/source.d.ts +141 -0
  154. package/types/lib/helpers/source.d.ts.map +1 -0
  155. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  156. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  157. package/types/lib/helpers/table.d.ts +6 -0
  158. package/types/lib/helpers/table.d.ts.map +1 -0
  159. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  160. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  161. package/types/lib/helpers/utils.d.ts +30 -11
  162. package/types/lib/helpers/utils.d.ts.map +1 -1
  163. package/types/lib/managers/binary.d.ts.map +1 -1
  164. package/types/lib/managers/docker.d.ts.map +1 -1
  165. package/types/lib/managers/oci.d.ts.map +1 -1
  166. package/types/lib/server/server.d.ts +0 -35
  167. package/types/lib/server/server.d.ts.map +1 -1
  168. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  170. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  171. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  172. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  173. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  174. package/types/lib/validator/bomValidator.d.ts +1 -0
  175. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  176. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  177. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  178. package/types/bin/dependencies.d.ts +0 -3
  179. package/types/bin/dependencies.d.ts.map +0 -1
  180. package/types/bin/licenses.d.ts +0 -3
  181. package/types/bin/licenses.d.ts.map +0 -1
@@ -1,7 +1,30 @@
1
+ import {
2
+ mkdirSync,
3
+ mkdtempSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import process from "node:process";
11
+ import { fileURLToPath } from "node:url";
12
+
1
13
  import esmock from "esmock";
2
14
  import { assert, describe, it } from "poku";
3
15
  import sinon from "sinon";
4
16
 
17
+ import { createChromeExtensionBom } from "./index.js";
18
+
19
+ const fixtureDir = join(
20
+ dirname(fileURLToPath(import.meta.url)),
21
+ "..",
22
+ "..",
23
+ "test",
24
+ "data",
25
+ "chrome-extensions",
26
+ );
27
+
5
28
  describe("CLI tests", () => {
6
29
  describe("submitBom()", () => {
7
30
  it("should successfully report the SBOM with given project id, name, version and a single tag", async () => {
@@ -120,5 +143,350 @@ describe("CLI tests", () => {
120
143
  assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
121
144
  assert.deepEqual(options.json, expectedRequestPayload);
122
145
  });
146
+
147
+ it("should include parentName and parentVersion when parent project name and version are passed", async () => {
148
+ const fakeGotResponse = {
149
+ json: sinon.stub().resolves({ success: true }),
150
+ };
151
+
152
+ const gotStub = sinon.stub().returns(fakeGotResponse);
153
+ gotStub.extend = sinon.stub().returns(gotStub);
154
+
155
+ const { submitBom } = await esmock("./index.js", {
156
+ got: { default: gotStub },
157
+ });
158
+
159
+ const serverUrl = "https://dtrack.example.com";
160
+ const projectName = "cdxgen-test-project";
161
+ const projectVersion = "2.0.0";
162
+ const parentProjectName = "parent-project";
163
+ const parentProjectVersion = "1.0.0";
164
+ const bomContent = {
165
+ bom: "test3",
166
+ };
167
+ const apiKey = "TEST_API_KEY";
168
+ const skipDtTlsCheck = false;
169
+
170
+ const expectedRequestPayload = {
171
+ autoCreate: "true",
172
+ bom: "eyJib20iOiJ0ZXN0MyJ9", // stringified and base64 encoded bomContent
173
+ parentName: parentProjectName,
174
+ parentVersion: parentProjectVersion,
175
+ projectName,
176
+ projectVersion,
177
+ };
178
+
179
+ await submitBom(
180
+ {
181
+ serverUrl,
182
+ projectName,
183
+ projectVersion,
184
+ parentProjectName,
185
+ parentProjectVersion,
186
+ apiKey,
187
+ skipDtTlsCheck,
188
+ },
189
+ bomContent,
190
+ );
191
+
192
+ sinon.assert.calledOnce(gotStub);
193
+ const [calledUrl, options] = gotStub.firstCall.args;
194
+
195
+ assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
196
+ assert.equal(options.method, "PUT");
197
+ assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
198
+ assert.equal(options.headers["X-Api-Key"], apiKey);
199
+ assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
200
+ assert.deepEqual(options.json, expectedRequestPayload);
201
+ });
202
+
203
+ it("should include configurable autoCreate and isLatest values in payload", async () => {
204
+ const fakeGotResponse = {
205
+ json: sinon.stub().resolves({ success: true }),
206
+ };
207
+
208
+ const gotStub = sinon.stub().returns(fakeGotResponse);
209
+ gotStub.extend = sinon.stub().returns(gotStub);
210
+
211
+ const { submitBom } = await esmock("./index.js", {
212
+ got: { default: gotStub },
213
+ });
214
+
215
+ const serverUrl = "https://dtrack.example.com";
216
+ const projectName = "cdxgen-test-project";
217
+ const apiKey = "TEST_API_KEY";
218
+
219
+ await submitBom(
220
+ {
221
+ serverUrl,
222
+ projectName,
223
+ apiKey,
224
+ autoCreate: false,
225
+ isLatest: true,
226
+ },
227
+ { bom: "test4" },
228
+ );
229
+
230
+ sinon.assert.calledOnce(gotStub);
231
+ const [_calledUrl, options] = gotStub.firstCall.args;
232
+ assert.equal(options.json.autoCreate, "false");
233
+ assert.equal(options.json.isLatest, true);
234
+ assert.equal(options.json.projectVersion, "main");
235
+ });
236
+
237
+ it("should reject invalid mixed parent modes before making network request", async () => {
238
+ const fakeGotResponse = {
239
+ json: sinon.stub().resolves({ success: true }),
240
+ };
241
+
242
+ const gotStub = sinon.stub().returns(fakeGotResponse);
243
+ gotStub.extend = sinon.stub().returns(gotStub);
244
+
245
+ const { submitBom } = await esmock("./index.js", {
246
+ got: { default: gotStub },
247
+ });
248
+
249
+ const response = await submitBom(
250
+ {
251
+ serverUrl: "https://dtrack.example.com",
252
+ projectName: "cdxgen-test-project",
253
+ parentProjectId: "5103b8b4-4ca3-46ea-8051-036a3b2ab17e",
254
+ parentProjectName: "parent",
255
+ parentProjectVersion: "1.0.0",
256
+ },
257
+ { bom: "test5" },
258
+ );
259
+
260
+ assert.equal(response, undefined);
261
+ sinon.assert.notCalled(gotStub);
262
+ });
263
+ });
264
+
265
+ describe("createCocoaBom()", () => {
266
+ it("should skip missing Podfile.lock when failOnError is false", async () => {
267
+ const { createCocoaBom } = await import("./index.js");
268
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-cocoa-"));
269
+ const podFile = join(tempDir, "Podfile");
270
+ writeFileSync(
271
+ podFile,
272
+ "platform :ios, '14.0'\n\ntarget 'TestApp' do\nend\n",
273
+ "utf-8",
274
+ );
275
+ const consoleLogStub = sinon.stub(console, "log");
276
+ try {
277
+ const bomData = await createCocoaBom(tempDir, {
278
+ deep: false,
279
+ failOnError: false,
280
+ installDeps: false,
281
+ multiProject: false,
282
+ });
283
+ assert.equal(bomData, undefined);
284
+ sinon.assert.calledWithMatch(
285
+ consoleLogStub,
286
+ sinon.match("No 'Podfile.lock' found"),
287
+ );
288
+ } finally {
289
+ consoleLogStub.restore();
290
+ rmSync(tempDir, { force: true, recursive: true });
291
+ }
292
+ });
293
+
294
+ it("should not warn or exit for deep mode when Podfile.lock exists", async () => {
295
+ const { createCocoaBom } = await import("./index.js");
296
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-cocoa-deep-"));
297
+ const podFile = join(tempDir, "Podfile");
298
+ const lockFile = join(tempDir, "Podfile.lock");
299
+ writeFileSync(
300
+ podFile,
301
+ "platform :ios, '14.0'\n\ntarget 'TestApp' do\nend\n",
302
+ "utf-8",
303
+ );
304
+ writeFileSync(lockFile, "PODS: []\nDEPENDENCIES: []\n", "utf-8");
305
+ const processExitStub = sinon.stub(process, "exit");
306
+ try {
307
+ await createCocoaBom(tempDir, {
308
+ deep: true,
309
+ failOnError: true,
310
+ installDeps: false,
311
+ multiProject: false,
312
+ });
313
+ sinon.assert.notCalled(processExitStub);
314
+ } finally {
315
+ processExitStub.restore();
316
+ rmSync(tempDir, { force: true, recursive: true });
317
+ }
318
+ });
319
+ });
320
+
321
+ describe("createChromeExtensionBom()", () => {
322
+ it("should catalog a directly provided extension and its node dependencies", async () => {
323
+ const tempRoot = mkdtempSync(join(tmpdir(), "cdxgen-chrome-ext-cli-"));
324
+ const extensionId = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
325
+ const extensionIdDir = join(tempRoot, extensionId);
326
+ const extensionVersionDir = join(extensionIdDir, "1.2.3");
327
+ try {
328
+ mkdirSync(extensionVersionDir, { recursive: true });
329
+ writeFileSync(
330
+ join(extensionVersionDir, "manifest.json"),
331
+ JSON.stringify({
332
+ manifest_version: 3,
333
+ name: "CLI Test Extension",
334
+ description: "Direct path test",
335
+ version: "1.2.3",
336
+ }),
337
+ "utf-8",
338
+ );
339
+ writeFileSync(
340
+ join(extensionVersionDir, "package.json"),
341
+ JSON.stringify({
342
+ name: "chrome-extension-cli-test",
343
+ version: "1.2.3",
344
+ dependencies: {
345
+ "left-pad": "1.3.0",
346
+ },
347
+ }),
348
+ "utf-8",
349
+ );
350
+ writeFileSync(
351
+ join(extensionVersionDir, "package-lock.json"),
352
+ JSON.stringify({
353
+ name: "chrome-extension-cli-test",
354
+ version: "1.2.3",
355
+ lockfileVersion: 3,
356
+ requires: true,
357
+ packages: {
358
+ "": {
359
+ name: "chrome-extension-cli-test",
360
+ version: "1.2.3",
361
+ dependencies: {
362
+ "left-pad": "1.3.0",
363
+ },
364
+ },
365
+ "node_modules/left-pad": {
366
+ version: "1.3.0",
367
+ },
368
+ },
369
+ }),
370
+ "utf-8",
371
+ );
372
+ const bomData = await createChromeExtensionBom(extensionIdDir, {
373
+ projectType: ["chrome-extension"],
374
+ multiProject: false,
375
+ });
376
+ const components = bomData?.bomJson?.components || [];
377
+ assert.ok(
378
+ components.some(
379
+ (component) =>
380
+ component.purl === `pkg:chrome-extension/${extensionId}@1.2.3`,
381
+ ),
382
+ );
383
+ assert.ok(
384
+ components.some(
385
+ (component) =>
386
+ component.name === "left-pad" &&
387
+ component.purl?.startsWith("pkg:npm/left-pad@1.3.0"),
388
+ ),
389
+ );
390
+ } finally {
391
+ rmSync(tempRoot, { recursive: true, force: true });
392
+ }
393
+ });
394
+
395
+ it("should parse an AI-targeted community extension manifest from direct version path", async () => {
396
+ const tempRoot = mkdtempSync(join(tmpdir(), "cdxgen-chrome-ext-cli-ai-"));
397
+ const extensionId = "llllllllllllllllllllllllllllllll";
398
+ const extensionVersion = "1.0.0";
399
+ const extensionVersionDir = join(tempRoot, extensionId, extensionVersion);
400
+ try {
401
+ mkdirSync(extensionVersionDir, { recursive: true });
402
+ writeFileSync(
403
+ join(extensionVersionDir, "manifest.json"),
404
+ readFileSync(
405
+ join(fixtureDir, "chrome-copilottts-manifest.json"),
406
+ "utf-8",
407
+ ),
408
+ "utf-8",
409
+ );
410
+ const bomData = await createChromeExtensionBom(extensionVersionDir, {
411
+ projectType: ["chrome-extension"],
412
+ multiProject: false,
413
+ });
414
+ const extensionComponent = (bomData?.bomJson?.components || []).find(
415
+ (component) =>
416
+ component.purl ===
417
+ `pkg:chrome-extension/${extensionId}@${extensionVersion}`,
418
+ );
419
+ assert.ok(extensionComponent, "expected direct extension component");
420
+ const properties = extensionComponent.properties || [];
421
+ assert.ok(
422
+ properties.some(
423
+ (prop) =>
424
+ prop.name === "cdx:chrome-extension:permissions" &&
425
+ prop.value.includes("scripting"),
426
+ ),
427
+ );
428
+ assert.ok(
429
+ properties.some(
430
+ (prop) =>
431
+ prop.name === "cdx:chrome-extension:capability:codeInjection" &&
432
+ prop.value === "true",
433
+ ),
434
+ );
435
+ assert.ok(
436
+ properties.some(
437
+ (prop) =>
438
+ prop.name === "cdx:chrome-extension:hostPermissions" &&
439
+ prop.value.includes("https://github.com/copilot/tasks/*"),
440
+ ),
441
+ );
442
+ } finally {
443
+ rmSync(tempRoot, { recursive: true, force: true });
444
+ }
445
+ });
446
+ });
447
+
448
+ describe("createMultiXBom()", () => {
449
+ it("should scan installed chrome extensions only once across multiple non-extension paths", async () => {
450
+ const tempRoot = mkdtempSync(join(tmpdir(), "cdxgen-chrome-ext-multi-"));
451
+ const pathA = join(tempRoot, "project-a");
452
+ const pathB = join(tempRoot, "project-b");
453
+ mkdirSync(pathA, { recursive: true });
454
+ mkdirSync(pathB, { recursive: true });
455
+ const collectInstalledChromeExtensions = sinon.stub().returns([
456
+ {
457
+ type: "application",
458
+ name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
459
+ version: "1.0.0",
460
+ purl: "pkg:chrome-extension/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@1.0.0",
461
+ "bom-ref":
462
+ "pkg:chrome-extension/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@1.0.0",
463
+ },
464
+ ]);
465
+ try {
466
+ const { createMultiXBom } = await esmock("./index.js", {
467
+ "../helpers/chromextutils.js": {
468
+ CHROME_EXTENSION_PURL_TYPE: "chrome-extension",
469
+ collectChromeExtensionsFromPath: sinon
470
+ .stub()
471
+ .returns({ components: [], extensionDirs: [] }),
472
+ collectInstalledChromeExtensions,
473
+ discoverChromiumExtensionDirs: sinon.stub().returns([
474
+ {
475
+ browser: "Google Chrome",
476
+ channel: "stable",
477
+ dir: join(tempRoot, "fake-browser-dir"),
478
+ },
479
+ ]),
480
+ },
481
+ });
482
+ await createMultiXBom([pathA, pathB], {
483
+ projectType: ["chrome-extension"],
484
+ multiProject: true,
485
+ });
486
+ sinon.assert.calledOnce(collectInstalledChromeExtensions);
487
+ } finally {
488
+ rmSync(tempRoot, { recursive: true, force: true });
489
+ }
490
+ });
123
491
  });
124
492
  });