@cdxgen/cdxgen 13.4.0 → 13.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.
Files changed (208) hide show
  1. package/README.md +7 -7
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +25 -5
  4. package/bin/evinse.js +5 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/data/cbomosdb-queries.json +13 -1
  8. package/data/component-tags.json +8 -0
  9. package/data/queries-darwin.json +25 -1
  10. package/data/queries-win.json +19 -1
  11. package/data/queries.json +41 -0
  12. package/data/remediations.json +283 -1
  13. package/data/rules/cbom-security.yaml +235 -4
  14. package/data/rules/go-binaries.yaml +46 -0
  15. package/data/rules/golem-go.yaml +80 -0
  16. package/data/rules/hbom-compliance.yaml +40 -22
  17. package/data/rules/hbom-performance.yaml +7 -1
  18. package/data/rules/host-topology.yaml +1 -1
  19. package/data/rules/obom-runtime.yaml +31 -2
  20. package/data/rules/php-runtime.yaml +143 -0
  21. package/lib/audit/index.js +14 -0
  22. package/lib/cli/bomAssembly.js +28 -3
  23. package/lib/cli/cliOptions.js +110 -3
  24. package/lib/cli/index.js +73 -4
  25. package/lib/cli/jvmBom.js +126 -20
  26. package/lib/cli/managedBom.js +711 -71
  27. package/lib/cli/nativeBom.js +225 -28
  28. package/lib/core/env.js +30 -8
  29. package/lib/core/fs.js +28 -19
  30. package/lib/core/httpClient.js +74 -45
  31. package/lib/core/projectConfig.js +1 -0
  32. package/lib/core/projectTooling.js +97 -0
  33. package/lib/core/propertySanitizer.js +72 -0
  34. package/lib/ecosystems/golem.js +550 -6
  35. package/lib/ecosystems/gradleutils.js +30 -20
  36. package/lib/ecosystems/parsers-dotnet.js +117 -18
  37. package/lib/ecosystems/parsers-go.js +1090 -367
  38. package/lib/ecosystems/parsers-jvm.js +259 -17
  39. package/lib/ecosystems/parsers-misc.js +229 -44
  40. package/lib/ecosystems/parsers-python.js +11 -10
  41. package/lib/ecosystems/rubyutils.js +6 -3
  42. package/lib/ecosystems/rusi.js +10 -0
  43. package/lib/ecosystems/scalacliutils.js +2 -1
  44. package/lib/ecosystems/scalasem.js +3 -1
  45. package/lib/evinser/evinser.js +126 -18
  46. package/lib/evinser/swiftsem.js +68 -5
  47. package/lib/inventory/agentSkillSpec.js +17 -0
  48. package/lib/inventory/apkInstalledDb.js +243 -0
  49. package/lib/inventory/atomUtils.js +33 -0
  50. package/lib/inventory/auditCategories.js +3 -3
  51. package/lib/inventory/cacheDir.js +52 -9
  52. package/lib/inventory/certificates.js +2371 -0
  53. package/lib/inventory/communityAiConfigParser.js +4 -9
  54. package/lib/inventory/deps.js +6 -4
  55. package/lib/inventory/display.js +200 -3
  56. package/lib/inventory/dosai.js +500 -58
  57. package/lib/inventory/dynamic.js +45 -3
  58. package/lib/inventory/envcontext.js +168 -5
  59. package/lib/inventory/evidenceUtils.js +436 -23
  60. package/lib/inventory/formulationParsers.js +3 -2
  61. package/lib/inventory/goBuildInfo.js +140 -0
  62. package/lib/inventory/hbom.js +67 -7
  63. package/lib/inventory/hbomAnalysis.js +16 -0
  64. package/lib/inventory/hostTopology.js +21 -0
  65. package/lib/inventory/osPackagePurl.js +244 -0
  66. package/lib/inventory/osPackageResolver.js +89 -87
  67. package/lib/inventory/osinfo.js +79 -31
  68. package/lib/inventory/osqueryTransform.js +114 -5
  69. package/lib/inventory/phpBuildInfo.js +487 -0
  70. package/lib/inventory/phpHostProbe.js +641 -0
  71. package/lib/inventory/rootfsCerts.js +426 -0
  72. package/lib/inventory/source.js +207 -3
  73. package/lib/inventory/spdx.js +144 -0
  74. package/lib/inventory/traceRunner.js +5 -1
  75. package/lib/inventory/tracedCrypto.js +129 -0
  76. package/lib/managers/binary.js +529 -183
  77. package/lib/managers/docker.js +166 -12
  78. package/lib/parsers/email.js +277 -0
  79. package/lib/parsers/largeJson.js +34 -2
  80. package/lib/server/server.js +186 -1
  81. package/lib/stages/postgen/annotator.js +17 -1
  82. package/lib/stages/postgen/introspection/reflect.js +4 -1
  83. package/lib/stages/postgen/postgen.js +5 -5
  84. package/lib/stages/postgen/ruleEngine.js +58 -1
  85. package/lib/stages/postgen/specVersionCompat.js +30 -0
  86. package/lib/stages/pregen/pregen.js +117 -5
  87. package/lib/validator/bomValidator.js +26 -5
  88. package/lib/validator/generated/runtime.mjs +13 -0
  89. package/lib/validator/generated/validate-1.6.mjs +26 -7
  90. package/lib/validator/generated/validate-1.7.mjs +31 -12
  91. package/lib/validator/generated/validate-2.0.mjs +30 -11
  92. package/package.json +17 -17
  93. package/types/bin/cdxgen-secure.d.ts +3 -0
  94. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  95. package/types/bin/repl.d.ts.map +1 -1
  96. package/types/lib/audit/index.d.ts.map +1 -1
  97. package/types/lib/cli/bomAssembly.d.ts +9 -2
  98. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  99. package/types/lib/cli/cliOptions.d.ts +43 -0
  100. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  101. package/types/lib/cli/index.d.ts.map +1 -1
  102. package/types/lib/cli/jvmBom.d.ts +20 -0
  103. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  104. package/types/lib/cli/managedBom.d.ts.map +1 -1
  105. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  106. package/types/lib/core/env.d.ts +18 -2
  107. package/types/lib/core/env.d.ts.map +1 -1
  108. package/types/lib/core/fs.d.ts.map +1 -1
  109. package/types/lib/core/httpClient.d.ts.map +1 -1
  110. package/types/lib/core/projectConfig.d.ts.map +1 -1
  111. package/types/lib/core/projectTooling.d.ts +41 -0
  112. package/types/lib/core/projectTooling.d.ts.map +1 -0
  113. package/types/lib/core/propertySanitizer.d.ts +26 -0
  114. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  115. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  116. package/types/lib/ecosystems/golem.d.ts +81 -1
  117. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  118. package/types/lib/ecosystems/gradleutils.d.ts +10 -8
  119. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  121. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  123. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  125. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  126. package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
  127. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  128. package/types/lib/ecosystems/parsers-python.d.ts +4 -3
  129. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  130. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  131. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  132. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  133. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  134. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  135. package/types/lib/evinser/evinser.d.ts +25 -0
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/evinser/swiftsem.d.ts +10 -0
  138. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  139. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  140. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  141. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  142. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  143. package/types/lib/inventory/atomUtils.d.ts +14 -0
  144. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  145. package/types/lib/inventory/cacheDir.d.ts +12 -4
  146. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  147. package/types/lib/inventory/certificates.d.ts +205 -0
  148. package/types/lib/inventory/certificates.d.ts.map +1 -0
  149. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  150. package/types/lib/inventory/deps.d.ts +2 -1
  151. package/types/lib/inventory/deps.d.ts.map +1 -1
  152. package/types/lib/inventory/display.d.ts +18 -0
  153. package/types/lib/inventory/display.d.ts.map +1 -1
  154. package/types/lib/inventory/dosai.d.ts +145 -6
  155. package/types/lib/inventory/dosai.d.ts.map +1 -1
  156. package/types/lib/inventory/dynamic.d.ts +6 -1
  157. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  158. package/types/lib/inventory/envcontext.d.ts +40 -2
  159. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  160. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  161. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  162. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  163. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  164. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  165. package/types/lib/inventory/hbom.d.ts.map +1 -1
  166. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  167. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  168. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  169. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  170. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  171. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  172. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  173. package/types/lib/inventory/osinfo.d.ts +31 -6
  174. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  175. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  176. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  177. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  178. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  179. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  180. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  181. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  182. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  183. package/types/lib/inventory/source.d.ts +28 -0
  184. package/types/lib/inventory/source.d.ts.map +1 -1
  185. package/types/lib/inventory/spdx.d.ts +15 -0
  186. package/types/lib/inventory/spdx.d.ts.map +1 -1
  187. package/types/lib/inventory/traceRunner.d.ts +4 -1
  188. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  189. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  190. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  191. package/types/lib/managers/binary.d.ts +10 -0
  192. package/types/lib/managers/binary.d.ts.map +1 -1
  193. package/types/lib/managers/docker.d.ts +12 -0
  194. package/types/lib/managers/docker.d.ts.map +1 -1
  195. package/types/lib/parsers/email.d.ts +19 -0
  196. package/types/lib/parsers/email.d.ts.map +1 -0
  197. package/types/lib/parsers/largeJson.d.ts +13 -0
  198. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  199. package/types/lib/server/server.d.ts +8 -0
  200. package/types/lib/server/server.d.ts.map +1 -1
  201. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  202. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  203. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  204. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  205. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  206. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  207. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  208. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -1,11 +1,24 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFileSync, statSync } from "node:fs";
