@cdxgen/cdxgen 13.0.0 → 13.1.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 (170) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +140 -8
  4. package/bin/convert.js +7 -0
  5. package/bin/evinse.js +1 -1
  6. package/bin/hbom.js +13 -4
  7. package/bin/tracebom.js +9 -1
  8. package/data/component-tags.json +8 -1
  9. package/data/frameworks-list.json +86 -1
  10. package/data/remediations.json +1144 -0
  11. package/data/rules/asar-archives.yaml +1 -1
  12. package/data/rules/build-fidelity.yaml +782 -0
  13. package/data/rules/package-integrity.yaml +13 -13
  14. package/lib/audit/index.js +48 -4
  15. package/lib/audit/scoring.js +2 -8
  16. package/lib/cli/bomAssembly.js +83 -15
  17. package/lib/cli/cliOptions.js +124 -7
  18. package/lib/cli/index.js +76 -48
  19. package/lib/cli/jsBom.js +32 -3
  20. package/lib/cli/jvmBom.js +1494 -1315
  21. package/lib/cli/managedBom.js +97 -28
  22. package/lib/cli/nativeBom.js +189 -10
  23. package/lib/core/buildLedger.js +799 -0
  24. package/lib/core/deferredExit.js +141 -0
  25. package/lib/core/env.js +69 -1
  26. package/lib/core/fs.js +60 -0
  27. package/lib/core/logger.js +76 -25
  28. package/lib/core/severity.js +16 -0
  29. package/lib/core/syncFileWriter.js +106 -0
  30. package/lib/core/ui.js +46 -2
  31. package/lib/ecosystems/ecosystems.js +9 -0
  32. package/lib/ecosystems/gradleutils.js +355 -15
  33. package/lib/ecosystems/parsers-js.js +26 -0
  34. package/lib/ecosystems/parsers-jvm.js +9 -0
  35. package/lib/ecosystems/parsers-misc.js +16 -2
  36. package/lib/ecosystems/parsers-python.js +171 -35
  37. package/lib/ecosystems/parsers-rust.js +12 -2
  38. package/lib/ecosystems/pypiClassifiers.js +326 -0
  39. package/lib/ecosystems/pythonutils.js +49 -1
  40. package/lib/ecosystems/rubyutils.js +13 -0
  41. package/lib/ecosystems/utils.js +9 -1
  42. package/lib/evinser/evinser.js +30 -12
  43. package/lib/helpers/remote/dependency-track.js +81 -14
  44. package/lib/inventory/auditCategories.js +9 -0
  45. package/lib/inventory/depsUtils.js +72 -0
  46. package/lib/inventory/dosai.js +431 -22
  47. package/lib/inventory/envcontext.js +211 -19
  48. package/lib/inventory/fetchBatch.js +57 -0
  49. package/lib/inventory/jvmToolEnv.js +593 -0
  50. package/lib/inventory/osinfo.js +123 -16
  51. package/lib/inventory/table.js +9 -5
  52. package/lib/inventory/toolRequirements.js +903 -0
  53. package/lib/managers/binary.js +216 -40
  54. package/lib/managers/docker.js +26 -5
  55. package/lib/server/server.js +1 -1
  56. package/lib/stages/postgen/annotationBuilder.js +97 -0
  57. package/lib/stages/postgen/auditBom.js +76 -79
  58. package/lib/stages/postgen/introspection/annotate.js +320 -0
  59. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  60. package/lib/stages/postgen/introspection/emit.js +192 -0
  61. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  62. package/lib/stages/postgen/introspection/markers.js +227 -0
  63. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  64. package/lib/stages/postgen/introspection/report.js +1208 -0
  65. package/lib/stages/postgen/introspection/score.js +1067 -0
  66. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  67. package/lib/stages/postgen/parentEdge.js +156 -0
  68. package/lib/stages/postgen/postgen.js +115 -2
  69. package/lib/stages/postgen/ruleEngine.js +17 -0
  70. package/lib/stages/pregen/envAudit.js +42 -2
  71. package/lib/stages/pregen/pregen.js +1171 -6
  72. package/lib/validator/bomValidator.js +11 -2
  73. package/package.json +14 -14
  74. package/types/lib/audit/index.d.ts.map +1 -1
  75. package/types/lib/audit/scoring.d.ts +2 -8
  76. package/types/lib/audit/scoring.d.ts.map +1 -1
  77. package/types/lib/cli/bomAssembly.d.ts +14 -0
  78. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  79. package/types/lib/cli/cliOptions.d.ts +2 -1
  80. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  81. package/types/lib/cli/index.d.ts +8 -0
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/cli/jsBom.d.ts.map +1 -1
  84. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  85. package/types/lib/cli/managedBom.d.ts.map +1 -1
  86. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  87. package/types/lib/core/buildLedger.d.ts +272 -0
  88. package/types/lib/core/buildLedger.d.ts.map +1 -0
  89. package/types/lib/core/deferredExit.d.ts +84 -0
  90. package/types/lib/core/deferredExit.d.ts.map +1 -0
  91. package/types/lib/core/env.d.ts +22 -1
  92. package/types/lib/core/env.d.ts.map +1 -1
  93. package/types/lib/core/fs.d.ts +21 -0
  94. package/types/lib/core/fs.d.ts.map +1 -1
  95. package/types/lib/core/logger.d.ts +14 -1
  96. package/types/lib/core/logger.d.ts.map +1 -1
  97. package/types/lib/core/severity.d.ts +11 -0
  98. package/types/lib/core/severity.d.ts.map +1 -0
  99. package/types/lib/core/syncFileWriter.d.ts +33 -0
  100. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  101. package/types/lib/core/ui.d.ts +1 -1
  102. package/types/lib/core/ui.d.ts.map +1 -1
  103. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  104. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  105. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  109. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  110. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  111. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  112. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  113. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  114. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  115. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  116. package/types/lib/ecosystems/utils.d.ts +4 -4
  117. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  118. package/types/lib/evinser/evinser.d.ts +5 -1
  119. package/types/lib/evinser/evinser.d.ts.map +1 -1
  120. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  121. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  122. package/types/lib/inventory/auditCategories.d.ts +8 -0
  123. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  125. package/types/lib/inventory/dosai.d.ts +20 -0
  126. package/types/lib/inventory/dosai.d.ts.map +1 -1
  127. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  128. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  129. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  130. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  131. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  132. package/types/lib/inventory/osinfo.d.ts +83 -2
  133. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  134. package/types/lib/inventory/table.d.ts +4 -1
  135. package/types/lib/inventory/table.d.ts.map +1 -1
  136. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  137. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  138. package/types/lib/managers/binary.d.ts +71 -0
  139. package/types/lib/managers/binary.d.ts.map +1 -1
  140. package/types/lib/managers/docker.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  142. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  144. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  145. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  146. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  147. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  148. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  149. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  150. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  151. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  152. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  153. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  154. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  155. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  156. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  157. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  158. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  159. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  160. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  161. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  162. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  163. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  164. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  165. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  166. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  167. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  168. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  169. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  170. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -6,6 +6,8 @@ import process from "node:process";
