@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
@@ -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.