@cyclonedx/cdxgen 11.4.4 → 11.5.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.
package/lib/cli/index.js CHANGED
@@ -111,6 +111,8 @@ import {
111
111
  parseCsProjAssetsData,
112
112
  parseCsProjData,
113
113
  parseEdnData,
114
+ parseFlakeLock,
115
+ parseFlakeNix,
114
116
  parseGemfileLockData,
115
117
  parseGemspecData,
116
118
  parseGitHubWorkflowData,
@@ -524,29 +526,47 @@ const addFormulationSection = (options, context) => {
524
526
  let environmentVars = gitBranch?.length
525
527
  ? [{ name: "GIT_BRANCH", value: gitBranch }]
526
528
  : [];
529
+ const envPrefixes = [
530
+ "GIT",
531
+ "ANDROID",
532
+ "DENO",
533
+ "DOTNET",
534
+ "JAVA_",
535
+ "SDKMAN",
536
+ "CARGO",
537
+ "CONDA",
538
+ "RUST",
539
+ "GEM_",
540
+ "SCALA_",
541
+ "MAVEN_",
542
+ "GRADLE_",
543
+ "NODE_",
544
+ ];
545
+ const envBlocklist = [
546
+ "key",
547
+ "token",
548
+ "pass",
549
+ "secret",
550
+ "user",
551
+ "email",
552
+ "auth",
553
+ "session",
554
+ "proxy",
555
+ "cred",
556
+ ];
557
+
527
558
  for (const aevar of Object.keys(process.env)) {
559
+ const lower = aevar.toLowerCase();
560
+ const value = process.env[aevar] ?? "";
528
561
  if (
529
- (aevar.startsWith("GIT") ||
530
- aevar.startsWith("ANDROID") ||
531
- aevar.startsWith("DENO") ||
532
- aevar.startsWith("DOTNET") ||
533
- aevar.startsWith("JAVA_") ||
534
- aevar.startsWith("SDKMAN") ||
535
- aevar.startsWith("CARGO") ||
536
- aevar.startsWith("CONDA") ||
537
- aevar.startsWith("RUST")) &&
538
- !aevar.toLowerCase().includes("key") &&
539
- !aevar.toLowerCase().includes("token") &&
540
- !aevar.toLowerCase().includes("pass") &&
541
- !aevar.toLowerCase().includes("secret") &&
542
- !aevar.toLowerCase().includes("user") &&
543
- !aevar.toLowerCase().includes("email") &&
544
- process.env[aevar] &&
545
- process.env[aevar].length
562
+ envPrefixes.some((p) => aevar.startsWith(p)) &&
563
+ !envBlocklist.some((b) => lower.includes(b)) &&
564
+ !envBlocklist.some((b) => value.includes(b)) &&
565
+ value.length
546
566
  ) {
547
567
  environmentVars.push({
548
568
  name: aevar,
549
- value: process.env[aevar],
569
+ value,
550
570
  });
551
571
  }
552
572
  }
@@ -1385,6 +1405,10 @@ export async function createJarBom(path, options) {
1385
1405
  if (!options.exclude) {
1386
1406
  options.exclude = [];
1387
1407
  }
1408
+ // We can look for jar files within node_modules directory
1409
+ if (typeof options.includeNodeModulesDir === "undefined" || options.deep) {
1410
+ options.includeNodeModulesDir = true;
1411
+ }
1388
1412
  // Exclude certain directories during oci sbom generation
1389
1413
  if (hasAnyProjectType(["oci"], options, false)) {
1390
1414
  options.exclude.push("**/android-sdk*/**");
@@ -2305,7 +2329,7 @@ export async function createJavaBom(path, options) {
2305
2329
  ) {
2306
2330
  bArgs = ["--bazelrc=.bazelrc", "build", bazelTarget];
2307
2331
  }
2308
- console.log("Executing", BAZEL_CMD, bArgs.join(" "), "in", basePath);
2332
+ console.log("Executing", BAZEL_CMD, "in", basePath);
2309
2333
  let result = safeSpawnSync(BAZEL_CMD, bArgs, {
2310
2334
  cwd: basePath,
2311
2335
  shell: true,
@@ -2639,7 +2663,7 @@ export async function createJavaBom(path, options) {
2639
2663
  }
2640
2664
  const millArgs = [...millCommonArgs, "__.ivyDepsTree"];
2641
2665
  if (DEBUG_MODE) {
2642
- console.log("Executing", millCmd, millArgs.join(" "), "in", millRootPath);
2666
+ console.log("Executing", millCmd, "in", millRootPath);
2643
2667
  }
2644
2668
  let sresult = safeSpawnSync(millCmd, millArgs, {
2645
2669
  cwd: millRootPath,
@@ -3761,7 +3785,7 @@ export async function createPythonBom(path, options) {
3761
3785
  // Retrieve the tree using virtualenv in deep mode and as a fallback
3762
3786
  // This is a slow operation
3763
3787
  if ((options.deep || !dependencies.length) && !f.endsWith("uv.lock")) {
3764
- retMap = getPipFrozenTree(basePath, f, tempDir, parentComponent);
3788
+ retMap = await getPipFrozenTree(basePath, f, tempDir, parentComponent);
3765
3789
  if (retMap.pkgList?.length) {
3766
3790
  pkgList = pkgList.concat(retMap.pkgList);
3767
3791
  }
@@ -3839,9 +3863,49 @@ export async function createPythonBom(path, options) {
3839
3863
  console.error("Pipfile.lock not found at", path);
3840
3864
  options.failOnError && process.exit(1);
3841
3865
  }
3842
- } else if (requirementsMode) {
3843
- metadataFilename = "requirements.txt";
3844
- if (reqFiles?.length) {
3866
+ } else if (reqDirFiles?.length) {
3867
+ for (const j in reqDirFiles) {
3868
+ const f = reqDirFiles[j];
3869
+ const dlist = await parseReqFile(f, false);
3870
+ if (dlist?.length) {
3871
+ pkgList = pkgList.concat(dlist);
3872
+ }
3873
+ }
3874
+ metadataFilename = reqDirFiles.join(", ");
3875
+ } else if (reqFiles?.length) {
3876
+ for (const f of reqFiles) {
3877
+ const dlist = await parseReqFile(f, true);
3878
+ if (dlist?.length) {
3879
+ pkgList = pkgList.concat(dlist);
3880
+ }
3881
+ }
3882
+ }
3883
+ }
3884
+
3885
+ const parentDependsOn = new Set();
3886
+
3887
+ // Use atom in requirements, setup.py and pyproject.toml mode
3888
+ if (requirementsMode || setupPyMode || pyProjectMode || options.deep) {
3889
+ /**
3890
+ * The order of preference is pyproject.toml (newer) and then setup.py
3891
+ */
3892
+ if (options.installDeps) {
3893
+ let pkgMap;
3894
+ if (pyProjectMode && !poetryMode) {
3895
+ pkgMap = await getPipFrozenTree(
3896
+ path,
3897
+ pyProjectFile,
3898
+ tempDir,
3899
+ parentComponent,
3900
+ );
3901
+ } else if (setupPyMode) {
3902
+ pkgMap = await getPipFrozenTree(
3903
+ path,
3904
+ setupPy,
3905
+ tempDir,
3906
+ parentComponent,
3907
+ );
3908
+ } else if (requirementsMode && reqFiles?.length) {
3845
3909
  if (options.installDeps && DEBUG_MODE) {
3846
3910
  console.log(
3847
3911
  "cdxgen will now attempt to generate an SBOM for 'build' lifecycle phase for Python. This would take some time ...\nTo speed up this step, invoke cdxgen from within a virtual environment with all the dependencies installed.\nAlternatively, pass the argument '--lifecycle pre-build' to generate a faster but less precise SBOM.",
@@ -3849,13 +3913,8 @@ export async function createPythonBom(path, options) {
3849
3913
  }
3850
3914
  for (const f of reqFiles) {
3851
3915
  const basePath = dirname(f);
3852
- let reqData;
3853
- let frozen = false;
3854
- // Attempt to pip freeze in a virtualenv to improve precision
3855
3916
  if (options.installDeps) {
3856
- // If there are multiple requirements files then the tree is getting constructed for each one
3857
- // adding to the delay.
3858
- const pkgMap = getPipFrozenTree(
3917
+ const pkgMap = await getPipFrozenTree(
3859
3918
  basePath,
3860
3919
  f,
3861
3920
  tempDir,
@@ -3863,10 +3922,11 @@ export async function createPythonBom(path, options) {
3863
3922
  );
3864
3923
  if (pkgMap.pkgList?.length) {
3865
3924
  pkgList = pkgList.concat(pkgMap.pkgList);
3866
- frozen = pkgMap.frozen;
3925
+ pkgList = trimComponents(pkgList);
3867
3926
  }
3868
3927
  if (pkgMap.formulationList?.length) {
3869
3928
  formulationList = formulationList.concat(pkgMap.formulationList);
3929
+ formulationList = trimComponents(formulationList);
3870
3930
  }
3871
3931
  if (pkgMap.dependenciesList) {
3872
3932
  dependencies = mergeDependencies(
@@ -3875,61 +3935,31 @@ export async function createPythonBom(path, options) {
3875
3935
  parentComponent,
3876
3936
  );
3877
3937
  }
3878
- }
3879
- // Fallback to parsing manually
3880
- if (!pkgList.length || !frozen) {
3881
- thoughtLog(
3882
- `Manually parsing ${f}. The result would include only direct dependencies.`,
3883
- );
3884
- if (DEBUG_MODE) {
3885
- console.log(
3886
- `Manually parsing ${f}. The result would include only direct dependencies.`,
3938
+ // Add the root packages from this file to the parent's dependencies
3939
+ for (const p of pkgMap.rootList) {
3940
+ if (
3941
+ parentComponent &&
3942
+ p.name === parentComponent.name &&
3943
+ (p.version === parentComponent.version ||
3944
+ p.version === "latest")
3945
+ ) {
3946
+ continue;
3947
+ }
3948
+ parentDependsOn.add(
3949
+ `pkg:pypi/${p.name.toLowerCase()}@${p.version}`,
3887
3950
  );
3888
3951
  }
3889
- reqData = readFileSync(f, { encoding: "utf-8" });
3890
- const dlist = await parseReqFile(reqData, true);
3891
- if (dlist?.length) {
3892
- pkgList = pkgList.concat(dlist);
3893
- }
3894
- }
3895
- } // for
3896
- metadataFilename = reqFiles.join(", ");
3897
- } else if (reqDirFiles?.length) {
3898
- for (const j in reqDirFiles) {
3899
- const f = reqDirFiles[j];
3900
- const reqData = readFileSync(f, { encoding: "utf-8" });
3901
- const dlist = await parseReqFile(reqData, false);
3902
- if (dlist?.length) {
3903
- pkgList = pkgList.concat(dlist);
3904
3952
  }
3905
3953
  }
3906
- metadataFilename = reqDirFiles.join(", ");
3907
- }
3908
- }
3909
- }
3910
- // Use atom in requirements, setup.py and pyproject.toml mode
3911
- if (requirementsMode || setupPyMode || pyProjectMode || options.deep) {
3912
- /**
3913
- * The order of preference is pyproject.toml (newer) and then setup.py
3914
- */
3915
- if (options.installDeps) {
3916
- let pkgMap;
3917
- if (pyProjectMode && !poetryMode) {
3918
- pkgMap = getPipFrozenTree(
3954
+ } else if (!poetryMode) {
3955
+ pkgMap = await getPipFrozenTree(
3919
3956
  path,
3920
- pyProjectFile,
3957
+ undefined,
3921
3958
  tempDir,
3922
3959
  parentComponent,
3923
3960
  );
3924
- } else if (setupPyMode) {
3925
- pkgMap = getPipFrozenTree(path, setupPy, tempDir, parentComponent);
3926
- } else if (!poetryMode) {
3927
- pkgMap = getPipFrozenTree(path, undefined, tempDir, parentComponent);
3928
3961
  }
3929
3962
 
3930
- // Get the imported modules and a dedupe list of packages
3931
- const parentDependsOn = new Set();
3932
-
3933
3963
  // ATOM parsedeps block
3934
3964
  // Atom parsedeps slices can be used to identify packages that are not declared in manifests
3935
3965
  // Since it is a slow operation, we only use it as a fallback or in deep mode
@@ -4253,11 +4283,21 @@ export async function createGoBom(path, options) {
4253
4283
  }
4254
4284
  if (gomodFiles.length) {
4255
4285
  let shouldManuallyParse = false;
4286
+ // Sort go.mod files by depth (shallowest first) to prioritize root modules
4287
+ const sortedGomodFiles = gomodFiles.sort((a, b) => {
4288
+ const relativePathA = relative(path, a);
4289
+ const relativePathB = relative(path, b);
4290
+ const depthA = relativePathA.split("/").length;
4291
+ const depthB = relativePathB.split("/").length;
4292
+ return depthA - depthB;
4293
+ });
4294
+
4295
+ let rootParentComponent = null;
4256
4296
  // Use the go list -deps and go mod why commands to generate a good quality BOM for non-docker invocations
4257
4297
  if (
4258
4298
  !hasAnyProjectType(["docker", "oci", "container", "os"], options, false)
4259
4299
  ) {
4260
- for (const f of gomodFiles) {
4300
+ for (const f of sortedGomodFiles) {
4261
4301
  const basePath = dirname(f);
4262
4302
  // Ignore vendor packages
4263
4303
  if (
@@ -4307,13 +4347,23 @@ export async function createGoBom(path, options) {
4307
4347
  if (retMap.pkgList?.length) {
4308
4348
  pkgList = pkgList.concat(retMap.pkgList);
4309
4349
  }
4310
- // We treat the main module as our parent
4350
+ // Prioritize the shallowest module as the root component
4311
4351
  if (
4312
4352
  retMap.parentComponent &&
4313
4353
  Object.keys(retMap.parentComponent).length
4314
4354
  ) {
4315
- parentComponent = retMap.parentComponent;
4316
- parentComponent.type = "application";
4355
+ if (!rootParentComponent) {
4356
+ // First (shallowest) module becomes the root
4357
+ rootParentComponent = retMap.parentComponent;
4358
+ rootParentComponent.type = "application";
4359
+ parentComponent = rootParentComponent;
4360
+ } else {
4361
+ // Subsequent modules become subcomponents
4362
+ if (!parentComponent.components) {
4363
+ parentComponent.components = [];
4364
+ }
4365
+ parentComponent.components.push(retMap.parentComponent);
4366
+ }
4317
4367
  }
4318
4368
  if (DEBUG_MODE) {
4319
4369
  console.log("Executing go mod graph in", basePath);
@@ -4399,11 +4449,14 @@ export async function createGoBom(path, options) {
4399
4449
  parentComponent,
4400
4450
  );
4401
4451
  }
4402
- // Retain the parent component hierarchy
4452
+ // Retain the parent component hierarchy, prioritizing the shallowest module
4403
4453
  if (Object.keys(retMap.parentComponent).length) {
4404
- if (gomodFiles.length === 1) {
4405
- parentComponent = retMap.parentComponent;
4454
+ if (!rootParentComponent) {
4455
+ // First (shallowest) module becomes the root
4456
+ rootParentComponent = retMap.parentComponent;
4457
+ parentComponent = rootParentComponent;
4406
4458
  } else {
4459
+ // Subsequent modules become subcomponents
4407
4460
  parentComponent.components = parentComponent.components || [];
4408
4461
  parentComponent.components.push(retMap.parentComponent);
4409
4462
  }
@@ -4429,7 +4482,7 @@ export async function createGoBom(path, options) {
4429
4482
  dependencies,
4430
4483
  parentComponent,
4431
4484
  src: path,
4432
- filename: gomodFiles.join(", "),
4485
+ filename: sortedGomodFiles.join(", "),
4433
4486
  });
4434
4487
  }
4435
4488
  }
@@ -4441,7 +4494,7 @@ export async function createGoBom(path, options) {
4441
4494
  "Manually parsing go.mod files. The resultant BOM would be incomplete.",
4442
4495
  );
4443
4496
  }
4444
- for (const f of gomodFiles) {
4497
+ for (const f of sortedGomodFiles) {
4445
4498
  if (DEBUG_MODE) {
4446
4499
  console.log(`Parsing ${f}`);
4447
4500
  }
@@ -4450,11 +4503,14 @@ export async function createGoBom(path, options) {
4450
4503
  if (retMap?.pkgList?.length) {
4451
4504
  pkgList = pkgList.concat(retMap.pkgList);
4452
4505
  }
4453
- // Retain the parent component hierarchy
4506
+ // Retain the parent component hierarchy, prioritizing the shallowest module
4454
4507
  if (Object.keys(retMap.parentComponent).length) {
4455
- if (gomodFiles.length === 1) {
4456
- parentComponent = retMap.parentComponent;
4508
+ if (!rootParentComponent) {
4509
+ // First (shallowest) module becomes the root
4510
+ rootParentComponent = retMap.parentComponent;
4511
+ parentComponent = rootParentComponent;
4457
4512
  } else {
4513
+ // Subsequent modules become subcomponents
4458
4514
  parentComponent.components = parentComponent.components || [];
4459
4515
  parentComponent.components.push(retMap.parentComponent);
4460
4516
  }
@@ -4479,7 +4535,7 @@ export async function createGoBom(path, options) {
4479
4535
  src: path,
4480
4536
  dependencies,
4481
4537
  parentComponent,
4482
- filename: gomodFiles.join(", "),
4538
+ filename: sortedGomodFiles.join(", "),
4483
4539
  });
4484
4540
  }
4485
4541
  if (gopkgLockFiles.length) {
@@ -4927,7 +4983,7 @@ export function createClojureBom(path, options) {
4927
4983
  console.log(`Parsing ${f}`);
4928
4984
  }
4929
4985
  const basePath = dirname(f);
4930
- console.log("Executing", LEIN_CMD, LEIN_ARGS.join(" "), "in", basePath);
4986
+ console.log("Executing", LEIN_CMD, "in", basePath);
4931
4987
  const result = safeSpawnSync(LEIN_CMD, LEIN_ARGS, {
4932
4988
  cwd: basePath,
4933
4989
  encoding: "utf-8",
@@ -4976,7 +5032,7 @@ export function createClojureBom(path, options) {
4976
5032
  }
4977
5033
  for (const f of ednFiles) {
4978
5034
  const basePath = dirname(f);
4979
- console.log("Executing", CLJ_CMD, CLJ_ARGS.join(" "), "in", basePath);
5035
+ console.log("Executing", CLJ_CMD, "in", basePath);
4980
5036
  const result = safeSpawnSync(CLJ_CMD, CLJ_ARGS, {
4981
5037
  cwd: basePath,
4982
5038
  encoding: "utf-8",
@@ -5584,6 +5640,128 @@ export async function createCocoaBom(path, options) {
5584
5640
  }
5585
5641
  }
5586
5642
 
5643
+ /**
5644
+ * Function to create bom string for Nix flakes
5645
+ *
5646
+ * @param {string} path to the project
5647
+ * @param {Object} options Parse options from the cli
5648
+ */
5649
+ export async function createNixBom(path, options) {
5650
+ let pkgList = [];
5651
+ let dependencies = [];
5652
+ let parentComponent = {};
5653
+
5654
+ const flakeNixFiles = getAllFiles(
5655
+ path,
5656
+ `${options.multiProject ? "**/" : ""}flake.nix`,
5657
+ options,
5658
+ );
5659
+ const flakeLockFiles = getAllFiles(
5660
+ path,
5661
+ `${options.multiProject ? "**/" : ""}flake.lock`,
5662
+ options,
5663
+ );
5664
+
5665
+ for (const flakeNixFile of flakeNixFiles) {
5666
+ if (DEBUG_MODE) {
5667
+ console.log(`Parsing ${flakeNixFile}`);
5668
+ }
5669
+ const { pkgList: nixPkgs, dependencies: nixDeps } =
5670
+ parseFlakeNix(flakeNixFile);
5671
+ if (nixPkgs?.length) {
5672
+ pkgList = pkgList.concat(nixPkgs);
5673
+ }
5674
+ if (nixDeps?.length) {
5675
+ dependencies = dependencies.concat(nixDeps);
5676
+ }
5677
+ }
5678
+
5679
+ for (const flakeLockFile of flakeLockFiles) {
5680
+ if (DEBUG_MODE) {
5681
+ console.log(`Parsing ${flakeLockFile}`);
5682
+ }
5683
+ const { pkgList: lockPkgs, dependencies: lockDeps } =
5684
+ parseFlakeLock(flakeLockFile);
5685
+ if (lockPkgs?.length) {
5686
+ const mergedPkgs = [];
5687
+ const existingNames = new Set();
5688
+
5689
+ for (const lockPkg of lockPkgs) {
5690
+ mergedPkgs.push(lockPkg);
5691
+ existingNames.add(lockPkg.name);
5692
+ }
5693
+
5694
+ for (const nixPkg of pkgList) {
5695
+ if (!existingNames.has(nixPkg.name)) {
5696
+ mergedPkgs.push(nixPkg);
5697
+ }
5698
+ }
5699
+
5700
+ pkgList = mergedPkgs;
5701
+ }
5702
+ if (lockDeps?.length) {
5703
+ dependencies = dependencies.concat(lockDeps);
5704
+ }
5705
+
5706
+ // Create parent component from flake.lock if found
5707
+ if (!Object.keys(parentComponent).length) {
5708
+ const flakeDir = dirname(flakeLockFile);
5709
+ const projectName = basename(flakeDir);
5710
+ parentComponent = {
5711
+ type: "application",
5712
+ name: projectName,
5713
+ version: "latest",
5714
+ description: `Nix flake project: ${projectName}`,
5715
+ "bom-ref": `pkg:nix/${projectName}@latest`,
5716
+ properties: [
5717
+ {
5718
+ name: "SrcFile",
5719
+ value: flakeLockFile,
5720
+ },
5721
+ {
5722
+ name: "cdx:nix:flake_dir",
5723
+ value: flakeDir,
5724
+ },
5725
+ ],
5726
+ };
5727
+ }
5728
+ }
5729
+
5730
+ // If no parent component was created from flake.lock, create one from flake.nix
5731
+ if (!Object.keys(parentComponent).length && flakeNixFiles.length > 0) {
5732
+ const flakeDir = dirname(flakeNixFiles[0]);
5733
+ const projectName = basename(flakeDir);
5734
+ parentComponent = {
5735
+ type: "application",
5736
+ name: projectName,
5737
+ version: "latest",
5738
+ description: `Nix flake project: ${projectName}`,
5739
+ "bom-ref": `pkg:nix/${projectName}@latest`,
5740
+ properties: [
5741
+ {
5742
+ name: "SrcFile",
5743
+ value: flakeNixFiles[0],
5744
+ },
5745
+ {
5746
+ name: "cdx:nix:flake_dir",
5747
+ value: flakeDir,
5748
+ },
5749
+ ],
5750
+ };
5751
+ }
5752
+
5753
+ if (pkgList.length || Object.keys(parentComponent).length) {
5754
+ return buildBomNSData(options, pkgList, "nix", {
5755
+ src: path,
5756
+ filename: [...flakeNixFiles, ...flakeLockFiles].join(", "),
5757
+ dependencies,
5758
+ parentComponent,
5759
+ });
5760
+ }
5761
+
5762
+ return {};
5763
+ }
5764
+
5587
5765
  /**
5588
5766
  * Function to create bom string for docker compose
5589
5767
  *
@@ -5921,6 +6099,10 @@ export async function createContainerSpecLikeBom(path, options) {
5921
6099
  export function createPHPBom(path, options) {
5922
6100
  let dependencies = [];
5923
6101
  let parentComponent = {};
6102
+ // We can look for composer files within node_modules directory
6103
+ if (typeof options.includeNodeModulesDir === "undefined" || options.deep) {
6104
+ options.includeNodeModulesDir = true;
6105
+ }
5924
6106
  const composerJsonFiles = getAllFiles(
5925
6107
  path,
5926
6108
  `${options.multiProject ? "**/" : ""}composer.json`,
@@ -6069,6 +6251,10 @@ export function createPHPBom(path, options) {
6069
6251
  * @param {Object} options Parse options from the cli
6070
6252
  */
6071
6253
  export async function createRubyBom(path, options) {
6254
+ // We can look for gem files within node_modules directory
6255
+ if (typeof options.includeNodeModulesDir === "undefined" || options.deep) {
6256
+ options.includeNodeModulesDir = true;
6257
+ }
6072
6258
  const excludeList = (options.exclude || []).concat(["**/vendor/cache/**"]);
6073
6259
  const gemLockExcludeList = (options.exclude || []).concat([
6074
6260
  "**/vendor/bundle/ruby/**/Gemfile.lock",
@@ -6861,8 +7047,8 @@ export function trimComponents(components) {
6861
7047
  }
6862
7048
  // comp.evidence.identity can be an array or object
6863
7049
  // Merge the evidence.identity based on methods or objects
6864
- const isArray = Array.isArray(comp.evidence.identity);
6865
- const identities = isArray
7050
+ const isIdentityArray = Array.isArray(comp.evidence.identity);
7051
+ const identities = isIdentityArray
6866
7052
  ? comp.evidence.identity
6867
7053
  : [comp.evidence.identity];
6868
7054
  for (const aident of identities) {
@@ -6893,9 +7079,23 @@ export function trimComponents(components) {
6893
7079
  existingComponent.evidence.identity.push(aident);
6894
7080
  }
6895
7081
  }
6896
- if (!isArray) {
7082
+ if (!isIdentityArray) {
7083
+ const firstIdentity = existingComponent.evidence.identity[0];
7084
+ let identConfidence = firstIdentity?.confidence;
7085
+ // We need to set the confidence to the max of all confidences
7086
+ if (firstIdentity?.methods?.length > 1) {
7087
+ for (const aidentMethod of firstIdentity.methods) {
7088
+ if (
7089
+ aidentMethod?.confidence &&
7090
+ aidentMethod.confidence > identConfidence
7091
+ ) {
7092
+ identConfidence = aidentMethod.confidence;
7093
+ }
7094
+ }
7095
+ }
7096
+ firstIdentity.confidence = identConfidence;
6897
7097
  existingComponent.evidence = {
6898
- identity: existingComponent.evidence.identity[0],
7098
+ identity: firstIdentity,
6899
7099
  };
6900
7100
  }
6901
7101
  }
@@ -7635,6 +7835,27 @@ export async function createMultiXBom(pathList, options) {
7635
7835
  }
7636
7836
  }
7637
7837
  }
7838
+ if (hasAnyProjectType(["oci", "nix"], options)) {
7839
+ bomData = await createNixBom(path, options);
7840
+ if (bomData?.bomJson?.components?.length) {
7841
+ if (DEBUG_MODE) {
7842
+ console.log(
7843
+ `Found ${bomData.bomJson.components.length} Nix flake packages at ${path}`,
7844
+ );
7845
+ }
7846
+ components = components.concat(bomData.bomJson.components);
7847
+ dependencies = mergeDependencies(
7848
+ dependencies,
7849
+ bomData.bomJson.dependencies,
7850
+ );
7851
+ if (
7852
+ bomData.parentComponent &&
7853
+ Object.keys(bomData.parentComponent).length
7854
+ ) {
7855
+ parentSubComponents.push(bomData.parentComponent);
7856
+ }
7857
+ }
7858
+ }
7638
7859
  // Collect any crypto keys
7639
7860
  if (options.specVersion >= 1.6 && options.includeCrypto) {
7640
7861
  if (!hasAnyProjectType(["oci"], options, false)) {
@@ -8063,6 +8284,21 @@ export async function createXBom(path, options) {
8063
8284
  if (cocoaFiles.length) {
8064
8285
  return await createCocoaBom(path, options);
8065
8286
  }
8287
+
8288
+ // Nix flakes
8289
+ const flakeNixFiles = getAllFiles(
8290
+ path,
8291
+ `${options.multiProject ? "**/" : ""}flake.nix`,
8292
+ options,
8293
+ );
8294
+ const flakeLockFiles = getAllFiles(
8295
+ path,
8296
+ `${options.multiProject ? "**/" : ""}flake.lock`,
8297
+ options,
8298
+ );
8299
+ if (flakeNixFiles.length || flakeLockFiles.length) {
8300
+ return await createNixBom(path, options);
8301
+ }
8066
8302
  }
8067
8303
 
8068
8304
  /**
@@ -8308,6 +8544,9 @@ export async function createBom(path, options) {
8308
8544
  if (PROJECT_TYPE_ALIASES["cocoa"].includes(projectType[0])) {
8309
8545
  return await createCocoaBom(path, options);
8310
8546
  }
8547
+ if (PROJECT_TYPE_ALIASES["nix"].includes(projectType[0])) {
8548
+ return await createNixBom(path, options);
8549
+ }
8311
8550
  switch (projectType[0]) {
8312
8551
  case "jar":
8313
8552
  return createJarBom(path, options);