6
6
  import { build } from "@cdxgen/cdx-purl";
7
7
 
8
8
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
9
+ import { recordDegradation } from "../core/buildLedger.js";
10
+ import { deferFailOnError } from "../core/deferredExit.js";
9
11
  import {
10
12
  DOTNET_CMD,
11
13
  hasAnyProjectType,
@@ -94,6 +96,7 @@ import {
94
96
  trimComponents,
95
97
  } from "../inventory/depsUtils.js";
96
98
  import {
99
+ collectDosaiServiceComponents,
97
100
  collectDosaiServicesFromMethods,
98
101
  createDosaiMethodsSlice,
99
102
  isDosaiDotnetLanguage,
@@ -102,6 +105,7 @@ import {
102
105
  } from "../inventory/dosai.js";
103
106
  import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
104
107
  import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
108
+ import { classifyProbeResult } from "../inventory/toolRequirements.js";
105
109
  import { getPluginToolComponents } from "../managers/binary.js";
106
110
  import { getTreeWithPlugin } from "../managers/piptree.js";
107
111
  import {
@@ -408,7 +412,11 @@ export async function createPythonBom(path, options) {
408
412
  }
409
413
  let retMap = await parsePyLockData(lockData, f);
410
414
  if (retMap?.workspaceWarningShown) {
411
- options.failOnError && process.exit(1);
415
+ deferFailOnError(options, {
416
+ ecosystem: "python",
417
+ tool: "python",
418
+ detail: "the python lock file could not be parsed",
419
+ });
412
420
  }
413
421
  if (retMap?.pyLockProperties?.length) {
414
422
  parentComponent.properties = parentComponent.properties || [];
@@ -458,15 +466,6 @@ export async function createPythonBom(path, options) {
458
466
  retMap.dependenciesList,
459
467
  parentComponent,
460
468
  );
461
- if (retMap.rootList) {
462
- const parentDependsOn = new Set();
463
- for (const p of retMap.rootList)
464
- parentDependsOn.add(pypiBomRef(p.name, p.version));
465
- dependencies.splice(0, 0, {
466
- ref: parentComponent["bom-ref"],
467
- dependsOn: [...parentDependsOn].sort(),
468
- });
469
- }
470
469
  } else {
471
470
  let exportedReqs = "";
472
471
  if (f.endsWith("poetry.lock")) {
@@ -493,6 +492,11 @@ export async function createPythonBom(path, options) {
493
492
  exportedReqs = expCmd.stdout.toString();
494
493
  }
495
494
  if (exportedReqs) {
495
+ // The export contains the resolved closure, not just the first
496
+ // level, so it contributes components only. The root edge comes
497
+ // from parsePyLockData (seeded from the manifest); for locks
498
+ // without a neighbouring pyproject.toml the generic postgen
499
+ // parent-edge repair keeps the tree reachable.
496
500
  const tmpReqFile = join(
497
501
  tempDir,
498
502
  `exported-${basename(basePath)}-reqs.txt`,
@@ -501,13 +505,6 @@ export async function createPythonBom(path, options) {
501
505
  const dlist = await parseReqFile(tmpReqFile, false);
502
506
  if (dlist?.length) {
503
507
  pkgList = pkgList.concat(dlist);
504
- const parentDependsOn = new Set();
505
- for (const p of dlist)
506
- parentDependsOn.add(pypiBomRef(p.name, p.version));
507
- dependencies.splice(0, 0, {
508
- ref: parentComponent["bom-ref"],
509
- dependsOn: [...parentDependsOn].sort(),
510
- });
511
508
  }
512
509
  }
513
510
  }
@@ -577,14 +574,30 @@ export async function createPythonBom(path, options) {
577
574
  safeSpawnSync("pipenv", ["install"], { cwd: path, shell: isWin });
578
575
  const piplockFile = join(path, "Pipfile.lock");
579
576
  if (safeExistsSync(piplockFile)) {
580
- const lockData = JSON.parse(readFileSync(piplockFile));
581
- const dlist = await parsePiplockData(lockData);
577
+ let dlist;
578
+ try {
579
+ const lockData = JSON.parse(readFileSync(piplockFile));
580
+ dlist = await parsePiplockData(lockData);
581
+ } catch (err) {
582
+ recordDegradation("python.lockfile-unparseable.pipenv", {
583
+ ecosystem: "python",
584
+ impact: "transitive-deps",
585
+ path: piplockFile,
586
+ detail:
587
+ "The Pipfile.lock could not be parsed, so no locked versions were captured from it.",
588
+ });
589
+ console.log(`Error while parsing the lock file ${piplockFile}.`, err);
590
+ }
582
591
  if (dlist?.length) {
583
592
  pkgList = pkgList.concat(dlist);
584
593
  }
585
594
  } else {
586
595
  console.error("Pipfile.lock not found at", path);
587
- options.failOnError && process.exit(1);
596
+ deferFailOnError(options, {
597
+ ecosystem: "python",
598
+ tool: "pipenv",
599
+ detail: "Pipfile.lock was not found",
600
+ });
588
601
  }
589
602
  } else if (reqDirFiles?.length) {
590
603
  for (const j in reqDirFiles) {
@@ -1087,13 +1100,33 @@ export function createPHPBom(path, options) {
1087
1100
  shell: isWin,
1088
1101
  });
1089
1102
  if (versionResult.status !== 0 || versionResult.error) {
1103
+ // A composer the runtime or a policy denied is not an unavailable
1104
+ // composer; the version probes report that as degraded evidence.
1105
+ if (classifyProbeResult(versionResult) === "missing") {
1106
+ recordDegradation("php.composer.missing", {
1107
+ ecosystem: "php",
1108
+ tool: "composer",
1109
+ impact: "components",
1110
+ command: "composer --version",
1111
+ exitCode:
1112
+ typeof versionResult.status === "number"
1113
+ ? versionResult.status
1114
+ : undefined,
1115
+ detail:
1116
+ "The composer executable could not be run, so the composer.lock generation could not be attempted.",
1117
+ });
1118
+ }
1090
1119
  console.error(
1091
1120
  "No composer version found. Check if composer is installed and available in PATH.",
1092
1121
  );
1093
1122
  if (DEBUG_MODE) {
1094
1123
  console.log(versionResult.error, versionResult.stderr);
1095
1124
  }
1096
- options.failOnError && process.exit(1);
1125
+ deferFailOnError(options, {
1126
+ ecosystem: "php",
1127
+ tool: "composer",
1128
+ detail: "the composer version command failed",
1129
+ });
1097
1130
  }
1098
1131
  let composerVersion;
1099
1132
  if (DEBUG_MODE) {
@@ -1129,7 +1162,11 @@ export function createPHPBom(path, options) {
1129
1162
  console.log(result.stderr);
1130
1163
  }
1131
1164
  console.log(result.error);
1132
- options.failOnError && process.exit(1);
1165
+ deferFailOnError(options, {
1166
+ ecosystem: "php",
1167
+ tool: "composer",
1168
+ detail: "the composer install command failed",
1169
+ });
1133
1170
  }
1134
1171
  }
1135
1172
  }
@@ -1206,6 +1243,15 @@ export function createPHPBom(path, options) {
1206
1243
  parentComponent,
1207
1244
  });
1208
1245
  }
1246
+ if (composerJsonMode && !composerLockMode) {
1247
+ recordDegradation("php.no-lockfile", {
1248
+ ecosystem: "php",
1249
+ tool: "composer",
1250
+ impact: "components",
1251
+ detail:
1252
+ "No composer.lock was found or generated, and the lock file is the source the Composer parser reads.",
1253
+ });
1254
+ }
1209
1255
  return {};
1210
1256
  }
1211
1257
 
@@ -1319,7 +1365,11 @@ export async function createRubyBom(path, options) {
1319
1365
  "Bundle install has failed. Check if bundle is installed and available in PATH.",
1320
1366
  );
1321
1367
  console.log(result.error, result.stderr);
1322
- options.failOnError && process.exit(1);
1368
+ deferFailOnError(options, {
1369
+ ecosystem: "ruby",
1370
+ tool: "bundler",
1371
+ detail: "the bundler command failed",
1372
+ });
1323
1373
  }
1324
1374
  }
