@cdxgen/cdxgen 13.3.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 (372) hide show
  1. package/README.md +12 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +115 -31
  4. package/bin/evinse.js +23 -1
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +115 -0
  7. package/bin/sign.js +45 -15
  8. package/bin/tracebom.js +5 -1
  9. package/bin/validate.js +84 -18
  10. package/bin/verify.js +155 -86
  11. package/data/cbomosdb-queries.json +13 -1
  12. package/data/component-tags.json +8 -0
  13. package/data/helpers/init.gradle +46 -21
  14. package/data/queries-darwin.json +25 -1
  15. package/data/queries-win.json +19 -1
  16. package/data/queries.json +41 -0
  17. package/data/remediations.json +344 -1
  18. package/data/rules/cbom-security.yaml +235 -4
  19. package/data/rules/dependency-sources.yaml +67 -0
  20. package/data/rules/go-binaries.yaml +46 -0
  21. package/data/rules/golem-go.yaml +80 -0
  22. package/data/rules/hbom-compliance.yaml +40 -22
  23. package/data/rules/hbom-performance.yaml +7 -1
  24. package/data/rules/host-topology.yaml +1 -1
  25. package/data/rules/obom-runtime.yaml +31 -2
  26. package/data/rules/php-runtime.yaml +143 -0
  27. package/data/rules/scala-evidence.yaml +115 -0
  28. package/lib/audit/index.js +52 -6
  29. package/lib/cli/bomAssembly.js +28 -3
  30. package/lib/cli/cliOptions.js +110 -3
  31. package/lib/cli/index.js +186 -58
  32. package/lib/cli/jsBom.js +50 -29
  33. package/lib/cli/jvmBom.js +779 -146
  34. package/lib/cli/managedBom.js +1021 -261
  35. package/lib/cli/nativeBom.js +364 -58
  36. package/lib/core/activity.js +12 -0
  37. package/lib/core/env.js +47 -17
  38. package/lib/core/fs.js +31 -21
  39. package/lib/core/httpClient.js +233 -69
  40. package/lib/core/pluginRun.js +184 -0
  41. package/lib/core/processTree.js +100 -0
  42. package/lib/core/projectConfig.js +1 -0
  43. package/lib/core/projectTooling.js +97 -0
  44. package/lib/core/propertySanitizer.js +72 -0
  45. package/lib/core/runState.js +39 -0
  46. package/lib/core/toml.js +72 -0
  47. package/lib/ecosystems/asarutils.js +4 -2
  48. package/lib/ecosystems/bunutils.js +5 -7
  49. package/lib/ecosystems/cargoCache.js +231 -8
  50. package/lib/ecosystems/cmakeResolver.js +10 -21
  51. package/lib/ecosystems/cppBuildContext.js +631 -0
  52. package/lib/ecosystems/cppEvidence.js +337 -22
  53. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  54. package/lib/ecosystems/denoutils.js +93 -66
  55. package/lib/ecosystems/ecosystems.js +1967 -788
  56. package/lib/ecosystems/golem.js +578 -18
  57. package/lib/ecosystems/gradleutils.js +340 -46
  58. package/lib/ecosystems/jsEvidence.js +85 -24
  59. package/lib/ecosystems/kosi.js +21 -4
  60. package/lib/ecosystems/npmutils.js +64 -0
  61. package/lib/ecosystems/parsers-dotnet.js +668 -153
  62. package/lib/ecosystems/parsers-elm.js +46 -28
  63. package/lib/ecosystems/parsers-gleam.js +3 -4
  64. package/lib/ecosystems/parsers-go.js +1283 -297
  65. package/lib/ecosystems/parsers-js.js +92 -26
  66. package/lib/ecosystems/parsers-julia.js +2 -3
  67. package/lib/ecosystems/parsers-jvm.js +463 -29
  68. package/lib/ecosystems/parsers-misc.js +405 -93
  69. package/lib/ecosystems/parsers-python.js +152 -20
  70. package/lib/ecosystems/parsers-rust.js +4 -4
  71. package/lib/ecosystems/parsers-terraform.js +1573 -19
  72. package/lib/ecosystems/pythonutils.js +102 -6
  73. package/lib/ecosystems/remote/huggingface.js +3 -0
  74. package/lib/ecosystems/rubyutils.js +73 -20
  75. package/lib/ecosystems/rusi.js +32 -9
  76. package/lib/ecosystems/sbtutils.js +243 -187
  77. package/lib/ecosystems/scalacliutils.js +343 -0
  78. package/lib/ecosystems/scalasem.js +2620 -0
  79. package/lib/ecosystems/terraformRegistry.js +288 -0
  80. package/lib/ecosystems/utils.js +2 -0
  81. package/lib/ecosystems/vendoredCode.js +156 -0
  82. package/lib/evinser/evinser.js +695 -78
  83. package/lib/evinser/swiftsem.js +68 -5
  84. package/lib/helpers/agenticToolutils.js +2 -2
  85. package/lib/helpers/bomSigner.js +576 -128
  86. package/lib/inventory/agentSkillSpec.js +17 -0
  87. package/lib/inventory/aiCollector.js +4 -1
  88. package/lib/inventory/analyzer.js +110 -127
  89. package/lib/inventory/apkInstalledDb.js +243 -0
  90. package/lib/inventory/atomUtils.js +204 -48
  91. package/lib/inventory/auditCategories.js +10 -3
  92. package/lib/inventory/buildToolRateLimit.js +144 -0
  93. package/lib/inventory/cacheCommands.js +2 -1
  94. package/lib/inventory/cacheDir.js +52 -9
  95. package/lib/inventory/certificates.js +2371 -0
  96. package/lib/inventory/cmakeBuildDirs.js +254 -0
  97. package/lib/inventory/communityAiConfigParser.js +4 -9
  98. package/lib/inventory/deps.js +507 -128
  99. package/lib/inventory/display.js +200 -3
  100. package/lib/inventory/dosai.js +747 -101
  101. package/lib/inventory/dosaiParsers.js +310 -15
  102. package/lib/inventory/dynamic.js +96 -8
  103. package/lib/inventory/envcontext.js +168 -5
  104. package/lib/inventory/evidenceUtils.js +590 -132
  105. package/lib/inventory/fetchBatch.js +574 -38
  106. package/lib/inventory/fetchDiskCache.js +597 -0
  107. package/lib/inventory/forgeEnricher.js +14 -5
  108. package/lib/inventory/formulationParsers.js +3 -2
  109. package/lib/inventory/goBuildInfo.js +140 -0
  110. package/lib/inventory/hbom.js +67 -7
  111. package/lib/inventory/hbomAnalysis.js +16 -0
  112. package/lib/inventory/hostTopology.js +21 -0
  113. package/lib/inventory/jvmLocalRepos.js +917 -0
  114. package/lib/inventory/mcpConfigParser.js +2 -3
  115. package/lib/inventory/osPackagePurl.js +244 -0
  116. package/lib/inventory/osPackageResolver.js +89 -87
  117. package/lib/inventory/osinfo.js +79 -31
  118. package/lib/inventory/osqueryTransform.js +114 -5
  119. package/lib/inventory/parentSelection.js +146 -0
  120. package/lib/inventory/phpBuildInfo.js +487 -0
  121. package/lib/inventory/phpHostProbe.js +641 -0
  122. package/lib/inventory/rootfsCerts.js +426 -0
  123. package/lib/inventory/rustFormulationParser.js +3 -4
  124. package/lib/inventory/scalaCoords.js +153 -0
  125. package/lib/inventory/source.js +215 -11
  126. package/lib/inventory/spdx.js +245 -0
  127. package/lib/inventory/traceRunner.js +93 -8
  128. package/lib/inventory/tracedCrypto.js +423 -0
  129. package/lib/managers/binary.js +529 -183
  130. package/lib/managers/docker.js +199 -44
  131. package/lib/managers/oci.js +24 -2
  132. package/lib/parsers/cmakeCache.js +69 -0
  133. package/lib/parsers/cmakeLists.js +16 -2
  134. package/lib/parsers/cmakePresets.js +400 -0
  135. package/lib/parsers/compileCommands.js +557 -0
  136. package/lib/parsers/email.js +277 -0
  137. package/lib/parsers/largeJson.js +34 -2
  138. package/lib/parsers/strictJson.js +322 -0
  139. package/lib/parsers/terraformConfig.js +1073 -0
  140. package/lib/parsers/terraformSource.js +652 -0
  141. package/lib/server/server.js +209 -1
  142. package/lib/stages/postgen/annotator.js +17 -1
  143. package/lib/stages/postgen/introspection/markers.js +1 -1
  144. package/lib/stages/postgen/introspection/reflect.js +4 -1
  145. package/lib/stages/postgen/postgen.js +20 -6
  146. package/lib/stages/postgen/ruleEngine.js +58 -1
  147. package/lib/stages/postgen/specVersionCompat.js +62 -0
  148. package/lib/stages/pregen/pregen.js +117 -5
  149. package/lib/validator/bomValidator.js +26 -5
  150. package/lib/validator/generated/runtime.mjs +13 -0
  151. package/lib/validator/generated/validate-1.6.mjs +26 -7
  152. package/lib/validator/generated/validate-1.7.mjs +31 -12
  153. package/lib/validator/generated/validate-2.0.mjs +30 -11
  154. package/lib/validator/index.js +25 -10
  155. package/lib/validator/reporters/json.js +1 -0
  156. package/package.json +23 -23
  157. package/types/bin/cdxgen-secure.d.ts +3 -0
  158. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  159. package/types/bin/repl.d.ts.map +1 -1
  160. package/types/lib/audit/index.d.ts.map +1 -1
  161. package/types/lib/cli/bomAssembly.d.ts +9 -2
  162. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  163. package/types/lib/cli/cliOptions.d.ts +43 -0
  164. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  165. package/types/lib/cli/index.d.ts.map +1 -1
  166. package/types/lib/cli/jsBom.d.ts.map +1 -1
  167. package/types/lib/cli/jvmBom.d.ts +44 -1
  168. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  169. package/types/lib/cli/managedBom.d.ts +7 -4
  170. package/types/lib/cli/managedBom.d.ts.map +1 -1
  171. package/types/lib/cli/nativeBom.d.ts +3 -1
  172. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  173. package/types/lib/core/activity.d.ts.map +1 -1
  174. package/types/lib/core/env.d.ts +27 -2
  175. package/types/lib/core/env.d.ts.map +1 -1
  176. package/types/lib/core/fs.d.ts.map +1 -1
  177. package/types/lib/core/httpClient.d.ts +13 -8
  178. package/types/lib/core/httpClient.d.ts.map +1 -1
  179. package/types/lib/core/pluginRun.d.ts +97 -0
  180. package/types/lib/core/pluginRun.d.ts.map +1 -0
  181. package/types/lib/core/processTree.d.ts +36 -0
  182. package/types/lib/core/processTree.d.ts.map +1 -0
  183. package/types/lib/core/projectConfig.d.ts.map +1 -1
  184. package/types/lib/core/projectTooling.d.ts +41 -0
  185. package/types/lib/core/projectTooling.d.ts.map +1 -0
  186. package/types/lib/core/propertySanitizer.d.ts +26 -0
  187. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  188. package/types/lib/core/runState.d.ts +29 -0
  189. package/types/lib/core/runState.d.ts.map +1 -0
  190. package/types/lib/core/toml.d.ts +20 -0
  191. package/types/lib/core/toml.d.ts.map +1 -0
  192. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  193. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  194. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  195. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  196. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  197. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  198. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  199. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  200. package/types/lib/ecosystems/cppEvidence.d.ts +19 -3
  201. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  202. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  203. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  204. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  205. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  207. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  208. package/types/lib/ecosystems/golem.d.ts +81 -1
  209. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  210. package/types/lib/ecosystems/gradleutils.d.ts +59 -7
  211. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  212. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  213. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  214. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  215. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  216. package/types/lib/ecosystems/parsers-dotnet.d.ts +155 -5
  217. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  218. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  219. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  220. package/types/lib/ecosystems/parsers-go.d.ts +119 -5
  221. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  222. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  223. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  224. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  225. package/types/lib/ecosystems/parsers-jvm.d.ts +74 -3
  226. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  227. package/types/lib/ecosystems/parsers-misc.d.ts +21 -59
  228. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  229. package/types/lib/ecosystems/parsers-python.d.ts +57 -6
  230. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  231. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  232. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  233. package/types/lib/ecosystems/pythonutils.d.ts +26 -1
  234. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  235. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  236. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  237. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  238. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  239. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  240. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  241. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  242. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  243. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  244. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  245. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  246. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  247. package/types/lib/ecosystems/utils.d.ts +2 -2
  248. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  249. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  250. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  251. package/types/lib/evinser/evinser.d.ts +100 -8
  252. package/types/lib/evinser/evinser.d.ts.map +1 -1
  253. package/types/lib/evinser/swiftsem.d.ts +10 -0
  254. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  255. package/types/lib/helpers/bomSigner.d.ts +80 -4
  256. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  257. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  258. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  259. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  260. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  261. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  262. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  263. package/types/lib/inventory/atomUtils.d.ts +96 -10
  264. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  265. package/types/lib/inventory/auditCategories.d.ts +3 -0
  266. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  267. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  268. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  269. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  270. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  271. package/types/lib/inventory/cacheDir.d.ts +12 -4
  272. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  273. package/types/lib/inventory/certificates.d.ts +205 -0
  274. package/types/lib/inventory/certificates.d.ts.map +1 -0
  275. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  276. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  277. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  278. package/types/lib/inventory/deps.d.ts +61 -2
  279. package/types/lib/inventory/deps.d.ts.map +1 -1
  280. package/types/lib/inventory/display.d.ts +18 -0
  281. package/types/lib/inventory/display.d.ts.map +1 -1
  282. package/types/lib/inventory/dosai.d.ts +202 -14
  283. package/types/lib/inventory/dosai.d.ts.map +1 -1
  284. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  285. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  286. package/types/lib/inventory/dynamic.d.ts +16 -1
  287. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  288. package/types/lib/inventory/envcontext.d.ts +40 -2
  289. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  290. package/types/lib/inventory/evidenceUtils.d.ts +48 -14
  291. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  292. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  293. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  294. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  295. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  296. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  297. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  298. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  299. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  300. package/types/lib/inventory/hbom.d.ts.map +1 -1
  301. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  302. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  303. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  304. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  305. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  306. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  307. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  308. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  309. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  310. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  311. package/types/lib/inventory/osinfo.d.ts +31 -6
  312. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  313. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  314. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  315. package/types/lib/inventory/parentSelection.d.ts +34 -0
  316. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  317. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  318. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  319. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  320. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  321. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  322. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  323. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  324. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  325. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  326. package/types/lib/inventory/source.d.ts +28 -0
  327. package/types/lib/inventory/source.d.ts.map +1 -1
  328. package/types/lib/inventory/spdx.d.ts +24 -0
  329. package/types/lib/inventory/spdx.d.ts.map +1 -1
  330. package/types/lib/inventory/traceRunner.d.ts +16 -1
  331. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  332. package/types/lib/inventory/tracedCrypto.d.ts +64 -0
  333. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  334. package/types/lib/managers/binary.d.ts +10 -0
  335. package/types/lib/managers/binary.d.ts.map +1 -1
  336. package/types/lib/managers/docker.d.ts +12 -0
  337. package/types/lib/managers/docker.d.ts.map +1 -1
  338. package/types/lib/managers/oci.d.ts +3 -1
  339. package/types/lib/managers/oci.d.ts.map +1 -1
  340. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  341. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  342. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  343. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  344. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  345. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  346. package/types/lib/parsers/compileCommands.d.ts +115 -0
  347. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  348. package/types/lib/parsers/email.d.ts +19 -0
  349. package/types/lib/parsers/email.d.ts.map +1 -0
  350. package/types/lib/parsers/largeJson.d.ts +13 -0
  351. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  352. package/types/lib/parsers/strictJson.d.ts +46 -0
  353. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  354. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  355. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  356. package/types/lib/parsers/terraformSource.d.ts +66 -0
  357. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  358. package/types/lib/server/server.d.ts +8 -0
  359. package/types/lib/server/server.d.ts.map +1 -1
  360. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  361. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  362. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  363. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  364. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  365. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  366. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  367. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  368. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  369. package/types/lib/validator/index.d.ts +8 -0
  370. package/types/lib/validator/index.d.ts.map +1 -1
  371. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  372. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -0,0 +1,2371 @@
