@cdxgen/cdxgen 13.4.0 → 13.6.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 (305) hide show
  1. package/README.md +9 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +29 -5
  4. package/bin/evinse.js +8 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/bin/validate.js +6 -0
  8. package/data/cbomosdb-queries.json +13 -1
  9. package/data/component-tags.json +8 -0
  10. package/data/license-aliases.json +24 -2
  11. package/data/queries-darwin.json +25 -1
  12. package/data/queries-win.json +19 -1
  13. package/data/queries.json +41 -0
  14. package/data/remediations.json +407 -1
  15. package/data/rules/ai-bom.yaml +1 -1
  16. package/data/rules/brotli-decoder.yaml +107 -0
  17. package/data/rules/cbom-security.yaml +235 -4
  18. package/data/rules/go-binaries.yaml +46 -0
  19. package/data/rules/golem-go.yaml +80 -0
  20. package/data/rules/hbom-compliance.yaml +40 -22
  21. package/data/rules/hbom-performance.yaml +7 -1
  22. package/data/rules/host-topology.yaml +1 -1
  23. package/data/rules/obom-runtime.yaml +31 -2
  24. package/data/rules/php-runtime.yaml +143 -0
  25. package/data/rules/vscode-extensions.yaml +75 -0
  26. package/data/spdx-license-list.json +121 -7
  27. package/data/spdx-licenses.json +69 -0
  28. package/data/spdx.schema.json +845 -2
  29. package/lib/audit/index.js +14 -0
  30. package/lib/cli/bomAssembly.js +120 -6
  31. package/lib/cli/cliOptions.js +110 -3
  32. package/lib/cli/index.js +182 -6
  33. package/lib/cli/jsBom.js +153 -64
  34. package/lib/cli/jvmBom.js +363 -66
  35. package/lib/cli/managedBom.js +853 -87
  36. package/lib/cli/nativeBom.js +267 -33
  37. package/lib/core/boundedFile.js +344 -0
  38. package/lib/core/brotliProbe.js +182 -0
  39. package/lib/core/env.js +38 -8
  40. package/lib/core/fs.js +40 -19
  41. package/lib/core/httpClient.js +172 -46
  42. package/lib/core/lockPaths.js +38 -0
  43. package/lib/core/paths.js +24 -1
  44. package/lib/core/processTree.js +5 -0
  45. package/lib/core/projectConfig.js +1 -0
  46. package/lib/core/projectTooling.js +97 -0
  47. package/lib/core/propertySanitizer.js +72 -0
  48. package/lib/core/toolRegistry.js +170 -0
  49. package/lib/ecosystems/cppEvidence.js +263 -106
  50. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  51. package/lib/ecosystems/ecosystems.js +25 -10
  52. package/lib/ecosystems/gemHostProbe.js +301 -0
  53. package/lib/ecosystems/golem.js +550 -6
  54. package/lib/ecosystems/gradleutils.js +858 -244
  55. package/lib/ecosystems/parsers-bazel.js +107 -6
  56. package/lib/ecosystems/parsers-dotnet.js +117 -18
  57. package/lib/ecosystems/parsers-go.js +1090 -367
  58. package/lib/ecosystems/parsers-js.js +505 -22
  59. package/lib/ecosystems/parsers-jvm.js +259 -17
  60. package/lib/ecosystems/parsers-misc.js +438 -64
  61. package/lib/ecosystems/parsers-python.js +100 -12
  62. package/lib/ecosystems/pythonutils.js +128 -2
  63. package/lib/ecosystems/rubyutils.js +544 -34
  64. package/lib/ecosystems/rusi.js +10 -0
  65. package/lib/ecosystems/scalacliutils.js +4 -1
  66. package/lib/ecosystems/scalasem.js +798 -151
  67. package/lib/ecosystems/vendoredCode.js +237 -3
  68. package/lib/ecosystems/venvProbe.js +360 -0
  69. package/lib/evinser/evinser.js +472 -61
  70. package/lib/evinser/swiftsem.js +68 -5
  71. package/lib/helpers/agenticToolutils.js +413 -93
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentSkillSpec.js +17 -0
  74. package/lib/inventory/apkInstalledDb.js +243 -0
  75. package/lib/inventory/atomUtils.js +110 -0
  76. package/lib/inventory/auditCategories.js +3 -3
  77. package/lib/inventory/brotliDecoder.js +142 -0
  78. package/lib/inventory/buildToolRateLimit.js +1 -1
  79. package/lib/inventory/cacheDir.js +52 -9
  80. package/lib/inventory/cbomutils.js +56 -7
  81. package/lib/inventory/certificates.js +2371 -0
  82. package/lib/inventory/communityAiConfigParser.js +4 -9
  83. package/lib/inventory/deps.js +41 -4
  84. package/lib/inventory/display.js +200 -3
  85. package/lib/inventory/dosai.js +639 -63
  86. package/lib/inventory/dotnetRuntime.js +217 -0
  87. package/lib/inventory/dynamic.js +45 -3
  88. package/lib/inventory/envcontext.js +475 -18
  89. package/lib/inventory/evidenceUtils.js +447 -23
  90. package/lib/inventory/fetchBatch.js +1 -1
  91. package/lib/inventory/formulationParsers.js +32 -3
  92. package/lib/inventory/goBuildInfo.js +140 -0
  93. package/lib/inventory/gtfobins.js +13 -0
  94. package/lib/inventory/hbom.js +67 -7
  95. package/lib/inventory/hbomAnalysis.js +16 -0
  96. package/lib/inventory/hostRuntimeProbe.js +686 -0
  97. package/lib/inventory/hostTopology.js +21 -0
  98. package/lib/inventory/jvmLocalRepos.js +9 -2
  99. package/lib/inventory/jvmToolEnv.js +29 -0
  100. package/lib/inventory/licenseEnhancer.js +12 -0
  101. package/lib/inventory/mavenCentralMirror.js +534 -0
  102. package/lib/inventory/nodeBuildInfo.js +218 -0
  103. package/lib/inventory/nodeHostProbe.js +202 -0
  104. package/lib/inventory/osPackagePurl.js +244 -0
  105. package/lib/inventory/osPackageResolver.js +271 -87
  106. package/lib/inventory/osinfo.js +79 -31
  107. package/lib/inventory/osqueryTransform.js +114 -5
  108. package/lib/inventory/phpBuildInfo.js +438 -0
  109. package/lib/inventory/phpHostProbe.js +185 -0
  110. package/lib/inventory/rootfsCerts.js +426 -0
  111. package/lib/inventory/rubyBuildInfo.js +972 -0
  112. package/lib/inventory/rubyHostProbe.js +218 -0
  113. package/lib/inventory/scalaCoords.js +4 -3
  114. package/lib/inventory/scalaDedupe.js +440 -0
  115. package/lib/inventory/source.js +256 -5
  116. package/lib/inventory/spdx.js +144 -0
  117. package/lib/inventory/traceRunner.js +5 -1
  118. package/lib/inventory/tracedCrypto.js +129 -0
  119. package/lib/managers/binary.js +866 -186
  120. package/lib/managers/docker.js +245 -12
  121. package/lib/parsers/email.js +277 -0
  122. package/lib/parsers/largeJson.js +62 -13
  123. package/lib/parsers/vscodeExtensionId.js +183 -0
  124. package/lib/server/server.js +198 -2
  125. package/lib/stages/postgen/annotator.js +17 -1
  126. package/lib/stages/postgen/introspection/reflect.js +4 -1
  127. package/lib/stages/postgen/postgen.js +5 -5
  128. package/lib/stages/postgen/ruleEngine.js +71 -6
  129. package/lib/stages/postgen/specVersionCompat.js +30 -0
  130. package/lib/stages/pregen/envAudit.js +1 -0
  131. package/lib/stages/pregen/pregen.js +231 -62
  132. package/lib/validator/bomValidator.js +32 -6
  133. package/lib/validator/generated/runtime.mjs +13 -0
  134. package/lib/validator/generated/validate-1.6.mjs +27 -8
  135. package/lib/validator/generated/validate-1.7.mjs +32 -13
  136. package/lib/validator/generated/validate-2.0.mjs +31 -12
  137. package/lib/validator/index.js +1 -1
  138. package/package.json +19 -18
  139. package/types/bin/cdxgen-secure.d.ts +3 -0
  140. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  141. package/types/bin/repl.d.ts.map +1 -1
  142. package/types/lib/audit/index.d.ts.map +1 -1
  143. package/types/lib/cli/bomAssembly.d.ts +22 -3
  144. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  145. package/types/lib/cli/cliOptions.d.ts +43 -0
  146. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  147. package/types/lib/cli/index.d.ts.map +1 -1
  148. package/types/lib/cli/jsBom.d.ts +1 -17
  149. package/types/lib/cli/jsBom.d.ts.map +1 -1
  150. package/types/lib/cli/jvmBom.d.ts +20 -0
  151. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  152. package/types/lib/cli/managedBom.d.ts.map +1 -1
  153. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  154. package/types/lib/core/boundedFile.d.ts +70 -0
  155. package/types/lib/core/boundedFile.d.ts.map +1 -0
  156. package/types/lib/core/brotliProbe.d.ts +37 -0
  157. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  158. package/types/lib/core/env.d.ts +22 -2
  159. package/types/lib/core/env.d.ts.map +1 -1
  160. package/types/lib/core/fs.d.ts.map +1 -1
  161. package/types/lib/core/httpClient.d.ts +24 -0
  162. package/types/lib/core/httpClient.d.ts.map +1 -1
  163. package/types/lib/core/lockPaths.d.ts +19 -0
  164. package/types/lib/core/lockPaths.d.ts.map +1 -0
  165. package/types/lib/core/processTree.d.ts +2 -0
  166. package/types/lib/core/processTree.d.ts.map +1 -1
  167. package/types/lib/core/projectConfig.d.ts.map +1 -1
  168. package/types/lib/core/projectTooling.d.ts +41 -0
  169. package/types/lib/core/projectTooling.d.ts.map +1 -0
  170. package/types/lib/core/propertySanitizer.d.ts +26 -0
  171. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  172. package/types/lib/core/toolRegistry.d.ts +36 -0
  173. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  174. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  175. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  176. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  177. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  178. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  179. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  180. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  181. package/types/lib/ecosystems/golem.d.ts +81 -1
  182. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  183. package/types/lib/ecosystems/gradleutils.d.ts +57 -21
  184. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  187. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  188. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  189. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  191. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +14 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  199. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  200. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  201. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  202. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  203. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  205. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  206. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  207. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  208. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  209. package/types/lib/evinser/evinser.d.ts +88 -8
  210. package/types/lib/evinser/evinser.d.ts.map +1 -1
  211. package/types/lib/evinser/swiftsem.d.ts +10 -0
  212. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  213. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  214. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  215. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  216. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  217. package/types/lib/inventory/atomUtils.d.ts +33 -0
  218. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  219. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  220. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  221. package/types/lib/inventory/cacheDir.d.ts +12 -4
  222. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  223. package/types/lib/inventory/cbomutils.d.ts +17 -0
  224. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  225. package/types/lib/inventory/certificates.d.ts +205 -0
  226. package/types/lib/inventory/certificates.d.ts.map +1 -0
  227. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  228. package/types/lib/inventory/deps.d.ts +2 -1
  229. package/types/lib/inventory/deps.d.ts.map +1 -1
  230. package/types/lib/inventory/display.d.ts +18 -0
  231. package/types/lib/inventory/display.d.ts.map +1 -1
  232. package/types/lib/inventory/dosai.d.ts +162 -7
  233. package/types/lib/inventory/dosai.d.ts.map +1 -1
  234. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  235. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  236. package/types/lib/inventory/dynamic.d.ts +6 -1
  237. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  238. package/types/lib/inventory/envcontext.d.ts +92 -8
  239. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  240. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  241. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  242. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  243. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  244. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  245. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  246. package/types/lib/inventory/hbom.d.ts.map +1 -1
  247. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  248. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  249. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  250. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  251. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  252. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  253. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  254. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  255. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  256. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  257. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  258. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  259. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  260. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  261. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  262. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  263. package/types/lib/inventory/osPackageResolver.d.ts +44 -2
  264. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  265. package/types/lib/inventory/osinfo.d.ts +31 -6
  266. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  267. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  268. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  269. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  270. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  271. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  272. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  273. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  274. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  275. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  276. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  277. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  278. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  279. package/types/lib/inventory/source.d.ts +38 -0
  280. package/types/lib/inventory/source.d.ts.map +1 -1
  281. package/types/lib/inventory/spdx.d.ts +15 -0
  282. package/types/lib/inventory/spdx.d.ts.map +1 -1
  283. package/types/lib/inventory/traceRunner.d.ts +4 -1
  284. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  285. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  286. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  287. package/types/lib/managers/binary.d.ts +10 -0
  288. package/types/lib/managers/binary.d.ts.map +1 -1
  289. package/types/lib/managers/docker.d.ts +24 -0
  290. package/types/lib/managers/docker.d.ts.map +1 -1
  291. package/types/lib/parsers/email.d.ts +19 -0
  292. package/types/lib/parsers/email.d.ts.map +1 -0
  293. package/types/lib/parsers/largeJson.d.ts +17 -3
  294. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  295. package/types/lib/server/server.d.ts +8 -0
  296. package/types/lib/server/server.d.ts.map +1 -1
  297. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  298. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  299. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  300. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  301. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  302. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  303. package/types/lib/stages/pregen/pregen.d.ts +26 -9
  304. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
