@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
@@ -5,6 +5,7 @@ import { parse as loadYaml } from "yaml";
5
5
 
6
6
  import { safeExistsSync } from "../core/fs.js";
7
7
  import { sanitizeBomPropertyValue } from "../core/propertySanitizer.js";
8
+ import { isAgentSkillSpecValid } from "./agentSkillSpec.js";
8
9
  import { parseJsonLike } from "./jsonLike.js";
9
10
  import {
10
11
  credentialIndicatorsForText,
@@ -279,18 +280,12 @@ function parseSkillFile(filePath, raw) {
279
280
  );
280
281
  }
281
282
  // Record whether the frontmatter satisfies the open Agent Skills constraints
282
- // (name: 1-64 lowercase-alnum-hyphen; description: 1-1024 chars) as a single
283
- // reviewer-friendly boolean rather than copying the raw values twice.
284
- const specValid =
285
- typeof metadata?.name === "string" &&
286
- /^[a-z0-9-]{1,64}$/u.test(metadata.name) &&
287
- typeof metadata?.description === "string" &&
288
- metadata.description.length >= 1 &&
289
- metadata.description.length <= 1024;
283
+ // as a single reviewer-friendly boolean rather than copying the raw values
284
+ // twice.
290
285
  addUniqueProperty(
291
286
  component.properties,
292
287
  "cdx:skill:specValid",
293
- specValid ? "true" : "false",
288
+ isAgentSkillSpecValid(metadata) ? "true" : "false",
294
289
  );
295
290
  const skillDir = dirname(filePath);