1
+ // Certificate identity model shared by every cdxgen producer: the
2
+ // trustinspector rootfs/host reports, the source-directory scan, and (later)
3
+ // observed TLS chains. One certificate is one component wherever it appears,
4
+ // keyed by its whole-DER SHA-256; one public key is one component keyed by
5
+ // its SPKI SHA-256, so cross-signed twins share it. Design contract:
6
+ // ~/work/cbom-cert-distrust/PLAN.md section 2.
7
+
8
+ import { createHash, X509Certificate } from "node:crypto";
9
+ import { basename } from "node:path";
10
+
11
+ import { parseAllDocuments } from "yaml";
12
+
13
+ import { xml2js } from "../parsers/xml.js";
14
+
15
+ // Post-quantum algorithms are named from their OIDs because the toolchains
16
+ // disagree: Go 1.26 prints ML-DSA, SLH-DSA and ML-KEM as "0", Go 1.27 knows
17
+ // only ML-DSA, and a Node linked against a system OpenSSL 3.0 cannot load the
18
+ // keys at all. OIDs and levels: RFC 9881 (ML-DSA, FIPS 204), RFC 9909
19
+ // (SLH-DSA, FIPS 205), RFC 9935 (ML-KEM, FIPS 203), NIST FIPS 204/205
20
+ // security categories. Composite ML-DSA is deliberately absent until its RFC
21
+ // is published; those OIDs stay dotted-OID names.
22
+ const PQ_ALGORITHM_BY_OID = {
23
+ "2.16.840.1.101.3.4.3.17": ["ML-DSA-44", "signature", "44", 2],
24
+ "2.16.840.1.101.3.4.3.18": ["ML-DSA-65", "signature", "65", 3],
25
+ "2.16.840.1.101.3.4.3.19": ["ML-DSA-87", "signature", "87", 5],
26
+ "2.16.840.1.101.3.4.3.20": ["SLH-DSA-SHA2-128s", "signature", "SHA2-128s", 1],
27
+ "2.16.840.1.101.3.4.3.21": ["SLH-DSA-SHA2-128f", "signature", "SHA2-128f", 1],
28
+ "2.16.840.1.101.3.4.3.22": ["SLH-DSA-SHA2-192s", "signature", "SHA2-192s", 3],
29
+ "2.16.840.1.101.3.4.3.23": ["SLH-DSA-SHA2-192f", "signature", "SHA2-192f", 3],
30
+ "2.16.840.1.101.3.4.3.24": ["SLH-DSA-SHA2-256s", "signature", "SHA2-256s", 5],
31
+ "2.16.840.1.101.3.4.3.25": ["SLH-DSA-SHA2-256f", "signature", "SHA2-256f", 5],
32
+ "2.16.840.1.101.3.4.3.26": [
33
+ "SLH-DSA-SHAKE-128s",
34
+ "signature",
35
+ "SHAKE-128s",
36
+ 1,
37
+ ],
38
+ "2.16.840.1.101.3.4.3.27": [
39
+ "SLH-DSA-SHAKE-128f",
40
+ "signature",
41
+ "SHAKE-128f",
42
+ 1,
43
+ ],
44
+ "2.16.840.1.101.3.4.3.28": [
45
+ "SLH-DSA-SHAKE-192s",
46
+ "signature",
47
+ "SHAKE-192s",
48
+ 3,
49
+ ],
50
+ "2.16.840.1.101.3.4.3.29": [
51
+ "SLH-DSA-SHAKE-192f",
52
+ "signature",
53
+ "SHAKE-192f",
54
+ 3,
55
+ ],
56
+ "2.16.840.1.101.3.4.3.30": [
57
+ "SLH-DSA-SHAKE-256s",
58
+ "signature",
59
+ "SHAKE-256s",
60
+ 5,
61
+ ],
62
+ "2.16.840.1.101.3.4.3.31": [
63
+ "SLH-DSA-SHAKE-256f",
64
+ "signature",
65
+ "SHAKE-256f",
66
+ 5,
67
+ ],
68
+ "2.16.840.1.101.3.4.4.1": ["ML-KEM-512", "kem", "512", 1],
69
+ "2.16.840.1.101.3.4.4.2": ["ML-KEM-768", "kem", "768", 3],
70
+ "2.16.840.1.101.3.4.4.3": ["ML-KEM-1024", "kem", "1024", 5],
71
+ };
72
+
73
+ // Classical signature OIDs to the names Go's crypto/x509 prints, so both
74
+ // producers agree (RFC 8017 for RSASSA, RFC 5480 for ECDSA, RFC 8410 for
75
+ // EdDSA).
76
+ const SIGNATURE_NAMES_BY_OID = {
77
+ "1.2.840.113549.1.1.4": "MD5-RSA",
78
+ "1.2.840.113549.1.1.5": "SHA1-RSA",
79
+ "1.2.840.113549.1.1.11": "SHA256-RSA",
80
+ "1.2.840.113549.1.1.12": "SHA384-RSA",
81
+ "1.2.840.113549.1.1.13": "SHA512-RSA",
82
+ "1.2.840.113549.1.1.14": "SHA224-RSA",
83
+ "1.2.840.113549.1.1.10": "RSASSA-PSS",
84
+ "1.2.840.10045.4.1": "ECDSA-SHA1",
85
+ "1.2.840.10045.4.3.1": "ECDSA-SHA224",
86
+ "1.2.840.10045.4.3.2": "ECDSA-SHA256",
87
+ "1.2.840.10045.4.3.3": "ECDSA-SHA384",
88
+ "1.2.840.10045.4.3.4": "ECDSA-SHA512",
89
+ "1.3.101.112": "Ed25519",
90
+ "1.3.101.113": "Ed448",
91
+ "1.2.840.10040.4.3": "DSA-SHA1",
92
+ "2.16.840.1.101.3.4.3.2": "DSA-SHA256",
93
+ };
94
+
95
+ // Public-key OIDs (RFC 8017, RFC 5480, RFC 8410, FIPS 186-4) and the curves
96
+ // that appear in SPKI parameters.
97
+ const KEY_NAMES_BY_OID = {
98
+ "1.2.840.113549.1.1.1": "RSA",
99
+ "1.2.840.113549.1.1.10": "RSA-PSS",
100
+ "1.2.840.10045.2.1": "ECDSA",
101
+ "1.2.840.10040.4.1": "DSA",
102
+ "1.3.101.112": "Ed25519",
103
+ "1.3.101.113": "Ed448",
104
+ };
105
+ // X9.62 prime curves (SEC 2 appendix C) and the NIST/SEC OIDs that carry
106
+ // them; bit sizes per SEC 2 section 2.
107
+ const CURVE_BITS_BY_OID = {
108
+ "1.2.840.10045.3.1.1": 192,
109
+ "1.2.840.10045.3.1.2": 192,
110
+ "1.2.840.10045.3.1.3": 192,
111
+ "1.2.840.10045.3.1.4": 239,
112
+ "1.2.840.10045.3.1.5": 239,
113
+ "1.2.840.10045.3.1.6": 239,
114
+ "1.2.840.10045.3.1.7": 256,
115
+ "1.3.132.0.10": 256,
116
+ "1.3.132.0.33": 224,
117
+ "1.3.132.0.34": 384,
118
+ "1.3.132.0.35": 521,
119
+ };
120
+
121
+ // Bounded parsing: a certificate DER larger than this is rejected, and a PEM
122
+ // bundle yields at most this many certificates (the macOS system keychain
123
+ // reports 158, JDK truststores 144; large public bundles stay under 700).
124
+ const MAX_CERT_DER_BYTES = 1024 * 1024;
125
+ const MAX_PEM_CERTIFICATES = 2000;
126
+ // Pairwise issuer verification is quadratic; past this many certificates in
127
+ // one collection the pass is skipped (trustinspector applies the same cap).
128
+ const ISSUER_LINK_LIMIT = 5000;
129
+ const SAN_DISPLAY_LIMIT = 20;
130
+
131
+ function isHex(value) {
132
+ return typeof value === "string" && /^[0-9a-fA-F]+$/.test(value);
133
+ }
134
+
135
+ export function toLowerHex(value) {
136
+ return isHex(value) ? value.toLowerCase() : undefined;
137
+ }
138
+
139
+ // A SHA-256 digest is 64 hex characters; anything else from a report is not
140
+ // a usable identity.
141
+ function toLowerSha256(value) {
142
+ const hex = toLowerHex(value);
143
+ return hex?.length === 64 ? hex : undefined;
144
+ }
145
+
146
+ // Serial numbers are lowercase hex with no colons and no DER zero padding
147
+ // (PLAN section 2.1); BigInt normalisation strips both.
148
+ export function normalizeSerialHex(value) {
149
+ if (typeof value !== "string" || !/^[0-9a-fA-F]+$/.test(value)) {
150
+ return undefined;
151
+ }
152
+ try {
153
+ return BigInt(`0x${value}`).toString(16);
154
+ } catch {
155
+ return value.toLowerCase();
156
+ }
157
+ }
158
+
159
+ export function serialDecimalToHex(value) {
160
+ if (typeof value !== "string" || !/^[0-9]+$/.test(value)) {
161
+ return undefined;
162
+ }
163
+ try {
164
+ return BigInt(value).toString(16);
165
+ } catch {
166
+ return undefined;
167
+ }
168
+ }
169
+
170
+ // RFC 7468 PEM text encoding: the label on the encapsulation delimiter line
171
+ // names the type of the base64 body that follows.
172
+ const PEM_BEGIN = /-----BEGIN ([A-Z0-9 ]+)-----/;
173
+
174
+ /**
175
+ * Split PEM text into labelled blocks without decoding them. Only the label
176
+ * line and the matching END line matter; the caller decides which labels to
177
+ * parse. Bounded by block count and total decoded size.
178
+ *
179
+ * @param {string} text file contents
180
+ * @returns {{label: string, body: string}[]} blocks in file order
181
+ */
182
+ export function splitPemBlocks(text) {
183
+ if (typeof text !== "string" || !text.includes("-----BEGIN ")) {
184
+ return [];
185
+ }
186
+ const blocks = [];
187
+ const lines = text.split(/\r?\n/);
188
+ let label;
189
+ let body = [];
190
+ for (const line of lines) {
191
+ const begin = PEM_BEGIN.exec(line);
192
+ if (begin) {
193
+ label = begin[1];
194
+ body = [];
195
+ continue;
196
+ }
197
+ if (label && line.includes("-----END ")) {
198
+ blocks.push({ label, body: body.join("") });
199
+ label = undefined;
200
+ body = [];
201
+ continue;
202
+ }
203
+ if (label) {
204
+ body.push(line.trim());
205
+ }
206
+ if (blocks.length >= MAX_PEM_CERTIFICATES) {
207
+ return blocks;
208
+ }
209
+ }
210
+ return blocks;
211
+ }
212
+
213
+ /**
214
+ * Decode the CERTIFICATE blocks of a PEM bundle (RFC 7468) into DER buffers.
215
+ * `TRUSTED CERTIFICATE` blocks (OpenSSL aux data after the DER) are not
216
+ * decoded here; the trustinspector plugin parses their trust/reject lists.
217
+ *
218
+ * @param {string} text PEM file contents
219
+ * @returns {Buffer[]} DER buffers in file order
220
+ */
221
+ export function pemCertificateDerBlocks(text) {
222
+ const ders = [];
223
+ for (const block of splitPemBlocks(text)) {
224
+ if (block.label !== "CERTIFICATE") {
225
+ continue;
226
+ }
227
+ try {
228
+ const der = Buffer.from(block.body, "base64");
229
+ if (der.length > 0 && der.length <= MAX_CERT_DER_BYTES) {
230
+ ders.push(der);
231
+ }
232
+ } catch {
233
+ // base64 garbage between delimiters is skipped
234
+ }
235
+ if (ders.length >= MAX_PEM_CERTIFICATES) {
236
+ break;
237
+ }
238
+ }
239
+ return ders;
240
+ }
241
+
242
+ // Private-key PEM labels are matched by header line only; the body is never
243
+ // decoded (PLAN section 1.7). `ENCRYPTED-` prefixes the format when the
244
+ // header says so.
245
+ const PRIVATE_KEY_LABELS = new Set([
246
+ "RSA PRIVATE KEY",
247
+ "EC PRIVATE KEY",
248
+ "DSA PRIVATE KEY",
249
+ "PRIVATE KEY",
250
+ "ENCRYPTED PRIVATE KEY",
251
+ "OPENSSH PRIVATE KEY",
252
+ ]);
253
+
254
+ export function privateKeyPemFormat(text) {
255
+ for (const block of splitPemBlocks(text)) {
256
+ if (!PRIVATE_KEY_LABELS.has(block.label)) {
257
+ continue;
258
+ }
259
+ switch (block.label) {
260
+ case "RSA PRIVATE KEY":
261
+ return "RSA";
262
+ case "EC PRIVATE KEY":
263
+ return "EC";
264
+ case "DSA PRIVATE KEY":
265
+ return "DSA";
266
+ case "PRIVATE KEY":
267
+ return "PKCS8";
268
+ case "ENCRYPTED PRIVATE KEY":
269
+ return "ENCRYPTED-PKCS8";
270
+ default:
271
+ return "OPENSSH";
272
+ }
273
+ }
274
+ return undefined;
275
+ }
276
+
277
+ /**
278
+ * A PEM public key (RFC 7468 PUBLIC KEY label) also classifies a matched
279
+ * file as key material rather than a certificate.
280
+ *
281
+ * @param {string} text file contents
282
+ * @returns {boolean} true when a PUBLIC KEY block is present
283
+ */
284
+ export function hasPemPublicKey(text) {
285
+ return splitPemBlocks(text).some((block) => block.label === "PUBLIC KEY");
286
+ }
287
+
288
+ /**
289
+ * Read one DER tag-length-value header. Multi-byte tags are rejected (none of
290
+ * the fields walked use them). X.690 BER/DER; certificate structure per
291
+ * RFC 5280 section 4.1.
292
+ *
293
+ * @param {Buffer} der input buffer
294
+ * @param {number} offset start of the TLV
295
+ * @param {number} end exclusive end
296
+ * @returns {object|undefined} `{tag, start, contentStart, contentEnd, next}` or undefined on malformed input
297
+ */
298
+ function readTLV(der, offset, end) {
299
+ if (offset < 0 || offset + 2 > end) {
300
+ return undefined;
301
+ }
302
+ const tag = der[offset];
303
+ if ((tag & 0x1f) === 0x1f) {
304
+ return undefined;
305
+ }
306
+ const firstLengthByte = der[offset + 1];
307
+ let headerSize = 2;
308
+ let length;
309
+ if (firstLengthByte < 0x80) {
310
+ length = firstLengthByte;
311
+ } else {
312
+ const lengthBytes = firstLengthByte & 0x7f;
313
+ // Indefinite lengths and lengths over 4 bytes never occur in a bounded
314
+ // certificate walk and would allow absurd skips.
315
+ if (lengthBytes === 0 || lengthBytes > 4) {
316
+ return undefined;
317
+ }
318
+ if (offset + 2 + lengthBytes > end) {
319
+ return undefined;
320
+ }
321
+ length = 0;
322
+ for (let i = 0; i < lengthBytes; i++) {
323
+ length = length * 256 + der[offset + 2 + i];
324
+ }
325
+ headerSize = 2 + lengthBytes;
326
+ }
327
+ if (length > end - offset - headerSize) {
328
+ return undefined;
329
+ }
330
+ return {
331
+ tag,
332
+ start: offset,
333
+ contentStart: offset + headerSize,
334
+ contentEnd: offset + headerSize + length,
335
+ next: offset + headerSize + length,
336
+ };
337
+ }
338
+
339
+ function readOID(der, start, end) {
340
+ if (start >= end) {
341
+ return undefined;
342
+ }
343
+ const arcs = [];
344
+ // BigInt: arcs such as UUID OIDs (2.25.<128-bit>) exceed 2^53.
345
+ let acc = 0n;
346
+ let closed = true;
347
+ for (let i = start; i < end; i++) {
348
+ const byte = der[i];
349
+ acc = acc * 128n + BigInt(byte & 0x7f);
350
+ if (byte & 0x80) {
351
+ closed = false;
352
+ continue;
353
+ }
354
+ closed = true;
355
+ arcs.push(acc);
356
+ acc = 0n;
357
+ }
358
+ if (!closed || arcs.length === 0) {
359
+ return undefined;
360
+ }
361
+ // X.690 8.19.4: the first subidentifier packs the first two arcs; the
362
+ // first arc is at most 2, and under 2 the second can exceed 39.
363
+ const first = arcs[0] < 40n ? 0n : arcs[0] < 80n ? 1n : 2n;
364
+ return [first, arcs[0] - first * 40n, ...arcs.slice(1)].join(".");
365
+ }
366
+
367
+ function readAlgorithmOid(der, seq) {
368
+ const first = readTLV(der, seq.contentStart, seq.contentEnd);
369
+ if (!first || first.tag !== 0x06) {
370
+ return undefined;
371
+ }
372
+ return readOID(der, first.contentStart, first.contentEnd);
373
+ }
374
+
375
+ function hexSlice(der, start, end) {
376
+ return Buffer.from(der.buffer, der.byteOffset + start, end - start)
377
+ .toString("hex")
378
+ .toLowerCase();
379
+ }
380
+
381
+ /**
382
+ * Walk a certificate DER far enough to learn what the platform parsers may
383
+ * not report: the SubjectPublicKeyInfo (for the SPKI hash and key algorithm
384
+ * OID), the outer signature algorithm OID, the X.509 version, the subject and
385
+ * issuer name DER (byte equality = self-issued), and the SKI/AKI key
386
+ * identifiers (RFC 5280 sections 4.1, 4.2.1.1, 4.2.1.2). Malformed input
387
+ * yields undefined; nothing throws.
388
+ *
389
+ * @param {Buffer} der certificate DER, at most MAX_CERT_DER_BYTES
390
+ * @returns {object|undefined} parsed fields
391
+ */
392
+ export function parseCertificateDer(der) {
393
+ if (
394
+ !Buffer.isBuffer(der) ||
395
+ der.length === 0 ||
396
+ der.length > MAX_CERT_DER_BYTES
397
+ ) {
398
+ return undefined;
399
+ }
400
+ const certificate = readTLV(der, 0, der.length);
401
+ if (!certificate || certificate.tag !== 0x30) {
402
+ return undefined;
403
+ }
404
+ const tbs = readTLV(der, certificate.contentStart, certificate.contentEnd);
405
+ if (!tbs || tbs.tag !== 0x30) {
406
+ return undefined;
407
+ }
408
+ let cursor = tbs.contentStart;
409
+ let version = 1;
410
+ const maybeVersion = readTLV(der, cursor, tbs.contentEnd);
411
+ if (maybeVersion && maybeVersion.tag === 0xa0) {
412
+ const inner = readTLV(
413
+ der,
414
+ maybeVersion.contentStart,
415
+ maybeVersion.contentEnd,
416
+ );
417
+ if (
418
+ !inner ||
419
+ inner.tag !== 0x02 ||
420
+ inner.contentEnd !== inner.contentStart + 1
421
+ ) {
422
+ return undefined;
423
+ }
424
+ version = der[inner.contentStart] + 1;
425
+ cursor = maybeVersion.next;
426
+ }
427
+ const serial = readTLV(der, cursor, tbs.contentEnd);
428
+ if (!serial || serial.tag !== 0x02) {
429
+ return undefined;
430
+ }
431
+ const tbsSignature = readTLV(der, serial.next, tbs.contentEnd);
432
+ if (!tbsSignature || tbsSignature.tag !== 0x30) {
433
+ return undefined;
434
+ }
435
+ const issuer = readTLV(der, tbsSignature.next, tbs.contentEnd);
436
+ if (!issuer || issuer.tag !== 0x30) {
437
+ return undefined;
438
+ }
439
+ const validity = readTLV(der, issuer.next, tbs.contentEnd);
440
+ if (!validity || validity.tag !== 0x30) {
441
+ return undefined;
442
+ }
443
+ const subject = readTLV(der, validity.next, tbs.contentEnd);
444
+ if (!subject || subject.tag !== 0x30) {
445
+ return undefined;
446
+ }
447
+ const spki = readTLV(der, subject.next, tbs.contentEnd);
448
+ if (!spki || spki.tag !== 0x30) {
449
+ return undefined;
450
+ }
451
+ const spkiAlgorithm = readTLV(der, spki.contentStart, spki.contentEnd);
452
+ if (!spkiAlgorithm || spkiAlgorithm.tag !== 0x30) {
453
+ return undefined;
454
+ }
455
+ const keyAlgorithmOid = readAlgorithmOid(der, spkiAlgorithm);
456
+ if (!keyAlgorithmOid) {
457
+ return undefined;
458
+ }
459
+ // Curve parameters sit inside the AlgorithmIdentifier as its second
460
+ // element (RFC 5480 section 2.1.1), not beside it.
461
+ const keyOidTlv = readTLV(
462
+ der,
463
+ spkiAlgorithm.contentStart,
464
+ spkiAlgorithm.contentEnd,
465
+ );
466
+ const keyParameters = readTLV(der, keyOidTlv.next, spkiAlgorithm.contentEnd);
467
+ const curveOid =
468
+ keyParameters && keyParameters.tag === 0x06
469
+ ? readOID(der, keyParameters.contentStart, keyParameters.contentEnd)
470
+ : undefined;
471
+ const outerSignature = readTLV(der, tbs.next, certificate.contentEnd);
472
+ if (!outerSignature || outerSignature.tag !== 0x30) {
473
+ return undefined;
474
+ }
475
+ const signatureAlgorithmOid = readAlgorithmOid(der, outerSignature);
476
+ const rsaKeyBits = rsaModulusBitLength(der, spkiAlgorithm, spki);
477
+ const ecKeyBits = ecCurveBitsFromSpki(der, spkiAlgorithm, spki);
478
+ // Extensions live under tbsCertificate [3]; issuer/subjectUniqueID ([1],
479
+ // [2]) may precede them and are skipped by tag.
480
+ let extensions;
481
+ let walkCursor = spki.next;
482
+ while (walkCursor < tbs.contentEnd) {
483
+ const entry = readTLV(der, walkCursor, tbs.contentEnd);
484
+ if (!entry) {
485
+ break;
486
+ }
487
+ if (entry.tag === 0xa3) {
488
+ extensions = entry;
489
+ }
490
+ walkCursor = entry.next;
491
+ }
492
+ let subjectKeyId;
493
+ let authorityKeyId;
494
+ if (extensions) {
495
+ const parsed = parseKeyIdentifierExtensions(der, extensions);
496
+ subjectKeyId = parsed.subjectKeyId;
497
+ authorityKeyId = parsed.authorityKeyId;
498
+ }
499
+ return {
500
+ version,
501
+ keyAlgorithmOid,
502
+ curveOid,
503
+ signatureAlgorithmOid,
504
+ rsaKeyBits,
505
+ ecKeyBits,
506
+ spkiDer: der.subarray(spki.start, spki.next),
507
+ subjectDer: der.subarray(subject.start, subject.next),
508
+ issuerDer: der.subarray(issuer.start, issuer.next),
509
+ subjectKeyId,
510
+ authorityKeyId,
511
+ };
512
+ }
513
+
514
+ // RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
515
+ // (RFC 8017 appendix A.1.1); the modulus' significant bit length is the key
516
+ // size trustinspector reports.
517
+ function rsaModulusBitLength(der, spkiAlgorithm, spki) {
518
+ const bitString = readTLV(der, spkiAlgorithm.next, spki.contentEnd);
519
+ if (
520
+ !bitString ||
521
+ bitString.tag !== 0x03 ||
522
+ bitString.contentStart >= bitString.contentEnd ||
523
+ der[bitString.contentStart] !== 0
524
+ ) {
525
+ return undefined;
526
+ }
527
+ const publicKey = readTLV(
528
+ der,
529
+ bitString.contentStart + 1,
530
+ bitString.contentEnd,
531
+ );
532
+ if (!publicKey || publicKey.tag !== 0x30) {
533
+ return undefined;
534
+ }
535
+ const modulus = readTLV(der, publicKey.contentStart, publicKey.contentEnd);
536
+ if (
537
+ !modulus ||
538
+ modulus.tag !== 0x02 ||
539
+ modulus.contentEnd <= modulus.contentStart
540
+ ) {
541
+ return undefined;
542
+ }
543
+ let start = modulus.contentStart;
544
+ while (start < modulus.contentEnd - 1 && der[start] === 0) {
545
+ start++;
546
+ }
547
+ const bits = (modulus.contentEnd - start) * 8;
548
+ const lead = der[start];
549
+ if (lead === 0) {
550
+ return bits;
551
+ }
552
+ let leadZeros = 0;
553
+ for (let mask = 0x80; mask > 0 && !(lead & mask); mask >>= 1) {
554
+ leadZeros++;
555
+ }
556
+ return bits - leadZeros;
557
+ }
558
+
559
+ // subjectKeyIdentifier (2.5.29.14) is an OCTET STRING wrapping the key
560
+ // identifier OCTET STRING; authorityKeyIdentifier (2.5.29.35) is a SEQUENCE
561
+ // whose [0] IMPLICIT field carries the identifier (RFC 5280 sections
562
+ // 4.2.1.1/4.2.1.2).
563
+ function parseKeyIdentifierExtensions(der, extensions) {
564
+ let subjectKeyId;
565
+ let authorityKeyId;
566
+ const container = readTLV(
567
+ der,
568
+ extensions.contentStart,
569
+ extensions.contentEnd,
570
+ );
571
+ if (!container || container.tag !== 0x30) {
572
+ return {};
573
+ }
574
+ let cursor = container.contentStart;
575
+ while (cursor < container.contentEnd) {
576
+ const extension = readTLV(der, cursor, container.contentEnd);
577
+ if (!extension) {
578
+ break;
579
+ }
580
+ cursor = extension.next;
581
+ if (extension.tag !== 0x30) {
582
+ continue;
583
+ }
584
+ const extnID = readTLV(der, extension.contentStart, extension.contentEnd);
585
+ if (!extnID || extnID.tag !== 0x06) {
586
+ continue;
587
+ }
588
+ const oid = readOID(der, extnID.contentStart, extnID.contentEnd);
589
+ let valueCursor = extnID.next;
590
+ const maybeCritical = readTLV(der, valueCursor, extension.contentEnd);
591
+ if (maybeCritical && maybeCritical.tag === 0x01) {
592
+ valueCursor = maybeCritical.next;
593
+ }
594
+ const extnValue = readTLV(der, valueCursor, extension.contentEnd);
595
+ if (!extnValue || extnValue.tag !== 0x04) {
596
+ continue;
597
+ }
598
+ if (oid === "2.5.29.14") {
599
+ const inner = readTLV(der, extnValue.contentStart, extnValue.contentEnd);
600
+ if (inner && inner.tag === 0x04) {
601
+ subjectKeyId = hexSlice(der, inner.contentStart, inner.contentEnd);
602
+ }
603
+ } else if (oid === "2.5.29.35") {
604
+ const inner = readTLV(der, extnValue.contentStart, extnValue.contentEnd);
605
+ if (inner && inner.tag === 0x30) {
606
+ const keyId = readTLV(der, inner.contentStart, inner.contentEnd);
607
+ if (keyId && keyId.tag === 0x80) {
608
+ authorityKeyId = hexSlice(der, keyId.contentStart, keyId.contentEnd);
609
+ }
610
+ }
611
+ }
612
+ }
613
+ return { subjectKeyId, authorityKeyId };
614
+ }
615
+
616
+ function sha256Hex(buffer) {
617
+ return createHash("sha256").update(buffer).digest("hex");
618
+ }
619
+
620
+ /**
621
+ * Name a key algorithm the way the BOM model requires: the post-quantum table
622
+ * first, then classical names derived from the SPKI OID and key size, never
623
+ * the platform's own name for an algorithm it may not know. Names agree with
624
+ * trustinspector's (`RSA-2048`, `ECDSA-P256`, `Ed25519`, `ML-DSA-65`);
625
+ * unknown algorithms keep their dotted OID.
626
+ *
627
+ * @param {object} context `{oid, curveOid, bits}` from the DER walk or report
628
+ * @returns {string|undefined} key name such as `ML-DSA-65`, `RSA-2048`, `ECDSA-P256`
629
+ */
630
+ /**
631
+ * The family name for a key algorithm component: like
632
+ * {@link keyAlgorithmName} but without the key-size suffix, so every key
633
+ * size sharing an OID (RSA-1024 and RSA-2048, ECDSA P-256 and P-384)
634
+ * references the same single algorithm component. Post-quantum names are
635
+ * already family names (`ML-DSA-65`).
636
+ *
637
+ * @param {string} oid SPKI algorithm OID
638
+ * @returns {string|undefined} family name such as `RSA`, `ECDSA`, `ML-DSA-65`
639
+ */
640
+ export function keyAlgorithmFamilyName(oid) {
641
+ const name = keyAlgorithmName({ oid });
642
+ if (!name) {
643
+ return undefined;
644
+ }
645
+ if (PQ_ALGORITHM_BY_OID[oid]) {
646
+ return name;
647
+ }
648
+ const KEY_FAMILY_BY_NAME = {
649
+ RSA: "RSA",
650
+ "RSA-PSS": "RSA-PSS",
651
+ ECDSA: "ECDSA",
652
+ DSA: "DSA",
653
+ Ed25519: "Ed25519",
654
+ Ed448: "Ed448",
655
+ };
656
+ return KEY_FAMILY_BY_NAME[name.replace(/-\d+$/, "")] || name;
657
+ }
658
+
659
+ export function keyAlgorithmName(context) {
660
+ const oid = context?.oid;
661
+ if (!oid) {
662
+ return undefined;
663
+ }
664
+ const pq = PQ_ALGORITHM_BY_OID[oid];
665
+ if (pq) {
666
+ return pq[0];
667
+ }
668
+ const bits = Number(context?.bits);
669
+ const hasBits = Number.isInteger(bits) && bits > 0;
670
+ if (oid === "1.2.840.113549.1.1.1" || oid === "1.2.840.113549.1.1.10") {
671
+ return hasBits ? `RSA-${bits}` : "RSA";
672
+ }
673
+ if (oid === "1.2.840.10045.2.1") {
674
+ const curveBits = context?.curveOid
675
+ ? CURVE_BITS_BY_OID[context.curveOid]
676
+ : undefined;
677
+ return curveBits
678
+ ? `ECDSA-P${curveBits}`
679
+ : hasBits
680
+ ? `ECDSA-P${bits}`
681
+ : "ECDSA";
682
+ }
683
+ const base = KEY_NAMES_BY_OID[oid];
684
+ if (!base) {
685
+ return oid;
686
+ }
687
+ return base;
688
+ }
689
+
690
+ /**
691
+ * Name a signature algorithm: the post-quantum table, then Go's classical
692
+ * names, then the dotted OID. Never "0" or "unknown".
693
+ *
694
+ * @param {object} context `{oid}` or a trusted producer name
695
+ * @returns {string|undefined} signature name such as `ML-DSA-65`, `SHA256-RSA`
696
+ */
697
+ export function signatureAlgorithmName(context) {
698
+ const oid = context?.oid;
699
+ if (!oid) {
700
+ return undefined;
701
+ }
702
+ return (
703
+ (PQ_ALGORITHM_BY_OID[oid] && PQ_ALGORITHM_BY_OID[oid][0]) ||
704
+ SIGNATURE_NAMES_BY_OID[oid] ||
705
+ oid
706
+ );
707
+ }
708
+
709
+ // The name an algorithm component carries for an OID. Key algorithms are
710
+ // named by family so one OID yields exactly one component whichever key sizes
711
+ // were seen; certificates must reference the same name.
712
+ function algorithmComponentName(oid, name) {
713
+ const isSignatureOnly = !KEY_NAMES_BY_OID[oid] && !PQ_ALGORITHM_BY_OID[oid];
714
+ return isSignatureOnly ? name : keyAlgorithmFamilyName(oid) || name;
715
+ }
716
+
717
+ /**
718
+ * Metadata for an algorithm component: primitive, parameter set and NIST
719
+ * quantum security level from the post-quantum table; classical algorithms
720
+ * carry level 0, which is a fact, not a policy.
721
+ *
722
+ * @param {string} oid dotted algorithm OID
723
+ * @param {string} kind `"signature"` or `"kem"`/key use
724
+ * @returns {object} `{primitive, parameterSetIdentifier, nistQuantumSecurityLevel}` when known
725
+ */
726
+ export function algorithmComponentMetadata(oid) {
727
+ const pq = PQ_ALGORITHM_BY_OID[oid];
728
+ if (pq) {
729
+ return {
730
+ primitive: pq[1],
731
+ parameterSetIdentifier: pq[2],
732
+ nistQuantumSecurityLevel: pq[3],
733
+ };
734
+ }
735
+ if (SIGNATURE_NAMES_BY_OID[oid] || KEY_NAMES_BY_OID[oid]) {
736
+ return { nistQuantumSecurityLevel: 0 };
737
+ }
738
+ return undefined;
739
+ }
740
+
741
+ function certificateBomRef(name, certSha256) {
742
+ return `crypto/certificate/${encodeURIComponent(name)}@sha256:${certSha256}`;
743
+ }
744
+
745
+ function publicKeyBomRef(keyName, spkiSha256) {
746
+ return `crypto/public-key/${encodeURIComponent(keyName)}@spki-sha256:${spkiSha256}`;
747
+ }
748
+
749
+ function algorithmBomRef(name, oid) {
750
+ return `crypto/algorithm/${encodeURIComponent(name)}@${oid}`;
751
+ }
752
+
753
+ function prop(name, value) {
754
+ return { name, value: String(value) };
755
+ }
756
+
757
+ function sortedUnique(values) {
758
+ return [...new Set(values.filter((value) => value !== undefined))].sort();
759
+ }
760
+
761
+ // Node renders the subject as one RDN per line in certificate order; Go's
762
+ // pkix.Name.String() (the contract's format) emits the reverse order joined
763
+ // by commas. Reversing Node's lines reproduces Go's string byte for byte for
764
+ // the attributes Node exposes.
765
+ function normalizeDn(dn) {
766
+ if (typeof dn !== "string") {
767
+ return undefined;
768
+ }
769
+ const lines = dn
770
+ .split(/\r?\n/)
771
+ .map((line) => line.trim())
772
+ .filter(Boolean);
773
+ if (!lines.length) {
774
+ return undefined;
775
+ }
776
+ return lines.reverse().join(",");
777
+ }
778
+
779
+ export function commonNameFromDn(dn) {
780
+ if (typeof dn !== "string") {
781
+ return undefined;
782
+ }
783
+ // Both Node's newline-per-RDN form and the joined single-line form; a
784
+ // comma escaped per RFC 4514 ("Foo\, Inc") stays inside its value.
785
+ const parts = dn
786
+ .split(/\n|(?<!\\),/)
787
+ .map((part) => part.trim().replace(/\\(.)/g, "$1"))
788
+ .filter(Boolean);
789
+ const cn = parts.find((part) => part.startsWith("CN="));
790
+ if (cn) {
791
+ return cn.slice(3);
792
+ }
793
+ const first = parts.find(Boolean);
794
+ if (!first) {
795
+ return undefined;
796
+ }
797
+ const eq = first.indexOf("=");
798
+ return eq >= 0 ? first.slice(eq + 1) : first;
799
+ }
800
+
801
+ function rfc3339(date) {
802
+ if (
803
+ date === undefined ||
804
+ date === null ||
805
+ (typeof date !== "string" && !(date instanceof Date))
806
+ ) {
807
+ return undefined;
808
+ }
809
+ const parsed = date instanceof Date ? date : new Date(date.trim());
810
+ if (Number.isNaN(parsed.getTime())) {
811
+ return undefined;
812
+ }
813
+ const iso = parsed.toISOString().replace(/\.\d{3}Z$/, "Z");
814
+ // RFC 3339 date-time allows a four-digit year only; toISOString prints
815
+ // expanded years as +YYYYYY/-YYYYYY, which no schema date-time accepts.
816
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(iso) ? iso : undefined;
817
+ }
818
+
819
+ // An uncompressed EC point is 0x04 followed by two equal-length coordinates
820
+ // (SEC 1 section 2.3.3); the coordinate length gives the curve size. Used
821
+ // when the curve OID is unknown to the table above.
822
+ function ecCurveBitsFromSpki(der, spkiAlgorithm, spki) {
823
+ const bitString = readTLV(der, spkiAlgorithm.next, spki.contentEnd);
824
+ if (
825
+ !bitString ||
826
+ bitString.tag !== 0x03 ||
827
+ bitString.contentStart >= bitString.contentEnd ||
828
+ der[bitString.contentStart] !== 0
829
+ ) {
830
+ return undefined;
831
+ }
832
+ const pointLength = bitString.contentEnd - bitString.contentStart - 1;
833
+ if (pointLength < 1 || (pointLength - 1) % 2 !== 0) {
834
+ return undefined;
835
+ }
836
+ return ((pointLength - 1) / 2) * 8;
837
+ }
838
+
839
+ // ---------------------------------------------------------------------------
840
+ // Certificate records and the collector
841
+ // ---------------------------------------------------------------------------
842
+
843
+ function emptyRecord(certSha256) {
844
+ return {
845
+ certSha256,
846
+ spkiSha256: undefined,
847
+ spkiSha256Base64: undefined,
848
+ name: undefined,
849
+ subject: undefined,
850
+ issuer: undefined,
851
+ serialHex: undefined,
852
+ version: undefined,
853
+ isCA: undefined,
854
+ selfSigned: undefined,
855
+ selfSignedUnverified: undefined,
856
+ role: undefined,
857
+ pathLen: undefined,
858
+ subjectKeyId: undefined,
859
+ authorityKeyId: undefined,
860
+ notValidBefore: undefined,
861
+ notValidAfter: undefined,
862
+ fileExtension: undefined,
863
+ keyName: undefined,
864
+ keyBits: undefined,
865
+ keyAlgorithmOid: undefined,
866
+ signatureAlgorithm: undefined,
867
+ signatureAlgorithmOid: undefined,
868
+ keyUsage: undefined,
869
+ extKeyUsage: undefined,
870
+ dnsNames: undefined,
871
+ ocspServers: undefined,
872
+ issuingCertificateUrls: undefined,
873
+ crlDistributionPoints: undefined,
874
+ policyOids: undefined,
875
+ hasNameConstraints: undefined,
876
+ trustAcceptedPurposes: undefined,
877
+ trustRejectedPurposes: undefined,
878
+ hostStores: undefined,
879
+ issuers: undefined,
880
+ issuersUnverified: undefined,
881
+ hashes: undefined,
882
+ algorithm: undefined,
883
+ keyStrength: undefined,
884
+ trustDomain: undefined,
885
+ occurrences: [],
886
+ };
887
+ }
888
+
889
+ /**
890
+ * Normalise a trustinspector material (report-1 or report-2) into a
891
+ * certificate record. Returns undefined for non-certificate kinds.
892
+ *
893
+ * @param {object} material trustinspector material
894
+ * @param {object} [context] `{locationRoot}` to relativise container paths
895
+ * @returns {object|undefined} record
896
+ */
897
+ export function certRecordFromTrustInspector(material, context = {}) {
898
+ if (!material || material.kind !== "certificate") {
899
+ return undefined;
900
+ }
901
+ const certSha256 =
902
+ toLowerSha256(material.certSha256) ||
903
+ toLowerSha256(material.fingerprint) ||
904
+ toLowerSha256(material.sha256);
905
+ if (!certSha256) {
906
+ return undefined;
907
+ }
908
+ const record = emptyRecord(certSha256);
909
+ record.spkiSha256 = toLowerSha256(material.spkiSha256);
910
+ record.spkiSha256Base64 = material.spkiSha256Base64;
911
+ record.serialHex =
912
+ normalizeSerialHex(material.serialHex) ||
913
+ serialDecimalToHex(material.serial);
914
+ record.subject =
915
+ typeof material.subject === "string" ? material.subject : undefined;
916
+ record.issuer =
917
+ typeof material.issuer === "string" ? material.issuer : undefined;
918
+ // The name is part of the bom-ref, so it comes from the subject by the rule
919
+ // the Node reader applies, and a certificate is named alike with or without
920
+ // the plugin. trustinspector 4.2.0 names a certificate without a CN after
921
+ // the first RDN in DER order, its country ("ES" for
922
+ // "OU=AC RAIZ FNMT-RCM,O=FNMT-RCM,C=ES"), where the most specific RDN names
923
+ // it; its name is only used when the subject gives none.
924
+ record.name =
925
+ commonNameFromDn(record.subject || "") ||
926
+ (typeof material.name === "string" && material.name) ||
927
+ undefined;
928
+ record.notValidBefore = rfc3339(material.createdAt);
929
+ record.notValidAfter = rfc3339(material.expiresAt);
930
+ record.fileExtension = material.fileExtension;
931
+ record.version = material.version;
932
+ record.isCA = typeof material.isCA === "boolean" ? material.isCA : undefined;
933
+ record.selfSigned =
934
+ material.selfSigned === undefined
935
+ ? undefined
936
+ : Boolean(material.selfSigned);
937
+ record.selfSignedUnverified = material.selfSignedUnverified
938
+ ? true
939
+ : undefined;
940
+ record.role = material.role;
941
+ record.pathLen = material.pathLen;
942
+ record.subjectKeyId = toLowerHex(material.subjectKeyId);
943
+ record.authorityKeyId = toLowerHex(material.authorityKeyId);
944
+ record.keyName = material.keyName;
945
+ record.keyBits = material.keyBits;
946
+ record.keyAlgorithmOid = material.keyAlgorithmOid;
947
+ record.signatureAlgorithm = material.signatureAlgorithm;
948
+ record.signatureAlgorithmOid = material.signatureAlgorithmOid;
949
+ record.keyUsage = Array.isArray(material.keyUsage)
950
+ ? [...material.keyUsage]
951
+ : undefined;
952
+ record.extKeyUsage = Array.isArray(material.extKeyUsage)
953
+ ? [...material.extKeyUsage]
954
+ : undefined;
955
+ record.dnsNames = Array.isArray(material.dnsNames)
956
+ ? [...material.dnsNames]
957
+ : undefined;
958
+ record.ocspServers = Array.isArray(material.ocspServers)
959
+ ? [...material.ocspServers]
960
+ : undefined;
961
+ record.issuingCertificateUrls = Array.isArray(material.issuingCertificateUrls)
962
+ ? [...material.issuingCertificateUrls]
963
+ : undefined;
964
+ record.crlDistributionPoints = Array.isArray(material.crlDistributionPoints)
965
+ ? [...material.crlDistributionPoints]
966
+ : undefined;
967
+ record.policyOids = Array.isArray(material.policyOids)
968
+ ? [...material.policyOids]
969
+ : undefined;
970
+ record.hasNameConstraints = material.hasNameConstraints ? true : undefined;
971
+ record.trustAcceptedPurposes = Array.isArray(material.trustAcceptedPurposes)
972
+ ? [...material.trustAcceptedPurposes]
973
+ : undefined;
974
+ record.trustRejectedPurposes = Array.isArray(material.trustRejectedPurposes)
975
+ ? [...material.trustRejectedPurposes]
976
+ : undefined;
977
+ record.hostStores = Array.isArray(material.hostStores)
978
+ ? [...material.hostStores]
979
+ : undefined;
980
+ record.issuers = Array.isArray(material.issuers)
981
+ ? [...material.issuers]
982
+ : undefined;
983
+ record.issuersUnverified = Array.isArray(material.issuersUnverified)
984
+ ? [...material.issuersUnverified]
985
+ : undefined;
986
+ record.algorithm = material.algorithm;
987
+ record.keyStrength = material.keyStrength;
988
+ record.trustDomain = material.trustDomain;
989
+ // File digests describe a file, not a certificate: they only survive on a
990
+ // single-occurrence certificate, where that one file is the cert.
991
+ const paths =
992
+ Array.isArray(material.paths) && material.paths.length
993
+ ? material.paths
994
+ : material.path
995
+ ? [material.path]
996
+ : [];
997
+ if (paths.length === 1 && material.sha256) {
998
+ record.hashes = [
999
+ ...(material.sha1 && isHex(material.sha1)
1000
+ ? [{ alg: "SHA-1", content: material.sha1.toLowerCase() }]
1001
+ : []),
1002
+ { alg: "SHA-256", content: material.sha256.toLowerCase() },
1003
+ ];
1004
+ }
1005
+ for (const location of paths) {
1006
+ record.occurrences.push({
1007
+ location: containerRelativePath(location, context.locationRoot),
1008
+ sourceType: material.sourceType,
1009
+ });
1010
+ }
1011
+ if (Array.isArray(material.sourceTypes) && material.sourceTypes.length) {
1012
+ record.sourceTypes = [...material.sourceTypes];
1013
+ } else if (material.sourceType) {
1014
+ record.sourceTypes = [material.sourceType];
1015
+ }
1016
+ return record;
1017
+ }
1018
+
1019
+ /**
1020
+ * Build a certificate record from a parsed certificate (Node fallback path).
1021
+ * The DER walk supplies the SPKI, algorithm OIDs, version and key
1022
+ * identifiers; the X509Certificate supplies names, validity and, on runtimes
1023
+ * that expose them, the signature algorithm. Fields the platform cannot
1024
+ * provide (key usage bits, path length) stay unset and are documented as
1025
+ * fallback limits.
1026
+ *
1027
+ * @param {Buffer} der certificate DER
1028
+ * @param {object} context `{location, sourceType, pem}`
1029
+ * @returns {object|undefined} record
1030
+ */
1031
+ export function certRecordFromDer(der, context = {}) {
1032
+ const walked = parseCertificateDer(der);
1033
+ if (!walked) {
1034
+ return undefined;
1035
+ }
1036
+ const spkiSha256 = sha256Hex(walked.spkiDer);
1037
+ const record = emptyRecord(sha256Hex(der));
1038
+ record.spkiSha256 = spkiSha256;
1039
+ record.spkiSha256Base64 = createHash("sha256")
1040
+ .update(walked.spkiDer)
1041
+ .digest("base64");
1042
+ record.subjectKeyId = walked.subjectKeyId;
1043
+ record.authorityKeyId = walked.authorityKeyId;
1044
+ record.version = walked.version;
1045
+ record.keyAlgorithmOid = walked.keyAlgorithmOid;
1046
+ record.signatureAlgorithmOid = walked.signatureAlgorithmOid;
1047
+ record.der = der;
1048
+ record.walked = walked;
1049
+ let x509;
1050
+ try {
1051
+ x509 = new X509Certificate(der);
1052
+ } catch {
1053
+ x509 = undefined;
1054
+ }
1055
+ if (x509) {
1056
+ record.subject = normalizeDn(x509.subject);
1057
+ record.issuer = normalizeDn(x509.issuer);
1058
+ record.serialHex = normalizeSerialHex(x509.serialNumber);
1059
+ record.notValidBefore = rfc3339(x509.validFrom);
1060
+ record.notValidAfter = rfc3339(x509.validTo);
1061
+ record.isCA = x509.ca === undefined ? undefined : Boolean(x509.ca);
1062
+ if (typeof x509.signatureAlgorithmOid === "string") {
1063
+ record.signatureAlgorithmOid = x509.signatureAlgorithmOid;
1064
+ record.signatureAlgorithm = signatureAlgorithmName({
1065
+ oid: x509.signatureAlgorithmOid,
1066
+ });
1067
+ }
1068
+ try {
1069
+ const bits = x509.publicKey?.asymmetricKeySize;
1070
+ if (Number.isInteger(bits) && bits > 0) {
1071
+ record.keyBits = bits;
1072
+ }
1073
+ } catch {
1074
+ // The runtime cannot load this key (post-quantum on an older OpenSSL);
1075
+ // the OID-based names already carry what is known.
1076
+ }
1077
+ if (x509.subjectAltName) {
1078
+ record.dnsNames = x509.subjectAltName
1079
+ .split(",")
1080
+ .map((entry) => entry.trim())
1081
+ .filter((entry) => entry.startsWith("DNS:"))
1082
+ .map((entry) => entry.slice(4));
1083
+ }
1084
+ }
1085
+ if (!record.keyBits) {
1086
+ if (walked.rsaKeyBits) {
1087
+ record.keyBits = walked.rsaKeyBits;
1088
+ } else if (walked.keyAlgorithmOid === "1.2.840.10045.2.1") {
1089
+ // Node reports no size for EC keys; the point size carries it.
1090
+ record.keyBits = walked.ecKeyBits;
1091
+ } else if (walked.keyAlgorithmOid === "1.3.101.112") {
1092
+ // Ed25519 keys are 256 bits (RFC 8410 section 4).
1093
+ record.keyBits = 256;
1094
+ }
1095
+ }
1096
+ record.keyName = keyAlgorithmName({
1097
+ oid: record.keyAlgorithmOid,
1098
+ curveOid: walked.curveOid,
1099
+ bits: record.keyBits,
1100
+ });
1101
+ if (!record.name) {
1102
+ record.name =
1103
+ commonNameFromDn(record.subject || "") ||
1104
+ basename(String(context.location || "")) ||
1105
+ undefined;
1106
+ }
1107
+ if (!record.signatureAlgorithm) {
1108
+ record.signatureAlgorithm = signatureAlgorithmName({
1109
+ oid: record.signatureAlgorithmOid,
1110
+ });
1111
+ }
1112
+ record.x509 = x509;
1113
+ classifySelfIssued(record);
1114
+ if (context.location) {
1115
+ record.occurrences.push({
1116
+ location: context.location,
1117
+ sourceType: context.sourceType,
1118
+ });
1119
+ record.sourceTypes = context.sourceType ? [context.sourceType] : [];
1120
+ }
1121
+ return record;
1122
+ }
1123
+
1124
+ // JSON legally carries unpaired surrogates, which encodeURIComponent (the
1125
+ // bom-ref builders) rejects with URIError; a presenter's free-text fields are
1126
+ // made well-formed (unpaired surrogates become U+FFFD) instead of failing
1127
+ // the whole BOM.
1128
+ export function toWellFormedText(value) {
1129
+ return typeof value === "string" && value ? value.toWellFormed() : undefined;
1130
+ }
1131
+
1132
+ // Serial numbers are at most 20 octets (RFC 5280 section 4.1.2.2); a longer
1133
+ // hex string is not a serial any conformant certificate carries.
1134
+ const MAX_SERIAL_HEX_CHARS = 64;
1135
+
1136
+ /**
1137
+ * Normalise one certificate of the chain a server presented in a TLS ≤ 1.2
1138
+ * handshake, as safer-exec's crypto result relays it (leaf first). The entry
1139
+ * carries no DER: identity is the two SHA-256 digests, and the server's
1140
+ * ordering is untrusted input, so the record carries no issuer edges and no
1141
+ * SKI/AKI. Fields safer-exec already classified per PLAN section 2.5
1142
+ * (selfSigned, keyName, signatureAlgorithm) are taken as given; `role` is
1143
+ * re-derived from them with the same rule as the DER path.
1144
+ *
1145
+ * @param {object} entry safer-exec CertChainEntry
1146
+ * @param {object} [context] `{host}` the safer-exec `host:port` the chain was
1147
+ * presented to; the occurrence `tls://<host>` is omitted without it
1148
+ * @returns {object|undefined} record, or undefined when the entry has no
1149
+ * usable certificate hash
1150
+ */
1151
+ export function certRecordFromObservedChainEntry(entry, context = {}) {
1152
+ const certSha256 = toLowerSha256(entry?.certSha256);
1153
+ if (!certSha256) {
1154
+ return undefined;
1155
+ }
1156
+ const record = emptyRecord(certSha256);
1157
+ record.spkiSha256 = toLowerSha256(entry.spkiSha256);
1158
+ record.spkiSha256Base64 =
1159
+ typeof entry.spkiSha256Base64 === "string"
1160
+ ? entry.spkiSha256Base64
1161
+ : undefined;
1162
+ record.subject = toWellFormedText(entry.subject);
1163
+ record.issuer = toWellFormedText(entry.issuer);
1164
+ // Named from the subject exactly as the DER path names it, so a CN-less
1165
+ // certificate gets one bom-ref from both producers (safer-exec's own name
1166
+ // takes the first RDN in DER order, cdxgen the first in RFC 4514 order).
1167
+ record.name =
1168
+ (record.subject && commonNameFromDn(record.subject)) ||
1169
+ toWellFormedText(entry.name);
1170
+ record.serialHex =
1171
+ typeof entry.serialNumber === "string" &&
1172
+ entry.serialNumber.length <= MAX_SERIAL_HEX_CHARS
1173
+ ? normalizeSerialHex(entry.serialNumber)
1174
+ : undefined;
1175
+ record.version = Number.isInteger(entry.version) ? entry.version : undefined;
1176
+ record.notValidBefore = rfc3339(entry.notValidBefore);
1177
+ record.notValidAfter = rfc3339(entry.notValidAfter);
1178
+ record.signatureAlgorithm = toWellFormedText(entry.signatureAlgorithm);
1179
+ record.signatureAlgorithmOid =
1180
+ typeof entry.signatureAlgorithmOid === "string" &&
1181
+ entry.signatureAlgorithmOid
1182
+ ? entry.signatureAlgorithmOid
1183
+ : undefined;
1184
+ record.keyName = toWellFormedText(entry.keyName);
1185
+ record.keyBits =
1186
+ Number.isInteger(entry.keyBits) &&
1187
+ entry.keyBits > 0 &&
1188
+ entry.keyBits <= 16384
1189
+ ? entry.keyBits
1190
+ : undefined;
1191
+ record.keyAlgorithmOid =
1192
+ typeof entry.keyAlgorithmOid === "string" && entry.keyAlgorithmOid
1193
+ ? entry.keyAlgorithmOid
1194
+ : undefined;
1195
+ record.isCA = typeof entry.isCA === "boolean" ? entry.isCA : undefined;
1196
+ record.selfSigned =
1197
+ typeof entry.selfSigned === "boolean" ? entry.selfSigned : undefined;
1198
+ record.selfSignedUnverified = entry.selfSignedUnverified ? true : undefined;
1199
+ record.role =
1200
+ record.selfSigned && (record.isCA || record.version === 1)
1201
+ ? "root"
1202
+ : record.isCA
1203
+ ? "intermediate"
1204
+ : "leaf";
1205
+ const host = typeof context.host === "string" ? context.host.trim() : "";
1206
+ if (host) {
1207
+ record.occurrences.push({
1208
+ location: `tls://${host}`,
1209
+ sourceType: "observed-tls",
1210
+ });
1211
+ record.sourceTypes = ["observed-tls"];
1212
+ }
1213
+ return record;
1214
+ }
1215
+
1216
+ // Determine selfSigned/role per PLAN section 2.3/2.5. Signature verification
1217
+ // uses the platform only inside try/catch; when it cannot check (unsupported
1218
+ // algorithm, MD5), a self-issued certificate (subject DER == issuer DER, AKI
1219
+ // absent or equal to SKI) still counts as self-signed and is marked
1220
+ // selfSignedUnverified.
1221
+ function classifySelfIssued(record) {
1222
+ const walked = record.walked;
1223
+ if (!walked) {
1224
+ return;
1225
+ }
1226
+ const selfIssued =
1227
+ walked.subjectDer.length === walked.issuerDer.length &&
1228
+ walked.subjectDer.equals(walked.issuerDer);
1229
+ let verified = false;
1230
+ let couldNotCheck = false;
1231
+ if (selfIssued && record.x509) {
1232
+ try {
1233
+ verified =
1234
+ record.x509.checkIssued(record.x509) &&
1235
+ record.x509.verify(record.x509.publicKey);
1236
+ } catch {
1237
+ // The runtime cannot check this signature (unsupported algorithm); a
1238
+ // signature that was checked and failed is NOT self-signed.
1239
+ couldNotCheck = true;
1240
+ }
1241
+ }
1242
+ let unverified = false;
1243
+ if (selfIssued && !verified && couldNotCheck) {
1244
+ // PLAN 2.5: the AKI is absent or equals the SKI.
1245
+ const keyIdsChain =
1246
+ !walked.authorityKeyId || walked.authorityKeyId === walked.subjectKeyId;
1247
+ if (keyIdsChain) {
1248
+ unverified = true;
1249
+ }
1250
+ }
1251
+ record.selfIssued = selfIssued;
1252
+ if (verified || unverified) {
1253
+ record.selfSigned = true;
1254
+ record.selfSignedUnverified = unverified && !verified ? true : undefined;
1255
+ } else {
1256
+ record.selfSigned = false;
1257
+ }
1258
+ if (record.selfSigned) {
1259
+ record.role = record.isCA || record.version === 1 ? "root" : "leaf";
1260
+ } else if (record.isCA) {
1261
+ record.role = "intermediate";
1262
+ } else {
1263
+ record.role = "leaf";
1264
+ }
1265
+ }
1266
+
1267
+ /**
1268
+ * Pairwise issuer linking over records parsed in one collection
1269
+ * (`certRecordFromDer` output). A verified edge needs the signature to check;
1270
+ * an uncheckable signature over chained names and key identifiers goes to
1271
+ * issuersUnverified. Capped at ISSUER_LINK_LIMIT certificates.
1272
+ *
1273
+ * @param {object[]} records records from certRecordFromDer
1274
+ */
1275
+ export function linkFallbackIssuers(records) {
1276
+ const parsed = records.filter((record) => record?.x509 && record?.walked);
1277
+ if (parsed.length > ISSUER_LINK_LIMIT) {
1278
+ return;
1279
+ }
1280
+ for (const child of parsed) {
1281
+ const verified = [];
1282
+ const unverified = [];
1283
+ for (const parent of parsed) {
1284
+ if (parent === child) {
1285
+ continue;
1286
+ }
1287
+ const namesChain =
1288
+ child.walked.issuerDer.length === parent.walked.subjectDer.length &&
1289
+ child.walked.issuerDer.equals(parent.walked.subjectDer);
1290
+ if (!namesChain) {
1291
+ continue;
1292
+ }
1293
+ const keyIdsChain =
1294
+ !child.walked.authorityKeyId ||
1295
+ !parent.walked.subjectKeyId ||
1296
+ child.walked.authorityKeyId === parent.walked.subjectKeyId;
1297
+ let signatureOk = false;
1298
+ let threw = false;
1299
+ try {
1300
+ signatureOk =
1301
+ child.x509.checkIssued(parent.x509) &&
1302
+ child.x509.verify(parent.x509.publicKey);
1303
+ } catch {
1304
+ threw = true;
1305
+ }
1306
+ if (signatureOk) {
1307
+ verified.push(parent.certSha256);
1308
+ } else if (threw && keyIdsChain && !child.selfIssued) {
1309
+ // Only a signature the runtime cannot check lands here; one that was
1310
+ // checked and failed means "not signed by this key" and is dropped.
1311
+ unverified.push(parent.spkiSha256);
1312
+ }
1313
+ }
1314
+ child.issuers = verified.sort();
1315
+ child.fallbackUnverifiedIssuerSpkis = sortedUnique(unverified);
1316
+ }
1317
+ }
1318
+
1319
+ /**
1320
+ * Relativise a host path against a scan root so producers agree on
1321
+ * locations: `/tmp/x/rootfs/etc/ssl` with root `/tmp/x/rootfs` becomes
1322
+ * `/etc/ssl` (PLAN section 4.4, G18).
1323
+ *
1324
+ * @param {string} location host or already-relative path
1325
+ * @param {string} [root] scan root to strip
1326
+ * @returns {string} normalised container-style path
1327
+ */
1328
+ export function containerRelativePath(location, root) {
1329
+ if (typeof location !== "string" || !location) {
1330
+ return undefined;
1331
+ }
1332
+ let normalized = location.replace(/\\/g, "/");
1333
+ if (root) {
1334
+ let cleanedRoot = root.replace(/\\/g, "/");
1335
+ let end = cleanedRoot.length;
1336
+ while (end > 1 && cleanedRoot[end - 1] === "/") {
1337
+ end--;
1338
+ }
1339
+ cleanedRoot = cleanedRoot.slice(0, end);
1340
+ const rootPrefix = `${cleanedRoot}/`;
1341
+ if (normalized.startsWith(rootPrefix)) {
1342
+ normalized = normalized.slice(rootPrefix.length - 1);
1343
+ }
1344
+ }
1345
+ if (!normalized.startsWith("/")) {
1346
+ normalized = `/${normalized}`;
1347
+ }
1348
+ return normalized;
1349
+ }
1350
+
1351
+ function mergeRecords(existing, incoming) {
1352
+ const fieldNames = [
1353
+ "spkiSha256",
1354
+ "spkiSha256Base64",
1355
+ "subject",
1356
+ "issuer",
1357
+ "serialHex",
1358
+ "version",
1359
+ "isCA",
1360
+ "selfSigned",
1361
+ "role",
1362
+ "pathLen",
1363
+ "subjectKeyId",
1364
+ "authorityKeyId",
1365
+ "notValidBefore",
1366
+ "notValidAfter",
1367
+ "fileExtension",
1368
+ "keyName",
1369
+ "keyBits",
1370
+ "keyAlgorithmOid",
1371
+ "signatureAlgorithm",
1372
+ "signatureAlgorithmOid",
1373
+ "keyUsage",
1374
+ "extKeyUsage",
1375
+ "dnsNames",
1376
+ "ocspServers",
1377
+ "issuingCertificateUrls",
1378
+ "crlDistributionPoints",
1379
+ "policyOids",
1380
+ "hasNameConstraints",
1381
+ "trustAcceptedPurposes",
1382
+ "trustRejectedPurposes",
1383
+ "hostStores",
1384
+ "algorithm",
1385
+ "keyStrength",
1386
+ "trustDomain",
1387
+ "name",
1388
+ ];
1389
+ for (const fieldName of fieldNames) {
1390
+ if (
1391
+ existing[fieldName] === undefined &&
1392
+ incoming[fieldName] !== undefined
1393
+ ) {
1394
+ existing[fieldName] = incoming[fieldName];
1395
+ }
1396
+ }
1397
+ if (
1398
+ existing.selfSignedUnverified === undefined &&
1399
+ incoming.selfSignedUnverified
1400
+ ) {
1401
+ existing.selfSignedUnverified = incoming.selfSignedUnverified;
1402
+ }
1403
+ if (existing.issuers === undefined && Array.isArray(incoming.issuers)) {
1404
+ existing.issuers = incoming.issuers;
1405
+ } else if (
1406
+ Array.isArray(existing.issuers) &&
1407
+ Array.isArray(incoming.issuers)
1408
+ ) {
1409
+ existing.issuers = sortedUnique(existing.issuers.concat(incoming.issuers));
1410
+ }
1411
+ if (Array.isArray(incoming.fallbackUnverifiedIssuerSpkis)) {
1412
+ existing.fallbackUnverifiedIssuerSpkis = sortedUnique(
1413
+ (existing.fallbackUnverifiedIssuerSpkis || []).concat(
1414
+ incoming.fallbackUnverifiedIssuerSpkis,
1415
+ ),
1416
+ );
1417
+ }
1418
+ if (Array.isArray(incoming.issuersUnverified)) {
1419
+ existing.issuersUnverified = sortedUnique(
1420
+ (existing.issuersUnverified || []).concat(incoming.issuersUnverified),
1421
+ );
1422
+ }
1423
+ if (!existing.k8sSecretName && incoming.k8sSecretName) {
1424
+ existing.k8sSecretName = incoming.k8sSecretName;
1425
+ }
1426
+ // Insertion order is preserved (deduplicated): the report's "first"
1427
+ // sourceType stays the first.
1428
+ existing.sourceTypes = [
1429
+ ...new Set((existing.sourceTypes || []).concat(incoming.sourceTypes || [])),
1430
+ ];
1431
+ const seenLocations = new Set(
1432
+ existing.occurrences.map((occ) => occ.location),
1433
+ );
1434
+ for (const occurrence of incoming.occurrences) {
1435
+ if (!seenLocations.has(occurrence.location)) {
1436
+ existing.occurrences.push(occurrence);
1437
+ seenLocations.add(occurrence.location);
1438
+ }
1439
+ }
1440
+ if (!existing.hashes?.length && incoming.hashes?.length) {
1441
+ existing.hashes = incoming.hashes;
1442
+ }
1443
+ // File digests describe one file; a certificate merged across producers
1444
+ // with several locations carries none.
1445
+ if (existing.hashes?.length && existing.occurrences.length > 1) {
1446
+ delete existing.hashes;
1447
+ }
1448
+ }
1449
+
1450
+ function buildCertificateExtensions(record) {
1451
+ if (
1452
+ record.role === undefined &&
1453
+ !record.keyUsage?.length &&
1454
+ !record.extKeyUsage?.length
1455
+ ) {
1456
+ return undefined;
1457
+ }
1458
+ const extensions = [];
1459
+ // Extensions are only emitted when the certificate can carry them and the
1460
+ // producer observed the constraint positively: a v1 certificate has no
1461
+ // extensions at all (RFC 5280 section 4.1.2.1), and "not a CA" is not the
1462
+ // same statement as "basicConstraints says CA:FALSE".
1463
+ const isCA =
1464
+ record.role === "root" || record.role === "intermediate" || record.isCA;
1465
+ if (record.version !== 1 && isCA) {
1466
+ // RFC 5280 section 4.2.1.9.
1467
+ let value = "CA:TRUE";
1468
+ if (Number.isInteger(record.pathLen)) {
1469
+ value += `, pathlen:${record.pathLen}`;
1470
+ }
1471
+ extensions.push({
1472
+ commonExtensionName: "basicConstraints",
1473
+ commonExtensionValue: value,
1474
+ });
1475
+ }
1476
+ if (record.keyUsage?.length) {
1477
+ // RFC 5280 section 4.2.1.3 names.
1478
+ extensions.push({
1479
+ commonExtensionName: "keyUsage",
1480
+ commonExtensionValue: record.keyUsage.join(","),
1481
+ });
1482
+ }
1483
+ if (record.extKeyUsage?.length) {
1484
+ // RFC 5280 section 4.2.1.12; unknown purposes keep their dotted OID.
1485
+ extensions.push({
1486
+ commonExtensionName: "extendedKeyUsage",
1487
+ commonExtensionValue: record.extKeyUsage.join(","),
1488
+ });
1489
+ }
1490
+ if (record.subjectKeyId) {
1491
+ extensions.push({
1492
+ commonExtensionName: "subjectKeyIdentifier",
1493
+ commonExtensionValue: record.subjectKeyId,
1494
+ });
1495
+ }
1496
+ if (record.authorityKeyId) {
1497
+ extensions.push({
1498
+ commonExtensionName: "authorityKeyIdentifier",
1499
+ commonExtensionValue: record.authorityKeyId,
1500
+ });
1501
+ }
1502
+ const aia = [];
1503
+ for (const url of record.issuingCertificateUrls || []) {
1504
+ aia.push(`caIssuers=${url}`);
1505
+ }
1506
+ for (const url of record.ocspServers || []) {
1507
+ aia.push(`ocsp=${url}`);
1508
+ }
1509
+ if (aia.length) {
1510
+ extensions.push({
1511
+ commonExtensionName: "authorityInformationAccess",
1512
+ commonExtensionValue: aia.join("; "),
1513
+ });
1514
+ }
1515
+ if (record.crlDistributionPoints?.length) {
1516
+ extensions.push({
1517
+ commonExtensionName: "crlDistributionPoints",
1518
+ commonExtensionValue: record.crlDistributionPoints.join(" "),
1519
+ });
1520
+ }
1521
+ if (record.policyOids?.length) {
1522
+ extensions.push({
1523
+ commonExtensionName: "certificatePolicies",
1524
+ commonExtensionValue: record.policyOids.join(","),
1525
+ });
1526
+ }
1527
+ if (record.role === "leaf" && record.dnsNames?.length) {
1528
+ const shown = record.dnsNames.slice(0, SAN_DISPLAY_LIMIT);
1529
+ const suffix =
1530
+ record.dnsNames.length > SAN_DISPLAY_LIMIT
1531
+ ? ` +${record.dnsNames.length - SAN_DISPLAY_LIMIT} more`
1532
+ : "";
1533
+ extensions.push({
1534
+ commonExtensionName: "subjectAlternativeName",
1535
+ commonExtensionValue: `${shown.join(", ")}${suffix}`,
1536
+ });
1537
+ }
1538
+ if (record.hasNameConstraints) {
1539
+ extensions.push({
1540
+ customExtensionName: "nameConstraints",
1541
+ customExtensionValue: "present",
1542
+ });
1543
+ }
1544
+ return extensions.length ? extensions : undefined;
1545
+ }
1546
+
1547
+ function certificateComponent(
1548
+ record,
1549
+ issuerSpkis,
1550
+ unverifiedIssuerSpkis,
1551
+ publicKeyRef,
1552
+ ) {
1553
+ const name = record.name || "certificate";
1554
+ const ref = certificateBomRef(name, record.certSha256);
1555
+ const occurrences = record.occurrences
1556
+ .slice()
1557
+ .sort((left, right) => left.location.localeCompare(right.location));
1558
+ const extensions = buildCertificateExtensions(record);
1559
+ const signatureAlgorithmRef =
1560
+ record.signatureAlgorithmOid &&
1561
+ algorithmComponentMetadata(record.signatureAlgorithmOid)
1562
+ ? algorithmBomRef(
1563
+ algorithmComponentName(
1564
+ record.signatureAlgorithmOid,
1565
+ signatureAlgorithmName({ oid: record.signatureAlgorithmOid }),
1566
+ ),
1567
+ record.signatureAlgorithmOid,
1568
+ )
1569
+ : undefined;
1570
+ const relatedAssets = [
1571
+ ...(publicKeyRef ? [{ type: "publicKey", ref: publicKeyRef }] : []),
1572
+ ...(signatureAlgorithmRef && record.signatureAlgorithm
1573
+ ? [{ type: "algorithm", ref: signatureAlgorithmRef }]
1574
+ : []),
1575
+ ];
1576
+ const properties = [
1577
+ ...(record.spkiSha256
1578
+ ? [prop("cdx:crypto:spkiSha256", record.spkiSha256)]
1579
+ : []),
1580
+ prop("cdx:crypto:fingerprint", record.certSha256),
1581
+ ...(record.role ? [prop("cdx:crypto:certRole", record.role)] : []),
1582
+ ...(record.selfSigned === undefined
1583
+ ? []
1584
+ : [prop("cdx:crypto:selfSigned", record.selfSigned)]),
1585
+ ...(record.selfSignedUnverified
1586
+ ? [prop("cdx:crypto:selfSignedUnverified", true)]
1587
+ : []),
1588
+ ...(record.isCA === undefined
1589
+ ? []
1590
+ : [prop("cdx:crypto:isCA", record.isCA)]),
1591
+ ...(Number.isInteger(record.pathLen)
1592
+ ? [prop("cdx:crypto:pathLen", record.pathLen)]
1593
+ : []),
1594
+ ...(record.subjectKeyId
1595
+ ? [prop("cdx:crypto:subjectKeyId", record.subjectKeyId)]
1596
+ : []),
1597
+ ...(record.authorityKeyId
1598
+ ? [prop("cdx:crypto:authorityKeyId", record.authorityKeyId)]
1599
+ : []),
1600
+ ...issuerSpkis.map((spki) => prop("cdx:crypto:issuerSpkiSha256", spki)),
1601
+ ...unverifiedIssuerSpkis.map((spki) =>
1602
+ prop("cdx:crypto:unverifiedIssuerSpkiSha256", spki),
1603
+ ),
1604
+ ...(record.signatureAlgorithm
1605
+ ? [prop("cdx:crypto:signatureAlgorithm", record.signatureAlgorithm)]
1606
+ : []),
1607
+ ...(record.signatureAlgorithmOid
1608
+ ? [prop("cdx:crypto:signatureAlgorithmOid", record.signatureAlgorithmOid)]
1609
+ : []),
1610
+ ...(record.keyAlgorithmOid
1611
+ ? [prop("cdx:crypto:publicKeyAlgorithmOid", record.keyAlgorithmOid)]
1612
+ : []),
1613
+ ...(record.trustAcceptedPurposes?.length
1614
+ ? [
1615
+ prop(
1616
+ "cdx:crypto:trustAcceptedPurposes",
1617
+ record.trustAcceptedPurposes.join(","),
1618
+ ),
1619
+ ]
1620
+ : []),
1621
+ ...(record.trustRejectedPurposes?.length
1622
+ ? [
1623
+ prop(
1624
+ "cdx:crypto:trustRejectedPurposes",
1625
+ record.trustRejectedPurposes.join(","),
1626
+ ),
1627
+ ]
1628
+ : []),
1629
+ ...(record.hostStores?.length
1630
+ ? [prop("cdx:crypto:hostStore", record.hostStores.join(","))]
1631
+ : []),
1632
+ ...(record.algorithm
1633
+ ? [prop("cdx:crypto:algorithm", record.algorithm)]
1634
+ : []),
1635
+ ...(record.keyStrength
1636
+ ? [prop("cdx:crypto:keyStrength", record.keyStrength)]
1637
+ : []),
1638
+ ...(record.trustDomain
1639
+ ? [prop("cdx:crypto:trustDomain", record.trustDomain)]
1640
+ : []),
1641
+ ...(record.k8sSecretName
1642
+ ? [prop("cdx:crypto:k8sSecretName", record.k8sSecretName)]
1643
+ : []),
1644
+ ...sourceTypeProperties(record.sourceTypes),
1645
+ ...(occurrences.length
1646
+ ? [prop("internal:SrcFile", srcFileOfLocation(occurrences[0].location))]
1647
+ : []),
1648
+ ];
1649
+ const component = {
1650
+ "bom-ref": ref,
1651
+ name,
1652
+ type: "cryptographic-asset",
1653
+ version: record.certSha256,
1654
+ ...(record.hashes?.length ? { hashes: record.hashes } : {}),
1655
+ description: record.subject || occurrences[0]?.location,
1656
+ cryptoProperties: {
1657
+ assetType: "certificate",
1658
+ algorithmProperties: {
1659
+ executionEnvironment: "unknown",
1660
+ implementationPlatform: "unknown",
1661
+ },
1662
+ certificateProperties: {
1663
+ ...(record.serialHex ? { serialNumber: record.serialHex } : {}),
1664
+ ...(record.subject ? { subjectName: record.subject } : {}),
1665
+ ...(record.issuer ? { issuerName: record.issuer } : {}),
1666
+ ...(record.notValidBefore
1667
+ ? { notValidBefore: record.notValidBefore }
1668
+ : {}),
1669
+ ...(record.notValidAfter
1670
+ ? { notValidAfter: record.notValidAfter }
1671
+ : {}),
1672
+ certificateFormat: "X.509",
1673
+ ...(record.fileExtension
1674
+ ? { certificateFileExtension: record.fileExtension }
1675
+ : {}),
1676
+ fingerprint: { alg: "SHA-256", content: record.certSha256 },
1677
+ ...(relatedAssets.length
1678
+ ? { relatedCryptographicAssets: relatedAssets }
1679
+ : {}),
1680
+ },
1681
+ },
1682
+ ...(occurrences.length
1683
+ ? {
1684
+ evidence: {
1685
+ occurrences: occurrences.map(({ location }) => ({ location })),
1686
+ },
1687
+ }
1688
+ : {}),
1689
+ properties,
1690
+ };
1691
+ if (extensions?.length) {
1692
+ component.cryptoProperties.certificateProperties.certificateExtensions =
1693
+ extensions;
1694
+ }
1695
+ return component;
1696
+ }
1697
+
1698
+ function publicKeyComponent(keyRecord) {
1699
+ const keyName = keyRecord.keyName || "public-key";
1700
+ const ref = publicKeyBomRef(keyName, keyRecord.spkiSha256);
1701
+ const algorithmRef =
1702
+ keyRecord.keyAlgorithmOid &&
1703
+ algorithmComponentMetadata(keyRecord.keyAlgorithmOid)
1704
+ ? algorithmBomRef(
1705
+ keyAlgorithmFamilyName(keyRecord.keyAlgorithmOid),
1706
+ keyRecord.keyAlgorithmOid,
1707
+ )
1708
+ : undefined;
1709
+ const occurrences = keyRecord.occurrences
1710
+ .slice()
1711
+ .sort((left, right) => left.location.localeCompare(right.location));
1712
+ const properties = [
1713
+ prop("cdx:crypto:spkiSha256", keyRecord.spkiSha256),
1714
+ ...(keyRecord.spkiSha256Base64
1715
+ ? [prop("cdx:crypto:spkiSha256Base64", keyRecord.spkiSha256Base64)]
1716
+ : []),
1717
+ ...(keyRecord.keyAlgorithmOid
1718
+ ? [prop("cdx:crypto:publicKeyAlgorithmOid", keyRecord.keyAlgorithmOid)]
1719
+ : []),
1720
+ ...sourceTypeProperties(keyRecord.sourceTypes || [keyRecord.sourceType]),
1721
+ ];
1722
+ const isPostQuantum =
1723
+ keyRecord.keyAlgorithmOid && PQ_ALGORITHM_BY_OID[keyRecord.keyAlgorithmOid];
1724
+ return {
1725
+ "bom-ref": ref,
1726
+ name: keyName,
1727
+ type: "cryptographic-asset",
1728
+ version: keyRecord.spkiSha256,
1729
+ cryptoProperties: {
1730
+ assetType: "related-crypto-material",
1731
+ relatedCryptoMaterialProperties: {
1732
+ type: "public-key",
1733
+ ...(!isPostQuantum && Number.isInteger(keyRecord.keyBits)
1734
+ ? { size: keyRecord.keyBits }
1735
+ : {}),
1736
+ format: "SPKI",
1737
+ fingerprint: { alg: "SHA-256", content: keyRecord.spkiSha256 },
1738
+ ...(keyRecord.subjectKeyId && !keyRecord.pin
1739
+ ? { id: keyRecord.subjectKeyId }
1740
+ : {}),
1741
+ ...(algorithmRef ? { algorithmRef } : {}),
1742
+ },
1743
+ },
1744
+ ...(occurrences.length
1745
+ ? {
1746
+ evidence: {
1747
+ occurrences: occurrences.map(({ location }) => ({ location })),
1748
+ },
1749
+ }
1750
+ : {}),
1751
+ properties,
1752
+ };
1753
+ }
1754
+
1755
+ function algorithmComponent(name, oid) {
1756
+ const metadata = algorithmComponentMetadata(oid);
1757
+ return {
1758
+ "bom-ref": algorithmBomRef(name, oid),
1759
+ name,
1760
+ type: "cryptographic-asset",
1761
+ cryptoProperties: {
1762
+ assetType: "algorithm",
1763
+ oid,
1764
+ ...(metadata ? { algorithmProperties: { ...metadata } } : {}),
1765
+ },
1766
+ properties: [],
1767
+ };
1768
+ }
1769
+
1770
+ /**
1771
+ * Collects certificate records from any producer, deduplicates them by
1772
+ * certificate SHA-256 and builds the shared component model: one certificate
1773
+ * component per certificate, one public-key component per SPKI, algorithm
1774
+ * components, and verified issuer `dependsOn` edges.
1775
+ */
1776
+ export class CertificateCollector {
1777
+ constructor() {
1778
+ this.recordsBySha = new Map();
1779
+ this.pins = [];
1780
+ }
1781
+
1782
+ addRecord(record) {
1783
+ if (!record?.certSha256) {
1784
+ return;
1785
+ }
1786
+ const existing = this.recordsBySha.get(record.certSha256);
1787
+ if (existing) {
1788
+ mergeRecords(existing, record);
1789
+ } else {
1790
+ this.recordsBySha.set(record.certSha256, record);
1791
+ }
1792
+ }
1793
+
1794
+ addRecords(records) {
1795
+ for (const record of records || []) {
1796
+ this.addRecord(record);
1797
+ }
1798
+ }
1799
+
1800
+ addTrustInspectorMaterials(materials, context) {
1801
+ for (const material of materials || []) {
1802
+ this.addRecord(certRecordFromTrustInspector(material, context));
1803
+ }
1804
+ }
1805
+
1806
+ addPin({ spkiSha256, location }) {
1807
+ const normalized = toLowerHex(spkiSha256);
1808
+ if (normalized && location) {
1809
+ this.pins.push({ spkiSha256: normalized, location });
1810
+ }
1811
+ }
1812
+
1813
+ /**
1814
+ * Build the shared model. Deterministic: components and dependency lists
1815
+ * are sorted, and no map iteration order reaches the output.
1816
+ *
1817
+ * @returns {{components: object[], dependencies: object[]}}
1818
+ */
1819
+ build() {
1820
+ const records = [...this.recordsBySha.values()];
1821
+ const bySha = this.recordsBySha;
1822
+ const components = [];
1823
+ const dependencies = [];
1824
+ const publicKeyRecords = new Map();
1825
+ // Seen occurrence locations per public key, so a key shared by many
1826
+ // certificates (a CA key every observed server presented) deduplicates
1827
+ // in linear, not quadratic, time.
1828
+ const keyRecordLocations = new Map();
1829
+ const algorithmOids = new Map();
1830
+ for (const record of records) {
1831
+ if (record.spkiSha256) {
1832
+ let keyRecord = publicKeyRecords.get(record.spkiSha256);
1833
+ if (!keyRecord) {
1834
+ keyRecord = {
1835
+ spkiSha256: record.spkiSha256,
1836
+ spkiSha256Base64: record.spkiSha256Base64,
1837
+ keyName: record.keyName,
1838
+ keyBits: record.keyBits,
1839
+ keyAlgorithmOid: record.keyAlgorithmOid,
1840
+ subjectKeyId: record.subjectKeyId,
1841
+ sourceType: "certificate",
1842
+ sourceTypes: ["certificate"],
1843
+ occurrences: [],
1844
+ };
1845
+ publicKeyRecords.set(record.spkiSha256, keyRecord);
1846
+ keyRecordLocations.set(record.spkiSha256, new Set());
1847
+ }
1848
+ if (!keyRecord.keyName && record.keyName) {
1849
+ keyRecord.keyName = record.keyName;
1850
+ }
1851
+ if (keyRecord.keyBits === undefined && record.keyBits !== undefined) {
1852
+ keyRecord.keyBits = record.keyBits;
1853
+ }
1854
+ if (!keyRecord.keyAlgorithmOid && record.keyAlgorithmOid) {
1855
+ keyRecord.keyAlgorithmOid = record.keyAlgorithmOid;
1856
+ }
1857
+ if (!keyRecord.spkiSha256Base64 && record.spkiSha256Base64) {
1858
+ keyRecord.spkiSha256Base64 = record.spkiSha256Base64;
1859
+ }
1860
+ const seenLocations = keyRecordLocations.get(record.spkiSha256);
1861
+ for (const occurrence of record.occurrences) {
1862
+ if (!seenLocations.has(occurrence.location)) {
1863
+ seenLocations.add(occurrence.location);
1864
+ keyRecord.occurrences.push(occurrence);
1865
+ }
1866
+ }
1867
+ if (record.keyAlgorithmOid) {
1868
+ algorithmOids.set(
1869
+ record.keyAlgorithmOid,
1870
+ keyAlgorithmFamilyName(record.keyAlgorithmOid) ||
1871
+ keyRecord.keyName ||
1872
+ record.keyAlgorithmOid,
1873
+ );
1874
+ }
1875
+ }
1876
+ if (record.signatureAlgorithmOid) {
1877
+ algorithmOids.set(
1878
+ record.signatureAlgorithmOid,
1879
+ signatureAlgorithmName({ oid: record.signatureAlgorithmOid }),
1880
+ );
1881
+ }
1882
+ }
1883
+ for (const pin of this.pins) {
1884
+ let keyRecord = publicKeyRecords.get(pin.spkiSha256);
1885
+ if (!keyRecord) {
1886
+ keyRecord = {
1887
+ spkiSha256: pin.spkiSha256,
1888
+ sourceType: "pin",
1889
+ sourceTypes: [],
1890
+ pin: true,
1891
+ occurrences: [],
1892
+ };
1893
+ publicKeyRecords.set(pin.spkiSha256, keyRecord);
1894
+ }
1895
+ keyRecord.pin = true;
1896
+ keyRecord.sourceType = "pin";
1897
+ if (!keyRecord.sourceTypes.includes("pin")) {
1898
+ keyRecord.sourceTypes.push("pin");
1899
+ }
1900
+ if (
1901
+ !keyRecord.occurrences.some(
1902
+ (occurrence) => occurrence.location === pin.location,
1903
+ )
1904
+ ) {
1905
+ keyRecord.occurrences.push({ location: pin.location });
1906
+ }
1907
+ }
1908
+ const publicKeyRefs = new Map(
1909
+ [...publicKeyRecords.values()].map((keyRecord) => [
1910
+ keyRecord.spkiSha256,
1911
+ publicKeyBomRef(
1912
+ keyRecord.keyName || "public-key",
1913
+ keyRecord.spkiSha256,
1914
+ ),
1915
+ ]),
1916
+ );
1917
+ for (const record of records) {
1918
+ const issuerSpkis = [];
1919
+ const unverifiedSpkis = [];
1920
+ for (const issuerSha of record.issuers || []) {
1921
+ const issuer = bySha.get(issuerSha);
1922
+ if (issuer?.spkiSha256) {
1923
+ issuerSpkis.push(issuer.spkiSha256);
1924
+ }
1925
+ }
1926
+ for (const issuerSha of record.issuersUnverified || []) {
1927
+ const issuer = bySha.get(issuerSha);
1928
+ if (issuer?.spkiSha256) {
1929
+ unverifiedSpkis.push(issuer.spkiSha256);
1930
+ }
1931
+ }
1932
+ unverifiedSpkis.push(...(record.fallbackUnverifiedIssuerSpkis || []));
1933
+ components.push(
1934
+ certificateComponent(
1935
+ record,
1936
+ sortedUnique(issuerSpkis),
1937
+ sortedUnique(unverifiedSpkis),
1938
+ record.spkiSha256 ? publicKeyRefs.get(record.spkiSha256) : undefined,
1939
+ ),
1940
+ );
1941
+ const issuerRefs = sortedUnique(
1942
+ (record.issuers || [])
1943
+ .map((issuerSha) => bySha.get(issuerSha))
1944
+ .filter((issuer) => issuer)
1945
+ .map((issuer) =>
1946
+ certificateBomRef(issuer.name || "certificate", issuer.certSha256),
1947
+ ),
1948
+ );
1949
+ if (issuerRefs.length) {
1950
+ dependencies.push({
1951
+ ref: certificateBomRef(
1952
+ record.name || "certificate",
1953
+ record.certSha256,
1954
+ ),
1955
+ dependsOn: issuerRefs,
1956
+ });
1957
+ }
1958
+ }
1959
+ for (const keyRecord of publicKeyRecords.values()) {
1960
+ components.push(publicKeyComponent(keyRecord));
1961
+ }
1962
+ for (const [oid, name] of algorithmOids) {
1963
+ // An unknown OID gets no algorithm component; its OID stays a property.
1964
+ if (algorithmComponentMetadata(oid)) {
1965
+ components.push(
1966
+ algorithmComponent(algorithmComponentName(oid, name), oid),
1967
+ );
1968
+ }
1969
+ }
1970
+ components.sort((left, right) =>
1971
+ left["bom-ref"].localeCompare(right["bom-ref"]),
1972
+ );
1973
+ dependencies.sort((left, right) => left.ref.localeCompare(right.ref));
1974
+ return { components, dependencies };
1975
+ }
1976
+ }
1977
+
1978
+ // Properties the model defines as repeatable (PLAN section 2.3): one entry
1979
+ // per issuer key, so a merge keeps every value.
1980
+ const REPEATABLE_PROPERTIES = new Set([
1981
+ "cdx:crypto:issuerSpkiSha256",
1982
+ "cdx:crypto:unverifiedIssuerSpkiSha256",
1983
+ "cdx:crypto:sourceType",
1984
+ ]);
1985
+
1986
+ const SOURCE_TYPE_PROPERTY = "cdx:crypto:sourceType";
1987
+
1988
+ /**
1989
+ * The file a certificate occurrence lives in. A Kubernetes secret occurrence
1990
+ * is `<file>#data.tls.crt` (or `#stringData.ca.crt`); the file path is what
1991
+ * internal:SrcFile records.
1992
+ *
1993
+ * @param {string} location occurrence location
1994
+ * @returns {string} file path without the secret key fragment
1995
+ */
1996
+ export function srcFileOfLocation(location) {
1997
+ // Secrets are only read from YAML files, so a certificate file whose own
1998
+ // name contains "#data." keeps its full path.
1999
+ return String(location).replace(
2000
+ /(\.ya?ml)#(?:data|stringData)\.[^#/]+$/i,
2001
+ "$1",
2002
+ );
2003
+ }
2004
+
2005
+ /**
2006
+ * One `cdx:crypto:sourceType` property per distinct producer, sorted, so the
2007
+ * value set (not the order the producers ran in) is what a BOM records.
2008
+ *
2009
+ * @param {string[]} [values] sourceType values
2010
+ * @returns {{name: string, value: string}[]} properties
2011
+ */
2012
+ function sourceTypeProperties(values) {
2013
+ return [...new Set((values || []).filter(Boolean))]
2014
+ .sort()
2015
+ .map((value) => ({ name: SOURCE_TYPE_PROPERTY, value }));
2016
+ }
2017
+
2018
+ // Collapse every sourceType property of a component into one sorted,
2019
+ // deduplicated block at the position of the first.
2020
+ function normalizeSourceTypeProperties(component) {
2021
+ const properties = component.properties;
2022
+ if (!Array.isArray(properties)) {
2023
+ return;
2024
+ }
2025
+ const first = properties.findIndex((p) => p?.name === SOURCE_TYPE_PROPERTY);
2026
+ if (first === -1) {
2027
+ return;
2028
+ }
2029
+ const block = sourceTypeProperties(
2030
+ properties
2031
+ .filter((p) => p?.name === SOURCE_TYPE_PROPERTY)
2032
+ .map((p) => p.value),
2033
+ );
2034
+ component.properties = [
2035
+ ...properties.slice(0, first),
2036
+ ...block,
2037
+ ...properties.slice(first).filter((p) => p?.name !== SOURCE_TYPE_PROPERTY),
2038
+ ];
2039
+ }
2040
+
2041
+ /**
2042
+ * Merge already-built certificate/public-key components that describe the
2043
+ * same asset (same bom-ref), unioning occurrences and properties. Used at the
2044
+ * final assembly step where producer component lists meet.
2045
+ *
2046
+ * @param {object[]} components component list, possibly with crypto duplicates
2047
+ * @returns {object[]} component list with certificate duplicates merged
2048
+ */
2049
+ export function mergeDuplicateCryptoComponents(components) {
2050
+ if (!Array.isArray(components)) {
2051
+ return components;
2052
+ }
2053
+ const output = [];
2054
+ const cryptoByRef = new Map();
2055
+ for (const component of components) {
2056
+ if (component?.type !== "cryptographic-asset" || !component["bom-ref"]) {
2057
+ output.push(component);
2058
+ continue;
2059
+ }
2060
+ const existing = cryptoByRef.get(component["bom-ref"]);
2061
+ if (!existing) {
2062
+ cryptoByRef.set(component["bom-ref"], component);
2063
+ output.push(component);
2064
+ continue;
2065
+ }
2066
+ const occurrences = [
2067
+ ...(existing.evidence?.occurrences || []),
2068
+ ...(component.evidence?.occurrences || []),
2069
+ ];
2070
+ const seen = new Set();
2071
+ existing.evidence = existing.evidence || {};
2072
+ existing.evidence.occurrences = occurrences
2073
+ .filter((occurrence) => {
2074
+ if (!occurrence?.location || seen.has(occurrence.location)) {
2075
+ return false;
2076
+ }
2077
+ seen.add(occurrence.location);
2078
+ return true;
2079
+ })
2080
+ .sort((left, right) => left.location.localeCompare(right.location));
2081
+ // The component builder keys internal:SrcFile on the first sorted
2082
+ // occurrence; keep a merged component consistent with its own evidence.
2083
+ const srcFileProperty = (existing.properties || []).find(
2084
+ (property) => property.name === "internal:SrcFile",
2085
+ );
2086
+ if (srcFileProperty && existing.evidence.occurrences.length) {
2087
+ srcFileProperty.value = srcFileOfLocation(
2088
+ existing.evidence.occurrences[0].location,
2089
+ );
2090
+ }
2091
+ // File digests describe one file; a certificate found in several no
2092
+ // longer has one.
2093
+ if (existing.evidence.occurrences.length > 1) {
2094
+ delete existing.hashes;
2095
+ }
2096
+ const propertyNames = new Set(
2097
+ (existing.properties || []).map((property) => property.name),
2098
+ );
2099
+ const seenValues = new Set(
2100
+ (existing.properties || []).map(
2101
+ (property) => `${property.name}=${property.value}`,
2102
+ ),
2103
+ );
2104
+ for (const property of component.properties || []) {
2105
+ const key = `${property.name}=${property.value}`;
2106
+ // Repeatable properties (one entry per issuer key) merge by value,
2107
+ // everything else by name.
2108
+ const duplicate = REPEATABLE_PROPERTIES.has(property.name)
2109
+ ? seenValues.has(key)
2110
+ : propertyNames.has(property.name);
2111
+ if (!duplicate) {
2112
+ (existing.properties ||= []).push(property);
2113
+ propertyNames.add(property.name);
2114
+ seenValues.add(key);
2115
+ }
2116
+ }
2117
+ normalizeSourceTypeProperties(existing);
2118
+ }
2119
+ return output;
2120
+ }
2121
+
2122
+ // ---------------------------------------------------------------------------
2123
+ // Kubernetes TLS secrets and Android certificate pins (PLAN section 4.4)
2124
+ // ---------------------------------------------------------------------------
2125
+
2126
+ // A kubernetes.io/tls Secret carries `tls.crt` (a PEM bundle) and `ca.crt`
2127
+ // either base64-encoded in `.data` or as plain PEM text in `.stringData`;
2128
+ // when both define a key, `stringData` wins, as the API server applies it
2129
+ // over `data`. `tls.key` is private and never read.
2130
+ // https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets
2131
+ function parseYamlDocuments(yamlText) {
2132
+ const documents = [];
2133
+ for (const doc of parseAllDocuments(yamlText)) {
2134
+ // A malformed document does not hide the ones around it.
2135
+ if (doc.errors?.length) {
2136
+ continue;
2137
+ }
2138
+ try {
2139
+ documents.push(doc.toJS());
2140
+ } catch {
2141
+ // alias limits and the like: skip this document only
2142
+ }
2143
+ }
2144
+ return documents;
2145
+ }
2146
+
2147
+ const K8S_CERT_KEYS = ["tls.crt", "ca.crt"];
2148
+
2149
+ export function kubernetesTlsSecretRecords(yamlText, { file } = {}) {
2150
+ let documents;
2151
+ try {
2152
+ documents = parseYamlDocuments(yamlText);
2153
+ } catch {
2154
+ return [];
2155
+ }
2156
+ const results = [];
2157
+ for (const doc of documents) {
2158
+ if (
2159
+ !doc ||
2160
+ typeof doc !== "object" ||
2161
+ doc.kind !== "Secret" ||
2162
+ doc.type !== "kubernetes.io/tls"
2163
+ ) {
2164
+ continue;
2165
+ }
2166
+ const data = doc.data && typeof doc.data === "object" ? doc.data : {};
2167
+ const stringData =
2168
+ doc.stringData && typeof doc.stringData === "object"
2169
+ ? doc.stringData
2170
+ : {};
2171
+ const secretName =
2172
+ typeof doc.metadata?.name === "string" ? doc.metadata.name : "";
2173
+ for (const key of K8S_CERT_KEYS) {
2174
+ let pem;
2175
+ let section;
2176
+ if (typeof stringData[key] === "string" && stringData[key]) {
2177
+ pem = stringData[key];
2178
+ section = "stringData";
2179
+ } else if (typeof data[key] === "string" && data[key]) {
2180
+ try {
2181
+ pem = Buffer.from(data[key], "base64").toString("utf-8");
2182
+ } catch {
2183
+ continue;
2184
+ }
2185
+ section = "data";
2186
+ } else {
2187
+ continue;
2188
+ }
2189
+ for (const der of pemCertificateDerBlocks(pem)) {
2190
+ const record = certRecordFromDer(der, {
2191
+ location: file ? `${file}#${section}.${key}` : `${section}.${key}`,
2192
+ sourceType: "k8s-secret",
2193
+ });
2194
+ if (record) {
2195
+ record.fileExtension = "crt";
2196
+ if (secretName) {
2197
+ record.k8sSecretName = secretName;
2198
+ }
2199
+ results.push(record);
2200
+ }
2201
+ }
2202
+ }
2203
+ }
2204
+ return results;
2205
+ }
2206
+
2207
+ // Android network security config pins the base64 SHA-256 of a SPKI:
2208
+ // <pin-set><pin digest="SHA-256">BASE64==</pin></pin-set> under
2209
+ // <domain-config>. https://developer.android.com/privacy-and-security/security-config
2210
+ export function androidNetworkSecurityConfigPins(xmlText, { file } = {}) {
2211
+ let parsed;
2212
+ try {
2213
+ parsed = xml2js(xmlText, {
2214
+ compact: true,
2215
+ textKey: "_",
2216
+ attributesKey: "$",
2217
+ });
2218
+ } catch {
2219
+ return [];
2220
+ }
2221
+ const root = parsed?.["network-security-config"];
2222
+ if (!root) {
2223
+ return [];
2224
+ }
2225
+ const pins = [];
2226
+ const visit = (node) => {
2227
+ if (!node || typeof node !== "object") {
2228
+ return;
2229
+ }
2230
+ if (Array.isArray(node)) {
2231
+ for (const child of node) {
2232
+ visit(child);
2233
+ }
2234
+ return;
2235
+ }
2236
+ for (const [key, value] of Object.entries(node)) {
2237
+ if (key === "pin" && typeof value === "object") {
2238
+ const entries = Array.isArray(value) ? value : [value];
2239
+ for (const entry of entries) {
2240
+ const digest = entry?.$?.digest;
2241
+ const text = typeof entry?._ === "string" ? entry._.trim() : "";
2242
+ if (digest === "SHA-256" && text) {
2243
+ try {
2244
+ const digestBuffer = Buffer.from(text, "base64");
2245
+ // A SHA-256 digest is exactly 32 bytes; anything else is not
2246
+ // a usable pin.
2247
+ if (digestBuffer.length === 32) {
2248
+ pins.push({
2249
+ spkiSha256: digestBuffer.toString("hex"),
2250
+ location: file,
2251
+ });
2252
+ }
2253
+ } catch {
2254
+ // invalid base64 pin is skipped
2255
+ }
2256
+ }
2257
+ }
2258
+ } else {
2259
+ visit(value);
2260
+ }
2261
+ }
2262
+ };
2263
+ visit(root);
2264
+ return pins;
2265
+ }
2266
+
2267
+ // ---------------------------------------------------------------------------
2268
+ // Content classification for the source-directory scan (PLAN section 4.4)
2269
+ // ---------------------------------------------------------------------------
2270
+
2271
+ const GPG_LABELS = new Set([
2272
+ "PGP PUBLIC KEY BLOCK",
2273
+ "PGP PRIVATE KEY BLOCK",
2274
+ "PGP MESSAGE",
2275
+ ]);
2276
+
2277
+ function sniffDerContainer(der) {
2278
+ // PKCS containers are DER SEQUENCEs. A PKCS #7 file is a ContentInfo
2279
+ // (RFC 5652 section 5.1): SEQUENCE { contentType OID, [0] content }, so
2280
+ // the signedData OID comes first. A PKCS #12 file is a PFX (RFC 7292
2281
+ // section 4): SEQUENCE { version INTEGER, authSafe ContentInfo, ... },
2282
+ // so the data OID sits inside the second element.
2283
+ const outer = readTLV(der, 0, der.length);
2284
+ if (!outer || outer.tag !== 0x30) {
2285
+ return undefined;
2286
+ }
2287
+ let candidate = readTLV(der, outer.contentStart, outer.contentEnd);
2288
+ if (candidate?.tag === 0x02) {
2289
+ candidate = readTLV(der, candidate.next, outer.contentEnd);
2290
+ }
2291
+ if (!candidate || candidate.tag !== 0x30) {
2292
+ return undefined;
2293
+ }
2294
+ const contentType = readTLV(
2295
+ der,
2296
+ candidate.contentStart,
2297
+ candidate.contentEnd,
2298
+ );
2299
+ if (!contentType || contentType.tag !== 0x06) {
2300
+ return undefined;
2301
+ }
2302
+ const oid = readOID(der, contentType.contentStart, contentType.contentEnd);
2303
+ if (oid === "1.2.840.113549.1.7.1") {
2304
+ return "PKCS12";
2305
+ }
2306
+ if (oid === "1.2.840.113549.1.7.2") {
2307
+ return "PKCS7";
2308
+ }
2309
+ return undefined;
2310
+ }
2311
+
2312
+ /**
2313
+ * Classify a matched file by its content: certificates (PEM bundle or DER),
2314
+ * a PEM private-key header, a GPG key, a PEM public key, or a keystore
2315
+ * container the fallback cannot read. Nothing is decoded beyond what the
2316
+ * header lines and a bounded DER walk reveal.
2317
+ *
2318
+ * @param {Buffer} bytes file contents (already size-capped by the caller)
2319
+ * @returns {object} one of `{kind:"certificates", ders}`,
2320
+ * `{kind:"private-key", format}`, `{kind:"gpg"}`, `{kind:"public-key"}`,
2321
+ * `{kind:"keystore", format}` or `{kind:"unknown"}`
2322
+ */
2323
+ export function classifyCryptoFile(bytes) {
2324
+ if (!Buffer.isBuffer(bytes) || bytes.length === 0) {
2325
+ return { kind: "unknown" };
2326
+ }
2327
+ const asText = bytes.toString("utf-8");
2328
+ if (asText.includes("-----BEGIN ")) {
2329
+ const blocks = splitPemBlocks(asText);
2330
+ const ders = pemCertificateDerBlocks(asText);
2331
+ if (ders.length) {
2332
+ return { kind: "certificates", ders };
2333
+ }
2334
+ const privateFormat = privateKeyPemFormat(asText);
2335
+ if (privateFormat) {
2336
+ return { kind: "private-key", format: privateFormat };
2337
+ }
2338
+ if (blocks.some((block) => GPG_LABELS.has(block.label))) {
2339
+ return { kind: "gpg" };
2340
+ }
2341
+ if (hasPemPublicKey(asText)) {
2342
+ return { kind: "public-key" };
2343
+ }
2344
+ // A TRUSTED CERTIFICATE block is certificate material only the plugin
2345
+ // parses (its aux trust data has no Node parser); report the container
2346
+ // as an unreadable bundle rather than calling it a certificate.
2347
+ if (blocks.some((block) => block.label === "TRUSTED CERTIFICATE")) {
2348
+ return { kind: "keystore", format: "PEM-TRUSTED" };
2349
+ }
2350
+ return { kind: "unknown" };
2351
+ }
2352
+ if (bytes.length >= 4 && bytes[0] === 0xfe && bytes[1] === 0xed) {
2353
+ // OpenJDK JavaKeyStore.engineLoad magic 0xfeedfeed.
2354
+ return { kind: "keystore", format: "JKS" };
2355
+ }
2356
+ if (bytes.length >= 4 && bytes[0] === 0xce && bytes[1] === 0xce) {
2357
+ return { kind: "keystore", format: "JCEKS" };
2358
+ }
2359
+ if (bytes[0] === 0x30) {
2360
+ const container = sniffDerContainer(bytes);
2361
+ if (container) {
2362
+ return { kind: "keystore", format: container };
2363
+ }
2364
+ // A structural walk decides: an unknown key algorithm OID (GOST, SM2,
2365
+ // pre-standard) is still a real certificate.
2366
+ if (parseCertificateDer(bytes)) {
2367
+ return { kind: "certificates", ders: [bytes] };
2368
+ }
2369
+ }
2370
+ return { kind: "unknown" };
2371
+ }