1325
1375
  }
@@ -1361,7 +1411,11 @@ export async function createRubyBom(path, options) {
1361
1411
  console.log(
1362
1412
  "SBOM generation using only gemspec files is imprecise and results in an inaccurate dependency tree.",
1363
1413
  );
1364
- options.failOnError && process.exit(1);
1414
+ deferFailOnError(options, {
1415
+ ecosystem: "ruby",
1416
+ tool: "bundler",
1417
+ detail: "only gemspec files were available to parse",
1418
+ });
1365
1419
  }
1366
1420
  for (const f of gemspecFiles) {
1367
1421
  const gemspecData = readFileSync(f, { encoding: "utf-8" });
@@ -1634,7 +1688,11 @@ export async function createCsharpBom(path, options) {
1634
1688
  "TIP: msbuild is not available for Linux. Try using a Windows build agent to generate an SBOM for this project.",
1635
1689
  );
1636
1690
  }
1637
- options.failOnError && process.exit(1);
1691
+ deferFailOnError(options, {
1692
+ ecosystem: "csharp",
1693
+ tool: "dotnet",
1694
+ detail: "the dotnet command failed",
1695
+ });
1638
1696
  }
1639
1697
  if (result?.stderr?.includes("To install missing framework")) {
1640
1698
  console.log(
@@ -1683,7 +1741,11 @@ export async function createCsharpBom(path, options) {
1683
1741
  console.log(result.stdout);
1684
1742
  }
1685
1743
  console.log("---------");
1686
- options.failOnError && process.exit(1);
1744
+ deferFailOnError(options, {
1745
+ ecosystem: "csharp",
1746
+ tool: "dotnet",
1747
+ detail: "the dotnet restore did not produce a usable assets file",
1748
+ });
1687
1749
  }
1688
1750
  }
