@cdxgen/cdxgen 13.2.0 → 13.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 (158) hide show
  1. package/README.md +28 -12
  2. package/bin/cdxgen.js +227 -119
  3. package/bin/evinse.js +5 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/remediations.json +24 -0
  9. package/lib/cli/bomAssembly.js +105 -15
  10. package/lib/cli/cliOptions.js +93 -1
  11. package/lib/cli/index.js +43 -4
  12. package/lib/cli/jsBom.js +49 -24
  13. package/lib/cli/jvmBom.js +46 -12
  14. package/lib/cli/managedBom.js +112 -41
  15. package/lib/cli/nativeBom.js +125 -14
  16. package/lib/core/env.js +5 -6
  17. package/lib/core/fs.js +174 -6
  18. package/lib/core/metadataProperties.js +97 -0
  19. package/lib/core/projectConfig.js +31 -4
  20. package/lib/ecosystems/bunutils.js +269 -101
  21. package/lib/ecosystems/caxa.js +434 -2
  22. package/lib/ecosystems/cmakeResolver.js +66 -0
  23. package/lib/ecosystems/cppEvidence.js +64 -31
  24. package/lib/ecosystems/ecosystems.js +19 -8
  25. package/lib/ecosystems/gradleutils.js +42 -7
  26. package/lib/ecosystems/jsEvidence.js +19 -20
  27. package/lib/ecosystems/kosi.js +10 -3
  28. package/lib/ecosystems/npmutils.js +110 -9
  29. package/lib/ecosystems/parsers-dotnet.js +51 -10
  30. package/lib/ecosystems/parsers-go.js +47 -9
  31. package/lib/ecosystems/parsers-js.js +171 -50
  32. package/lib/ecosystems/parsers-misc.js +747 -161
  33. package/lib/ecosystems/parsers-python.js +26 -14
  34. package/lib/ecosystems/parsers-rust.js +24 -18
  35. package/lib/ecosystems/rubyutils.js +1426 -166
  36. package/lib/ecosystems/rusi.js +32 -1
  37. package/lib/evinser/evinser.js +436 -128
  38. package/lib/evinser/swiftsem.js +1914 -302
  39. package/lib/helpers/bomSigner.js +652 -221
  40. package/lib/inventory/atomUtils.js +568 -31
  41. package/lib/inventory/cdxrs.js +103 -12
  42. package/lib/inventory/depsUtils.js +183 -11
  43. package/lib/inventory/display.js +4 -5
  44. package/lib/inventory/dosai.js +550 -13
  45. package/lib/inventory/dynamic.js +39 -1
  46. package/lib/inventory/evidenceUtils.js +14 -6
  47. package/lib/inventory/fetchBatch.js +116 -17
  48. package/lib/inventory/protobom.js +8 -1
  49. package/lib/inventory/purl.js +152 -3
  50. package/lib/inventory/sbomDocument.js +2 -70
  51. package/lib/inventory/source.js +37 -19
  52. package/lib/inventory/traceRunner.js +210 -21
  53. package/lib/managers/binary.js +115 -24
  54. package/lib/managers/docker.js +440 -20
  55. package/lib/managers/oci.js +39 -1
  56. package/lib/parsers/cmakeLists.js +452 -0
  57. package/lib/parsers/largeJson.js +454 -0
  58. package/lib/parsers/npmrc.js +19 -1
  59. package/lib/stages/postgen/postgen.js +68 -15
  60. package/lib/validator/bomValidator.js +6 -16
  61. package/lib/validator/index.js +48 -10
  62. package/lib/validator/reporters/console.js +3 -0
  63. package/lib/validator/reporters/json.js +1 -0
  64. package/lib/validator/reporters/sarif.js +7 -0
  65. package/package.json +20 -20
  66. package/types/lib/cli/bomAssembly.d.ts +2 -1
  67. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  68. package/types/lib/cli/cliOptions.d.ts +33 -1
  69. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  70. package/types/lib/cli/index.d.ts.map +1 -1
  71. package/types/lib/cli/jsBom.d.ts.map +1 -1
  72. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  73. package/types/lib/cli/managedBom.d.ts.map +1 -1
  74. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  75. package/types/lib/core/env.d.ts.map +1 -1
  76. package/types/lib/core/fs.d.ts +55 -0
  77. package/types/lib/core/fs.d.ts.map +1 -1
  78. package/types/lib/core/metadataProperties.d.ts +39 -0
  79. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  80. package/types/lib/core/projectConfig.d.ts +6 -2
  81. package/types/lib/core/projectConfig.d.ts.map +1 -1
  82. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  83. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  84. package/types/lib/ecosystems/caxa.d.ts +14 -0
  85. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  86. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  87. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  88. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  89. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  90. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  91. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  92. package/types/lib/ecosystems/gradleutils.d.ts +20 -4
  93. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  94. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  95. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  96. package/types/lib/ecosystems/kosi.d.ts +13 -0
  97. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  98. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  99. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  102. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  103. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  104. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  105. package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
  106. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  109. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  110. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  111. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  112. package/types/lib/ecosystems/rusi.d.ts +13 -0
  113. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  114. package/types/lib/evinser/evinser.d.ts +59 -9
  115. package/types/lib/evinser/evinser.d.ts.map +1 -1
  116. package/types/lib/evinser/swiftsem.d.ts +280 -15
  117. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  118. package/types/lib/helpers/bomSigner.d.ts +106 -13
  119. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  120. package/types/lib/inventory/atomUtils.d.ts +145 -3
  121. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  122. package/types/lib/inventory/cdxrs.d.ts +8 -3
  123. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts +15 -0
  125. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  126. package/types/lib/inventory/display.d.ts.map +1 -1
  127. package/types/lib/inventory/dosai.d.ts +64 -1
  128. package/types/lib/inventory/dosai.d.ts.map +1 -1
  129. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  130. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  131. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  132. package/types/lib/inventory/plugins.d.ts.map +1 -1
  133. package/types/lib/inventory/protobom.d.ts.map +1 -1
  134. package/types/lib/inventory/purl.d.ts +71 -2
  135. package/types/lib/inventory/purl.d.ts.map +1 -1
  136. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  137. package/types/lib/inventory/source.d.ts.map +1 -1
  138. package/types/lib/inventory/traceRunner.d.ts +47 -1
  139. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  140. package/types/lib/managers/binary.d.ts +30 -1
  141. package/types/lib/managers/binary.d.ts.map +1 -1
  142. package/types/lib/managers/docker.d.ts +29 -0
  143. package/types/lib/managers/docker.d.ts.map +1 -1
  144. package/types/lib/managers/oci.d.ts +14 -0
  145. package/types/lib/managers/oci.d.ts.map +1 -1
  146. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  147. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  148. package/types/lib/parsers/largeJson.d.ts +52 -0
  149. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  150. package/types/lib/parsers/npmrc.d.ts +5 -0
  151. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  152. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  153. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  154. package/types/lib/validator/index.d.ts +10 -1
  155. package/types/lib/validator/index.d.ts.map +1 -1
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  157. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  158. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
package/lib/cli/jvmBom.js CHANGED
@@ -167,14 +167,14 @@ export async function createJarBom(path, options) {
167
167
  const parentComponent = createDefaultParentComponent(path, "maven", options);
168
168
  if (options.useGradleCache) {
169
169
  nsMapping = await collectGradleDependencies(
170
- getGradleCommand(path, null),
170
+ getGradleCommand(path, null, options),
171
171
  path,
172
172
  false,
173
173
  true,
174
174
  );
175
175
  } else if (options.useMavenCache) {
176
176
  nsMapping = await collectMvnDependencies(
177
- getMavenCommand(path, null),
177
+ getMavenCommand(path, null, options),
178
178
  null,
179
179
  false,
180
180
  true,
@@ -245,6 +245,8 @@ export function discoverSbtProjectsFromCmd(basePath, sbtCmd, env) {
245
245
  {
246
246
  cwd: basePath,
247
247
  shell: true,
248
+ // The quotes around the sbt command are for the shell to strip.
249
+ cdxgenRawShellArgs: true,
248
250
  env,
249
251
  encoding: "utf-8",
250
252
  },
@@ -453,7 +455,7 @@ export async function createJavaBom(path, options) {
453
455
  }
454
456
  }
455
457
  const firstPom = pomFiles.length ? pomFiles[0] : undefined;
456
- let mavenCmd = getMavenCommand(path, path);
458
+ let mavenCmd = getMavenCommand(path, path, options);
457
459
  for (const f of pomFiles) {
458
460
  const basePath = dirname(f);
459
461
  if (
@@ -473,7 +475,7 @@ export async function createJavaBom(path, options) {
473
475
  );
474
476
  }
475
477
  if (mavenCmd?.endsWith("mvn")) {
476
- mavenCmd = getMavenCommand(basePath, path);
478
+ mavenCmd = getMavenCommand(basePath, path, options);
477
479
  }
478
480
  // Should we attempt to resolve class names
479
481
  if (options.resolveClass || options.deep) {
@@ -1041,11 +1043,23 @@ export async function createJavaBom(path, options) {
1041
1043
  if (resolveGradleDistribution && gradleInitArgs.length) {
1042
1044
  gradleInitArgs.push("-PcdxgenResolveDistribution=true");
1043
1045
  }
1044
- parallelPropTaskOut = executeParallelGradleProperties(
1045
- gradleRootPath,
1046
- [null].concat(includedBuilds),
1047
- gradleInitArgs,
1048
- );
1046
+ // `gradle properties` evaluates the project's build scripts, so it is
1047
+ // part of the dependency-tooling surface gated by --no-install-deps
1048
+ // (and the pre-build lifecycle).
1049
+ const gradleToolingAllowed =
1050
+ options.installDeps !== false || options.introspect === true;
1051
+ if (gradleToolingAllowed) {
1052
+ parallelPropTaskOut = executeParallelGradleProperties(
1053
+ gradleRootPath,
1054
+ [null].concat(includedBuilds),
1055
+ gradleInitArgs,
1056
+ options,
1057
+ );
1058
+ } else {
1059
+ console.log(
1060
+ "Skipping 'gradle properties' since dependency installation is disabled. Only statically parsed Gradle metadata will be used.",
1061
+ );
1062
+ }
1049
1063
  if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS") === undefined) {
1050
1064
  const outputLines = parallelPropTaskOut.split("\n");
1051
1065
  for (const [_i, line] of outputLines.entries()) {
@@ -1073,7 +1087,14 @@ export async function createJavaBom(path, options) {
1073
1087
  }
1074
1088
  parallelPropTaskOut = parallelPropTaskOut.concat(
1075
1089
  "\n",
1076
- executeParallelGradleProperties(gradleRootPath, includedBuilds),
1090
+ gradleToolingAllowed
1091
+ ? executeParallelGradleProperties(
1092
+ gradleRootPath,
1093
+ includedBuilds,
1094
+ [],
1095
+ options,
1096
+ )
1097
+ : "",
1077
1098
  );
1078
1099
  }
1079
1100
  }
@@ -1114,6 +1135,8 @@ export async function createJavaBom(path, options) {
1114
1135
  parallelPropTaskOut = executeParallelGradleProperties(
1115
1136
  gradleRootPath,
1116
1137
  allProjectsStr.filter((module) => !modulesToSkip.includes(module)),
1138
+ [],
1139
+ options,
1117
1140
  );
1118
1141
  const splitPropTaskOut = splitOutputByGradleProjects(
1119
1142
  parallelPropTaskOut,
@@ -1161,7 +1184,7 @@ export async function createJavaBom(path, options) {
1161
1184
  )
1162
1185
  ) {
1163
1186
  allProjects.push(parentComponent);
1164
- const gradleCmd = getGradleCommand(gradleRootPath, null);
1187
+ const gradleCmd = getGradleCommand(gradleRootPath, null, options);
1165
1188
  const gradleDepTask = readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1166
1189
  ? readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1167
1190
  : "dependencies";
@@ -1639,6 +1662,13 @@ export async function createJavaBom(path, options) {
1639
1662
  pkgList = pkgList.concat(dlist);
1640
1663
  }
1641
1664
  }
1665
+ } else if (options.installDeps === false && !options.introspect) {
1666
+ // Executing sbt evaluates the project's build definition (arbitrary
1667
+ // Scala), so it belongs to the dependency-tooling surface disabled
1668
+ // by --no-install-deps and the pre-build lifecycle.
1669
+ console.log(
1670
+ "Skipping sbt dependencyTree since dependency installation is disabled. Only statically parsed sbt metadata will be used.",
1671
+ );
1642
1672
  } else {
1643
1673
  const SBT_CMD = readEnvironmentVariable("SBT_CMD") || "sbt";
1644
1674
  let sbtVersion = determineSbtVersion(path);
@@ -1754,6 +1784,9 @@ export async function createJavaBom(path, options) {
1754
1784
  const result = safeSpawnSync(SBT_CMD, sbtArgs, {
1755
1785
  cwd: basePath,
1756
1786
  shell: true,
1787
+ // The sbt commands below embed their own shell quoting, which the
1788
+ // shell must strip for sbt to see separate commands.
1789
+ cdxgenRawShellArgs: true,
1757
1790
  env,
1758
1791
  });
1759
1792
  if (result.status !== 0 || result.error) {
@@ -1893,6 +1926,7 @@ export async function createJavaBom(path, options) {
1893
1926
 
1894
1927
  if (
1895
1928
  millFiles?.length &&
1929
+ (options.installDeps !== false || options.introspect === true) &&
1896
1930
  isPackageManagerAllowed(
1897
1931
  "mill",
1898
1932
  ["bazel", "sbt", "gradle", "maven"],
@@ -1905,7 +1939,7 @@ export async function createJavaBom(path, options) {
1905
1939
  "maven",
1906
1940
  options,
1907
1941
  );
1908
- const millCmd = getMillCommand(millRootPath);
1942
+ const millCmd = getMillCommand(millRootPath, options);
1909
1943
  const millCommonArgs = [
1910
1944
  "--color",
1911
1945
  "false",
@@ -73,7 +73,10 @@ import {
73
73
  getPyModules,
74
74
  } from "../ecosystems/pythonutils.js";
75
75
  import {
76
+ addRubyGemsDistributionUrls,
77
+ addRubyRuntimeComponent,
76
78
  collectGemModuleNames,
79
+ describeProjectGem,
77
80
  enrichGemsFromLocalCache,
78
81
  parseBundleConfig,
79
82
  parseGemfileLockData,
@@ -104,7 +107,7 @@ import {
104
107
  createDosaiMethodsSlice,
105
108
  isDosaiDotnetLanguage,
106
109
  normalizeDosaiServiceMap,
107
- readDosaiJsonFile,
110
+ readDosaiMethodsReport,
108
111
  } from "../inventory/dosai.js";
109
112
  import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
110
113
  import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
@@ -573,8 +576,17 @@ export async function createPythonBom(path, options) {
573
576
  }
574
577
  if (requirementsMode || pipenvMode) {
575
578
  if (pipenvMode) {
576
- // TODO: Support for nested directories
577
- safeSpawnSync("pipenv", ["install"], { cwd: path, shell: isWin });
579
+ // `pipenv install` builds and installs the project's own dependencies
580
+ // (including `-e .` self-references that execute setup.py), so unlike
581
+ // the lockfile parsing below it must honor --no-install-deps and the
582
+ // pre-build lifecycle.
583
+ if (options.installDeps !== false) {
584
+ safeSpawnSync("pipenv", ["install"], { cwd: path, shell: isWin });
585
+ } else {
586
+ console.log(
587
+ "Skipping 'pipenv install' since dependency installation is disabled.",
588
+ );
589
+ }
578
590
  const piplockFile = join(path, "Pipfile.lock");
579
591
  if (safeExistsSync(piplockFile)) {
580
592
  let dlist;
@@ -1352,6 +1364,7 @@ export async function createRubyBom(path, options) {
1352
1364
  let pkgList = [];
1353
1365
  let dependencies = [];
1354
1366
  let rootList = [];
1367
+ const projectGemRefs = [];
1355
1368
  const parentComponent = createDefaultParentComponent(path, "gem", options);
1356
1369
  const gemFileMode = gemFiles.length;
1357
1370
  const gemLockMode = gemLockFiles.length;
@@ -1405,10 +1418,18 @@ export async function createRubyBom(path, options) {
1405
1418
  if (retMap.rootList?.length) {
1406
1419
  rootList = rootList.concat(retMap.rootList);
1407
1420
  }
1421
+ // `PATH remote: .` is relative to the lockfile, so only the lockfiles
1422
+ // of the scanned directory can serve the gem the root gemspec builds
1423
+ if (
1424
+ retMap.projectGemRefs?.length &&
1425
+ dirname(resolve(f)) === resolve(path)
1426
+ ) {
1427
+ projectGemRefs.push(...retMap.projectGemRefs);
1428
+ }
1408
1429
  }
1409
1430
  }
1410
1431
  // Parsing .gemspec files would help us get more metadata such as description, authors, licenses etc
1411
- let rootGemspecComponent;
1432
+ let projectGem;
1412
1433
  if (gemspecFiles.length) {
1413
1434
  if (!gemLockFiles.length && !hasAnyProjectType(["oci"], options, false)) {
1414
1435
  console.log(
@@ -1420,38 +1441,51 @@ export async function createRubyBom(path, options) {
1420
1441
  detail: "only gemspec files were available to parse",
1421
1442
  });
1422
1443
  }
1444
+ // Bundler evaluated the root gemspec to lock it under `PATH remote: .`,
1445
+ // so the locked release stands in for a version computed at runtime. It
1446
+ // is resolved before the merge, so that the gemspec and lockfile
1447
+ // sightings of the gem reconcile into one component.
1448
+ const lockedVersions = Object.create(null);
1449
+ for (const p of pkgList) {
1450
+ if (p.version && projectGemRefs.includes(p["bom-ref"])) {
1451
+ lockedVersions[p.name] = p.version;
1452
+ }
1453
+ }
1423
1454
  for (const f of gemspecFiles) {
1424
1455
  const gemspecData = readFileSync(f, { encoding: "utf-8" });
1425
- const gpkgList = await parseGemspecData(gemspecData, f);
1426
- if (gpkgList.length) {
1427
- pkgList = pkgList.concat(gpkgList);
1428
- pkgList = trimComponents(pkgList);
1429
- if (
1430
- !rootGemspecComponent &&
1431
- dirname(resolve(f)) === resolve(path) &&
1432
- gpkgList[0]?.name
1433
- ) {
1434
- rootGemspecComponent = gpkgList[0];
1435
- }
1456
+ const isRootGemspec = dirname(resolve(f)) === resolve(path);
1457
+ const gpkgList = await parseGemspecData(
1458
+ gemspecData,
1459
+ f,
1460
+ isRootGemspec ? { lockedVersions } : {},
1461
+ );
1462
+ if (!gpkgList.length) {
1463
+ continue;
1436
1464
  }
1437
- }
1438
- if (
1439
- rootGemspecComponent &&
1440
- !("project-name" in options) &&
1441
- options.projectName === undefined
1442
- ) {
1443
- parentComponent.name = rootGemspecComponent.name;
1444
- parentComponent.version = rootGemspecComponent.version || "latest";
1445
- const parentPurl = build({
1446
- type: "gem",
1447
- namespace: parentComponent.group || null,
1448
- name: parentComponent.name,
1449
- version: parentComponent.version || null,
1450
- });
1451
- parentComponent["bom-ref"] = decodeURIComponent(parentPurl);
1452
- parentComponent["purl"] = parentPurl;
1465
+ // The gemspec in the scanned directory is the gem the project builds
1466
+ if (!projectGem && isRootGemspec && gpkgList[0]?.name) {
1467
+ projectGem = gpkgList[0];
1468
+ }
1469
+ pkgList = trimComponents(pkgList.concat(gpkgList));
1453
1470
  }
1454
1471
  }
1472
+ // Describe that gem in metadata.component, unless `--project-name` names a
1473
+ // different project or an image is being scanned
1474
+ if (
1475
+ projectGem &&
1476
+ !hasAnyProjectType(["oci"], options, false) &&
1477
+ (!("project-name" in options) || options.projectName === projectGem.name)
1478
+ ) {
1479
+ ({ pkgList, dependencies, rootList } = describeProjectGem(
1480
+ parentComponent,
1481
+ projectGem,
1482
+ pkgList,
1483
+ dependencies,
1484
+ rootList,
1485
+ projectGemRefs,
1486
+ options,
1487
+ ));
1488
+ }
1455
1489
  if (rootList.length) {
1456
1490
  dependencies = mergeDependencies(
1457
1491
  dependencies,
@@ -1547,6 +1581,21 @@ export async function createRubyBom(path, options) {
1547
1581
  // lookup, gives us the sha256 of the exact native build, and unlike the
1548
1582
  // registry lookups it also works in dry-run mode.
1549
1583
  pkgList = await enrichGemsFromLocalCache(pkgList, { gemHome });
1584
+ // The download URL of a gem from the public registry needs no network call
1585
+ pkgList = addRubyGemsDistributionUrls(pkgList);
1586
+ // Gems declare the Ruby they run on; surface it as the runtime they depend
1587
+ // on. The component says the runtime is external, which only CycloneDX 1.7
1588
+ // can express, and a container image ships its own Ruby.
1589
+ if (
1590
+ options.specVersion >= 1.7 &&
1591
+ !hasAnyProjectType(["oci"], options, false)
1592
+ ) {
1593
+ ({ pkgList, dependencies } = addRubyRuntimeComponent(
1594
+ pkgList,
1595
+ parentComponent,
1596
+ dependencies,
1597
+ ));
1598
+ }
1550
1599
  return buildBomNSData(options, pkgList, "gem", {
1551
1600
  src: path,
1552
1601
  dependencies,
@@ -2006,9 +2055,13 @@ export async function createCsharpBom(path, options) {
2006
2055
  pkgList = trimComponents(pkgList);
2007
2056
  // Perform deep analysis using dosai
2008
2057
  if (options.deep) {
2058
+ // resolve (not join) so a fully-qualified --deps-slices-file is used
2059
+ // as-is: join would append it to the scan path, which is never a valid
2060
+ // path on Windows and silently loses the .NET evidence elsewhere
2061
+ // (issue 4394). A relative value still resolves against the scan path.
2009
2062
  const slicesFile = resolve(
2010
2063
  options.depsSlicesFile
2011
- ? join(path, options.depsSlicesFile)
2064
+ ? resolve(path, options.depsSlicesFile)
2012
2065
  : join(getTmpDir(), "dosai.json"),
2013
2066
  );
2014
2067
  // Create the slices file if it doesn't exist
@@ -2021,14 +2074,24 @@ export async function createCsharpBom(path, options) {
2021
2074
  resolve(slicesFile),
2022
2075
  options,
2023
2076
  );
2024
- if (!sliceResult && DEBUG_MODE) {
2025
- console.log(
2026
- "Slicing with dosai was unsuccessful. Check the errors reported in the logs above.",
2077
+ if (!sliceResult) {
2078
+ // Visible without --debug: otherwise the components silently lose
2079
+ // the dependency-slice evidence while the scan still exits 0.
2080
+ // --evidence (which implies --deep) runs dosai again during evinse
2081
+ // and can still add occurrences, so this names only what is lost.
2082
+ console.warn(
2083
+ "Slicing with dosai was unsuccessful, so the .NET components get no occurrence, reachability, or services evidence from the dependency slice. Re-run with --debug for the dosai errors.",
2027
2084
  );
2028
2085
  }
2086
+ } else if (DEBUG_MODE) {
2087
+ console.log(
2088
+ `Reusing the existing dosai methods slice "${slicesFile}" without checking whether it honours the current --exclude patterns. Delete the file to force a fresh run.`,
2089
+ );
2029
2090
  }
2030
2091
  pkgList = addEvidenceForDotnet(pkgList, slicesFile);
2031
- const methodsSlice = readDosaiJsonFile(slicesFile);
2092
+ const methodsSlice = readDosaiMethodsReport(slicesFile, {
2093
+ sections: ["Metadata", "Services", "ApiEndpoints"],
2094
+ });
2032
2095
  // Provider-backed Services[] first so bom-refs, trust zones, and data
2033
2096
  // classifications survive; ApiEndpoints derivation fills in the rest.
2034
2097
  const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
@@ -2036,12 +2099,20 @@ export async function createCsharpBom(path, options) {
2036
2099
  services = normalizeDosaiServiceMap(servicesMap);
2037
2100
  }
2038
2101
  }
2039
- // Parent dependency tree
2102
+ // Parent dependency tree. A single project file already contributed an
2103
+ // entry for the same parent, so the lock file roots are merged into it
2104
+ // rather than added as a second entry for the same ref.
2040
2105
  if (parentDependsOn.size && parentComponent?.["bom-ref"]) {
2041
- dependencies.splice(0, 0, {
2042
- ref: parentComponent["bom-ref"],
2043
- dependsOn: Array.from(parentDependsOn).sort(),
2044
- });
2106
+ dependencies = mergeDependencies(
2107
+ [
2108
+ {
2109
+ ref: parentComponent["bom-ref"],
2110
+ dependsOn: Array.from(parentDependsOn).sort(),
2111
+ },
2112
+ ],
2113
+ dependencies,
2114
+ parentComponent,
2115
+ );
2045
2116
  }
2046
2117
  if (shouldFetchLicense()) {
2047
2118
  const retMap = await getNugetMetadata(pkgList, dependencies);
@@ -31,7 +31,9 @@ import { dirNameStr, isMac } from "../core/paths.js";
31
31
  import {
32
32
  buildDependentPurl,
33
33
  collapseCmakeVersions,
34
+ preferFetchedCmakeDependencies,
34
35
  resolveCmakeContext,
36
+ withoutScrapedFetchDependency,
35
37
  } from "../ecosystems/cmakeResolver.js";
36
38
  import { getCppModules } from "../ecosystems/cppEvidence.js";
37
39
  import {
@@ -48,6 +50,7 @@ import {
48
50
  } from "../ecosystems/parsers-elm.js";
49
51
  import { parseGleamProject } from "../ecosystems/parsers-gleam.js";
50
52
  import {
53
+ isGoModWhyNotNeeded,
51
54
  parseGoListDep,
52
55
  parseGoModData,
53
56
  parseGoModGraph,
@@ -69,6 +72,7 @@ import {
69
72
  } from "../ecosystems/parsers-lua.js";
70
73
  import {
71
74
  buildObjectForCocoaPod,
75
+ classifySwiftDependencyScopes,
72
76
  executeAlpmList,
73
77
  executeApkList,
74
78
  executeDpkgList,
@@ -196,7 +200,6 @@ export function getCargoCacheDir() {
196
200
  export async function createGoBom(path, options) {
197
201
  let pkgList = [];
198
202
  let dependencies = [];
199
- const allImports = {};
200
203
  let parentComponent = createDefaultParentComponent(path, "golang", options);
201
204
  // Is this a binary file
202
205
  let maybeBinary;
@@ -236,6 +239,8 @@ export async function createGoBom(path, options) {
236
239
  }
237
240
  }
238
241
  const doneList = {};
242
+ const requiredModules = new Set();
243
+ const unneededModules = new Set();
239
244
  let circuitBreak = false;
240
245
  if (DEBUG_MODE) {
241
246
  console.log(
@@ -249,12 +254,12 @@ export async function createGoBom(path, options) {
249
254
  }
250
255
  const pkgFullName = `${apkg.name}`;
251
256
  if (apkg.scope === "required") {
252
- allImports[pkgFullName] = true;
257
+ requiredModules.add(pkgFullName);
253
258
  continue;
254
259
  }
255
260
  if (
256
261
  apkg.scope === "optional" ||
257
- allImports[pkgFullName] ||
262
+ requiredModules.has(pkgFullName) ||
258
263
  doneList[pkgFullName]
259
264
  ) {
260
265
  continue;
@@ -283,19 +288,40 @@ export async function createGoBom(path, options) {
283
288
  const mstdout = mresult.stdout;
284
289
  if (mstdout) {
285
290
  const cmdOutput = Buffer.from(mstdout).toString();
286
- const whyPkg = parseGoModWhy(cmdOutput);
287
- // whyPkg would include this package string
288
- // github.com/golang/protobuf/proto github.com/golang/protobuf
289
- // golang.org/x/tools/cmd/goimports golang.org/x/tools
290
- if (whyPkg?.includes(pkgFullName)) {
291
- allImports[pkgFullName] = true;
291
+ if (isGoModWhyNotNeeded(cmdOutput)) {
292
+ unneededModules.add(pkgFullName);
293
+ } else {
294
+ // The last line of the import chain is a package of the module:
295
+ // github.com/golang/protobuf/proto for github.com/golang/protobuf
296
+ const whyPkg = parseGoModWhy(cmdOutput);
297
+ if (
298
+ whyPkg === pkgFullName ||
299
+ whyPkg?.startsWith(`${pkgFullName}/`)
300
+ ) {
301
+ requiredModules.add(pkgFullName);
302
+ }
292
303
  }
293
304
  doneList[pkgFullName] = true;
294
305
  }
295
306
  }
296
307
  }
308
+ // A module go reports as unneeded is optional, but only when every module
309
+ // was checked: once go mod why fails, the answers are incomplete, and a
310
+ // tool failure is not evidence that a module is unused.
311
+ for (const apkg of pkgList) {
312
+ if (apkg.scope) {
313
+ continue;
314
+ }
315
+ if (requiredModules.has(apkg.name)) {
316
+ apkg.scope = "required";
317
+ } else if (!circuitBreak && unneededModules.has(apkg.name)) {
318
+ apkg.scope = "optional";
319
+ }
320
+ }
297
321
  if (DEBUG_MODE) {
298
- console.log(`Required packages: ${Object.keys(allImports).length}`);
322
+ console.log(
323
+ `Required packages: ${requiredModules.size}, not needed: ${circuitBreak ? "unknown" : unneededModules.size}`,
324
+ );
299
325
  }
300
326
  return buildBomNSData(options, pkgList, "golang", {
301
327
  src: path,
@@ -560,7 +586,6 @@ export async function createGoBom(path, options) {
560
586
  }
561
587
  if (pkgList.length && !shouldManuallyParse) {
562
588
  return buildBomNSData(options, pkgList, "golang", {
563
- allImports,
564
589
  dependencies,
565
590
  parentComponent,
566
591
  src: path,
@@ -1415,22 +1440,31 @@ export function createCppBom(path, options) {
1415
1440
  }
1416
1441
  if (cmakeLikeFiles.length) {
1417
1442
  const beforeCmake = pkgList.length;
1443
+ // The files at the scan root name the project, so they are read before
1444
+ // the files of any sub-project (the sort is stable otherwise). The scan
1445
+ // root may be given as a relative path while the files come back absolute.
1446
+ const scanRoot = resolve(path);
1447
+ const atScanRoot = (f) => resolve(dirname(f)) === scanRoot;
1448
+ cmakeLikeFiles.sort(
1449
+ (a, b) => (atScanRoot(a) ? 0 : 1) - (atScanRoot(b) ? 0 : 1),
1450
+ );
1418
1451
  for (const f of cmakeLikeFiles) {
1419
1452
  if (DEBUG_MODE) {
1420
1453
  console.log(`Parsing ${f}`);
1421
1454
  }
1422
- const basePath = dirname(f);
1423
1455
  const retMap = parseCmakeLikeFile(f, "generic");
1424
1456
  if (retMap.pkgList?.length) {
1425
1457
  pkgList = pkgList.concat(retMap.pkgList);
1426
1458
  }
1427
1459
  if (
1428
- basePath === path &&
1460
+ atScanRoot(f) &&
1429
1461
  retMap.parentComponent &&
1430
1462
  Object.keys(retMap.parentComponent).length
1431
1463
  ) {
1432
1464
  if (!parentComponent) {
1433
1465
  parentComponent = retMap.parentComponent;
1466
+ // a sub-project reusing the root project's name is the root itself
1467
+ addedParentComponentsMap[retMap.parentComponent.name] = true;
1434
1468
  } else {
1435
1469
  parentComponent.components = parentComponent.components || [];
1436
1470
  if (!addedParentComponentsMap[retMap.parentComponent.name]) {
@@ -1445,6 +1479,7 @@ export function createCppBom(path, options) {
1445
1479
  ) {
1446
1480
  retMap.parentComponent.type = "library";
1447
1481
  pkgList.push(retMap.parentComponent);
1482
+ addedParentComponentsMap[retMap.parentComponent.name] = true;
1448
1483
  }
1449
1484
  // Retain the dependency tree from cmake
1450
1485
  if (retMap.dependenciesList) {
@@ -1464,7 +1499,11 @@ export function createCppBom(path, options) {
1464
1499
  // resolved versions and are left as they are.
1465
1500
  pkgList = pkgList
1466
1501
  .slice(0, beforeCmake)
1467
- .concat(collapseCmakeVersions(pkgList.slice(beforeCmake)));
1502
+ .concat(
1503
+ collapseCmakeVersions(
1504
+ preferFetchedCmakeDependencies(pkgList.slice(beforeCmake)),
1505
+ ),
1506
+ );
1468
1507
  }
1469
1508
  // CMake describes how a project is built from source. An image or an OS
1470
1509
  // installation carries build output, not a build tree, so resolving CMake
@@ -1554,6 +1593,7 @@ export function createCppBom(path, options) {
1554
1593
  { name: "cdx:cmake:resolvedVia", value: "gitclone-script" },
1555
1594
  ],
1556
1595
  };
1596
+ pkgList = withoutScrapedFetchDependency(pkgList, dep.name);
1557
1597
  if (dep.sourceDir) {
1558
1598
  depComponent.properties.push({
1559
1599
  name: "cdx:cmake:sourceDir",
@@ -2024,6 +2064,50 @@ export async function createSwiftBom(path, options) {
2024
2064
  let parentComponent = {};
2025
2065
  const completedPath = [];
2026
2066
  let packageArgsMessageShown = false;
2067
+ // Packages that only the root package's tests use are scoped optional so
2068
+ // that `--required-only` can narrow the BOM. A package some project (or
2069
+ // some production path) needs stays required.
2070
+ const swiftScopes = {
2071
+ optionalRefs: new Set(),
2072
+ requiredRefs: new Set(),
2073
+ optionalNames: new Set(),
2074
+ requiredNames: new Set(),
2075
+ };
2076
+ const classifiedPaths = new Set();
2077
+ const classifyScopes = (basePath, swiftCommand, treeData) => {
2078
+ if (classifiedPaths.has(basePath)) {
2079
+ return;
2080
+ }
2081
+ classifiedPaths.add(basePath);
2082
+ const dumpArgs =
2083
+ swiftCommand === "xcrun"
2084
+ ? ["swift", "package", "dump-package"]
2085
+ : ["package", "dump-package"];
2086
+ const dumpResult = safeSpawnSync(swiftCommand, dumpArgs, {
2087
+ cwd: basePath,
2088
+ });
2089
+ if (!dumpResult?.stdout) {
2090
+ return;
2091
+ }
2092
+ let dumpJson;
2093
+ let treeJson;
2094
+ try {
2095
+ dumpJson = JSON.parse(Buffer.from(dumpResult.stdout).toString());
2096
+ } catch (_e) {
2097
+ return;
2098
+ }
2099
+ try {
2100
+ treeJson = treeData ? JSON.parse(treeData) : undefined;
2101
+ } catch (_e) {
2102
+ treeJson = undefined;
2103
+ }
2104
+ const scopes = classifySwiftDependencyScopes(dumpJson, treeJson);
2105
+ for (const key of Object.keys(swiftScopes)) {
2106
+ for (const value of scopes[key]) {
2107
+ swiftScopes[key].add(value);
2108
+ }
2109
+ }
2110
+ };
2027
2111
  if (pkgResolvedFiles.length) {
2028
2112
  for (const f of pkgResolvedFiles) {
2029
2113
  if (!parentComponent || !Object.keys(parentComponent).length) {
@@ -2147,6 +2231,33 @@ export async function createSwiftBom(path, options) {
2147
2231
  detail: "the swift package command failed",
2148
2232
  });
2149
2233
  }
2234
+ // Scope classification also works without the dependency graph
2235
+ classifyScopes(basePath, swiftCommand, treeData);
2236
+ }
2237
+ }
2238
+ // Scope packages that only tests use as optional. A package some project or
2239
+ // production path needs keeps its default (required) scope; evidence-based
2240
+ // filtering after generation can still promote an optional package when
2241
+ // the sources are found to use it.
2242
+ for (const apkg of pkgList) {
2243
+ if (apkg?.scope || !apkg?.name) {
2244
+ continue;
2245
+ }
2246
+ const refs = [apkg["bom-ref"], apkg.purl].filter(Boolean);
2247
+ const names = [String(apkg.name).toLowerCase()];
2248
+ for (const aproperty of apkg.properties || []) {
2249
+ if (aproperty?.name === "cdx:swift:packageName" && aproperty.value) {
2250
+ names.push(String(aproperty.value).toLowerCase());
2251
+ }
2252
+ }
2253
+ const isOptional =
2254
+ refs.some((r) => swiftScopes.optionalRefs.has(r)) ||
2255
+ names.some((n) => swiftScopes.optionalNames.has(n));
2256
+ const isRequired =
2257
+ refs.some((r) => swiftScopes.requiredRefs.has(r)) ||
2258
+ names.some((n) => swiftScopes.requiredNames.has(n));
2259
+ if (isOptional && !isRequired) {
2260
+ apkg.scope = "optional";
2150
2261
  }
2151
2262
  }
2152
2263
  if (shouldFetchLicense()) {
package/lib/core/env.js CHANGED
@@ -491,7 +491,7 @@ export const PROJECT_TYPE_ALIASES = {
491
491
  "visionos",
492
492
  ],
493
493
  binary: ["binary", "blint"],
494
- oci: ["docker", "oci", "container", "podman", "rootfs", "oci-dir"],
494
+ oci: ["docker", "image", "oci", "container", "podman", "rootfs", "oci-dir"],
495
495
  cocoa: ["cocoa", "cocoapods", "objective-c", "swift", "ios"],
496
496
  scala: ["scala", "scala3", "sbt", "mill"],
497
497
  nix: ["nix", "nixos", "flake"],
@@ -643,11 +643,10 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
643
643
  const allProjectTypes = [...projectTypes];
644
644
  // Convert the project types into base types
645
645
  const baseProjectTypes = [];
646
- // Support for arbitrary versioned ruby type
647
- if (
648
- options.projectType?.length &&
649
- projectTypes.filter((p) => p.startsWith("ruby")).length
650
- ) {
646
+ // Support for arbitrary versioned ruby type: a requested ruby3.4.1 is a
647
+ // ruby project. The test is on the requested types; testing the queried
648
+ // ones made every scan a ruby scan whenever "ruby" was asked about.
649
+ if (options.projectType?.some((p) => p.startsWith("ruby"))) {
651
650
  baseProjectTypes.push("ruby");
652
651
  }
653
652
  // Versioned JVM build tool types such as maven3.9.9 or gradle8.14 are java