@cyclonedx/cdxgen 12.4.1 → 12.4.2

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.
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) {
@@ -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,
@@ -286,6 +286,86 @@ describe("CLI tests", () => {
286
286
  );
287
287
  assert.strictEqual(components[0].type, "data");
288
288
  });
289
+
290
+ it("does not invoke dosai crypto analysis for non-.NET CBOM scans", async () => {
291
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-cbom-non-dotnet-"));
292
+ const collectDosaiCryptoComponents = sinon.stub().resolves([]);
293
+ try {
294
+ const { createCryptoCertsBom } = await esmock("./index.js", {
295
+ "../helpers/cbomutils.js": {
296
+ collectDosaiCryptoComponents,
297
+ collectSourceCryptoComponents: sinon.stub().resolves([]),
298
+ },
299
+ });
300
+
301
+ await createCryptoCertsBom(tempDir, {
302
+ projectType: ["js"],
303
+ specVersion: 1.7,
304
+ });
305
+
306
+ sinon.assert.notCalled(collectDosaiCryptoComponents);
307
+ } finally {
308
+ rmSync(tempDir, { force: true, recursive: true });
309
+ }
310
+ });
311
+
312
+ it("invokes dosai crypto analysis for explicit .NET CBOM scans", async () => {
313
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-cbom-dotnet-"));
314
+ const collectDosaiCryptoComponents = sinon.stub().resolves([
315
+ {
316
+ name: "sha-256",
317
+ type: "cryptographic-asset",
318
+ "bom-ref": "crypto/algorithm/sha-256@2.16.840.1.101.3.4.2.1",
319
+ cryptoProperties: {
320
+ assetType: "algorithm",
321
+ oid: "2.16.840.1.101.3.4.2.1",
322
+ },
323
+ },
324
+ ]);
325
+ try {
326
+ const { createCryptoCertsBom } = await esmock("./index.js", {
327
+ "../helpers/cbomutils.js": {
328
+ collectDosaiCryptoComponents,
329
+ collectSourceCryptoComponents: sinon.stub().resolves([]),
330
+ },
331
+ });
332
+
333
+ const bomData = await createCryptoCertsBom(tempDir, {
334
+ projectType: ["dotnet"],
335
+ specVersion: 1.7,
336
+ });
337
+
338
+ sinon.assert.calledOnce(collectDosaiCryptoComponents);
339
+ assert.strictEqual(bomData.bomJson.components[0].name, "sha-256");
340
+ } finally {
341
+ rmSync(tempDir, { force: true, recursive: true });
342
+ }
343
+ });
344
+
345
+ it("invokes dosai crypto analysis when universal scans contain .NET project files", async () => {
346
+ const tempDir = mkdtempSync(
347
+ join(tmpdir(), "cdxgen-cbom-dotnet-indicator-"),
348
+ );
349
+ const collectDosaiCryptoComponents = sinon.stub().resolves([]);
350
+ try {
351
+ writeFileSync(join(tempDir, "app.csproj"), "<Project />");
352
+ const { createCryptoCertsBom } = await esmock("./index.js", {
353
+ "../helpers/cbomutils.js": {
354
+ collectDosaiCryptoComponents,
355
+ collectSourceCryptoComponents: sinon.stub().resolves([]),
356
+ },
357
+ });
358
+
359
+ await createCryptoCertsBom(tempDir, {
360
+ projectType: ["universal"],
361
+ specVersion: 1.7,
362
+ });
363
+
364
+ sinon.assert.calledOnce(collectDosaiCryptoComponents);
365
+ } finally {
366
+ rmSync(tempDir, { force: true, recursive: true });
367
+ }
368
+ });
289
369
  });
290
370
 
291
371
  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
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
 
4
4
  import { executeOsQuery } from "../managers/binary.js";
5
5
  import { detectJsCryptoInventory } from "./analyzer.js";
6
+ import { analyzeDosaiCrypto } from "./dosai.js";
6
7
  import {
7
8
  createOccurrenceEvidence,
8
9
  formatOccurrenceEvidence,
@@ -239,16 +240,19 @@ function mergeAlgorithmComponentUsage(component, usage, src, options) {
239
240
  }
240
241
  }
241
242
  if (usage.source) {
243
+ const sourceType =
244
+ usage.source === "dosai" ? undefined : `js-ast:${usage.source}`;
242
245
  if (
246
+ sourceType &&
243
247
  !properties.some(
244
248
  (property) =>
245
249
  property.name === "cdx:crypto:sourceType" &&
246
- property.value === `js-ast:${usage.source}`,
250
+ property.value === sourceType,
247
251
  )
248
252
  ) {
249
253
  properties.push({
250
254
  name: "cdx:crypto:sourceType",
251
- value: `js-ast:${usage.source}`,
255
+ value: sourceType,
252
256
  });
253
257
  }
254
258
  }
@@ -271,6 +275,93 @@ function mergeAlgorithmComponentUsage(component, usage, src, options) {
271
275
  return component;
272
276
  }
273
277
 