296
291
  for (const [subdir, property] of [
@@ -1,5 +1,5 @@
1
1
  import { lstatSync, readFileSync, statSync } from "node:fs";
2
- import { homedir, tmpdir } from "node:os";
2
+ import { homedir } from "node:os";
3
3
  import { delimiter as _delimiter, basename, dirname, join } from "node:path";
4
4
 
5
5
  import { build, Purl } from "@cdxgen/cdx-purl";
@@ -10,6 +10,7 @@ import { commandOutputText } from "../core/buildLedger.js";
10
10
  import { parseMavenArgs } from "../core/env.js";
11
11
  import {
12
12
  getAllFiles,
13
+ getTmpDir,
13
14
  multiChecksumFile,
14
15
  safeExistsSync,
15
16
  safeMkdtempSync,
@@ -99,7 +100,8 @@ function jarPurl(group, name, version, classifier = undefined) {
99
100
  * Collect maven dependencies
100
101
  *
101
102
  * @param {string} mavenCmd Maven command to use
102
- * @param {string} basePath Path to the maven project
103
+ * @param {string} basePath Path to the maven project. Without it Maven is not
104
+ * run, and only the local repository is read when includeCacheDir is set.
103
105
  * @param {boolean} cleanup Remove temporary directories
104
106
  * @param {boolean} includeCacheDir Include maven and gradle cache directories
105
107
  */
@@ -118,7 +120,7 @@ export async function collectMvnDependencies(
118
120
  safeExistsSync(repo),
119
121
  ) ||
120
122
  join(homedir(), ".m2", "repository");
121
- const tempDir = safeMkdtempSync(join(tmpdir(), "mvn-deps-"));
123
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "mvn-deps-"));
122
124
  // No -U: forcing update checks re-requests every maven-metadata.xml and
123
125
  // every artifact Maven already knows is missing, once per module.
124
126
  let copyArgs = [
@@ -178,7 +180,7 @@ export async function collectMvnDependencies(
178
180
  }
179
181
 
180
182
  // Clean up
181
- if (cleanup && tempDir?.startsWith(tmpdir())) {
183
+ if (cleanup && tempDir?.startsWith(getTmpDir())) {
182
184
  safeRmSync(tempDir, { recursive: true, force: true });
183
185
  }
184
186
  return jarNSMapping;
@@ -14,6 +14,7 @@ import { readReachablesSlices } from "./atomUtils.js";
14
14
  import { formatOccurrenceEvidence } from "./evidenceUtils.js";
15
15
  import {
16
16
  formatHbomHardwareClassSummary,
17
+ getHbomPrivilegeGuidance,
17
18
  getHbomSummary,
18
19
  isHbomLikeBom,
19
20
  } from "./hbomAnalysis.js";
@@ -475,7 +476,9 @@ export const buildTableSummaryLines = (
475
476
  );
476
477
  if (hbomSummary.requiresPrivilegedEnrichment) {
477
478
  summaryLines.push(
478
- "Permission-sensitive enrichments were skipped or blocked. Re-run with --privileged where policy allows.",
479
+ hbomSummary.platform === "windows"
480
+ ? getHbomPrivilegeGuidance(hbomSummary.platform)
481
+ : "Permission-sensitive enrichments were skipped or blocked. Re-run with --privileged where policy allows.",
479
482
  );
480
483
  }
481
484
  }
@@ -526,6 +529,7 @@ const HBOM_COLUMN_PRIORITY = Object.freeze([
526
529
  ["cdx:hbom:securityMode", "securityMode"],
527
530
  ["cdx:hbom:health", "health"],
528
531
  ["cdx:hbom:smartStatus", "smartStatus"],
532
+ ["cdx:hbom:healthStatus", "healthStatus"],
529
533
  ["cdx:hbom:powerSource", "powerSource"],
530
534
  ["cdx:hbom:maximumCapacity", "maximumCapacity"],
531
535
  ["cdx:hbom:chargePercent", "chargePercent"],
@@ -546,6 +550,7 @@ const HBOM_COLUMN_PRIORITY = Object.freeze([
546
550
  ["cdx:hostview:kernel_modules:count", "kernelMods"],
547
551
  ["cdx:hostview:mount_hardening:count", "runtimeMounts"],
548
552
  ["cdx:hostview:runtime-storage:count", "runtimeStorage"],
553
+ ["cdx:hostview:logical_drives:count", "runtimeDrives"],
549
554
  ["cdx:hostview:linkedRuntimeCategoryCount", "runtimeLinks"],
550
555
  ]);
551
556
 
@@ -616,6 +621,7 @@ const HBOM_CLASS_PROPERTY_PRIORITY = Object.freeze({
616
621
  storage: Object.freeze([
617
622
  ["cdx:hbom:capacity", "capacity"],
618
623
  ["cdx:hbom:smartStatus", "smart"],
624
+ ["cdx:hbom:healthStatus", "healthStatus"],
619
625
  ["cdx:hbom:wearPercentageUsed", "wearUsed"],
620
626
  ["cdx:hbom:transport", "transport"],
621
627
  ["cdx:hbom:firmwareVersion", "firmware"],
@@ -630,6 +636,7 @@ const HBOM_CLASS_PROPERTY_PRIORITY = Object.freeze({
630
636
  ["cdx:hbom:isRemovable", "removable"],
631
637
  ["cdx:hostview:mount_hardening:count", "runtimeMounts"],
632
638
  ["cdx:hostview:runtime-storage:count", "runtimeStorage"],
639
+ ["cdx:hostview:logical_drives:count", "runtimeDrives"],
633
640
  ]),
634
641
  sensor: Object.freeze([
635
642
  ["cdx:hbom:temperatureCelsius", "tempC"],
@@ -870,8 +877,13 @@ export function printTable(
870
877
  }
871
878
  if (
872
879
  bomJson.metadata?.component &&
873
- ["operating-system", "platform"].includes(bomJson.metadata.component.type)
880
+ ["operating-system", "platform"].includes(
881
+ bomJson.metadata.component.type,
882
+ ) &&
883
+ !filterTypes?.includes("cryptographic-asset")
874
884
  ) {
885
+ // A filtered crypto view stays a crypto view even on an OS BOM; only the
886
+ // unfiltered component listing falls through to the OS table.
875
887
  return printOSTable(bomJson);
876
888
  }
877
889
  if (isHbomLikeBom(bomJson) && !filterTypes?.includes("cryptographic-asset")) {
@@ -1037,7 +1049,13 @@ const locationComparator = (a, b) => {
1037
1049
  const tmpB = b.split("#");
1038
1050
  if (tmpA.length === 2 && tmpB.length === 2) {
1039
1051
  if (tmpA[0] === tmpB[0]) {
1040
- return tmpA[1] - tmpB[1];
1052
+ // Line numbers sort numerically; other fragments (a secret key such
1053
+ // as data.tls.crt) by text, never NaN.
1054
+ const lineA = Number(tmpA[1]);
1055
+ const lineB = Number(tmpB[1]);
1056
+ return Number.isNaN(lineA) || Number.isNaN(lineB)
1057
+ ? tmpA[1].localeCompare(tmpB[1])
1058
+ : lineA - lineB;
1041
1059
  }
1042
1060
  }
1043
1061
  }
@@ -2125,3 +2143,182 @@ export function displaySelfThreatModel(
2125
2143
  process.exit(1);
2126
2144
  }
2127
2145
  }
2146
+
2147
+ function certificateComponents(bomJson) {
2148
+ return (bomJson?.components || []).filter(
2149
+ (component) => component?.cryptoProperties?.assetType === "certificate",
2150
+ );
2151
+ }
2152
+
2153
+ function auditedBomRefs(bomJson) {
2154
+ // Audit findings land as document annotations whose location names the
2155
+ // component; a certificate listed there is flagged in the table. The
2156
+ // annotation text is rendered as a markdown property table, so the rows
2157
+ // are parsed back out of it (the same way the REPL's audit view does).
2158
+ const refs = new Set();
2159
+ for (const annotation of bomJson?.annotations || []) {
2160
+ const text = typeof annotation?.text === "string" ? annotation.text : "";
2161
+ for (const line of text.split(/\r?\n/)) {
2162
+ if (!line.includes("cdx:audit:location:bomRef")) {
2163
+ continue;
2164
+ }
2165
+ const cells = line.split("|").map((cell) => cell.trim());
2166
+ if (
2167
+ cells.length >= 2 &&
2168
+ cells[1] === "cdx:audit:location:bomRef" &&
2169
+ cells[2]
2170
+ ) {
2171
+ refs.add(cells[2]);
2172
+ }
2173
+ }
2174
+ }
2175
+ return refs;
2176
+ }
2177
+
2178
+ /**
2179
+ * Print a table of the BOM's certificate components: role, name, issuer,
2180
+ * expiry, the SPKI prefix and how many locations each certificate appears
2181
+ * in, with a marker when the audit flagged it.
2182
+ *
2183
+ * @param {Object} bomJson CycloneDX BOM JSON object
2184
+ * @returns {void}
2185
+ */
2186
+ export function printCertificatesTable(bomJson) {
2187
+ const certificates = certificateComponents(bomJson);
2188
+ if (!certificates.length) {
2189
+ console.log(
2190
+ "No certificate components found. Generate a CBOM with --include-crypto to use this view.",
2191
+ );
2192
+ return;
2193
+ }
2194
+ const flagged = auditedBomRefs(bomJson);
2195
+ const stream = createStream({
2196
+ columnDefault: { width: 24 },
2197
+ columnCount: 6,
2198
+ columns: [
2199
+ { width: 13 },
2200
+ { width: 30 },
2201
+ { width: 30 },
2202
+ { width: 21 },
2203
+ { width: 18 },
2204
+ { width: 5, alignment: "center" },
2205
+ ],
2206
+ });
2207
+ stream.write([
2208
+ "Role",
2209
+ "Name",
2210
+ "Issuer CN",
2211
+ "Not valid after",
2212
+ "SPKI (first 16)",
2213
+ "Locs",
2214
+ ]);
2215
+ for (const certificate of certificates) {
2216
+ const issuerCn = commonNameFromIssuer(certificate);
2217
+ stream.write([
2218
+ getPropertyValue(certificate, "cdx:crypto:certRole") || "?",
2219
+ certificate.name || "",
2220
+ issuerCn ||
2221
+ certificate.cryptoProperties?.certificateProperties?.issuerName ||
2222
+ "",
2223
+ certificate.cryptoProperties?.certificateProperties?.notValidAfter || "",
2224
+ (getPropertyValue(certificate, "cdx:crypto:spkiSha256") || "").slice(
2225
+ 0,
2226
+ 16,
2227
+ ),
2228
+ `${certificate.evidence?.occurrences?.length || 0}${flagged.has(certificate["bom-ref"]) ? " ⚠" : ""}`,
2229
+ ]);
2230
+ }
2231
+ stream.end();
2232
+ console.log();
2233
+ console.log(`Found ${certificates.length} certificate component(s).`);
2234
+ }
2235
+
2236
+ function commonNameFromIssuer(certificate) {
2237
+ const issuer =
2238
+ certificate.cryptoProperties?.certificateProperties?.issuerName || "";
2239
+ const parts = issuer.split(",").map((part) => part.trim());
2240
+ const cn = parts.find((part) => part.startsWith("CN="));
2241
+ return cn ? cn.slice(3) : parts[0];
2242
+ }
2243
+
2244
+ /**
2245
+ * Print the issuer chain above a certificate and the certificates issued
2246
+ * under it, walking the verified `dependsOn` edges.
2247
+ *
2248
+ * @param {Object} bomJson CycloneDX BOM JSON object
2249
+ * @param {string} selector bom-ref, SPKI hex prefix or certificate ref prefix
2250
+ * @returns {void}
2251
+ */
2252
+ export function printCertificateChain(bomJson, selector) {
2253
+ const query = String(selector || "").trim();
2254
+ if (!query) {
2255
+ console.log("Usage: .certchain <bom-ref|spki-prefix|cert-prefix>");
2256
+ return;
2257
+ }
2258
+ const certificates = certificateComponents(bomJson);
2259
+ const byRef = new Map(certificates.map((c) => [c["bom-ref"], c]));
2260
+ const edges = new Map(
2261
+ (bomJson?.dependencies || []).map((dep) => [
2262
+ dep.ref,
2263
+ (dep.dependsOn || []).filter((ref) => byRef.has(ref)),
2264
+ ]),
2265
+ );
2266
+ const issuersOf = new Map();
2267
+ for (const [ref, issuerRefs] of edges) {
2268
+ for (const issuerRef of issuerRefs) {
2269
+ if (!issuersOf.has(issuerRef)) {
2270
+ issuersOf.set(issuerRef, []);
2271
+ }
2272
+ issuersOf.get(issuerRef).push(ref);
2273
+ }
2274
+ }
2275
+ let target;
2276
+ if (byRef.has(query)) {
2277
+ target = byRef.get(query);
2278
+ } else {
2279
+ target = certificates.find(
2280
+ (certificate) =>
2281
+ certificate["bom-ref"].includes(query) ||
2282
+ (
2283
+ getPropertyValue(certificate, "cdx:crypto:spkiSha256") || ""
2284
+ ).startsWith(query.toLowerCase()),
2285
+ );
2286
+ }
2287
+ if (!target) {
2288
+ console.log(`No certificate matches '${query}'.`);
2289
+ return;
2290
+ }
2291
+ const describe = (certificate) =>
2292
+ `${getPropertyValue(certificate, "cdx:crypto:certRole") || "?"} ${certificate.name} (${(getPropertyValue(certificate, "cdx:crypto:spkiSha256") || "").slice(0, 16)}…)`;
2293
+ const chain = [];
2294
+ let cursor = target["bom-ref"];
2295
+ const seen = new Set([cursor]);
2296
+ while (edges.has(cursor)) {
2297
+ const next = (edges.get(cursor) || []).filter((ref) => !seen.has(ref));
2298
+ if (!next.length) {
2299
+ break;
2300
+ }
2301
+ cursor = next[0];
2302
+ seen.add(cursor);
2303
+ chain.push(byRef.get(cursor));
2304
+ }
2305
+ console.log(`Chain above ${describe(target)}:`);
2306
+ if (chain.length) {
2307
+ chain.forEach((certificate, index) => {
2308
+ console.log(`${" ".repeat(index + 1)}← ${describe(certificate)}`);
2309
+ });
2310
+ } else {
2311
+ console.log(" (no verified issuer in this BOM)");
2312
+ }
2313
+ const issued = (issuersOf.get(target["bom-ref"]) || []).filter((ref) =>
2314
+ byRef.has(ref),
2315
+ );
2316
+ console.log(`Issued under ${describe(target)}:`);
2317
+ if (issued.length) {
2318
+ for (const ref of issued) {
2319
+ console.log(` → ${describe(byRef.get(ref))}`);
2320
+ }
2321
+ } else {
2322
+ console.log(" (none in this BOM)");
2323
+ }
2324
+ }