@cyclonedx/cdxgen 12.4.1 → 12.4.3

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 (34) hide show
  1. package/bin/evinse.js +15 -0
  2. package/lib/cli/index.js +60 -9
  3. package/lib/cli/index.poku.js +161 -0
  4. package/lib/evinser/evinser.js +118 -3
  5. package/lib/helpers/cbomutils.js +162 -2
  6. package/lib/helpers/cbomutils.poku.js +100 -0
  7. package/lib/helpers/ciParsers/githubActions.js +15 -3
  8. package/lib/helpers/ciParsers/githubActions.poku.js +52 -0
  9. package/lib/helpers/display.js +12 -6
  10. package/lib/helpers/display.poku.js +38 -0
  11. package/lib/helpers/dosai.js +433 -0
  12. package/lib/helpers/dosai.poku.js +302 -0
  13. package/lib/helpers/dosaiParsers.js +103 -0
  14. package/lib/helpers/utils.js +198 -1
  15. package/lib/helpers/utils.poku.js +352 -0
  16. package/lib/stages/postgen/annotator.js +2 -1
  17. package/lib/stages/postgen/annotator.poku.js +28 -0
  18. package/package.json +12 -12
  19. package/types/lib/cli/index.d.ts.map +1 -1
  20. package/types/lib/evinser/evinser.d.ts +15 -0
  21. package/types/lib/evinser/evinser.d.ts.map +1 -1
  22. package/types/lib/helpers/bomUtils.d.ts +1 -3
  23. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  24. package/types/lib/helpers/cbomutils.d.ts +1 -0
  25. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  26. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  27. package/types/lib/helpers/display.d.ts.map +1 -1
  28. package/types/lib/helpers/dosai.d.ts +24 -0
  29. package/types/lib/helpers/dosai.d.ts.map +1 -0
  30. package/types/lib/helpers/dosaiParsers.d.ts +8 -0
  31. package/types/lib/helpers/dosaiParsers.d.ts.map +1 -0
  32. package/types/lib/helpers/utils.d.ts.map +1 -1
  33. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  34. package/types/lib/validator/bomValidator.d.ts.map +1 -1
package/bin/evinse.js CHANGED
@@ -51,15 +51,30 @@ const args = yargs(hideBin(process.argv))
51
51
  "py",
52
52
  "python",
53
53
  "android",
54
+ "csharp",
55
+ "cs",
54
56
  "c",
55
57
  "cpp",
58
+ "dotnet",
56
59
  "php",
57
60
  "swift",
58
61
  "ios",
59
62
  "ruby",
60
63
  "scala",
64
+ "vb",
65
+ "vbnet",
66
+ "visualbasic",
67
+ "f#",
68
+ "fs",
69
+ "fsharp",
61
70
  ],
62
71
  })