2
2
  import { platform as _platform } from "node:os";
3
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
+ import {
4
+ basename,
5
+ dirname,
6
+ extname,
7
+ isAbsolute,
8
+ join,
9
+ resolve,
10
+ } from "node:path";
4
11
  import process from "node:process";
5
12
 
6
13
  import { build } from "@cdxgen/cdx-purl";
7
14
 
8
- import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
15
+ import {
16
+ DEBUG_MODE,
17
+ hasDangerousUnicode,
18
+ isDryRun,
19
+ readEnvironmentVariable,
20
+ recordActivity,
21
+ } from "../core/activity.js";
9
22
  import { recordDegradation } from "../core/buildLedger.js";
10
23
  import { deferFailOnError } from "../core/deferredExit.js";
11
24
  import {
@@ -26,7 +39,10 @@ import {
26
39
  safeWriteSync,
27
40
  } from "../core/fs.js";
28
41
  import { thoughtLog } from "../core/logger.js";
29
- import { mapWithConcurrency } from "../core/parallel.js";
42
+ import {
43
+ isProjectToolingAllowed,
44
+ reportProjectToolingSkipped,
45
+ } from "../core/projectTooling.js";
30
46
  import {
31
47
  extractJarArchive,
32
48
  getNugetMetadata,
@@ -35,7 +51,9 @@ import {
35
51
  } from "../ecosystems/ecosystems.js";
36
52
  import {
37
53
  agreedNugetVersions,
54
+ applyNugetVersion,
38
55
  dotnetManifestProjectDir,
56
+ dotnetRestoredProjectFile,
39
57
  getCentralPackageVersions,
40
58
  indexPropsProperties,
41
59
  parseCsPkgData,
@@ -100,6 +118,15 @@ import {
100
118
  collectDosaiCryptoComponents,
101
119
  collectSourceCryptoComponents,
102
120
  } from "../inventory/cbomutils.js";
121
+ import {
122
+ androidNetworkSecurityConfigPins,
123
+ CertificateCollector,
124
+ certRecordFromDer,
125
+ classifyCryptoFile,
126
+ containerRelativePath,
127
+ kubernetesTlsSecretRecords,
128
+ linkFallbackIssuers,
129
+ } from "../inventory/certificates.js";
103
130
  import { readZipEntry } from "../inventory/deps.js";
104
131
  import {
105
132
  filterInvalidCryptoComponents,
@@ -113,12 +140,17 @@ import {
113
140
  collectDosaiServiceComponents,
114
141
  collectDosaiServicesFromMethods,
115
142
  createDosaiMethodsSlice,
116
- dosaiRunWasStopped,
143
+ dosaiFailureReported,
144
+ dosaiMethodsSectionsArgument,
145
+ dosaiReportFileOmittedSections,
146
+ dosaiToolComponents,
117
147
  isDosaiDotnetLanguage,
118
148
  normalizeDosaiServiceMap,
119
149
  readDosaiMethodsReport,
150
+ runsEvidenceAnalysis,
120
151
  } from "../inventory/dosai.js";
121
152
  import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
153
+ import { resolvePluginBinary } from "../inventory/plugins.js";
122
154
  import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
123
155
  import { classifyProbeResult } from "../inventory/toolRequirements.js";
124
156
  import { getPluginToolComponents } from "../managers/binary.js";
@@ -225,11 +257,14 @@ export function createPixiBom(path, options) {
225
257
  generatePixiLockFile(path);
226
258
 
227
259
  const pixiLockFile = join(path, "pixi.lock");
228
- if (!safeExistsSync(pixiLockFile) && DEBUG_MODE) {
229
- console.log(
230
- "Unexpected Error tried to generate pixi.lock file but failed.",
231
- );
232
- console.log("This will result in creations of empty BOM.");
260
+ if (!safeExistsSync(pixiLockFile)) {
261
+ if (DEBUG_MODE) {
262
+ console.log(
263
+ "Unexpected Error tried to generate pixi.lock file but failed.",
264
+ );
265
+ }
266
+ // Let createPythonBOM() generate the BOM from the other manifests.
267
+ return null;
233
268
  }
234
269
  PixiLockData = parsePixiLockFile(pixiLockFile);
235
270
  metadataFilename = "pixi.lock";
@@ -494,11 +529,14 @@ export async function createPythonBom(path, options) {
494
529
  retMap.dependenciesList,
495
530
  parentComponent,
496
531
  );
497
- } else {
532
+ } else if (isProjectToolingAllowed(options)) {
533
+ // poetry export and pdm export load plugins from the project tree
534
+ // (.poetry/plugins, .pdm-plugins), so they run only when the
535
+ // project's own tooling may.
498
536
  let exportedReqs = "";
499
537
  if (f.endsWith("poetry.lock")) {
500
538
  thoughtLog(
501
- "Using poetry export as a safe, static alternative to pip install.",
539
+ "Using poetry export to collect the resolved closure without installing anything.",
502
540
  );
503
541
  const expCmd = safeSpawnSync(
504
542
  "poetry",
@@ -509,7 +547,7 @@ export async function createPythonBom(path, options) {
509
547
  exportedReqs = expCmd.stdout.toString();
510
548
  } else if (f.endsWith("pdm.lock")) {
511
549
  thoughtLog(
512
- "Using pdm export as a safe, static alternative to pip install.",
550
+ "Using pdm export to collect the resolved closure without installing anything.",
513
551
  );
514
552
  const expCmd = safeSpawnSync(
515
553
  "pdm",
@@ -539,6 +577,19 @@ export async function createPythonBom(path, options) {
539
577
  pkgList = pkgList.concat(dlist);
540
578
  }
541
579
  }
580
+ } else {
581
+ reportProjectToolingSkipped(
582
+ options,
583
+ "python.poetry-export.tooling-disabled",
584
+ "Skipping 'poetry export' and 'pdm export' since dependency installation is disabled. Only the packages parsed from the lock file will be used.",
585
+ {
586
+ ecosystem: "python",
587
+ tool: f.endsWith("pdm.lock") ? "pdm" : "poetry",
588
+ impact: "transitive-deps",
589
+ detail:
590
+ "Dependency installation is disabled, so poetry or pdm export was not run and only the packages parsed from the lock file are available.",
591
+ },
592
+ );
542
593
  }
543
594
  }
544
595
  }
@@ -879,9 +930,11 @@ export async function createPythonBom(path, options) {
879
930
  );
880
931
  }
881
932
  emitAiInventorySummary(aiInventory, path);
882
- // Check and complete the dependency tree
933
+ // Check and complete the dependency tree. This installs every package into a
934
+ // virtual environment, so --no-install-deps turns it off too.
883
935
  if (
884
936
  isFeatureEnabled(options, "safe-pip-install") &&
937
+ options.installDeps !== false &&
885
938
  pkgList.length &&
886
939
  isPartialTree(dependencies, pkgList.length)
887
940
  ) {
@@ -1108,6 +1161,28 @@ export function createHelmBom(path, options) {
1108
1161
  return {};
1109
1162
  }
1110
1163
 
1164
+ /**
1165
+ * The properties of a list followed by those of another it does not hold yet.
1166
+ * It returns a new array, so a shallow copy of a component keeps its own
1167
+ * properties.
1168
+ *
1169
+ * @param {{name: string, value: string}[]|undefined} existing Properties held so far
1170
+ * @param {{name: string, value: string}[]} additions Properties to add
1171
+ * @returns {{name: string, value: string}[]} The merged properties
1172
+ */
1173
+ function withMissingProperties(existing, additions) {
1174
+ const held = existing || [];
1175
+ return [
1176
+ ...held,
1177
+ ...additions.filter(
1178
+ (added) =>
1179
+ !held.some(
1180
+ (known) => known.name === added.name && known.value === added.value,
1181
+ ),
1182
+ ),
1183
+ ];
1184
+ }
1185
+
1111
1186
  /**
1112
1187
  * Function to create bom string for php projects
1113
1188
  *
@@ -1140,6 +1215,10 @@ export function createPHPBom(path, options) {
1140
1215
  composerLockSearchOptions,
1141
1216
  );
1142
1217
  let pkgList = [];
1218
+ // Platform facts of the scanned directory's lock when its composer.json names
1219
+ // no project: there is no module component to carry them, so they go on the
1220
+ // document. A nested lock like that has nowhere to put them.
1221
+ let metadataProperties = [];
1143
1222
  const composerJsonMode = composerJsonFiles.length;
1144
1223
  const composerLockMode = composerLockFiles.length;
1145
1224
  // Create a composer.lock file for each composer.json file if needed.
@@ -1253,6 +1332,37 @@ export function createPHPBom(path, options) {
1253
1332
  }
1254
1333
  }
1255
1334
  const retMap = parseComposerLock(f, rootRequires);
1335
+ if (retMap.platformProperties?.length) {
1336
+ if (moduleParent?.["bom-ref"]) {
1337
+ // The root composer.json is parsed twice, once for the parent
1338
+ // component and once per lock, so the object that ends up as
1339
+ // metadata.component is not always the module parsed here.
1340
+ for (const module of [moduleParent, parentComponent]) {
1341
+ if (module?.["bom-ref"] === moduleParent["bom-ref"]) {
1342
+ module.properties = withMissingProperties(
1343
+ module.properties,
1344
+ retMap.platformProperties,
1345
+ );
1346
+ }
1347
+ }
1348
+ } else if (resolve(basePath) === resolve(path)) {
1349
+ // The document describes the scanned directory, so only the lock
1350
+ // in it may put its facts on the document.
1351
+ metadataProperties = withMissingProperties(
1352
+ metadataProperties,
1353
+ retMap.platformProperties,
1354
+ );
1355
+ } else {
1356
+ recordDegradation("php.composer.platform-unattributed", {
1357
+ ecosystem: "php",
1358
+ tool: "composer",
1359
+ impact: "versions",
1360
+ path: f,
1361
+ detail:
1362
+ "The composer.json beside this lock names no project, so no component can carry the PHP and extension requirements of the lock.",
1363
+ });
1364
+ }
1365
+ }
1256
1366
  if (retMap.pkgList?.length) {
1257
1367
  pkgList = pkgList.concat(retMap.pkgList);
1258
1368
  pkgList = trimComponents(pkgList);
@@ -1292,6 +1402,7 @@ export function createPHPBom(path, options) {
1292
1402
  filename: composerLockFiles.join(", "),
1293
1403
  dependencies,
1294
1404
  parentComponent,
1405
+ metadataProperties,
1295
1406
  });
1296
1407
  }
1297
1408
  if (composerJsonMode && !composerLockMode) {
@@ -1539,7 +1650,14 @@ export async function createRubyBom(path, options) {
1539
1650
  options.resolveClass ||
1540
1651
  (options.deep && !hasAnyProjectType(["oci"], options, false))
1541
1652
  ) {
1542
- if (gemHome && !isGemHomeEmpty) {
1653
+ // `bundle exec` evaluates the project's Gemfile, a Ruby program
1654
+ if (gemHome && !isGemHomeEmpty && !isProjectToolingAllowed(options)) {
1655
+ reportProjectToolingSkipped(
1656
+ options,
1657
+ null,
1658
+ "Skipping the module names of gems since dependency installation is disabled. Collecting them runs 'bundle exec', which evaluates the Gemfile.",
1659
+ );
1660
+ } else if (gemHome && !isGemHomeEmpty) {
1543
1661
  const rubyCommand =
1544
1662
  readEnvironmentVariable("CDXGEN_RUBY_CMD") ||
1545
1663
  readEnvironmentVariable("RUBY_CMD") ||
@@ -1655,6 +1773,12 @@ export async function createCsharpBom(path, options) {
1655
1773
  return createBinaryBom(path, options);
1656
1774
  }
1657
1775
  let parentComponent = createDefaultParentComponent(path, "nuget", options);
1776
+ // The directory a scan starts from is not a package, so it has a version
1777
+ // only when --project-version gives it one; "latest" is not a version.
1778
+ applyNugetVersion(
1779
+ parentComponent,
1780
+ options.projectVersion ? `${options.projectVersion}` : undefined,
1781
+ );
1658
1782
  const slnFiles = getAllFiles(
1659
1783
  path,
1660
1784
  `${options.multiProject ? "**/" : ""}*.sln*`,
@@ -1939,6 +2063,47 @@ export async function createCsharpBom(path, options) {
1939
2063
  // one project's manifest are not dropped just because another format exists
1940
2064
  // elsewhere in the scan.
1941
2065
  const assetsProjectDirs = new Set();
2066
+ // Restore records a project file the way the build was pointed at it, which
2067
+ // on a case-insensitive file system can differ in case from the file found
2068
+ // here. Such a path takes the found file's spelling, unless that is ambiguous.
2069
+ const foundProjectFiles = new Set();
2070
+ const foundProjectFilesByKey = new Map();
2071
+ for (const f of csProjFiles) {
2072
+ const file = resolve(f);
2073
+ const key = file.toLowerCase();
2074
+ foundProjectFiles.add(file);
2075
+ foundProjectFilesByKey.set(
2076
+ key,
2077
+ foundProjectFilesByKey.has(key) ? null : file,
2078
+ );
2079
+ }
2080
+ const foundProjectFile = (file) =>
2081
+ !file || foundProjectFiles.has(file)
2082
+ ? file
2083
+ : foundProjectFilesByKey.get(file.toLowerCase()) || file;
2084
+ // The package id and version each project's restore output records, by
2085
+ // project file. A project whose restore outputs disagree, such as a stale
2086
+ // obj/ beside an artifacts directory, takes neither.
2087
+ const restoredIdentities = new Map();
2088
+ const recordRestoredIdentity = (projectFile, identity) => {
2089
+ if (!projectFile || !identity) {
2090
+ return;
2091
+ }
2092
+ const known = restoredIdentities.get(projectFile);
2093
+ if (known === undefined) {
2094
+ restoredIdentities.set(projectFile, identity);
2095
+ } else if (
2096
+ known &&
2097
+ (known.name !== identity.name || known.version !== identity.version)
2098
+ ) {
2099
+ if (DEBUG_MODE) {
2100
+ console.log(
2101
+ `Restore outputs disagree on the package id or version of ${projectFile}, so the identity its project file states is used.`,
2102
+ );
2103
+ }
2104
+ restoredIdentities.set(projectFile, null);
2105
+ }
2106
+ };
1942
2107
  // project.assets.json parsing
1943
2108
  if (projAssetsFiles.length) {
1944
2109
  manifestFiles = manifestFiles.concat(projAssetsFiles);
@@ -1952,9 +2117,13 @@ export async function createCsharpBom(path, options) {
1952
2117
  const dlist = results["pkgList"];
1953
2118
  // Restore output names its project; an artifacts layout keeps it outside
1954
2119
  // the project directory.
1955
- const projectDir = dotnetManifestProjectDir(af, results.projectPath);
2120
+ const projectFile = foundProjectFile(
2121
+ dotnetRestoredProjectFile(af, results.projectPath, results.outputPath),
2122
+ );
2123
+ const projectDir = dotnetManifestProjectDir(af, projectFile);
1956
2124
  assetsProjectDirs.add(projectDir);
1957
2125
  recordProjectVersions(projectDir, dlist);
2126
+ recordRestoredIdentity(projectFile, results.projectIdentity);
1958
2127
  if (dlist?.length) {
1959
2128
  pkgList = pkgList.concat(dlist);
1960
2129
  }
@@ -2065,6 +2234,7 @@ export async function createCsharpBom(path, options) {
2065
2234
  }
2066
2235
  const projectRefByDir = {};
2067
2236
  let services = [];
2237
+ let dosaiTools = [];
2068
2238
  if (csProjFiles.length) {
2069
2239
  manifestFiles = manifestFiles.concat(csProjFiles);
2070
2240
  // Parsing csproj is quite error-prone. Some project files may not have
@@ -2073,11 +2243,18 @@ export async function createCsharpBom(path, options) {
2073
2243
  // whole scan agrees on.
2074
2244
  const agreedVersions = agreedNugetVersions(pkgList);
2075
2245
  let msbuildInstalled = false;
2076
- const result = safeSpawnSync(DOTNET_CMD, ["msbuild", "--version"], {
2077
- shell: isWin,
2078
- });
2079
- if (result.status === 0 && !result.error && result.stdout?.trim()) {
2080
- msbuildInstalled = true;
2246
+ // `dotnet msbuild` evaluates the project (loading Directory.Build.rsp,
2247
+ // Directory.Build.props/targets and build extensions), so it belongs to
2248
+ // the dependency-tooling surface disabled by --no-install-deps and the
2249
+ // pre-build lifecycle. Version labels it would resolve then fall back to
2250
+ // the static props/central-version resolution.
2251
+ if (isProjectToolingAllowed(options)) {
2252
+ const result = safeSpawnSync(DOTNET_CMD, ["msbuild", "--version"], {
2253
+ shell: isWin,
2254
+ });
2255
+ if (result.status === 0 && !result.error && result.stdout?.trim()) {
2256
+ msbuildInstalled = true;
2257
+ }
2081
2258
  }
2082
2259
  // `$(Name)` version labels resolve from the props files that can apply to
2083
2260
  // each project (beside and above it), read once for the whole scan.
@@ -2103,6 +2280,7 @@ export async function createCsharpBom(path, options) {
2103
2280
  getCentralPackageVersions(f, centralVersionsCache),
2104
2281
  agreedVersions,
2105
2282
  projectNames[projectDir] || {},
2283
+ restoredIdentities.get(resolve(f)) || undefined,
2106
2284
  );
2107
2285
  if (retMap?.parentComponent?.purl) {
2108
2286
  projectRefByDir[projectDir] ??= retMap.parentComponent["bom-ref"];
@@ -2127,6 +2305,24 @@ export async function createCsharpBom(path, options) {
2127
2305
  parentComponent,
2128
2306
  );
2129
2307
  }
2308
+ // Only a version that needed msbuild makes its skip a loss
2309
+ if (
2310
+ retMap?.unresolvedVersionLabels?.length &&
2311
+ !isProjectToolingAllowed(options)
2312
+ ) {
2313
+ reportProjectToolingSkipped(
2314
+ options,
2315
+ "dotnet.msbuild.tooling-disabled",
2316
+ "Skipping 'dotnet msbuild' property evaluation since dependency installation is disabled. Package versions that come from MSBuild properties no project or props file sets stay empty.",
2317
+ {
2318
+ ecosystem: "csharp",
2319
+ tool: "msbuild",
2320
+ impact: "versions",
2321
+ detail:
2322
+ "Dependency installation is disabled, so $(Property) version references that the project and props files do not set were not evaluated with msbuild.",
2323
+ },
2324
+ );
2325
+ }
2130
2326
  }
2131
2327
  }
2132
2328
  // A lock file's or packages.config's direct dependencies belong to the
@@ -2148,6 +2344,17 @@ export async function createCsharpBom(path, options) {
2148
2344
  }
2149
2345
  }
2150
2346
  }
2347
+ // A scan of several projects is made of them, so the directory depends on
2348
+ // each, as a Gradle build does on its subprojects. Restore output also lists
2349
+ // every project it restored as a package; the parent and its projects are
2350
+ // already in the BOM, and a second component with the same bom-ref would
2351
+ // merge the two in every consumer.
2352
+ const projectRefs = new Set([parentComponent?.["bom-ref"]]);
2353
+ for (const project of parentComponent?.components || []) {
2354
+ parentDependsOn.add(project["bom-ref"]);
2355
+ projectRefs.add(project["bom-ref"]);
2356
+ }
2357
+ pkgList = pkgList.filter((p) => !projectRefs.has(p["bom-ref"]));
2151
2358
  if (pkgList.length) {
2152
2359
  pkgList = trimComponents(pkgList);
2153
2360
  // Perform deep analysis using dosai
@@ -2166,19 +2373,46 @@ export async function createCsharpBom(path, options) {
2166
2373
  ? resolve(path, options.depsSlicesFile)
2167
2374
  : join(slicesDir, "dosai.json");
2168
2375
  try {
2376
+ // A run that will run evinse (--evidence or --include-crypto, after
2377
+ // profiles are applied) keeps the report full: evinse reuses this
2378
+ // file as its usages slice and persists it as the native report.
2379
+ // Anything else needs only the sections cdxgen reads, so dosai
2380
+ // leaves the rest out (Metadata.OmittedSections names it).
2381
+ const willRunEvinse = runsEvidenceAnalysis(options);
2382
+ // Reuse an existing slice as-is, with one exception: a trimmed file
2383
+ // (an earlier --deep run left it behind) is regenerated full before
2384
+ // an evidence run uses it, so dosai still runs once per scan.
2385
+ let reuseExisting = safeExistsSync(slicesFile);
2386
+ if (reuseExisting && willRunEvinse) {
2387
+ const omitted = dosaiReportFileOmittedSections(slicesFile);
2388
+ if (omitted.length) {
2389
+ if (DEBUG_MODE) {
2390
+ console.log(
2391
+ `The existing dosai methods slice "${slicesFile}" omits sections (${omitted.join(", ")}); regenerating the full report because this run performs evidence analysis.`,
2392
+ );
2393
+ }
2394
+ reuseExisting = false;
2395
+ }
2396
+ }
2169
2397
  // Create the slices file if it doesn't exist
2170
- if (!safeExistsSync(slicesFile)) {
2398
+ if (!reuseExisting) {
2171
2399
  thoughtLog(
2172
2400
  "Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
2173
2401
  );
2174
2402
  const sliceResult = createDosaiMethodsSlice(
2175
2403
  resolve(path),
2176
2404
  resolve(slicesFile),
2177
- options,
2405
+ {
2406
+ ...options,
2407
+ dosaiMethodsSections: willRunEvinse
2408
+ ? undefined
2409
+ : dosaiMethodsSectionsArgument(),
2410
+ },
2178
2411
  );
2179
- // A stopped run has already said so and what to change; the debug
2180
- // output would show no dosai error for it (issue 4438).
2181
- if (!sliceResult && !dosaiRunWasStopped(resolve(slicesFile))) {
2412
+ // A stopped run, or one the .NET host refused to start, has
2413
+ // already said why and what to change; the debug output would show
2414
+ // no other dosai error for it (issue 4438).
2415
+ if (!sliceResult && !dosaiFailureReported(resolve(slicesFile))) {
2182
2416
  // Visible without debug output: otherwise the components silently
2183
2417
  // lose the dependency-slice evidence while the scan still exits 0.
2184
2418
  // --evidence (which implies --deep) runs dosai again during evinse
@@ -2205,6 +2439,11 @@ export async function createCsharpBom(path, options) {
2205
2439
  const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
2206
2440
  collectDosaiServicesFromMethods(methodsSlice, servicesMap);
2207
2441
  services = normalizeDosaiServiceMap(servicesMap);
2442
+ dosaiTools = dosaiToolComponents(
2443
+ slicesFile,
2444
+ methodsSlice,
2445
+ getPluginToolComponents(["dosai"])[0],
2446
+ );
2208
2447
  } finally {
2209
2448
  if (slicesDir) {
2210
2449
  safeRmSync(slicesDir, { recursive: true, force: true });
@@ -2244,7 +2483,7 @@ export async function createCsharpBom(path, options) {
2244
2483
  dependencies,
2245
2484
  parentComponent,
2246
2485
  services,
2247
- tools: options.deep ? getPluginToolComponents(["dosai"]) : [],
2486
+ tools: dosaiTools,
2248
2487
  });
2249
2488
  }
2250
2489
 
@@ -2263,58 +2502,239 @@ export async function createCryptoCertsBom(path, options) {
2263
2502
  },
2264
2503
  };
2265
2504
  }
2505
+ const collector = new CertificateCollector();
2266
2506
  const pkgList = [];
2267
- const certFiles = getAllFiles(
2268
- path,
2269
- `${
2270
- options.multiProject ? "**/" : ""
2271
- }*.{p12,jks,jceks,bks,keystore,key,pem,cer,gpg,pub}`,
2272
- options,
2507
+ // In rootfs/container mode the scan path sits inside the extracted tree;
2508
+ // locations are recorded relative to that tree so they merge with the
2509
+ // trustinspector rootfs producer (G18).
2510
+ const locationRoot = options.allLayersExplodedDir;
2511
+ const relativeLocation = (file) =>
2512
+ containerRelativePath(file, locationRoot) || file;
2513
+ // Bom-refs never carry host paths: outside rootfs mode, key and keystore
2514
+ // refs are relative to the scanned path.
2515
+ const refLocation = (file) =>
2516
+ containerRelativePath(file, locationRoot || path) || file;
2517
+ const certFiles = [
2518
+ ...new Set([
2519
+ ...getAllFiles(
2520
+ path,
2521
+ `${
2522
+ options.multiProject ? "**/" : ""
2523
+ }*.{p12,jks,jceks,bks,keystore,key,pem,cer,crt,der,p7b,p7c,pfx,truststore,gpg,pub}`,
2524
+ options,
2525
+ ),
2526
+ // Java truststores carry no extension; match them by exact name.
2527
+ ...getAllFiles(
2528
+ path,
2529
+ `${options.multiProject ? "**/" : ""}{cacerts,jssecacerts}`,
2530
+ options,
2531
+ ),
2532
+ ]),
2533
+ ];
2534
+ // Newlines would corrupt the plugin's stdin file list, and oversized
2535
+ // files are capped in the fallback; the plugin list gets the same caps.
2536
+ const pluginCandidateFiles = certFiles.filter(
2537
+ (file) =>
2538
+ !/[\r\n]/.test(file) &&
2539
+ statSync(file, { throwIfNoEntry: false })?.size <= MAX_CRYPTO_FILE_BYTES,
2273
2540
  );
2274
- const certHashes = await mapWithConcurrency(certFiles, (f) =>
2275
- checksumFile("sha256", f),
2541
+ const pluginMaterials = runTrustInspectorCertsMode(
2542
+ pluginCandidateFiles,
2543
+ locationRoot,
2276
2544
  );
2277
- for (let ci = 0; ci < certFiles.length; ci++) {
2278
- const f = certFiles[ci];
2279
- const fileHash = certHashes[ci];
2280
- const name = basename(f);
2281
- let evidence;
2282
- if (options.evidence) {
2283
- const identityEvidence = {
2284
- field: "name",
2285
- confidence: 1,
2286
- concludedValue: name,
2287
- methods: [
2545
+ const fallbackRecords = [];
2546
+ if (pluginMaterials) {
2547
+ collector.addTrustInspectorMaterials(pluginMaterials);
2548
+ for (const material of pluginMaterials) {
2549
+ if (typeof material?.path !== "string" || !material.path) {
2550
+ continue;
2551
+ }
2552
+ if (material.kind === "keystore") {
2553
+ pkgList.push(
2554
+ sourceKeystoreComponent(material, refLocation(material.path)),
2555
+ );
2556
+ } else if (material.kind === "private-key") {
2557
+ pkgList.push(
2558
+ sourcePrivateKeyComponent(material, refLocation(material.path)),
2559
+ );
2560
+ }
2561
+ }
2562
+ }
2563
+ // Files the plugin was not given (names with line breaks, oversized
2564
+ // files) are still read here, so no name can hide a file from the
2565
+ // inventory; without a plugin every file is. The files it was given are
2566
+ // its own, except for the keys it builds nothing from: it reports nothing
2567
+ // for a bare PEM public key, and an OpenPGP key only as public-key
2568
+ // material, so both are built here whether it saw them or not.
2569
+ const pluginFiles = new Set(pluginMaterials ? pluginCandidateFiles : []);
2570
+ for (const f of certFiles) {
2571
+ let bytes;
2572
+ try {
2573
+ const stats = statSync(f);
2574
+ if (stats.size > MAX_CRYPTO_FILE_BYTES) {
2575
+ continue;
2576
+ }
2577
+ bytes = readFileSync(f);
2578
+ } catch {
2579
+ continue;
2580
+ }
2581
+ const location = relativeLocation(f);
2582
+ const classification = classifyCryptoFile(bytes);
2583
+ if (
2584
+ pluginFiles.has(f) &&
2585
+ classification.kind !== "gpg" &&
2586
+ classification.kind !== "public-key"
2587
+ ) {
2588
+ continue;
2589
+ }
2590
+ if (classification.kind === "certificates") {
2591
+ for (const der of classification.ders) {
2592
+ const record = certRecordFromDer(der, {
2593
+ location,
2594
+ sourceType: deriveCryptoSourceType(f),
2595
+ });
2596
+ if (record) {
2597
+ record.fileExtension = extname(f).replace(/^\./, "") || undefined;
2598
+ fallbackRecords.push(record);
2599
+ }
2600
+ }
2601
+ } else if (classification.kind === "private-key") {
2602
+ pkgList.push(
2603
+ sourcePrivateKeyComponent(
2288
2604
  {
2289
- technique: "filename",
2290
- confidence: 1,
2291
- value: f,
2605
+ format: classification.format,
2606
+ sourceType: deriveCryptoSourceType(f),
2292
2607
  },
2293
- ],
2294
- };
2295
- evidence = {
2296
- identity:
2297
- options.specVersion >= 1.6 ? [identityEvidence] : identityEvidence,
2298
- occurrences: [{ location: f }],
2299
- };
2608
+ refLocation(f),
2609
+ ),
2610
+ );
2611
+ } else if (classification.kind === "gpg") {
2612
+ pkgList.push(await sourceGpgComponent(f, location, options));
2613
+ } else if (classification.kind === "public-key") {
2614
+ pkgList.push(await sourcePemPublicKeyComponent(f, location, options));
2615
+ } else if (classification.kind === "keystore") {
2616
+ pkgList.push(
2617
+ sourceKeystoreComponent(
2618
+ {
2619
+ format: classification.format,
2620
+ sourceType: deriveCryptoSourceType(f),
2621
+ },
2622
+ refLocation(f),
2623
+ ),
2624
+ );
2300
2625
  }
2301
- const apkg = {
2302
- name,
2303
- type: "cryptographic-asset",
2304
- version: fileHash,
2305
- "bom-ref": `crypto/certificate/${name}@sha256:${fileHash}`,
2306
- cryptoProperties: {
2307
- assetType: "certificate",
2308
- algorithmProperties: {
2309
- executionEnvironment: "unknown",
2310
- implementationPlatform: "unknown",
2626
+ }
2627
+ // Kubernetes TLS secrets and Android SPKI pins are cdxgen-side producers:
2628
+ // the plugin has no YAML or Android-config parser.
2629
+ const yamlFiles = getAllFiles(
2630
+ path,
2631
+ `${options.multiProject ? "**/" : ""}*.{yaml,yml}`,
2632
+ options,
2633
+ );
2634
+ for (const yamlFile of yamlFiles) {
2635
+ let yamlText;
2636
+ try {
2637
+ const stats = statSync(yamlFile);
2638
+ if (stats.size > MAX_CRYPTO_FILE_BYTES) {
2639
+ continue;
2640
+ }
2641
+ yamlText = readFileSync(yamlFile, "utf-8");
2642
+ } catch {
2643
+ continue;
2644
+ }
2645
+ // Cheap pre-filter: the Secret type string must be present before the
2646
+ // YAML parser runs.
2647
+ if (!yamlText.includes("kubernetes.io/tls")) {
2648
+ continue;
2649
+ }
2650
+ for (const record of kubernetesTlsSecretRecords(yamlText, {
2651
+ file: relativeLocation(yamlFile),
2652
+ })) {
2653
+ fallbackRecords.push(record);
2654
+ }
2655
+ }
2656
+ const androidXmlFiles = getAllFiles(path, "**/res/xml/*.xml", options);
2657
+ for (const xmlFile of androidXmlFiles) {
2658
+ let xmlText;
2659
+ try {
2660
+ const stats = statSync(xmlFile, { throwIfNoEntry: false });
2661
+ if (!stats || stats.size > MAX_CRYPTO_FILE_BYTES) {
2662
+ continue;
2663
+ }
2664
+ xmlText = readFileSync(xmlFile, "utf-8");
2665
+ } catch {
2666
+ continue;
2667
+ }
2668
+ if (!xmlText.includes("<network-security-config")) {
2669
+ continue;
2670
+ }
2671
+ for (const pin of androidNetworkSecurityConfigPins(xmlText, {
2672
+ file: relativeLocation(xmlFile),
2673
+ })) {
2674
+ collector.addPin(pin);
2675
+ }
2676
+ }
2677
+ if (fallbackRecords.length) {
2678
+ linkFallbackIssuers(fallbackRecords);
2679
+ collector.addRecords(fallbackRecords);
2680
+ }
2681
+ const {
2682
+ components: certificateComponents,
2683
+ dependencies: certificateDependencies,
2684
+ } = collector.build();
2685
+ if (pluginMaterials) {
2686
+ // The plugin reports the certificates a readable keystore holds, not the
2687
+ // keystore itself; give it the same component the fallback emits and
2688
+ // link it to what it holds.
2689
+ const keystoreMaterials = new Map();
2690
+ for (const material of pluginMaterials) {
2691
+ if (material?.kind === "certificate" && material.path) {
2692
+ keystoreMaterials.set(material.path, material);
2693
+ }
2694
+ }
2695
+ for (const file of pluginCandidateFiles) {
2696
+ const location = relativeLocation(file);
2697
+ const material = keystoreMaterials.get(location);
2698
+ if (!material) {
2699
+ continue;
2700
+ }
2701
+ let classification;
2702
+ try {
2703
+ classification = classifyCryptoFile(readFileSync(file));
2704
+ } catch {
2705
+ continue;
2706
+ }
2707
+ if (classification.kind !== "keystore") {
2708
+ continue;
2709
+ }
2710
+ const keystore = sourceKeystoreComponent(
2711
+ {
2712
+ format: classification.format,
2713
+ sourceType: material.sourceType || deriveCryptoSourceType(file),
2714
+ readable: true,
2311
2715
  },
2312
- },
2313
- ...(evidence ? { evidence } : {}),
2314
- properties: [{ name: "internal:SrcFile", value: f }],
2315
- };
2316
- pkgList.push(apkg);
2716
+ refLocation(file),
2717
+ );
2718
+ pkgList.push(keystore);
2719
+ const held = certificateComponents
2720
+ .filter(
2721
+ (component) =>
2722
+ component.cryptoProperties?.assetType === "certificate" &&
2723
+ component.evidence?.occurrences?.some(
2724
+ (occurrence) => occurrence.location === location,
2725
+ ),
2726
+ )
2727
+ .map((component) => component["bom-ref"])
2728
+ .sort();
2729
+ if (held.length) {
2730
+ certificateDependencies.push({
2731
+ ref: keystore["bom-ref"],
2732
+ dependsOn: held,
2733
+ });
2734
+ }
2735
+ }
2317
2736
  }
2737
+ pkgList.push(...certificateComponents);
2318
2738
  const sourceCryptoComponents = await collectSourceCryptoComponents(
2319
2739
  path,
2320
2740
  options,
@@ -2335,9 +2755,229 @@ export async function createCryptoCertsBom(path, options) {
2335
2755
  bomJson: {
2336
2756
  components: filterInvalidCryptoComponents(pkgList),
2337
2757
  },
2758
+ dependencies: certificateDependencies,
2338
2759
  };
2339
2760
  }
2340
2761
 
2762
+ // Files larger than this are not classified: no real certificate bundle or
2763
+ // keystore comes close, and the cap bounds hostile inputs.
2764
+ const MAX_CRYPTO_FILE_BYTES = 16 * 1024 * 1024;
2765
+
2766
+ function deriveCryptoSourceType(filePath) {
2767
+ const normalized = String(filePath).replace(/\\/g, "/");
2768
+ if (
2769
+ normalized.includes("/etc/ssl/certs") ||
2770
+ normalized.includes("ca-certificates")
2771
+ ) {
2772
+ return "ca-store";
2773
+ }
2774
+ if (normalized.includes("certifi") || normalized.includes("botocore")) {
2775
+ return "language-bundle";
2776
+ }
2777
+ if (
2778
+ /\/etc\/(nginx|apache2|httpd|haproxy|letsencrypt|kubernetes|docker|containerd)\//.test(
2779
+ normalized,
2780
+ )
2781
+ ) {
2782
+ return "server-config";
2783
+ }
2784
+ if (
2785
+ normalized.includes("cacerts") ||
2786
+ /\.(jks|jceks|p12|pfx|truststore)$/i.test(normalized)
2787
+ ) {
2788
+ return "java-truststore";
2789
+ }
2790
+ return "generic";
2791
+ }
2792
+
2793
+ function sourceCryptoFileEvidence(location, options) {
2794
+ if (!options.evidence) {
2795
+ return undefined;
2796
+ }
2797
+ const identityEvidence = {
2798
+ field: "name",
2799
+ confidence: 1,
2800
+ concludedValue: basename(location),
2801
+ methods: [
2802
+ {
2803
+ technique: "filename",
2804
+ confidence: 1,
2805
+ value: location,
2806
+ },
2807
+ ],
2808
+ };
2809
+ return {
2810
+ identity:
2811
+ options.specVersion >= 1.6 ? [identityEvidence] : identityEvidence,
2812
+ occurrences: [{ location }],
2813
+ };
2814
+ }
2815
+
2816
+ async function sourceGpgComponent(srcFile, location, options) {
2817
+ const fileHash = await checksumFile("sha256", srcFile);
2818
+ return {
2819
+ name: basename(location),
2820
+ type: "cryptographic-asset",
2821
+ version: fileHash,
2822
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
2823
+ cryptoProperties: {
2824
+ assetType: "related-crypto-material",
2825
+ relatedCryptoMaterialProperties: {
2826
+ type: "public-key",
2827
+ format: "OpenPGP",
2828
+ },
2829
+ },
2830
+ ...(sourceCryptoFileEvidence(location, options)
2831
+ ? { evidence: sourceCryptoFileEvidence(location, options) }
2832
+ : {}),
2833
+ properties: [
2834
+ { name: "internal:SrcFile", value: location },
2835
+ { name: "cdx:crypto:sourceType", value: "generic" },
2836
+ ],
2837
+ };
2838
+ }
2839
+
2840
+ async function sourcePemPublicKeyComponent(srcFile, location, options) {
2841
+ const fileHash = await checksumFile("sha256", srcFile);
2842
+ return {
2843
+ name: basename(location),
2844
+ type: "cryptographic-asset",
2845
+ version: fileHash,
2846
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
2847
+ cryptoProperties: {
2848
+ assetType: "related-crypto-material",
2849
+ relatedCryptoMaterialProperties: {
2850
+ type: "public-key",
2851
+ format: "SPKI",
2852
+ },
2853
+ },
2854
+ ...(sourceCryptoFileEvidence(location, options)
2855
+ ? { evidence: sourceCryptoFileEvidence(location, options) }
2856
+ : {}),
2857
+ properties: [
2858
+ { name: "internal:SrcFile", value: location },
2859
+ { name: "cdx:crypto:sourceType", value: "generic" },
2860
+ ],
2861
+ };
2862
+ }
2863
+
2864
+ function sourcePrivateKeyComponent(material, location) {
2865
+ return {
2866
+ name: basename(location),
2867
+ type: "cryptographic-asset",
2868
+ "bom-ref": `crypto/related-crypto-material/private-key/${encodeURIComponent(location)}`,
2869
+ cryptoProperties: {
2870
+ assetType: "related-crypto-material",
2871
+ relatedCryptoMaterialProperties: {
2872
+ type: "private-key",
2873
+ ...(material?.format ? { format: material.format } : {}),
2874
+ },
2875
+ },
2876
+ evidence: { occurrences: [{ location }] },
2877
+ properties: [
2878
+ { name: "internal:SrcFile", value: location },
2879
+ ...(material?.sourceType
2880
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
2881
+ : []),
2882
+ ],
2883
+ };
2884
+ }
2885
+
2886
+ function sourceKeystoreComponent(material, location) {
2887
+ const readable = (material?.properties || []).find(
2888
+ (property) => property.name === "cdx:crypto:keystoreReadable",
2889
+ )?.value;
2890
+ return {
2891
+ name: basename(location),
2892
+ type: "cryptographic-asset",
2893
+ "bom-ref": `crypto/related-crypto-material/keystore/${encodeURIComponent(location)}`,
2894
+ cryptoProperties: {
2895
+ assetType: "related-crypto-material",
2896
+ relatedCryptoMaterialProperties: {
2897
+ type: "other",
2898
+ ...(material?.format ? { format: material.format } : {}),
2899
+ },
2900
+ },
2901
+ evidence: { occurrences: [{ location }] },
2902
+ properties: [
2903
+ { name: "internal:SrcFile", value: location },
2904
+ ...(material?.sourceType
2905
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
2906
+ : []),
2907
+ // A keystore the plugin read has no keystoreReadable property.
2908
+ ...(material?.readable
2909
+ ? []
2910
+ : [
2911
+ { name: "cdx:crypto:keystoreReadable", value: readable || "false" },
2912
+ ]),
2913
+ ],
2914
+ };
2915
+ }
2916
+
2917
+ /**
2918
+ * Parse the candidate certificate files with the trustinspector `certs`
2919
+ * mode. Only a report-2 envelope is accepted: report-1 output has no
2920
+ * certificate identity to build the shared model from, and the Node
2921
+ * fallback covers that case.
2922
+ *
2923
+ * @param {string[]} files candidate files
2924
+ * @param {string} root paths are reported relative to this directory
2925
+ * @returns {object[]|undefined} materials, or undefined when the plugin is
2926
+ * absent, blocked or not report-2
2927
+ */
2928
+ function runTrustInspectorCertsMode(files, root) {
2929
+ if (!files?.length) {
2930
+ return undefined;
2931
+ }
2932
+ if (isDryRun) {
2933
+ recordActivity({
2934
+ kind: "trustinspector",
2935
+ reason:
2936
+ "Dry run mode blocks trustinspector execution and reports the requested inspection instead.",
2937
+ status: "blocked",
2938
+ target: `certs --stdin (${files.length} files)`,
2939
+ });
2940
+ return undefined;
2941
+ }
2942
+ const trustInspectorBin = resolvePluginBinary("trustinspector");
2943
+ if (!trustInspectorBin) {
2944
+ return undefined;
2945
+ }
2946
+ // `--root` is passed only in rootfs/container mode, where locations must
2947
+ // come out relative to the extracted tree; a plain source scan keeps the
2948
+ // host paths every other producer records. The value reaches a spawned
2949
+ // command, so it gets the same rejection the rootfs path applies:
2950
+ // dangerous unicode or line breaks disable the plugin call.
2951
+ const args = ["certs", "--stdin"];
2952
+ const safeRoot =
2953
+ typeof root === "string" &&
2954
+ !hasDangerousUnicode(root) &&
2955
+ !/[\r\n]/.test(root)
2956
+ ? root
2957
+ : undefined;
2958
+ if (safeRoot) {
2959
+ args.push("--root", safeRoot);
2960
+ }
2961
+ const result = safeSpawnSync(trustInspectorBin, args, {
2962
+ input: `${files.join("\n")}\n`,
2963
+ maxBuffer: 256 * 1024 * 1024,
2964
+ // A hung plugin must not hang cdxgen; the Node fallback takes over.
2965
+ timeout: 10 * 60 * 1000,
2966
+ });
2967
+ if (result?.status !== 0 || result?.error || !result?.stdout) {
2968
+ return undefined;
2969
+ }
2970
+ try {
2971
+ const report = JSON.parse(result.stdout);
2972
+ if (report?.schema !== "trustinspector/report-2") {
2973
+ return undefined;
2974
+ }
2975
+ return report.materials || [];
2976
+ } catch {
2977
+ return undefined;
2978
+ }
2979
+ }
2980
+
2341
2981
  /**
2342
2982
  * Function to create bom string for R projects using renv.
2343
2983
  *