@cyclonedx/cdxgen 11.4.3 → 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,
@@ -2900,6 +2924,10 @@ export async function createNodejsBom(path, options) {
2900
2924
  pkgMgr = mgr;
2901
2925
  } else if (pkgData?.engines?.yarn) {
2902
2926
  pkgMgr = "yarn";
2927
+ } else if (
2928
+ isPackageManagerAllowed("npm", ["yarn", "pnpm", "rush"], options)
2929
+ ) {
2930
+ pkgMgr = "npm";
2903
2931
  } else if (
2904
2932
  isPackageManagerAllowed("yarn", ["npm", "pnpm", "rush"], options)
2905
2933
  ) {
@@ -3327,10 +3355,10 @@ export async function createNodejsBom(path, options) {
3327
3355
  });
3328
3356
  }
3329
3357
  if (safeExistsSync(pnpmLock)) {
3330
- let pkgList = await parsePnpmLock(pnpmLock);
3358
+ const pnpmLockObj = await parsePnpmLock(pnpmLock);
3331
3359
  if (allImports && Object.keys(allImports).length) {
3332
3360
  pkgList = await addEvidenceForImports(
3333
- pkgList,
3361
+ pnpmLockObj.pkgList,
3334
3362
  allImports,
3335
3363
  allExports,
3336
3364
  options.deep,
@@ -3757,7 +3785,7 @@ export async function createPythonBom(path, options) {
3757
3785
  // Retrieve the tree using virtualenv in deep mode and as a fallback
3758
3786
  // This is a slow operation
3759
3787
  if ((options.deep || !dependencies.length) && !f.endsWith("uv.lock")) {
3760
- retMap = getPipFrozenTree(basePath, f, tempDir, parentComponent);
3788
+ retMap = await getPipFrozenTree(basePath, f, tempDir, parentComponent);
3761
3789
  if (retMap.pkgList?.length) {
3762
3790
  pkgList = pkgList.concat(retMap.pkgList);
3763
3791
  }
@@ -3835,9 +3863,49 @@ export async function createPythonBom(path, options) {
3835
3863
  console.error("Pipfile.lock not found at", path);
3836
3864
  options.failOnError && process.exit(1);
3837
3865
  }
3838
- } else if (requirementsMode) {
3839
- metadataFilename = "requirements.txt";
3840
- 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) {
3841
3909
  if (options.installDeps && DEBUG_MODE) {
3842
3910
  console.log(
3843
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.",
@@ -3845,13 +3913,8 @@ export async function createPythonBom(path, options) {
3845
3913
  }
3846
3914
  for (const f of reqFiles) {
3847
3915
  const basePath = dirname(f);
3848
- let reqData;
3849
- let frozen = false;
3850
- // Attempt to pip freeze in a virtualenv to improve precision
3851
3916
  if (options.installDeps) {
3852
- // If there are multiple requirements files then the tree is getting constructed for each one
3853
- // adding to the delay.
3854
- const pkgMap = getPipFrozenTree(
3917
+ const pkgMap = await getPipFrozenTree(
3855
3918
  basePath,
3856
3919
  f,
3857
3920
  tempDir,
@@ -3859,10 +3922,11 @@ export async function createPythonBom(path, options) {
3859
3922
  );
3860
3923
  if (pkgMap.pkgList?.length) {
3861
3924
  pkgList = pkgList.concat(pkgMap.pkgList);
3862
- frozen = pkgMap.frozen;
3925
+ pkgList = trimComponents(pkgList);
3863
3926
  }
3864
3927
  if (pkgMap.formulationList?.length) {
3865
3928
  formulationList = formulationList.concat(pkgMap.formulationList);
3929
+ formulationList = trimComponents(formulationList);
3866
3930
  }
3867
3931
  if (pkgMap.dependenciesList) {
3868
3932
  dependencies = mergeDependencies(
@@ -3871,61 +3935,31 @@ export async function createPythonBom(path, options) {
3871
3935
  parentComponent,
3872
3936
  );
3873
3937
  }
3874
- }
3875
- // Fallback to parsing manually
3876
- if (!pkgList.length || !frozen) {
3877
- thoughtLog(
3878
- `Manually parsing ${f}. The result would include only direct dependencies.`,
3879
- );
3880
- if (DEBUG_MODE) {
3881
- console.log(
3882
- `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}`,
3883
3950
  );
3884
3951
  }
3885
- reqData = readFileSync(f, { encoding: "utf-8" });
3886
- const dlist = await parseReqFile(reqData, true);
3887
- if (dlist?.length) {
3888
- pkgList = pkgList.concat(dlist);
3889
- }
3890
- }
3891
- } // for
3892
- metadataFilename = reqFiles.join(", ");
3893
- } else if (reqDirFiles?.length) {
3894
- for (const j in reqDirFiles) {
3895
- const f = reqDirFiles[j];
3896
- const reqData = readFileSync(f, { encoding: "utf-8" });
3897
- const dlist = await parseReqFile(reqData, false);
3898
- if (dlist?.length) {
3899
- pkgList = pkgList.concat(dlist);
3900
3952
  }
3901
3953
  }
3902
- metadataFilename = reqDirFiles.join(", ");
3903
- }
3904
- }
3905
- }
3906
- // Use atom in requirements, setup.py and pyproject.toml mode
3907
- if (requirementsMode || setupPyMode || pyProjectMode || options.deep) {
3908
- /**
3909
- * The order of preference is pyproject.toml (newer) and then setup.py
3910
- */
3911
- if (options.installDeps) {
3912
- let pkgMap;
3913
- if (pyProjectMode && !poetryMode) {
3914
- pkgMap = getPipFrozenTree(
3954
+ } else if (!poetryMode) {
3955
+ pkgMap = await getPipFrozenTree(
3915
3956
  path,
3916
- pyProjectFile,
3957
+ undefined,
3917
3958
  tempDir,
3918
3959
  parentComponent,
3919
3960
  );
3920
- } else if (setupPyMode) {
3921
- pkgMap = getPipFrozenTree(path, setupPy, tempDir, parentComponent);
3922
- } else if (!poetryMode) {
3923
- pkgMap = getPipFrozenTree(path, undefined, tempDir, parentComponent);
3924
3961
  }
3925
3962
 
3926
- // Get the imported modules and a dedupe list of packages
3927
- const parentDependsOn = new Set();
3928
-
3929
3963
  // ATOM parsedeps block
3930
3964
  // Atom parsedeps slices can be used to identify packages that are not declared in manifests
3931
3965
  // Since it is a slow operation, we only use it as a fallback or in deep mode
@@ -4249,11 +4283,21 @@ export async function createGoBom(path, options) {
4249
4283
  }
4250
4284
  if (gomodFiles.length) {
4251
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;
4252
4296
  // Use the go list -deps and go mod why commands to generate a good quality BOM for non-docker invocations
4253
4297
  if (
4254
4298
  !hasAnyProjectType(["docker", "oci", "container", "os"], options, false)
4255
4299
  ) {
4256
- for (const f of gomodFiles) {
4300
+ for (const f of sortedGomodFiles) {
4257
4301
  const basePath = dirname(f);
4258
4302
  // Ignore vendor packages
4259
4303
  if (
@@ -4303,13 +4347,23 @@ export async function createGoBom(path, options) {
4303
4347
  if (retMap.pkgList?.length) {
4304
4348
  pkgList = pkgList.concat(retMap.pkgList);
4305
4349
  }
4306
- // We treat the main module as our parent
4350
+ // Prioritize the shallowest module as the root component
4307
4351
  if (
4308
4352
  retMap.parentComponent &&
4309
4353
  Object.keys(retMap.parentComponent).length
4310
4354
  ) {
4311
- parentComponent = retMap.parentComponent;
4312
- 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
+ }
4313
4367
  }
4314
4368
  if (DEBUG_MODE) {
4315
4369
  console.log("Executing go mod graph in", basePath);
@@ -4395,11 +4449,14 @@ export async function createGoBom(path, options) {
4395
4449
  parentComponent,
4396
4450
  );
4397
4451
  }
4398
- // Retain the parent component hierarchy
4452
+ // Retain the parent component hierarchy, prioritizing the shallowest module
4399
4453
  if (Object.keys(retMap.parentComponent).length) {
4400
- if (gomodFiles.length === 1) {
4401
- parentComponent = retMap.parentComponent;
4454
+ if (!rootParentComponent) {
4455
+ // First (shallowest) module becomes the root
4456
+ rootParentComponent = retMap.parentComponent;
4457
+ parentComponent = rootParentComponent;
4402
4458
  } else {
4459
+ // Subsequent modules become subcomponents
4403
4460
  parentComponent.components = parentComponent.components || [];
4404
4461
  parentComponent.components.push(retMap.parentComponent);
4405
4462
  }
@@ -4425,7 +4482,7 @@ export async function createGoBom(path, options) {
4425
4482
  dependencies,
4426
4483
  parentComponent,
4427
4484
  src: path,
4428
- filename: gomodFiles.join(", "),
4485
+ filename: sortedGomodFiles.join(", "),
4429
4486
  });
4430
4487
  }
4431
4488
  }
@@ -4437,7 +4494,7 @@ export async function createGoBom(path, options) {
4437
4494
  "Manually parsing go.mod files. The resultant BOM would be incomplete.",
4438
4495
  );
4439
4496
  }
4440
- for (const f of gomodFiles) {
4497
+ for (const f of sortedGomodFiles) {
4441
4498
  if (DEBUG_MODE) {
4442
4499
  console.log(`Parsing ${f}`);
4443
4500
  }
@@ -4446,11 +4503,14 @@ export async function createGoBom(path, options) {
4446
4503
  if (retMap?.pkgList?.length) {
4447
4504
  pkgList = pkgList.concat(retMap.pkgList);
4448
4505
  }
4449
- // Retain the parent component hierarchy
4506
+ // Retain the parent component hierarchy, prioritizing the shallowest module
4450
4507
  if (Object.keys(retMap.parentComponent).length) {
4451
- if (gomodFiles.length === 1) {
4452
- parentComponent = retMap.parentComponent;
4508
+ if (!rootParentComponent) {
4509
+ // First (shallowest) module becomes the root
4510
+ rootParentComponent = retMap.parentComponent;
4511
+ parentComponent = rootParentComponent;
4453
4512
  } else {
4513
+ // Subsequent modules become subcomponents
4454
4514
  parentComponent.components = parentComponent.components || [];
4455
4515
  parentComponent.components.push(retMap.parentComponent);
4456
4516
  }
@@ -4475,7 +4535,7 @@ export async function createGoBom(path, options) {
4475
4535
  src: path,
4476
4536
  dependencies,
4477
4537
  parentComponent,
4478
- filename: gomodFiles.join(", "),
4538
+ filename: sortedGomodFiles.join(", "),
4479
4539
  });
4480
4540
  }
4481
4541
  if (gopkgLockFiles.length) {
@@ -4923,7 +4983,7 @@ export function createClojureBom(path, options) {
4923
4983
  console.log(`Parsing ${f}`);
4924
4984
  }
4925
4985
  const basePath = dirname(f);
4926
- console.log("Executing", LEIN_CMD, LEIN_ARGS.join(" "), "in", basePath);
4986
+ console.log("Executing", LEIN_CMD, "in", basePath);
4927
4987
  const result = safeSpawnSync(LEIN_CMD, LEIN_ARGS, {
4928
4988
  cwd: basePath,
4929
4989
  encoding: "utf-8",
@@ -4972,7 +5032,7 @@ export function createClojureBom(path, options) {
4972
5032
  }
4973
5033
  for (const f of ednFiles) {
4974
5034
  const basePath = dirname(f);
4975
- console.log("Executing", CLJ_CMD, CLJ_ARGS.join(" "), "in", basePath);
5035
+ console.log("Executing", CLJ_CMD, "in", basePath);
4976
5036
  const result = safeSpawnSync(CLJ_CMD, CLJ_ARGS, {
4977
5037
  cwd: basePath,
4978
5038
  encoding: "utf-8",
@@ -5580,6 +5640,128 @@ export async function createCocoaBom(path, options) {
5580
5640
  }
5581
5641
  }
5582
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
+
5583
5765
  /**
5584
5766
  * Function to create bom string for docker compose
5585
5767
  *
@@ -5917,6 +6099,10 @@ export async function createContainerSpecLikeBom(path, options) {
5917
6099
  export function createPHPBom(path, options) {
5918
6100
  let dependencies = [];
5919
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
+ }
5920
6106
  const composerJsonFiles = getAllFiles(
5921
6107
  path,
5922
6108
  `${options.multiProject ? "**/" : ""}composer.json`,
@@ -6065,6 +6251,10 @@ export function createPHPBom(path, options) {
6065
6251
  * @param {Object} options Parse options from the cli
6066
6252
  */
6067
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
+ }
6068
6258
  const excludeList = (options.exclude || []).concat(["**/vendor/cache/**"]);
6069
6259
  const gemLockExcludeList = (options.exclude || []).concat([
6070
6260
  "**/vendor/bundle/ruby/**/Gemfile.lock",
@@ -6609,11 +6799,11 @@ export async function createCsharpBom(path, options) {
6609
6799
  }
6610
6800
  }
6611
6801
  }
6802
+ const pkgNameVersions = {};
6612
6803
  if (csProjFiles.length) {
6613
6804
  manifestFiles = manifestFiles.concat(csProjFiles);
6614
6805
  // Parsing csproj is quite error-prone. Some project files may not have versions specified
6615
6806
  // To work around this, we make use of the version from the existing list
6616
- const pkgNameVersions = {};
6617
6807
  for (const p of pkgList) {
6618
6808
  if (p.version) {
6619
6809
  pkgNameVersions[p.name] = p.version;
@@ -6857,8 +7047,8 @@ export function trimComponents(components) {
6857
7047
  }
6858
7048
  // comp.evidence.identity can be an array or object
6859
7049
  // Merge the evidence.identity based on methods or objects
6860
- const isArray = Array.isArray(comp.evidence.identity);
6861
- const identities = isArray
7050
+ const isIdentityArray = Array.isArray(comp.evidence.identity);
7051
+ const identities = isIdentityArray
6862
7052
  ? comp.evidence.identity
6863
7053
  : [comp.evidence.identity];
6864
7054
  for (const aident of identities) {
@@ -6889,9 +7079,23 @@ export function trimComponents(components) {
6889
7079
  existingComponent.evidence.identity.push(aident);
6890
7080
  }
6891
7081
  }
6892
- 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;
6893
7097
  existingComponent.evidence = {
6894
- identity: existingComponent.evidence.identity[0],
7098
+ identity: firstIdentity,
6895
7099
  };
6896
7100
  }
6897
7101
  }
@@ -7631,6 +7835,27 @@ export async function createMultiXBom(pathList, options) {
7631
7835
  }
7632
7836
  }
7633
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
+ }
7634
7859
  // Collect any crypto keys
7635
7860
  if (options.specVersion >= 1.6 && options.includeCrypto) {
7636
7861
  if (!hasAnyProjectType(["oci"], options, false)) {
@@ -8059,6 +8284,21 @@ export async function createXBom(path, options) {
8059
8284
  if (cocoaFiles.length) {
8060
8285
  return await createCocoaBom(path, options);
8061
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
+ }
8062
8302
  }
8063
8303
 
8064
8304
  /**
@@ -8304,6 +8544,9 @@ export async function createBom(path, options) {
8304
8544
  if (PROJECT_TYPE_ALIASES["cocoa"].includes(projectType[0])) {
8305
8545
  return await createCocoaBom(path, options);
8306
8546
  }
8547
+ if (PROJECT_TYPE_ALIASES["nix"].includes(projectType[0])) {
8548
+ return await createNixBom(path, options);
8549
+ }
8307
8550
  switch (projectType[0]) {
8308
8551
  case "jar":
8309
8552
  return createJarBom(path, options);