- import { URL } from "node:url";
4
+ import { fileURLToPath, URL } from "node:url";
5
5
 
6
6
  import { Purl } from "@cdxgen/cdx-purl";
7
7
  import { coerce, diff, prerelease } from "semver";
@@ -30,6 +30,7 @@ import {
30
30
  prefetchedResponse,
31
31
  prefetchJson,
32
32
  } from "./fetchBatch.js";
33
+ import { mavenCentralFetchUrl } from "./mavenCentralMirror.js";
33
34
  import { canonicalPurlFromLooseString } from "./purl.js";
34
35
 
35
36
  /** Raised when a caller omits a registry helper that the purl type requires. */
@@ -111,6 +112,9 @@ function inferGitOperation(args = []) {
111
112
  * @param {string[]} args git arguments
112
113
  * @param {Object} options command options
113
114
  * @param {string|undefined} options.cwd working directory
115
+ * @param {boolean|undefined} options.noRepository run outside any repository,
116
+ * so the config of a repository in the working directory, such as url.*.insteadOf
117
+ * or core.sshCommand, cannot change where a remote command connects
114
118
  * @returns {Object} spawn result
115
119
  */
116
120
  export function hardenedGitCommand(args, options = {}) {
@@ -122,8 +126,14 @@ export function hardenedGitCommand(args, options = {}) {
122
126
  GIT_CONFIG_VALUE_0: "false",
123
127
  GIT_CONFIG_KEY_1: "safe.bareRepository",
124
128
  GIT_CONFIG_VALUE_1: "explicit",
129
+ GIT_NO_LAZY_FETCH: "1",
125
130
  GIT_TERMINAL_PROMPT: "0",
126
131
  };
132
+ if (options.noRepository) {
133
+ // ls-remote works without a repository. Pointing GIT_DIR at a path that
134
+ // does not exist stops git from discovering one in the working directory.
135
+ envConfigs.GIT_DIR = path.join(getTmpDir(), "cdxgen-no-repository.invalid");
136
+ }
127
137
  const env = isSecureMode
128
138
  ? {
129
139
  ...process.env,
@@ -275,10 +285,14 @@ export function buildReleaseNotesFromGit(repoPath, options = {}) {
275
285
  typeof remoteUrl === "string"
276
286
  ? validateAndRejectGitSource(remoteUrl)
277
287
  : null;
288
+ // remote.origin.url comes from the scanned repository. Only a remote that
289
+ // validateAndRejectGitSource recognises and checks may reach ls-remote; git
290
+ // also treats forms such as host:path as remotes, which skip that check.
278
291
  const canDiscoverRemoteTags =
279
292
  (!currentTag || !previousTag) &&
280
293
  typeof remoteUrl === "string" &&
281
294
  !remoteUrl.startsWith("-") &&
295
+ maybeRemotePath(remoteUrl) &&
282
296
  !remoteUrlValidationError &&
283
297
  /github\.com[:/]/i.test(remoteUrl);
284
298
  if (canDiscoverRemoteTags) {
@@ -298,7 +312,7 @@ export function buildReleaseNotesFromGit(repoPath, options = {}) {
298
312
  "--",
299
313
  remoteUrl,
300
314
  ],
301
- {},
315
+ { noRepository: true },
302
316
  );
303
317
  const remoteTags = parseLsRemoteTags(remoteTagsResult.stdout);
304
318
  if (!currentTag && remoteTags.length) {
@@ -508,6 +522,59 @@ export function isAllowedPath(p) {
508
522
  });
509
523
  }
510
524
 
525
+ /**
526
+ * Resolve symlinks in a path. When the path does not exist yet, the nearest
527
+ * existing parent is resolved and the remaining segments are appended, so
528
+ * /tmp/new on macOS still becomes /private/tmp/new.
529
+ *
530
+ * @param {string} p Path string
531
+ * @returns {string} real path
532
+ */
533
+ function realPathOrResolved(p) {
534
+ let current = path.resolve(p);
535
+ const missing = [];
536
+ while (true) {
537
+ try {
538
+ return path.join(fs.realpathSync(current), ...missing);
539
+ } catch (_err) {
540
+ const parent = path.dirname(current);
541
+ if (parent === current) {
542
+ return path.resolve(p);
543
+ }
544
+ missing.unshift(path.basename(current));
545
+ current = parent;
546
+ }
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Checks where the given path really points, after following symlinks, against
552
+ * where the allowed paths really point. Both sides are resolved, so an allowed
553
+ * directory that is itself reached through a link (such as /tmp on macOS) still
554
+ * matches, while a link inside an allowed directory that leads outside it does
555
+ * not.
556
+ *
557
+ * @param {string} p Path string to check
558
+ * @returns {boolean} true if the real path is inside a real allowed path
559
+ */
560
+ export function isAllowedRealPath(p) {
561
+ if (typeof p !== "string" || hasDangerousUnicode(p)) {
562
+ return false;
563
+ }
564
+ const allowedPaths = getAllowedPaths();
565
+ if (!allowedPaths.length) {
566
+ return true;
567
+ }
568
+ const realP = realPathOrResolved(p);
569
+ return allowedPaths.some((ap) => {
570
+ const relativePath = path.relative(realPathOrResolved(ap), realP);
571
+ return (
572
+ relativePath === "" ||
573
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
574
+ );
575
+ });
576
+ }
577
+
511
578
  /**
512
579
  * Determine if the path could be a package URL.
513
580
  *
@@ -586,6 +653,49 @@ export function validateAndRejectGitSource(filePath) {
586
653
  };
587
654
  }
588
655
 
656
+ if (hasAmbiguousRemoteCharacters(filePath)) {
657
+ return {
658
+ status: 400,
659
+ error: "Invalid URL Format",
660
+ details:
661
+ "The provided Git URL contains backslashes, whitespace or control characters.",
662
+ };
663
+ }
664
+
665
+ // git reads file:// URLs from the local disk, so they get the path
666
+ // allowlist rather than the host allowlist.
667
+ if (gitUrlObj.protocol === "file:") {
668
+ let localPath;
669
+ try {
670
+ localPath = fileURLToPath(gitUrlObj);
671
+ } catch (_err) {
672
+ localPath = undefined;
673
+ }
674
+ if (
675
+ gitUrlObj.hostname ||
676
+ !localPath ||
677
+ !isAllowedPath(localPath) ||
678
+ (isWin && !isAllowedWinPath(localPath))
679
+ ) {
680
+ return {
681
+ status: 403,
682
+ error: "Path Not Allowed",
683
+ details: "Path is not allowed as per the allowlist.",
684
+ };
685
+ }
686
+ return null;
687
+ }
688
+
689
+ // git, libcurl and OpenSSH must connect to the host that was checked.
690
+ const gitRemoteHost = getGitRemoteHost(filePath);
691
+ if (!gitRemoteHost || gitUrlObj.hostname.toLowerCase() !== gitRemoteHost) {
692
+ return {
693
+ status: 400,
694
+ error: "Invalid URL Format",
695
+ details: "The host of the provided Git URL is ambiguous.",
696
+ };
697
+ }
698
+
589
699
  if (!isAllowedHost(gitUrlObj.hostname)) {
590
700
  return {
591
701
  status: 403,
@@ -598,6 +708,136 @@ export function validateAndRejectGitSource(filePath) {
598
708
  return null;
599
709
  }
600
710
 
711
+ /**
712
+ * Detect characters that git, libcurl or OpenSSH read differently from the
713
+ * WHATWG URL parser. A backslash ends the host for new URL() but is part of
714
+ * the user name for git, and new URL() silently drops tabs and newlines.
715
+ *
716
+ * @param {string} remote Git URL or SCP-like address
717
+ * @returns {boolean} true if the remote contains such characters
718
+ */
719
+ function hasAmbiguousRemoteCharacters(remote) {
720
+ if (hasDangerousUnicode(remote)) {
721
+ return true;
722
+ }
723
+ for (const ch of remote) {
724
+ const code = ch.codePointAt(0);
725
+ if (ch === "\\" || code <= 0x20 || (code >= 0x7f && code <= 0x9f)) {
726
+ return true;
727
+ }
728
+ }
729
+ return false;
730
+ }
731
+
732
+ /**
733
+ * Return the host a git client connects to for a remote, read the way git,
734
+ * libcurl and OpenSSH read it rather than the way new URL() does. git ends the
735
+ * authority of ssh:// and git:// URLs only at the first slash, percent-decodes
736
+ * it before splitting off the host, and reads a leading "[...]" as the host.
737
+ * OpenSSH takes the host after the last "@". So a "?" or "#" before the first
738
+ * slash, a second "@", brackets in the user name, or a percent sign in an
739
+ * ssh:// or git:// authority make the host ambiguous.
740
+ *
741
+ * @param {string} remote Git URL or SCP-like address
742
+ * @returns {string|undefined} lower-cased host, or undefined when it is ambiguous
743
+ */
744
+ export function getGitRemoteHost(remote) {
745
+ if (typeof remote !== "string") {
746
+ return undefined;
747
+ }
748
+ let authority;
749
+ const schemeEnd = remote.indexOf("://");
750
+ if (schemeEnd > -1) {
751
+ const scheme = remote.slice(0, schemeEnd).toLowerCase();
752
+ const rest = remote.slice(schemeEnd + 3);
753
+ const slash = rest.indexOf("/");
754
+ authority = slash > -1 ? rest.slice(0, slash) : rest;
755
+ // libcurl reads the http(s) authority as written. git decodes the others.
756
+ if (!["http", "https"].includes(scheme) && authority.includes("%")) {
757
+ return undefined;
758
+ }
759
+ } else {
760
+ // git reads user@host:path as SCP-like only when the colon comes before
761
+ // any slash. Otherwise it is a local path.
762
+ const colon = remote.indexOf(":");
763
+ const slash = remote.indexOf("/");
764
+ if (colon < 0 || (slash > -1 && slash < colon)) {
765
+ return undefined;
766
+ }
767
+ authority = remote.slice(0, colon);
768
+ }
769
+ if (authority.includes("?") || authority.includes("#")) {
770
+ return undefined;
771
+ }
772
+ const at = authority.indexOf("@");
773
+ if (at !== authority.lastIndexOf("@")) {
774
+ return undefined;
775
+ }
776
+ const userInfo = at > -1 ? authority.slice(0, at) : "";
777
+ if (userInfo.includes("[") || userInfo.includes("]")) {
778
+ return undefined;
779
+ }
780
+ let host = authority.slice(at + 1);
781
+ if (host.startsWith("[")) {
782
+ const close = host.indexOf("]");
783
+ if (close < 0) {
784
+ return undefined;
785
+ }
786
+ host = host.slice(0, close + 1);
787
+ } else {
788
+ const portSep = host.indexOf(":");
789
+ if (portSep > -1) {
790
+ host = host.slice(0, portSep);
791
+ }
792
+ }
793
+ if (!host || host.startsWith("-")) {
794
+ return undefined;
795
+ }
796
+ return host.toLowerCase();
797
+ }
798
+
799
+ /**
800
+ * Remove credentials from git output before it is logged. Every URL in the
801
+ * text loses its user name and password, which git copies into its error
802
+ * messages from the remote it was given.
803
+ *
804
+ * @param {string|undefined} text Output of a git command
805
+ * @param {string|undefined} repoUrl Remote the command was given
806
+ * @returns {string|undefined} Output with credentials replaced by ***
807
+ */
808
+ export function redactGitOutput(text, repoUrl) {
809
+ if (!text || typeof text !== "string") {
810
+ return text;
811
+ }
812
+ let redacted = text;
813
+ if (repoUrl && typeof repoUrl === "string") {
814
+ redacted = redacted.split(repoUrl).join(sanitizeRemoteUrlForLogs(repoUrl));
815
+ }
816
+ let from = 0;
817
+ while (from < redacted.length) {
818
+ const schemeEnd = redacted.indexOf("://", from);
819
+ if (schemeEnd === -1) {
820
+ break;
821
+ }
822
+ const authorityStart = schemeEnd + 3;
823
+ let authorityEnd = authorityStart;
824
+ while (
825
+ authorityEnd < redacted.length &&
826
+ !"/?#'\"<> \t\r\n".includes(redacted[authorityEnd])
827
+ ) {
828
+ authorityEnd++;
829
+ }
830
+ const at = redacted.lastIndexOf("@", authorityEnd - 1);
831
+ if (at >= authorityStart) {
832
+ redacted = `${redacted.slice(0, authorityStart)}***:***${redacted.slice(at)}`;
833
+ from = authorityStart + "***:***".length;
834
+ } else {
835
+ from = authorityEnd;
836
+ }
837
+ }
838
+ return redacted;
839
+ }
840
+
601
841
  /**
602
842
  * Clone a git repository into a temporary directory.
603
843
  *
@@ -606,6 +846,11 @@ export function validateAndRejectGitSource(filePath) {
606
846
  * @returns {string} cloned directory path
607
847
  */
608
848
  export function gitClone(repoUrl, branch = null) {
849
+ // Callers validate first, but git must never see a remote that fails it.
850
+ const validationError = validateAndRejectGitSource(repoUrl);
851
+ if (validationError) {
852
+ throw new Error(`${validationError.error}: ${validationError.details}`);
853
+ }
609
854
  let baseDirName = path.basename(repoUrl);
610
855
  if (!/^[a-zA-Z0-9_-]+$/.test(baseDirName)) {
611
856
  baseDirName = "repo-";
@@ -635,6 +880,8 @@ export function gitClone(repoUrl, branch = null) {
635
880
  "safe.bareRepository=explicit",
636
881
  "-c",
637
882
  "core.hooksPath=/dev/null",
883
+ "-c",
884
+ "core.symlinks=false",
638
885
  "clone",
639
886
  "--template=",
640
887
  repoUrl,
@@ -655,8 +902,11 @@ export function gitClone(repoUrl, branch = null) {
655
902
  `Cloning Repo${branch ? ` with branch ${branch}` : ""} to ${tempDir}`,
656
903
  );
657
904
  const result = hardenedGitCommand(gitArgs);
658
- if (result.status !== 0) {
659
- console.error(result.stderr);
905
+ if (result.status !== 0 || result.error) {
906
+ console.error(redactGitOutput(result.stderr, repoUrl));
907
+ // The directory was created by this call, so it is always safe to remove.
908
+ safeRmSync(tempDir, { recursive: true, force: true });
909
+ throw new Error("Git clone failed.");
660
910
  }
661
911
 
662
912
  return tempDir;
@@ -735,7 +985,7 @@ export function findGitRefForPurlVersion(repoUrl, purlResolution) {
735
985
  "--",
736
986
  repoUrl,
737
987
  ],
738
- {},
988
+ { noRepository: true },
739
989
  );
740
990
  if (result.status !== 0 || !result.stdout) {
741
991
  return undefined;
@@ -1384,6 +1634,7 @@ export async function resolveGitUrlFromPurl(purlString, helpers = {}) {
1384
1634
  break;
1385
1635
  }
1386
1636
  const mavenCentralUrl =
1637
+ mavenCentralFetchUrl() ||
1387
1638
  readEnvironmentVariable("MAVEN_CENTRAL_URL") ||
1388
1639
  "https://repo1.maven.org/maven2/";
1389
1640
  registry = mavenCentralUrl.endsWith("/")
@@ -440,6 +440,150 @@ export function licenseIdFromText(content) {
440
440
  return undefined;
441
441
  }
442
442
 
443
+ // The GNU licenses by the title line their text opens with, and their SPDX
444
+ // ids by version.
445
+ const GNU_LICENSE_TITLES = {
446
+ "GNU AFFERO GENERAL PUBLIC LICENSE": { 3: "AGPL-3.0-only" },
447
+ "GNU LESSER GENERAL PUBLIC LICENSE": {
448
+ 3: "LGPL-3.0-only",
449
+ 2.1: "LGPL-2.1-only",
450
+ 2: "LGPL-2.0-only",
451
+ },
452
+ "GNU LIBRARY GENERAL PUBLIC LICENSE": {
453
+ 2.1: "LGPL-2.1-only",
454
+ 2: "LGPL-2.0-only",
455
+ },
456
+ "GNU GENERAL PUBLIC LICENSE": {
457
+ 3: "GPL-3.0-only",
458
+ 2: "GPL-2.0-only",
459
+ 1: "GPL-1.0-only",
460
+ },
461
+ };
462
+
463
+ // Phrases that only the grant of one license carries, so finding one anywhere
464
+ // in a notice means that license is granted, not merely mentioned.
465
+ const LICENSE_GRANT_PHRASES = [
466
+ ["PERMISSION IS HEREBY GRANTED, FREE OF CHARGE", "MIT"],
467
+ [
468
+ "PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED",
469
+ "ISC",
470
+ ],
471
+ [
472
+ "PERMISSION IS GRANTED TO ANYONE TO USE THIS SOFTWARE FOR ANY PURPOSE",
473
+ "Zlib",
474
+ ],
475
+ [
476
+ "THIS IS FREE AND UNENCUMBERED SOFTWARE RELEASED INTO THE PUBLIC DOMAIN",
477
+ "Unlicense",
478
+ ],
479
+ ["BOOST SOFTWARE LICENSE - VERSION 1.0", "BSL-1.0"],
480
+ ["MOZILLA PUBLIC LICENSE VERSION 2.0", "MPL-2.0"],
481
+ ["MOZILLA PUBLIC LICENSE, VERSION 2.0", "MPL-2.0"],
482
+ ["MOZILLA PUBLIC LICENSE, V. 2.0", "MPL-2.0"],
483
+ [
484
+ "CREATIVE COMMONS ATTRIBUTION 4.0 INTERNATIONAL PUBLIC LICENSE",
485
+ "CC-BY-4.0",
486
+ ],
487
+ [
488
+ "CREATIVE COMMONS ATTRIBUTION-SHAREALIKE 4.0 INTERNATIONAL PUBLIC LICENSE",
489
+ "CC-BY-SA-4.0",
490
+ ],
491
+ ["CC0 1.0 UNIVERSAL", "CC0-1.0"],
492
+ ];
493
+
494
+ /**
495
+ * Identify every license a notice grants, in the order the notice grants
496
+ * them. A notice can grant several: one LICENSE file holding an MIT grant and
497
+ * an Apache-2.0 grant (gopkg.in/yaml.v3), or a choice of licenses followed by
498
+ * each full text (github.com/spdx/tools-golang). A license text quotes others
499
+ * (the GNU GPL names the GNU Lesser GPL, the MPL names the GNU licenses), so
500
+ * only phrases that a grant carries count: a GNU license is found by its title
501
+ * standing on a line of its own, the others by a sentence of their grant.
502
+ * A notice none of these phrases match falls back to {@link guessLicenseId}.
503
+ *
504
+ * @param {string} content License file contents
505
+ * @returns {string[]} SPDX license ids or expressions; empty when none is
506
+ * recognized
507
+ */
508
+ export function guessLicenseIds(content) {
509
+ if (typeof content !== "string" || !content.trim()) {
510
+ return [];
511
+ }
512
+ const text = content.replace(/\s+/g, " ").toUpperCase();
513
+ const found = [];
514
+ const add = (at, id) => {
515
+ if (at >= 0 && id) {
516
+ found.push({ at, id });
517
+ }
518
+ };
519
+ // Walk the lines alongside `text`, so a title line is placed in it.
520
+ let offset = 0;
521
+ for (const line of content.split("\n")) {
522
+ const normalized = line.replace(/\s+/g, " ").trim().toUpperCase();
523
+ const at = normalized ? text.indexOf(normalized, offset) : -1;
524
+ if (at < 0) {
525
+ continue;
526
+ }
527
+ offset = at + normalized.length;
528
+ const ids = GNU_LICENSE_TITLES[normalized];
529
+ if (ids) {
530
+ const version = text
531
+ .slice(offset, offset + 60)
532
+ .match(/VERSION (\d(?:\.\d)?)/)?.[1];
533
+ add(at, ids[version]);
534
+ }
535
+ }
536
+ for (const marker of [
537
+ "APACHE LICENSE VERSION 2.0",
538
+ "APACHE LICENSE, VERSION 2.0",
539
+ "APACHE LICENSE 2.0",
540
+ ]) {
541
+ add(
542
+ text.indexOf(marker),
543
+ text.includes("LLVM EXCEPTION")
544
+ ? "Apache-2.0 WITH LLVM-exception"
545
+ : "Apache-2.0",
546
+ );
547
+ }
548
+ for (const [phrase, id] of LICENSE_GRANT_PHRASES) {
549
+ add(text.indexOf(phrase), id);
550
+ }
551
+ // A notice can hold several BSD texts (one per copyright holder), each read
552
+ // up to where the next one starts.
553
+ const bsdPhrase = "REDISTRIBUTION AND USE IN SOURCE AND BINARY FORMS";
554
+ for (
555
+ let bsdAt = text.indexOf(bsdPhrase);
556
+ bsdAt >= 0;
557
+ bsdAt = text.indexOf(bsdPhrase, bsdAt + bsdPhrase.length)
558
+ ) {
559
+ const nextAt = text.indexOf(bsdPhrase, bsdAt + bsdPhrase.length);
560
+ const clauses = text.slice(
561
+ bsdAt,
562
+ Math.min(bsdAt + 2000, nextAt < 0 ? text.length : nextAt),
563
+ );
564
+ add(
565
+ bsdAt,
566
+ clauses.includes("ALL ADVERTISING MATERIALS MENTIONING")
567
+ ? "BSD-4-Clause"
568
+ : clauses.includes("NEITHER THE NAME") ||
569
+ clauses.includes("THE NAME OF THE AUTHOR MAY NOT BE USED")
570
+ ? "BSD-3-Clause"
571
+ : "BSD-2-Clause",
572
+ );
573
+ }
574
+ const ids = [];
575
+ for (const { id } of found.sort((a, b) => a.at - b.at)) {
576
+ if (!ids.includes(id)) {
577
+ ids.push(id);
578
+ }
579
+ }
580
+ if (ids.length) {
581
+ return ids;
582
+ }
583
+ const guessed = guessLicenseId(content);
584
+ return guessed ? [guessed] : [];
585
+ }
586
+
443
587
  /**
444
588
  * Method to guess the spdx license id from license contents
445
589
  *
@@ -195,7 +195,7 @@ export function resolveSaferExecBinary() {
195
195
  * @param {boolean} [options.denyPersistenceWrites] - Deny writes to auto-execution and persistence locations
196
196
  * @param {boolean} [options.privateTmp] - Private tmpfs /tmp and /var/tmp (Linux)
197
197
  * @param {string} [options.protectHome] - Home isolation: read-only, tmpfs, or off (Linux)
198
- * @returns {Promise<{libPaths: string[], httpAccessEntries: Object[], egressEntries?: Object[], dryRunSummary?: Object}>} Collected libraries, HTTP URLs, proxy egress decisions, and the dry-run summary
198
+ * @returns {Promise<{libPaths: string[], httpAccessEntries: Object[], cryptoEntries?: Object[], cryptoComponents?: Object[], cryptoResult?: Object, egressEntries?: Object[], dryRunSummary?: Object}>} Collected libraries, HTTP URLs, crypto observations, proxy egress decisions, and the dry-run summary
199
199
  * @throws {Error} When the @cdxgen/safer-exec tracing runtime is not installed
200
200
  * (for example after pruning it from a standalone payload), since the traced
201
201
  * command could never run and the BOM would silently be empty
@@ -531,6 +531,9 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
531
531
  httpAccessEntries: [...collectedUrls, ...urls],
532
532
  cryptoEntries: [...collectedCrypto, ...cryptoLogs],
533
533
  cryptoComponents,
534
+ // The CRYPTO result from the report channel: the cipher entries carry
535
+ // the certificate chains servers presented (safer-exec ≥ 1.2.0).
536
+ cryptoResult: result?.crypto,
534
537
  egressEntries: parseEgressEntries(result.auditLog),
535
538
  dryRunSummary,
536
539
  };
@@ -541,6 +544,7 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
541
544
  httpAccessEntries: collectedUrls,
542
545
  cryptoEntries: collectedCrypto,
543
546
  cryptoComponents,
547
+ cryptoResult: result?.crypto,
544
548
  egressEntries: [],
545
549
  dryRunSummary,
546
550
  };
@@ -3,6 +3,11 @@ import { join } from "node:path";
3
3
 
4
4
  import { thoughtLog } from "../core/logger.js";
5
5
  import { dirNameStr } from "../core/paths.js";
6
+ import {
7
+ CertificateCollector,
8
+ certRecordFromObservedChainEntry,
9
+ toWellFormedText,
10
+ } from "./certificates.js";
6
11
  import { applyAlgorithmProperties } from "./cryptoAlgorithmFamily.js";
7
12
 
8
13
  const cryptoOids = JSON.parse(
@@ -270,6 +275,130 @@ export function normalizeTracedCryptoComponents(cbomComponents) {
270
275
  return Array.from(byRef.values());
271
276
  }
272
277
 
278
+ // safer-exec walks at most this many certificates of one server's chain
279
+ // (tlsobserve maxChainCerts); a longer array did not come from it, so only the
280
+ // first entries are read.
281
+ const MAX_CHAIN_CERTS = 10;
282
+
283
+ /**
284
+ * The bom-ref normalizeTracedCryptoComponents gives the protocol component of
285
+ * a negotiated TLS version: safer-exec's CBOM names it "TLSv1.2" with the
286
+ * version the prefix leaves ("1.2").
287
+ *
288
+ * @param {string} protocol negotiated protocol name from the crypto result
289
+ * @returns {string} protocol component bom-ref
290
+ */
291
+ function tracedProtocolBomRef(protocol) {
292
+ const name = String(protocol || "");
293
+ const version = name.startsWith("TLSv") ? name.slice("TLSv".length) : name;
294
+ return `crypto/protocol/${encodeURIComponent(name)}@${encodeURIComponent(version || "unknown")}`;
295
+ }
296
+
297
+ /**
298
+ * Builds the shared certificate model for the chains servers presented in TLS
299
+ * ≤ 1.2 handshakes, from the `chain` arrays of safer-exec's crypto result
300
+ * (report channel, safer-exec ≥ 1.2.0; earlier releases and TLS 1.3 entries
301
+ * carry no chain and yield nothing). The records go through the shared
302
+ * certificate collector, so bom-refs, properties and dedup match certificates
303
+ * found on disk. A presented chain is untrusted server input: no issuer edges
304
+ * are inferred from it, and the certificate and public-key components link
305
+ * through the collector as every producer's do. Each TLS protocol component
306
+ * provides the certificates and keys presented under it — the edge safer-
307
+ * exec's own CBOM records — which answers which servers presented a key.
308
+ *
309
+ * @param {Object} cryptoResult safer-exec CRYPTO result (`result.crypto`)
310
+ * @param {Set<string>} protocolRefs bom-refs of the protocol components in the BOM;
311
+ * a provides edge is only emitted for a protocol that is actually present
312
+ * @returns {{components: Object[], dependencies: Object[]}} certificate,
313
+ * public-key and algorithm components, and the protocol provides edges
314
+ */
315
+ export function observedChainComponents(
316
+ cryptoResult,
317
+ protocolRefs = new Set(),
318
+ ) {
319
+ const collector = new CertificateCollector();
320
+ // Protocol name -> {certSha256, spkiSha256} pairs in presentation order,
321
+ // leaf first; sorted to refs at the end so the output never depends on the
322
+ // connection order the engine happened to report.
323
+ const chainEntriesByProtocol = new Map();
324
+ // The collector keeps the first record's fields for a certSha256, so two
325
+ // servers presenting one hash with conflicting text would otherwise make
326
+ // the name (and bom-ref) depend on connection order; sort first.
327
+ const ciphers = (
328
+ Array.isArray(cryptoResult?.ciphers) ? cryptoResult.ciphers : []
329
+ )
330
+ .map((cipher) => [JSON.stringify(cipher) ?? "", cipher])
331
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
332
+ .map(([, cipher]) => cipher);
333
+ for (const cipher of ciphers) {
334
+ const chain = cipher?.chain;
335
+ if (!Array.isArray(chain) || chain.length === 0) {
336
+ // safer-exec ≤ 1.1.2, or a TLS 1.3 handshake (encrypted Certificate
337
+ // message): no chain was observed.
338
+ continue;
339
+ }
340
+ const host = typeof cipher.host === "string" ? cipher.host : "";
341
+ for (const entry of chain.slice(0, MAX_CHAIN_CERTS)) {
342
+ const record = certRecordFromObservedChainEntry(entry, { host });
343
+ if (record) {
344
+ collector.addRecord(record);
345
+ }
346
+ }
347
+ // A protocol name is free text from the report channel; it only reaches a
348
+ // bom-ref through encodeURIComponent, so it must be well-formed.
349
+ const protocol = toWellFormedText(cipher.protocol);
350
+ if (protocol) {
351
+ let entries = chainEntriesByProtocol.get(protocol);
352
+ if (!entries) {
353
+ entries = [];
354
+ chainEntriesByProtocol.set(protocol, entries);
355
+ }
356
+ for (const entry of chain.slice(0, MAX_CHAIN_CERTS)) {
357
+ const certSha256 = String(entry?.certSha256 || "").toLowerCase();
358
+ const spkiSha256 = String(entry?.spkiSha256 || "").toLowerCase();
359
+ if (certSha256.length === 64) {
360
+ entries.push({ certSha256, spkiSha256 });
361
+ }
362
+ }
363
+ }
364
+ }
365
+ const { components, dependencies } = collector.build();
366
+ // The collector's components identify their asset by the hash in `version`
367
+ // (certSha256 for certificates, spkiSha256 for public keys).
368
+ const certRefBySha = new Map();
369
+ const keyRefBySpki = new Map();
370
+ for (const component of components) {
371
+ const assetType = component.cryptoProperties?.assetType;
372
+ if (assetType === "certificate") {
373
+ certRefBySha.set(component.version, component["bom-ref"]);
374
+ } else if (assetType === "related-crypto-material") {
375
+ keyRefBySpki.set(component.version, component["bom-ref"]);
376
+ }
377
+ }
378
+ const outputDependencies = [...dependencies];
379
+ for (const [protocol, entries] of chainEntriesByProtocol) {
380
+ const protocolRef = tracedProtocolBomRef(protocol);
381
+ if (!protocolRefs.has(protocolRef)) {
382
+ continue;
383
+ }
384
+ const provides = [
385
+ ...new Set(
386
+ entries.flatMap(({ certSha256, spkiSha256 }) => [
387
+ certRefBySha.get(certSha256),
388
+ keyRefBySpki.get(spkiSha256),
389
+ ]),
390
+ ),
391
+ ]
392
+ .filter((ref) => ref)
393
+ .sort();
394
+ if (provides.length) {
395
+ outputDependencies.push({ ref: protocolRef, provides });
396
+ }
397
+ }
398
+ outputDependencies.sort((left, right) => left.ref.localeCompare(right.ref));
399
+ return { components, dependencies: outputDependencies };
400
+ }
401
+
273
402
  /**
274
403
  * Maps the crypto libraries safer-exec reported to the paths they were loaded
275
404
  * from.