1689
1751
  // Collect the assets, lock, and nupkg files generated from restore
@@ -1786,7 +1848,11 @@ export async function createCsharpBom(path, options) {
1786
1848
  console.log(
1787
1849
  "Alternatively, try using the custom `ghcr.io/cdxgen/cdxgen-dotnet:v13` container image, which bundles a range of dotnet SDKs.",
1788
1850
  );
1789
- options.failOnError && process.exit(1);
1851
+ deferFailOnError(options, {
1852
+ ecosystem: "csharp",
1853
+ tool: "dotnet",
1854
+ detail: "no dotnet dependency evidence could be collected",
1855
+ });
1790
1856
  }
1791
1857
  }
1792
1858
  // packages.lock.json parsing for any project without a project.assets.json
@@ -1960,7 +2026,10 @@ export async function createCsharpBom(path, options) {
1960
2026
  }
1961
2027
  pkgList = addEvidenceForDotnet(pkgList, slicesFile);
1962
2028
  const methodsSlice = readDosaiJsonFile(slicesFile);
1963
- const servicesMap = collectDosaiServicesFromMethods(methodsSlice, {});
2029
+ // Provider-backed Services[] first so bom-refs, trust zones, and data
2030
+ // classifications survive; ApiEndpoints derivation fills in the rest.
2031
+ const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
2032
+ collectDosaiServicesFromMethods(methodsSlice, servicesMap);
1964
2033
  services = normalizeDosaiServiceMap(servicesMap);