72
+ .option("profile", {
73
+ description:
74
+ "Evidence profile. The research profile enables dosai data-flow and crypto analysis for .NET projects.",
75
+ default: "generic",
76
+ choices: ["generic", "research"],
77
+ })
63
78
  .option("db-path", {
64
79
  description: "Atom slices DB path. Unused",
65
80
  default: undefined,
package/lib/cli/index.js CHANGED
@@ -46,7 +46,10 @@ import {
46
46
  toCycloneDxSpecVersionString,
47
47
  } from "../helpers/bomUtils.js";
48
48
  import { parseCaxaMetadata } from "../helpers/caxa.js";
49
- import { collectSourceCryptoComponents } from "../helpers/cbomutils.js";
49
+ import {
50
+ collectDosaiCryptoComponents,
51
+ collectSourceCryptoComponents,
52
+ } from "../helpers/cbomutils.js";
50
53
  import {
51
54
  CHROME_EXTENSION_PURL_TYPE,
52
55
  collectChromeExtensionsFromPath,
@@ -58,6 +61,13 @@ import {
58
61
  mergeServices,
59
62
  trimComponents,
60
63
  } from "../helpers/depsUtils.js";
64
+ import {
65
+ collectDosaiServicesFromMethods,
66
+ createDosaiMethodsSlice,
67
+ isDosaiDotnetLanguage,
68
+ normalizeDosaiServiceMap,
69
+ readDosaiJsonFile,
70
+ } from "../helpers/dosai.js";
61
71
  import { GIT_COMMAND } from "../helpers/envcontext.js";
62
72
  import {
63
73
  createHbomDocument,
@@ -246,7 +256,6 @@ import {
246
256
  enrichOSComponentsWithTrustData,
247
257
  executeOsQuery,
248
258
  getBinaryBom,
249
- getDotnetSlices,
250
259
  getOSPackages,
251
260
  getPluginToolComponents,
252
261
  } from "../managers/binary.js";
@@ -400,6 +409,27 @@ const hasExplicitProjectTypeSelection = (options, baseProjectType) => {
400
409
  );
401
410
  };
402
411
 
412
+ const hasDotnetProjectIndicators = (src, options = {}) => {
413
+ return Boolean(
414
+ getAllFiles(src, "**/*.{csproj,fsproj,vbproj,sln}", options)?.length,
415
+ );
416
+ };
417
+
418
+ const shouldCollectDosaiCrypto = (src, options = {}) => {
419
+ const projectTypes = Array.isArray(options.projectType)
420
+ ? options.projectType
421
+ : options.projectType
422
+ ? [options.projectType]
423
+ : [];
424
+ if (projectTypes.some((projectType) => isDosaiDotnetLanguage(projectType))) {
425
+ return true;
426
+ }
427
+ if (!projectTypes.length || projectTypes.includes("universal")) {
428
+ return hasDotnetProjectIndicators(src, options);
429
+ }
430
+ return false;
431
+ };
432
+
403
433
  const determineParentComponent = (options) => {
404
434
  let parentComponent;
405
435
  if (options.parentComponent && Object.keys(options.parentComponent).length) {
@@ -1749,7 +1779,7 @@ export async function createJavaBom(path, options) {
1749
1779
  console.log(`Executing '${mavenCmd}' in`, basePath);
1750
1780
  result = safeSpawnSync(mavenCmd, mvnArgs, {
1751
1781
  cwd: basePath,
1752
- shell: true,
1782
+ shell: isWin,
1753
1783
  });
1754
1784
  // Check if the cyclonedx plugin created the required bom.json file
1755
1785
  // Sometimes the plugin fails silently for complex maven projects
@@ -1807,7 +1837,7 @@ export async function createJavaBom(path, options) {
1807
1837
  }
1808
1838
  result = safeSpawnSync("mvn", findParentComponentArgs, {
1809
1839
  cwd: basePath,
1810
- shell: true,
1840
+ shell: isWin,
1811
1841
  });
1812
1842
  if (result.status === 0) {
1813
1843
  if (safeExistsSync(tempMvnParentTree)) {
@@ -1840,7 +1870,7 @@ export async function createJavaBom(path, options) {
1840
1870
  mvnTreeArgs,
1841
1871
  {
1842
1872
  cwd: basePath,
1843
- shell: true,
1873
+ shell: isWin,
1844
1874
  },
1845
1875
  );
1846
1876
  if (result.status !== 0 || result.error) {
@@ -2385,7 +2415,7 @@ export async function createJavaBom(path, options) {
2385
2415
  console.log("Executing", BAZEL_CMD, "in", basePath);
2386
2416
  let result = safeSpawnSync(BAZEL_CMD, bArgs, {
2387
2417
  cwd: basePath,
2388
- shell: true,
2418
+ shell: isWin,
2389
2419
  });
2390
2420
  if (result.status !== 0 || result.error) {
2391
2421
  if (result.stderr) {
@@ -7742,6 +7772,7 @@ export async function createCsharpBom(path, options) {
7742
7772
  }
7743
7773
  }
7744
7774
  const pkgNameVersions = {};
7775
+ let services = [];
7745
7776
  if (csProjFiles.length) {
7746
7777
  manifestFiles = manifestFiles.concat(csProjFiles);
7747
7778
  // Parsing csproj is quite error-prone. Some project files may not have versions specified
@@ -7803,21 +7834,30 @@ export async function createCsharpBom(path, options) {
7803
7834
  // Perform deep analysis using dosai
7804
7835
  if (options.deep) {
7805
7836
  const slicesFile = resolve(
7806
- join(path, options.depsSlicesFile) || join(getTmpDir(), "dosai.json"),
7837
+ options.depsSlicesFile
7838
+ ? join(path, options.depsSlicesFile)
7839
+ : join(getTmpDir(), "dosai.json"),
7807
7840
  );
7808
7841
  // Create the slices file if it doesn't exist
7809
7842
  if (!safeExistsSync(slicesFile)) {
7810
7843
  thoughtLog(
7811
7844
  "Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
7812
7845
  );
7813
- const sliceResult = getDotnetSlices(resolve(path), resolve(slicesFile));
7846
+ const sliceResult = createDosaiMethodsSlice(
7847
+ resolve(path),
7848
+ resolve(slicesFile),
7849
+ options,
7850
+ );
7814
7851
  if (!sliceResult && DEBUG_MODE) {
7815
7852
  console.log(
7816
7853
  "Slicing with dosai was unsuccessful. Check the errors reported in the logs above.",
7817
7854
  );
7818
7855
  }
7819
7856
  }
7820
- pkgList = addEvidenceForDotnet(pkgList, slicesFile, options);
7857
+ pkgList = addEvidenceForDotnet(pkgList, slicesFile);
7858
+ const methodsSlice = readDosaiJsonFile(slicesFile);
7859
+ const servicesMap = collectDosaiServicesFromMethods(methodsSlice, {});
7860
+ services = normalizeDosaiServiceMap(servicesMap);
7821
7861
  }
7822
7862
  }
7823
7863
  // Parent dependency tree
@@ -7843,6 +7883,8 @@ export async function createCsharpBom(path, options) {
7843
7883
  filename: manifestFiles.join(", "),
7844
7884
  dependencies,
7845
7885
  parentComponent,
7886
+ services,
7887
+ tools: options.deep ? getPluginToolComponents(["dosai"]) : [],
7846
7888
  });
7847
7889
  }
7848
7890
 
@@ -8354,6 +8396,15 @@ export async function createCryptoCertsBom(path, options) {
8354
8396
  if (sourceCryptoComponents.length) {
8355
8397
  pkgList.push(...sourceCryptoComponents);
8356
8398
  }
8399
+ if (shouldCollectDosaiCrypto(path, options)) {
8400
+ const dosaiCryptoComponents = await collectDosaiCryptoComponents(
8401
+ path,
8402
+ options,
8403
+ );
8404
+ if (dosaiCryptoComponents.length) {
8405
+ pkgList.push(...dosaiCryptoComponents);
8406
+ }
8407
+ }
8357
8408
  return {
8358
8409
  bomJson: {
8359
8410
  components: pkgList,
@@ -1,5 +1,6 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
2
  import {
3
+ chmodSync,
3
4
  copyFileSync,
4
5
  existsSync,
5
6
  mkdirSync,
@@ -29,6 +30,7 @@ import { postProcess } from "../stages/postgen/postgen.js";
29
30
  import {
30
31
  createBom,
31
32
  createChromeExtensionBom,
33
+ createJavaBom,
32
34
  createNodejsBom,
33
35
  createPHPBom,
34
36
  createPythonBom,
@@ -286,6 +288,165 @@ describe("CLI tests", () => {
286
288
  );
287
289
  assert.strictEqual(components[0].type, "data");
288
290
  });
291
+
292
+ it("does not invoke dosai crypto analysis for non-.NET CBOM scans", async () => {
293
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-cbom-non-dotnet-"));
294
+ const collectDosaiCryptoComponents = sinon.stub().resolves([]);
295
+ try {
296
+ const { createCryptoCertsBom } = await esmock("./index.js", {
297
+ "../helpers/cbomutils.js": {
298
+ collectDosaiCryptoComponents,
299
+ collectSourceCryptoComponents: sinon.stub().resolves([]),
300
+ },
301
+ });
302
+
303
+ await createCryptoCertsBom(tempDir, {
304
+ projectType: ["js"],
305
+ specVersion: 1.7,
306
+ });
307
+
308
+ sinon.assert.notCalled(collectDosaiCryptoComponents);
309
+ } finally {
310
+ rmSync(tempDir, { force: true, recursive: true });
311
+ }
312
+ });
313
+
314
+ it("invokes dosai crypto analysis for explicit .NET CBOM scans", async () => {
315
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-cbom-dotnet-"));
316
+ const collectDosaiCryptoComponents = sinon.stub().resolves([
317
+ {
318
+ name: "sha-256",
319
+ type: "cryptographic-asset",
320
+ "bom-ref": "crypto/algorithm/sha-256@2.16.840.1.101.3.4.2.1",
321
+ cryptoProperties: {
322
+ assetType: "algorithm",
323
+ oid: "2.16.840.1.101.3.4.2.1",
324
+ },
325
+ },
326
+ ]);
327
+ try {
328
+ const { createCryptoCertsBom } = await esmock("./index.js", {
329
+ "../helpers/cbomutils.js": {
330
+ collectDosaiCryptoComponents,
331
+ collectSourceCryptoComponents: sinon.stub().resolves([]),
332
+ },
333
+ });
334
+
335
+ const bomData = await createCryptoCertsBom(tempDir, {
336
+ projectType: ["dotnet"],
337
+ specVersion: 1.7,
338
+ });
339
+
340
+ sinon.assert.calledOnce(collectDosaiCryptoComponents);
341
+ assert.strictEqual(bomData.bomJson.components[0].name, "sha-256");
342
+ } finally {
343
+ rmSync(tempDir, { force: true, recursive: true });
344
+ }
345
+ });
346
+
347
+ it("invokes dosai crypto analysis when universal scans contain .NET project files", async () => {
348
+ const tempDir = mkdtempSync(
349
+ join(tmpdir(), "cdxgen-cbom-dotnet-indicator-"),
350
+ );
351
+ const collectDosaiCryptoComponents = sinon.stub().resolves([]);
352
+ try {
353
+ writeFileSync(join(tempDir, "app.csproj"), "<Project />");
354
+ const { createCryptoCertsBom } = await esmock("./index.js", {
355
+ "../helpers/cbomutils.js": {
356
+ collectDosaiCryptoComponents,
357
+ collectSourceCryptoComponents: sinon.stub().resolves([]),
358
+ },
359
+ });
360
+
361
+ await createCryptoCertsBom(tempDir, {
362
+ projectType: ["universal"],
363
+ specVersion: 1.7,
364
+ });
365
+
366
+ sinon.assert.calledOnce(collectDosaiCryptoComponents);
367
+ } finally {
368
+ rmSync(tempDir, { force: true, recursive: true });
369
+ }
370
+ });
371
+
372
+ it("does not interpret shell metacharacters in Maven module paths", async () => {
373
+ if (process.platform === "win32") {
374
+ return;
375
+ }
376
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-maven-shell-"));
377
+ const fakeBinDir = join(tempDir, "bin");
378
+ const repoDir = join(tempDir, "repo");
379
+ const markerFile = join(tmpdir(), "CDXGEN_GITURL_E2E_MARKER_TEST");
380
+ const shellIfs = "$" + "{IFS}";
381
+ const maliciousDirName = `evil;cd${shellIfs}..;cd${shellIfs}..;printf${shellIfs}CDXGEN_MAVEN_GIT_URL_E2E_SHELL_INJECTION>CDXGEN_GITURL_E2E_MARKER_TEST;#`;
382
+ const maliciousModuleDir = join(repoDir, maliciousDirName);
383
+ const originalPath = process.env.PATH;
384
+ const originalMvnCmd = process.env.MVN_CMD;
385
+ const originalMavenCmd = process.env.MAVEN_CMD;
386
+ const originalMvnArgs = process.env.MVN_ARGS;
387
+
388
+ try {
389
+ rmSync(markerFile, { force: true });
390
+ mkdirSync(fakeBinDir, { recursive: true });
391
+ mkdirSync(maliciousModuleDir, { recursive: true });
392
+ writeFileSync(
393
+ join(maliciousModuleDir, "pom.xml"),
394
+ "<project><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>evil</artifactId><version>1.0.0</version></project>",
395
+ );
396
+ writeFileSync(join(maliciousModuleDir, "settings.xml"), "<settings />");
397
+ const fakeMvn = join(fakeBinDir, "mvn");
398
+ writeFileSync(
399
+ fakeMvn,
400
+ `#!/bin/sh
401
+ for arg do
402
+ case "$arg" in
403
+ -DoutputFile=*)
404
+ output="\${arg#-DoutputFile=}"
405
+ mkdir -p "$(dirname "$output")"
406
+ printf 'org.example:evil:jar:1.0.0:compile\\n' > "$output"
407
+ ;;
408
+ esac
409
+ done
410
+ `,
411
+ );
412
+ chmodSync(fakeMvn, 0o755);
413
+ process.env.PATH = `${fakeBinDir}${process.env.PATH ? `:${process.env.PATH}` : ""}`;
414
+ delete process.env.MVN_CMD;
415
+ delete process.env.MAVEN_CMD;
416
+ delete process.env.MVN_ARGS;
417
+
418
+ await createJavaBom(repoDir, {
419
+ multiProject: true,
420
+ projectType: ["java"],
421
+ specVersion: 1.6,
422
+ });
423
+
424
+ assert.strictEqual(existsSync(markerFile), false);
425
+ } finally {
426
+ if (originalPath === undefined) {
427
+ delete process.env.PATH;
428
+ } else {
429
+ process.env.PATH = originalPath;
430
+ }
431
+ if (originalMvnCmd === undefined) {
432
+ delete process.env.MVN_CMD;
433
+ } else {
434
+ process.env.MVN_CMD = originalMvnCmd;
435
+ }
436
+ if (originalMavenCmd === undefined) {
437
+ delete process.env.MAVEN_CMD;
438
+ } else {
439
+ process.env.MAVEN_CMD = originalMavenCmd;
440
+ }
441
+ if (originalMvnArgs === undefined) {
442
+ delete process.env.MVN_ARGS;
443
+ } else {
444
+ process.env.MVN_ARGS = originalMvnArgs;
445
+ }
446
+ rmSync(markerFile, { force: true });
447
+ rmSync(tempDir, { force: true, recursive: true });
448
+ }
449
+ });
289
450
  });
290
451
 
291
452
  describe("distribution filters", () => {
@@ -4,7 +4,19 @@ import process from "node:process";
4
4
 
5
5
  import { PackageURL } from "packageurl-js";
6
6
 
7
- import { findCryptoAlgos } from "../helpers/cbomutils.js";
7
+ import {
8
+ collectDosaiCryptoComponents,
9
+ findCryptoAlgos,
10
+ } from "../helpers/cbomutils.js";
11
+ import { mergeServices } from "../helpers/depsUtils.js";
12
+ import {
13
+ collectDosaiDataFlowFrames,
14
+ collectDosaiPurlEvidence,
15
+ collectDosaiServicesFromMethods,
16
+ createDosaiDataFlowSlice,
17
+ createDosaiMethodsSlice,
18
+ isDosaiDotnetLanguage,
19
+ } from "../helpers/dosai.js";
8
20
  import { parseOccurrenceEvidenceLocation } from "../helpers/evidenceUtils.js";
9
21
  import {
10
22
  collectGradleDependencies,
@@ -230,6 +242,8 @@ export async function createSlice(
230
242
  language = "python";
231
243
  } else if (PROJECT_TYPE_ALIASES.scala.includes(language)) {
232
244
  language = "scala";
245
+ } else if (isDosaiDotnetLanguage(language)) {
246
+ language = "csharp";
233
247
  }
234
248
  if (
235
249
  PROJECT_TYPE_ALIASES.swift.includes(language) &&
@@ -270,6 +284,33 @@ export async function createSlice(
270
284
  }
271
285
  return { tempDir: sliceOutputDir, tempDirOwned, slicesFile };
272
286
  }
287
+ if (isDosaiDotnetLanguage(language)) {
288
+ console.log(
289
+ `Creating ${sliceType} slice for ${resolve(filePath)} using dosai. Please wait ...`,
290
+ );
291
+ const sliceResult =
292
+ sliceType === "data-flow"
293
+ ? createDosaiDataFlowSlice(
294
+ resolve(filePath),
295
+ resolve(slicesFile),
296
+ options,
297
+ )
298
+ : createDosaiMethodsSlice(
299
+ resolve(filePath),
300
+ resolve(slicesFile),
301
+ options,
302
+ );
303
+ if (!sliceResult) {
304
+ console.warn(
305
+ `Unable to generate ${sliceType} slice using dosai. Check if this is a supported .NET project.`,
306
+ );
307
+ }
308
+ return {
309
+ tempDir: sliceOutputDir,
310
+ tempDirOwned,
311
+ slicesFile,
312
+ };
313
+ }
273
314
  console.log(
274
315
  `Creating ${sliceType} slice for ${resolve(filePath)}. Please wait ...`,
275
316
  );
@@ -393,6 +434,9 @@ export function purlToLanguage(purl, filePath) {
393
434
  case "gem":
394
435
  language = "ruby";
395
436
  break;
437
+ case "nuget":
438
+ language = "csharp";
439
+ break;
396
440
  case "generic":
397
441
  language = "c";
398
442
  }
@@ -474,6 +518,77 @@ export async function analyzeProject(dbObjMap, options) {
474
518
  // Load any existing purl-location information from the sbom.
475
519
  // For eg: cdxgen populates this information for javascript projects
476
520
  let { purlLocationMap, purlImportsMap } = initFromSbom(components, language);
521
+ if (isDosaiDotnetLanguage(language)) {
522
+ if (options.profile === "research") {
523
+ options.withDataFlow = true;
524
+ options.includeCrypto = true;
525
+ }
526
+ if (
527
+ options.usagesSlicesFile &&
528
+ usableSlicesFile(options.usagesSlicesFile)
529
+ ) {
530
+ usageSlice = JSON.parse(
531
+ fs.readFileSync(options.usagesSlicesFile, "utf-8"),
532
+ );
533
+ usagesSlicesFile = options.usagesSlicesFile;
534
+ } else {
535
+ retMap = await createSlice(language, dirPath, "usages", options);
536
+ if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
537
+ usageSlice = JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8"));
538
+ usagesSlicesFile = retMap.slicesFile;
539
+ }
540
+ }
541
+ if (usageSlice && Object.keys(usageSlice).length) {
542
+ const dosaiEvidence = collectDosaiPurlEvidence(usageSlice, components);
543
+ for (const [purl, locations] of Object.entries(
544
+ dosaiEvidence.purlLocationMap,
545
+ )) {
546
+ purlLocationMap[purl] ??= new Set();
547
+ for (const location of locations) {
548
+ purlLocationMap[purl].add(location);
549
+ }
550
+ }
551
+ servicesMap = collectDosaiServicesFromMethods(usageSlice, servicesMap);
552
+ userDefinedTypesMap = {};
553
+ }
554
+ if (options.withDataFlow) {
555
+ if (
556
+ options.dataFlowSlicesFile &&
557
+ safeExistsSync(options.dataFlowSlicesFile)
558
+ ) {
559
+ dataFlowSlicesFile = options.dataFlowSlicesFile;
560
+ dataFlowSlice = JSON.parse(
561
+ fs.readFileSync(options.dataFlowSlicesFile, "utf-8"),
562
+ );
563
+ } else {
564
+ retMap = await createSlice(language, dirPath, "data-flow", options);
565
+ if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
566
+ dataFlowSlicesFile = retMap.slicesFile;
567
+ dataFlowSlice = JSON.parse(
568
+ fs.readFileSync(retMap.slicesFile, "utf-8"),
569
+ );
570
+ }
571
+ }
572
+ if (dataFlowSlice && Object.keys(dataFlowSlice).length) {
573
+ dataFlowFrames = collectDosaiDataFlowFrames(dataFlowSlice, components);
574
+ }
575
+ }
576
+ if (options.includeCrypto) {
577
+ cryptoComponents = await collectDosaiCryptoComponents(dirPath, options);
578
+ }
579
+ return {
580
+ usagesSlicesFile,
581
+ dataFlowSlicesFile,
582
+ purlLocationMap,
583
+ servicesMap,
584
+ dataFlowFrames,
585
+ tempDir: retMap?.tempDir,
586
+ tempDirOwned: retMap?.tempDirOwned,
587
+ userDefinedTypesMap,
588
+ cryptoComponents,
589
+ cryptoGeneratePurls,
590
+ };
591
+ }
477
592
  // Do reachables first so that usages slicing can reuse the atom file
478
593
  // We need reachables slicing even when trying to infer crypto packages
479
594
  if (options.withReachables || options.includeCrypto) {
@@ -1472,8 +1587,8 @@ export function createEvinseFile(sliceArtefacts, options) {
1472
1587
  properties: servicesMap[serviceName].properties,
1473
1588
  });
1474
1589
  }
1475
- // Add to existing services
1476
- bomJson.services = (bomJson.services || []).concat(services);
1590
+ // Add to existing services while preserving CycloneDX uniqueItems validity
1591
+ bomJson.services = mergeServices(bomJson.services || [], services);
1477
1592
  servicesPresent = true;
1478
1593
  }
1479
1594
  // Add the crypto components to the components list