278
+ function normalizeDosaiCryptoNames(cryptoObject) {
279
+ const rawName = cryptoObject?.Name || cryptoObject;
280
+ const names = new Set([rawName]);
281
+ const cleanName = String(rawName || "").trim();
282
+ if (!cleanName) {
283
+ return [];
284
+ }
285
+ if (cleanName.includes("/")) {
286
+ for (const part of cleanName
287
+ .split("/")
288
+ .map((candidate) => candidate.trim())
289
+ .filter(Boolean)) {
290
+ names.add(part);
291
+ }
292
+ }
293
+ if (/^SHA-?256$/i.test(cleanName)) {
294
+ names.add("sha-256");
295
+ } else if (/^SHA-?384$/i.test(cleanName)) {
296
+ names.add("sha-384");
297
+ } else if (/^SHA-?512$/i.test(cleanName)) {
298
+ names.add("sha-512");
299
+ } else if (/^SHA-?1$/i.test(cleanName)) {
300
+ names.add("sha-1");
301
+ }
302
+ const context = [
303
+ cryptoObject?.Symbol,
304
+ cryptoObject?.Code,
305
+ cryptoObject?.Algorithm,
306
+ ]
307
+ .filter(Boolean)
308
+ .join(" ");
309
+ if (/^SHA-?2$/i.test(cleanName)) {
310
+ if (/SHA-?256/i.test(context)) {
311
+ names.add("sha-256");
312
+ }
313
+ if (/SHA-?384/i.test(context)) {
314
+ names.add("sha-384");
315
+ }
316
+ if (/SHA-?512/i.test(context)) {
317
+ names.add("sha-512");
318
+ }
319
+ }
320
+ return Array.from(names);
321
+ }
322
+
323
+ function dosaiCryptoUsage(assetOrOperation) {
324
+ const location = assetOrOperation.Location || {};
325
+ return {
326
+ fileName: location.Path || location.FileName,
327
+ lineNumber: location.LineNumber || undefined,
328
+ columnNumber: location.ColumnNumber || undefined,
329
+ primitive: assetOrOperation.Family || assetOrOperation.OperationType,
330
+ source: "dosai",
331
+ };
332
+ }
333
+
334
+ function addDosaiProperties(component, dosaiObject, evidenceType) {
335
+ const properties = component.properties || [];
336
+ const addProperty = (name, value) => {
337
+ if (value === undefined || value === null || value === "") {
338
+ return;
339
+ }
340
+ if (
341
+ !properties.some(
342
+ (property) =>
343
+ property.name === name && property.value === String(value),
344
+ )
345
+ ) {
346
+ properties.push({ name, value: String(value) });
347
+ }
348
+ };
349
+ addProperty("cdx:crypto:sourceType", `dosai:${evidenceType}`);
350
+ addProperty("cdx:dosai:crypto:id", dosaiObject.Id);
351
+ addProperty("cdx:dosai:crypto:strength", dosaiObject.Strength);
352
+ addProperty(
353
+ "cdx:dosai:crypto:reachableFromEntryPoint",
354
+ dosaiObject.ReachableFromEntryPoint,
355
+ );
356
+ if (dosaiObject.EntryPointIds?.length) {
357
+ addProperty(
358
+ "cdx:dosai:crypto:entryPointCount",
359
+ dosaiObject.EntryPointIds.length,
360
+ );
361
+ }
362
+ component.properties = properties;
363
+ }
364
+
274
365
  export async function collectSourceCryptoComponents(src, options = {}) {
275
366
  const inventory = await detectJsCryptoInventory(src, Boolean(options.deep));
276
367
  const componentsByRef = new Map();
@@ -317,6 +408,75 @@ export async function collectSourceCryptoComponents(src, options = {}) {
317
408
  );
318
409
  }
319
410
 
411
+ export async function collectDosaiCryptoComponents(src, options = {}) {
412
+ const dosaiCrypto = analyzeDosaiCrypto(src, options);
413
+ if (!dosaiCrypto) {
414
+ return [];
415
+ }
416
+ const componentsByRef = new Map();
417
+ const cryptoObjects = [
418
+ ...(dosaiCrypto.Assets || []).filter(
419
+ (asset) => asset.AssetType === "algorithm",
420
+ ),
421
+ ...(dosaiCrypto.Operations || []).map((operation) => ({
422
+ ...operation,
423
+ Name: operation.Algorithm,
424
+ Family: operation.OperationType,
425
+ })),
426
+ ];
427
+ for (const cryptoObject of cryptoObjects) {
428
+ for (const candidateName of normalizeDosaiCryptoNames(cryptoObject)) {
429
+ const normalizedName = normalizeDetectedCryptoAlgorithmName(
430
+ candidateName,
431
+ "algorithm",
432
+ );
433
+ const algorithmMetadata =
434
+ cbomCryptoOids[normalizedName] || cbomCryptoOids[candidateName];
435
+ if (!algorithmMetadata?.oid) {
436
+ continue;
437
+ }
438
+ const bomRef = cryptoAlgorithmBomRef(
439
+ normalizedName,
440
+ algorithmMetadata.oid,
441
+ );
442
+ const component = componentsByRef.get(bomRef) || {
443
+ type: "cryptographic-asset",
444
+ name: normalizedName,
445
+ "bom-ref": bomRef,
446
+ description:
447
+ algorithmMetadata.description ||
448
+ "Cryptographic algorithm detected by dosai source analysis",
449
+ cryptoProperties: {
450
+ assetType: "algorithm",
451
+ oid: algorithmMetadata.oid,
452
+ },
453
+ properties: [],
454
+ };
455
+ mergeAlgorithmComponentUsage(
456
+ component,
457
+ dosaiCryptoUsage(cryptoObject),
458
+ src,
459
+ options,
460
+ );
461
+ addDosaiProperties(
462
+ component,
463
+ cryptoObject,
464
+ cryptoObject.OperationType ? "operation" : "asset",
465
+ );
466
+ componentsByRef.set(bomRef, component);
467
+ }
468
+ }
469
+ const components = Array.from(componentsByRef.values());
470
+ components.forEach((component) => {
471
+ normalizeCryptoComponentEvidence(component, options);
472
+ });
473
+ return components.sort((left, right) =>
474
+ `${left.name}:${left["bom-ref"]}`.localeCompare(
475
+ `${right.name}:${right["bom-ref"]}`,
476
+ ),
477
+ );
478
+ }
479
+
320
480
  /**
321
481
  * Find crypto algorithm in the given code snippet
322
482
  *