1965
2034
  }
1966
2035
  }
@@ -8,6 +8,8 @@ import { build } from "@cdxgen/cdx-purl";
8
8
  import { parse as loadYaml } from "yaml";
9
9
 
10
10
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
11
+ import { recordDegradation } from "../core/buildLedger.js";
12
+ import { deferFailOnError } from "../core/deferredExit.js";
11
13
  import {
12
14
  CARGO_CMD,
13
15
  CLJ_CMD,
@@ -86,6 +88,10 @@ import { resolveZigGraph } from "../ecosystems/zigResolver.js";
86
88
  import { mergeDependencies, trimComponents } from "../inventory/depsUtils.js";
87
89
  import { convertOSQueryResults } from "../inventory/evidenceUtils.js";
88
90
  import { applyPurl, nixBomRef, tryBuildPurl } from "../inventory/purl.js";
91
+ import {
92
+ classifyProbeResult,
93
+ classifySpawnRestriction,
94
+ } from "../inventory/toolRequirements.js";
89
95
  import { executeOsQuery } from "../managers/binary.js";
90
96
  import {
91
97
  buildBomNSData,
@@ -357,13 +363,34 @@ export async function createGoBom(path, options) {
357
363
  // So we support a fallback to only operate with go mod graph command output in such instances
358
364
  console.log("go list -deps command has failed for", basePath);
359
365
  shouldManuallyParse = true;
366
+ // A spawn the runtime or a policy denied is neither a missing
367
+ // toolchain nor an engaged fallback; the version probes report
368
+ // that as degraded evidence and the policy entries name the fix.
369
+ if (
370
+ !classifySpawnRestriction("go") &&
371
+ result.error &&
372
+ classifyProbeResult(result) === "missing"
373
+ ) {
374
+ recordDegradation("go.toolchain.missing", {
375
+ ecosystem: "go",
376
+ tool: "go",
377
+ impact: "transitive-deps",
378
+ command: "go list -deps ./...",
379
+ detail:
380
+ "The go executable could not be run, so the module graph was not resolved.",
381
+ });
382
+ }
360
383
  if (DEBUG_MODE && result.stdout) {
361
384
  console.log(result.stdout);
362
385
  }
363
386
  if (DEBUG_MODE && result.stderr) {
364
387
  console.log(result.stderr);
365
388
  }
366
- options.failOnError && process.exit(1);
389
+ deferFailOnError(options, {
390
+ ecosystem: "go",
391
+ tool: "go",
392
+ detail: "the go module graph command failed",
393
+ });
367
394
  }
368
395
  const stdout = result.stdout;
369
396
  if (stdout) {
@@ -411,7 +438,11 @@ export async function createGoBom(path, options) {
411
438
  if (DEBUG_MODE && result.stderr) {
412
439
  console.log(result.stderr);
413
440
  }
414
- options.failOnError && process.exit(1);
441
+ deferFailOnError(options, {
442
+ ecosystem: "go",
443
+ tool: "go",
444
+ detail: "the go list command failed",
445
+ });
415
446
  }
416
447
  if (result.stdout) {
417
448
  cmdOutput = Buffer.from(result.stdout).toString();
@@ -492,7 +523,11 @@ export async function createGoBom(path, options) {
492
523
  console.log(
493
524
  "3. Alternatively, consider generating a post-build SBOM from the built binary using blint. Use the official container image and invoke cdxgen with the arguments `-t binary --lifecycle post-build`.",
494
525
  );
495
- options.failOnError && process.exit(1);
526
+ deferFailOnError(options, {
527
+ ecosystem: "go",
528
+ tool: "go",
529
+ detail: "no go dependency evidence could be collected",
530
+ });
496
531
  }
497
532
  }
498
533
  }
@@ -510,6 +545,19 @@ export async function createGoBom(path, options) {
510
545
  if (
511
546
  !hasAnyProjectType(["docker", "oci", "container", "os"], options, false)
512
547
  ) {
548
+ // When the environment itself denies the go command, the version
549
+ // probes record the restriction as degraded evidence and the policy
550
+ // entries name the fix; the fallback is the finding only when the
551
+ // toolchain was free to run and could not resolve the graph.
552
+ if (!classifySpawnRestriction("go")) {
553
+ recordDegradation("go.mod-only-fallback", {
554
+ ecosystem: "go",
555
+ tool: "go",
556
+ impact: "transitive-deps",
557
+ detail:
558
+ "The go toolchain could not resolve the module graph, so go.mod was parsed manually and components carry only the declared requires.",
559
+ });
560
+ }
513
561
  console.log(
514
562
  "Manually parsing go.mod files. The resultant BOM would be incomplete.",
515
563
  );
@@ -584,6 +632,31 @@ export async function createGoBom(path, options) {
584
632
  return {};
585
633
  }
586
634
 
635
+ /**
636
+ * Whether the cargo executable answers a version probe.
637
+ *
638
+ * A cargo command run through a shell — the Windows path — reports an absent
639
+ * executable as a non-zero exit with the shell's own complaint on stderr, not
640
+ * as a spawn error, so a failed build alone cannot tell a missing toolchain
641
+ * from a broken project. The probe answers that question directly.
642
+ *
643
+ * @param {string} cwd Directory to probe from.
644
+ * @returns {boolean} True when cargo answered.
645
+ */
646
+ function cargoAnswersVersion(cwd) {
647
+ const probe = safeSpawnSync(CARGO_CMD, ["--version"], {
648
+ cdxgenActivity: {
649
+ kind: "probe",
650
+ metadata: { tool: "cargo" },
651
+ probeType: "build-tool-availability",
652
+ },
653
+ cwd,
654
+ encoding: "utf-8",
655
+ shell: isWin,
656
+ });
657
+ return classifyProbeResult(probe) === "found";
658
+ }
659
+
587
660
  /**
588
661
  * Function to create bom string for Rust projects
589
662
  *
@@ -649,7 +722,27 @@ export async function createRustBom(path, options) {
649
722
  if (cargoInstallResult.status !== 0 || cargoInstallResult.error) {
650
723
  console.error("Error running the cargo command");
651
724
  console.log(cargoInstallResult.error, cargoInstallResult.stderr);
652
- options.failOnError && process.exit(1);
725
+ // A spawn the runtime or a policy denied is neither a missing
726
+ // toolchain nor an engaged fallback; the version probes record
727
+ // that as degraded evidence.
728
+ const toolchainMissing = cargoInstallResult.error
729
+ ? classifyProbeResult(cargoInstallResult) === "missing"
730
+ : !cargoAnswersVersion(basePath);
731
+ if (!classifySpawnRestriction(CARGO_CMD) && toolchainMissing) {
732
+ recordDegradation("rust.toolchain.missing", {
733
+ ecosystem: "rust",
734
+ tool: "cargo",
735
+ impact: "transitive-deps",
736
+ command: `${basename(CARGO_CMD)} ${cargoArgs.join(" ")}`,
737
+ detail:
738
+ "The cargo executable could not be run, so the lock file could not be generated.",
739
+ });
740
+ }
741
+ deferFailOnError(options, {
742
+ ecosystem: "rust",
743
+ tool: "cargo",
744
+ detail: "the cargo command failed",
745
+ });
653
746
  }
654
747
  }
655
748
  }
@@ -667,6 +760,15 @@ export async function createRustBom(path, options) {
667
760
  options,
668
761
  );
669
762
  const cargoLockMode = cargoLockFiles.length;
763
+ if (cargoFiles.length && !cargoLockMode) {
764
+ recordDegradation("rust.cargo-lock-missing", {
765
+ ecosystem: "rust",
766
+ tool: "cargo",
767
+ impact: "transitive-deps",
768
+ detail:
769
+ "No Cargo.lock was found beside the manifest, so components carry the declared dependencies without locked versions or the resolved crate graph.",
770
+ });
771
+ }
670
772
  for (const f of cargoFiles) {
671
773
  if (DEBUG_MODE) {
672
774
  console.log(`Parsing ${f}`);
@@ -868,6 +970,15 @@ export async function createDartBom(path, options) {
868
970
  `${options.multiProject ? "**/" : ""}pubspec.yaml`,
869
971
  options,
870
972
  );
973
+ if (pubSpecYamlFiles.length && !pubFiles.length) {
974
+ recordDegradation("dart.pub-get-needed", {
975
+ ecosystem: "dart",
976
+ tool: "dart",
977
+ impact: "versions",
978
+ detail:
979
+ "pubspec.yaml was parsed without pubspec.lock, so components carry version constraints instead of the locked versions.",
980
+ });
981
+ }
871
982
  let dependencies = [];
872
983
  let pkgList = [];
873
984
  let parentComponent;
@@ -1367,8 +1478,22 @@ export function createClojureBom(path, options) {
1367
1478
  if (result.status !== 0 || result.error) {
1368
1479
  if (result.stderr) {
1369
1480
  console.error(result.stdout, result.stderr);
1370
- options.failOnError && process.exit(1);
1481
+ deferFailOnError(options, {
1482
+ ecosystem: "clojure",
1483
+ tool: "lein",
1484
+ detail: "the leiningen command failed",
1485
+ });
1371
1486
  }
1487
+ recordDegradation("clojure.lein.missing", {
1488
+ ecosystem: "clojure",
1489
+ tool: "lein",
1490
+ impact: "transitive-deps",
1491
+ command: `${LEIN_CMD} ${LEIN_ARGS.join(" ")}`,
1492
+ exitCode:
1493
+ typeof result.status === "number" ? result.status : undefined,
1494
+ detail:
1495
+ "lein did not produce a dependency list, so the project.clj manifest is parsed manually.",
1496
+ });
1372
1497
  console.log(
1373
1498
  "Check if the correct version of lein is installed and available in PATH. Falling back to manual parsing.",
1374
1499
  );
@@ -1390,7 +1515,11 @@ export function createClojureBom(path, options) {
1390
1515
  }
1391
1516
  } else {
1392
1517
  console.error("lein unexpectedly didn't return any output");
1393
- options.failOnError && process.exit(1);
1518
+ deferFailOnError(options, {
1519
+ ecosystem: "clojure",
1520
+ tool: "lein",
1521
+ detail: "the leiningen command produced no output",
1522
+ });
1394
1523
  }
1395
1524
  }
1396
1525
  }
@@ -1413,8 +1542,22 @@ export function createClojureBom(path, options) {
1413
1542
  if (result.status !== 0 || result.error) {
1414
1543
  if (result.stderr) {
1415
1544
  console.error(result.stdout, result.stderr);
1416
- options.failOnError && process.exit(1);
1545
+ deferFailOnError(options, {
1546
+ ecosystem: "clojure",
1547
+ tool: "clj",
1548
+ detail: "the clojure cli command failed",
1549
+ });
1417
1550
  }
1551
+ recordDegradation("clojure.cli.missing", {
1552
+ ecosystem: "clojure",
1553
+ tool: "clojure",
1554
+ impact: "transitive-deps",
1555
+ command: `${CLJ_CMD} ${CLJ_ARGS.join(" ")}`,
1556
+ exitCode:
1557
+ typeof result.status === "number" ? result.status : undefined,
1558
+ detail:
1559
+ "The clojure CLI did not produce a dependency tree, so the deps.edn manifest is parsed manually.",
1560
+ });
1418
1561
  console.log(
1419
1562
  "Check if the correct version of clojure cli is installed and available in PATH. Falling back to manual parsing.",
1420
1563
  );
@@ -1436,7 +1579,11 @@ export function createClojureBom(path, options) {
1436
1579
  }
1437
1580
  } else {
1438
1581
  console.error("clj unexpectedly didn't return any output");
1439
- options.failOnError && process.exit(1);
1582
+ deferFailOnError(options, {
1583
+ ecosystem: "clojure",
1584
+ tool: "clj",
1585
+ detail: "the clojure cli command produced no output",
1586
+ });
1440
1587
  }
1441
1588
  }
1442
1589
  }
@@ -1479,6 +1626,13 @@ export function createHaskellBom(path, options) {
1479
1626
  filename: cabalFiles.join(", "),
1480
1627
  });
1481
1628
  }
1629
+ recordDegradation("haskell.freeze-missing", {
1630
+ ecosystem: "haskell",
1631
+ tool: "cabal",
1632
+ impact: "components",
1633
+ detail:
1634
+ "No cabal.project.freeze was found, and the freeze file is the source the parser reads for the package set.",
1635
+ });
1482
1636
  return {};
1483
1637
  }
1484
1638
 
@@ -1512,6 +1666,13 @@ export function createElixirBom(path, options) {
1512
1666
  filename: mixFiles.join(", "),
1513
1667
  });
1514
1668
  }
1669
+ recordDegradation("elixir.deps-not-fetched", {
1670
+ ecosystem: "elixir",
1671
+ tool: "mix",
1672
+ impact: "components",
1673
+ detail:
1674
+ "No mix.lock was found, and the lock file written by fetching the dependencies is the source the parser reads.",
1675
+ });
1515
1676
  return {};
1516
1677
  }
1517
1678
 
@@ -1634,6 +1795,16 @@ export async function createSwiftBom(path, options) {
1634
1795
  `It looks like the 'Sources' directory is missing, so we are missing the components and dependencies for '${basename(basePath)}'.`,
1635
1796
  );
1636
1797
  } else if (readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true") {
1798
+ recordDegradation("swift.toolchain.missing", {
1799
+ ecosystem: "swift",
1800
+ tool: "swift",
1801
+ impact: "transitive-deps",
1802
+ command: `${swiftCommand} ${packageArgs.join(" ")}`,
1803
+ exitCode:
1804
+ typeof result.status === "number" ? result.status : undefined,
1805
+ detail:
1806
+ "The swift package command did not succeed, so the swift package graph was not resolved.",
1807
+ });
1637
1808
  console.log(
1638
1809
  "Consider using the cdxgen container image (`ghcr.io/cdxgen/cdxgen`), which includes Swift and additional build tools.",
1639
1810
  );
@@ -1645,7 +1816,11 @@ export async function createSwiftBom(path, options) {
1645
1816
  }
1646
1817
  }
1647
1818
  console.error(result.stderr);
1648
- options.failOnError && process.exit(1);
1819
+ deferFailOnError(options, {
1820
+ ecosystem: "swift",
1821
+ tool: "swift",
1822
+ detail: "the swift package command failed",
1823
+ });
1649
1824
  }
1650
1825
  }
1651
1826
  }
@@ -1691,7 +1866,11 @@ export async function createCocoaBom(path, options) {
1691
1866
  "No 'Podfile.lock' found and '--no-install-deps' is set -- A Podfile.lock is needed to parse dependencies!",
1692
1867
  );
1693
1868
  missingLockWarningShown = true;
1694
- options.failOnError && process.exit(1);
1869
+ deferFailOnError(options, {
1870
+ ecosystem: "cocoa",
1871
+ tool: "pod",
1872
+ detail: "no Podfile.lock was found",
1873
+ });
1695
1874
  }
1696
1875
  } else if (options.deep && options.installDeps) {
1697
1876
  executePodCommand(["install"], projectPath, options);