@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
@@ -1,20 +1,21 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { readFileSync } from "node:fs";
3
- import { join } from "node:path";
2
+ import { readdirSync, readFileSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { delimiter, dirname, join, resolve } from "node:path";
4
5
 
5
6
  import { Purl } from "@cdxgen/cdx-purl";
7
+ import { compare as compareSemver } from "semver";
6
8
 
7
- import { DEBUG_MODE } from "../core/activity.js";
9
+ import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
8
10
  import { shouldFetchLicense, shouldFetchVCS } from "../core/env.js";
9
- import { safeExistsSync } from "../core/fs.js";
10
11
  import {
11
12
  applyPurl,
12
13
  canonicalPurlFromLooseString,
13
14
  tryBuildPurl,
14
15
  tryParsePurl,
15
16
  } from "../inventory/purl.js";
17
+ import { guessLicenseIds, isSpdxLicenseExpression } from "../inventory/spdx.js";
16
18
  import {
17
- _clearMetadataCache,
18
19
  getGoPkgLicense,
19
20
  getGoPkgVCSUrl,
20
21
  prefetchGoPkgMetadata,
@@ -30,6 +31,494 @@ function toBase64(hexString) {
30
31
  return Buffer.from(hexString, "hex").toString("base64");
31
32
  }
32
33
 
34
+ /**
35
+ * The root of the Go module cache on this machine.
36
+ *
37
+ * @returns {string|undefined} `$GOMODCACHE`, or `$GOPATH/pkg/mod`, or the
38
+ * default `~/go/pkg/mod`.
39
+ */
40
+ function goModuleCacheDir() {
41
+ const gomodcache = readEnvironmentVariable("GOMODCACHE");
42
+ if (gomodcache) {
43
+ return gomodcache;
44
+ }
45
+ const gopath = readEnvironmentVariable("GOPATH");
46
+ if (gopath) {
47
+ return join(gopath.split(delimiter)[0], "pkg", "mod");
48
+ }
49
+ return join(homedir(), "go", "pkg", "mod");
50
+ }
51
+
52
+ /**
53
+ * Escape a module path or version the way Go's module cache directory names
54
+ * do (module.EscapePath and module.EscapeVersion): every upper-case letter
55
+ * becomes `!` followed by the lower-case one, so the cache stays
56
+ * case-preserving on case-insensitive file systems (`github.com/Azure` is
57
+ * stored as `github.com/!azure`, and `v2.10.17-RC.1` as `v2.10.17-!r!c.1`).
58
+ *
59
+ * @param {string} value Module path or version
60
+ * @returns {string} The escaped value
61
+ */
62
+ function escapeGoCacheName(value) {
63
+ let escaped = "";
64
+ for (const ch of value) {
65
+ if (ch >= "A" && ch <= "Z") {
66
+ escaped += `!${ch.toLowerCase()}`;
67
+ } else {
68
+ escaped += ch;
69
+ }
70
+ }
71
+ return escaped;
72
+ }
73
+
74
+ // Path elements Windows reserves, which Go rejects in every module path.
75
+ const GO_WINDOWS_RESERVED_NAMES = new Set([
76
+ "CON",
77
+ "PRN",
78
+ "AUX",
79
+ "NUL",
80
+ ..."123456789".split("").flatMap((n) => [`COM${n}`, `LPT${n}`]),
81
+ ]);
82
+ // ASCII punctuation a file name element may hold, after module.fileNameOK.
83
+ const GO_FILE_NAME_PUNCTUATION = "!#$%&()+,-.=@[]^_{}~ ";
84
+
85
+ /**
86
+ * Whether one character may appear in a path element of the given kind, as
87
+ * module.modPathOK, module.importPathOK and module.fileNameOK decide.
88
+ *
89
+ * @param {string} ch One character
90
+ * @param {"module"|"import"|"file"} kind What the element belongs to
91
+ * @returns {boolean}
92
+ */
93
+ function isGoPathCharOK(ch, kind) {
94
+ if (/[A-Za-z0-9]/.test(ch)) {
95
+ return true;
96
+ }
97
+ if (kind === "file") {
98
+ return (
99
+ GO_FILE_NAME_PUNCTUATION.includes(ch) ||
100
+ (ch.charCodeAt(0) >= 0x80 && /\p{L}/u.test(ch))
101
+ );
102
+ }
103
+ return "-._~".includes(ch) || (kind === "import" && ch === "+");
104
+ }
105
+
106
+ /**
107
+ * Whether a path element is one the Go toolchain accepts, as module.checkElem
108
+ * decides. No accepted element is `.` or `..`, or holds a path separator, so a
109
+ * name that passes cannot leave the directory it is joined to.
110
+ *
111
+ * @param {string} elem Path element
112
+ * @param {"module"|"import"|"file"} kind What the element belongs to
113
+ * @returns {boolean}
114
+ */
115
+ function isGoPathElementOK(elem, kind) {
116
+ if (!elem || /^\.+$/.test(elem) || elem.endsWith(".")) {
117
+ return false;
118
+ }
119
+ if (kind === "module" && elem.startsWith(".")) {
120
+ return false;
121
+ }
122
+ for (const ch of elem) {
123
+ if (!isGoPathCharOK(ch, kind)) {
124
+ return false;
125
+ }
126
+ }
127
+ const dot = elem.indexOf(".");
128
+ const short = dot < 0 ? elem : elem.slice(0, dot);
129
+ if (GO_WINDOWS_RESERVED_NAMES.has(short.toUpperCase())) {
130
+ return false;
131
+ }
132
+ if (kind === "file") {
133
+ return true;
134
+ }
135
+ // Names that look like Windows short names, such as PROGRA~1.
136
+ const tilde = short.lastIndexOf("~");
137
+ return !(tilde >= 0 && /^\d+$/.test(short.slice(tilde + 1)));
138
+ }
139
+
140
+ /**
141
+ * Whether a slash-separated path is one the Go toolchain accepts, as
142
+ * module.checkPath decides.
143
+ *
144
+ * @param {string} path Module or import path
145
+ * @param {"module"|"import"} kind What the path is
146
+ * @returns {boolean}
147
+ */
148
+ function isGoPathOK(path, kind) {
149
+ return (
150
+ typeof path === "string" &&
151
+ path.length > 0 &&
152
+ !path.startsWith("-") &&
153
+ path.split("/").every((elem) => isGoPathElementOK(elem, kind))
154
+ );
155
+ }
156
+
157
+ /**
158
+ * Whether a module path passes module.CheckPath: a valid path whose first
159
+ * element is a lower-case host name with a dot in it. Go writes no module
160
+ * cache directory for any other path.
161
+ *
162
+ * @param {string} path Module path
163
+ * @returns {boolean}
164
+ */
165
+ function isValidGoModulePath(path) {
166
+ if (!isGoPathOK(path, "module")) {
167
+ return false;
168
+ }
169
+ const host = path.split("/")[0];
170
+ return host.includes(".") && /^[a-z0-9.-]+$/.test(host);
171
+ }
172
+
173
+ /**
174
+ * Whether a version passes module.EscapeVersion: a single valid file name
175
+ * element without `!`.
176
+ *
177
+ * @param {string} version Module version
178
+ * @returns {boolean}
179
+ */
180
+ function isValidGoVersion(version) {
181
+ return (
182
+ typeof version === "string" &&
183
+ !version.includes("!") &&
184
+ isGoPathElementOK(version, "file")
185
+ );
186
+ }
187
+
188
+ // The licence notice file names pkg.go.dev reads, compared without regard to
189
+ // case (golang/pkgsite internal/licenses FileNames).
190
+ const GO_LICENSE_FILE_NAMES = new Set(
191
+ [
192
+ "COPYING",
193
+ "COPYING.md",
194
+ "COPYING.markdown",
195
+ "COPYING.txt",
196
+ "COPYING.LESSER",
197
+ "COPYING.LESSER.md",
198
+ "COPYING.LESSER.markdown",
199
+ "COPYING.LESSER.txt",
200
+ "LICENCE",
201
+ "LICENCE.md",
202
+ "LICENCE.markdown",
203
+ "LICENCE.txt",
204
+ "LICENSE",
205
+ "LICENSE.md",
206
+ "LICENSE.markdown",
207
+ "LICENSE.txt",
208
+ "LICENSE-2.0.txt",
209
+ "LICENCE-2.0.txt",
210
+ "LICENSE-APACHE",
211
+ "LICENCE-APACHE",
212
+ "LICENSE-APACHE-2.0.txt",
213
+ "LICENCE-APACHE-2.0.txt",
214
+ "LICENSE-MIT",
215
+ "LICENCE-MIT",
216
+ "LICENSE.MIT",
217
+ "LICENCE.MIT",
218
+ "LICENSE.code",
219
+ "LICENCE.code",
220
+ "LICENSE.docs",
221
+ "LICENCE.docs",
222
+ "LICENSE.rst",
223
+ "LICENCE.rst",
224
+ "MIT-LICENSE",
225
+ "MIT-LICENCE",
226
+ "MIT-LICENSE.md",
227
+ "MIT-LICENCE.md",
228
+ "MIT-LICENSE.markdown",
229
+ "MIT-LICENCE.markdown",
230
+ "MIT-LICENSE.txt",
231
+ "MIT-LICENCE.txt",
232
+ "MIT_LICENSE",
233
+ "MIT_LICENCE",
234
+ "UNLICENSE",
235
+ "UNLICENCE",
236
+ ].map((f) => f.toLowerCase()),
237
+ );
238
+
239
+ /**
240
+ * Whether a file name is one of the licence notices pkg.go.dev reads, whose
241
+ * text describes the module's licence without a registry round.
242
+ *
243
+ * @param {string} fileName Candidate file name
244
+ * @returns {boolean}
245
+ */
246
+ function isGoLicenseFileName(fileName) {
247
+ return GO_LICENSE_FILE_NAMES.has(fileName.toLowerCase());
248
+ }
249
+
250
+ /**
251
+ * The licence notices of a module that is already on this machine, from the
252
+ * directory `go list` reported, from the project's `vendor/` tree, or from the
253
+ * module cache. Reading them costs no network round trip, so they answer
254
+ * before pkg.go.dev is asked. Every notice of the first directory holding
255
+ * one is returned, as pkg.go.dev reads every notice of a module.
256
+ *
257
+ * The directory `go list` reported is always read. The vendor tree and the
258
+ * module cache stand in for pkg.go.dev, so they are read only when licences
259
+ * are being fetched. Their directories are built from a module path and
260
+ * version read from modules.txt or go.sum, so a path or version the Go
261
+ * toolchain would reject, such as one with a `..` element, is not looked up:
262
+ * Go never writes such a directory, and joining it could leave the tree.
263
+ *
264
+ * @param {string} name Full module path
265
+ * @param {string} [version] Module version, for the module cache layout
266
+ * @param {Object} [options] Options
267
+ * @param {string} [options.dir] The module's directory, as `go list` reports it
268
+ * @param {string} [options.projectRoot] Project root holding a `vendor/` tree
269
+ * @param {string} [options.vendorPath] The path the module is vendored under,
270
+ * when it differs from `name`: Go vendors a replaced module under the path
271
+ * it replaces
272
+ * @returns {{fileName: string, text: string}[]} The notices, in file name
273
+ * order; empty when none is held locally
274
+ */
275
+ export function readLocalGoLicenses(name, version, options = {}) {
276
+ if (!name) {
277
+ return [];
278
+ }
279
+ const candidateDirs = [];
280
+ if (options.dir) {
281
+ candidateDirs.push(options.dir);
282
+ }
283
+ const fetchingLicenses = shouldFetchLicense();
284
+ const vendorPath = options.vendorPath || name;
285
+ if (
286
+ fetchingLicenses &&
287
+ options.projectRoot &&
288
+ isGoPathOK(vendorPath, "import")
289
+ ) {
290
+ candidateDirs.push(
291
+ join(options.projectRoot, "vendor", ...vendorPath.split("/")),
292
+ );
293
+ }
294
+ if (
295
+ fetchingLicenses &&
296
+ version &&
297
+ isValidGoModulePath(name) &&
298
+ isValidGoVersion(version)
299
+ ) {
300
+ const cacheRoot = goModuleCacheDir();
301
+ if (cacheRoot) {
302
+ candidateDirs.push(
303
+ join(
304
+ cacheRoot,
305
+ `${escapeGoCacheName(name)}@${escapeGoCacheName(version)}`,
306
+ ),
307
+ );
308
+ }
309
+ }
310
+ for (const dir of candidateDirs) {
311
+ let entries;
312
+ try {
313
+ entries = readdirSync(dir, { withFileTypes: true });
314
+ } catch {
315
+ continue;
316
+ }
317
+ // Only regular files: a module zip holds no links, so a link named like
318
+ // a notice did not come from the module.
319
+ const fileNames = entries
320
+ .filter((e) => e.isFile() && isGoLicenseFileName(e.name))
321
+ .map((e) => e.name)
322
+ .sort((a, b) => a.localeCompare(b));
323
+ const notices = [];
324
+ for (const fileName of fileNames) {
325
+ try {
326
+ const text = readFileSync(join(dir, fileName), {
327
+ encoding: "utf-8",
328
+ });
329
+ if (text?.length) {
330
+ notices.push({ fileName, text });
331
+ }
332
+ } catch {
333
+ // An unreadable notice is the same as none.
334
+ }
335
+ }
336
+ if (notices.length) {
337
+ return notices;
338
+ }
339
+ }
340
+ return [];
341
+ }
342
+
343
+ /**
344
+ * The text of the first licence notice {@link readLocalGoLicenses} finds.
345
+ *
346
+ * @param {string} name Full module path
347
+ * @param {string} [version] Module version, for the module cache layout
348
+ * @param {Object} [options] The options {@link readLocalGoLicenses} takes
349
+ * @returns {string|undefined} The notice text, or undefined when none is held
350
+ * locally
351
+ */
352
+ export function readLocalGoLicense(name, version, options = {}) {
353
+ return readLocalGoLicenses(name, version, options)[0]?.text;
354
+ }
355
+
356
+ /**
357
+ * Whether a registry can hold this module: a module path and version the Go
358
+ * toolchain accepts. Any other module was never published, and its path in a
359
+ * pkg.go.dev URL could resolve to a different module (`../x` would ask for
360
+ * `x`).
361
+ *
362
+ * @param {string} name Full module path
363
+ * @param {string} [version] Module version
364
+ * @returns {boolean}
365
+ */
366
+ function isGoRegistryModule(name, version) {
367
+ return isValidGoModulePath(name) && (!version || isValidGoVersion(version));
368
+ }
369
+
370
+ /**
371
+ * The modules the pkg.go.dev batch asks about, each marked when its licence
372
+ * notice is held on this machine, so the batch skips its licence tab but
373
+ * still fetches its module page for the repository URL. A module no registry
374
+ * holds (the main module, local code, or a path Go rejects) is left out, as
375
+ * getGoPkgComponent never looks it up.
376
+ *
377
+ * @param {Object[]} modules Modules with `name`, `version`,
378
+ * `skipRegistryLookups` and the options {@link readLocalGoLicenses} takes
379
+ * @returns {Object[]} Copies of the modules to ask about, with
380
+ * `licenseKnown` set
381
+ */
382
+ function goModulesToPrefetch(modules) {
383
+ return modules
384
+ .filter(
385
+ (m) => !m.skipRegistryLookups && isGoRegistryModule(m.name, m.version),
386
+ )
387
+ .map((m) => ({
388
+ ...m,
389
+ licenseKnown: readLocalGoLicenses(m.name, m.version, m).length > 0,
390
+ }));
391
+ }
392
+
393
+ /**
394
+ * The CycloneDX licences of notices read from disk. Every licence each notice
395
+ * grants is listed with the notice as its text, and a notice that names none
396
+ * is a CUSTOM licence. A licence with an exception is an SPDX expression,
397
+ * which CycloneDX only carries as the sole entry, so then every licence found
398
+ * is joined into one expression.
399
+ *
400
+ * @param {{text: string}[]} notices Notices read by {@link readLocalGoLicenses}
401
+ * @returns {Object[]} CycloneDX licence choices
402
+ */
403
+ function localGoLicenses(notices) {
404
+ const licenses = [];
405
+ const ids = [];
406
+ for (const { text } of notices) {
407
+ const textObject = { contentType: "text/plain", content: text };
408
+ const noticeIds = guessLicenseIds(text);
409
+ if (!noticeIds.length) {
410
+ licenses.push({ license: { name: "CUSTOM", text: textObject } });
411
+ continue;
412
+ }
413
+ for (const id of noticeIds) {
414
+ if (ids.includes(id)) {
415
+ continue;
416
+ }
417
+ ids.push(id);
418
+ licenses.push({ license: { id, text: textObject } });
419
+ }
420
+ }
421
+ if (ids.some((id) => isSpdxLicenseExpression(id))) {
422
+ return [
423
+ {
424
+ expression: ids
425
+ .map((id) => (ids.length > 1 && id.includes(" ") ? `(${id})` : id))
426
+ .join(" AND "),
427
+ },
428
+ ];
429
+ }
430
+ return licenses;
431
+ }
432
+
433
+ /**
434
+ * What a replace directive makes of one module, in the shape every Go parser
435
+ * reports it. The Go toolchain builds the replacement, so a module replacement
436
+ * is reported under the replacement's path and version, as govulncheck and
437
+ * syft report it. A directory replacement is local code that no registry
438
+ * holds: it keeps the module path it replaces and has no version, so its purl
439
+ * names no registry release.
440
+ *
441
+ * @param {string} path The module path being replaced
442
+ * @param {string} [version] The version being replaced, absent for a
443
+ * replacement of every version
444
+ * @param {string} newPath The replacement module path or directory
445
+ * @param {string} [newVersion] The replacement version, absent (or `(devel)`
446
+ * in build info) for a directory
447
+ * @returns {Object|undefined} `name` and `version` to report, `local` for a
448
+ * directory, `localDir` the directory as written, and `replaces` the
449
+ * `path@version` it stands in for
450
+ */
451
+ function goReplacement(path, version, newPath, newVersion) {
452
+ if (!path || !newPath) {
453
+ return undefined;
454
+ }
455
+ const replaces = version ? `${path}@${version}` : path;
456
+ if (!newVersion || newVersion === "(devel)") {
457
+ return {
458
+ name: path,
459
+ version: undefined,
460
+ local: true,
461
+ localDir: newPath,
462
+ replaces,
463
+ };
464
+ }
465
+ return { name: newPath, version: newVersion, local: false, replaces };
466
+ }
467
+
468
+ /**
469
+ * Record on a component the module a replace directive put it in place of.
470
+ *
471
+ * @param {Object} component Component built for the replacement
472
+ * @param {Object} [replacement] Result of {@link goReplacement}
473
+ * @returns {Object} The same component
474
+ */
475
+ function addGoReplacementProperties(component, replacement) {
476
+ if (!replacement) {
477
+ return component;
478
+ }
479
+ component.properties = component.properties || [];
480
+ component.properties.push({
481
+ name: "cdx:go:replaces",
482
+ value: replacement.replaces,
483
+ });
484
+ if (replacement.localDir) {
485
+ component.properties.push({
486
+ name: "cdx:go:local_dir",
487
+ value: replacement.localDir,
488
+ });
489
+ }
490
+ return component;
491
+ }
492
+
493
+ /**
494
+ * Order two Go module versions by semantic version precedence, as Go's semver
495
+ * package does: a pre-release, pseudo-versions included, sorts before its
496
+ * release, and build metadata such as `+incompatible` does not count.
497
+ *
498
+ * @param {string} a A module version
499
+ * @param {string} b Another module version
500
+ * @returns {number} Negative, zero or positive as `a` sorts before, with or
501
+ * after `b`
502
+ */
503
+ export function compareGoVersions(a, b) {
504
+ try {
505
+ return compareSemver(a, b);
506
+ } catch {
507
+ return (a || "").localeCompare(b || "");
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Convert a Go checksum (`h1:` followed by the base64 SHA-256) to the
513
+ * integrity string components carry.
514
+ *
515
+ * @param {string} [sum] Go checksum
516
+ * @returns {string|undefined} `sha256-…`, or undefined without a checksum
517
+ */
518
+ function goSumIntegrity(sum) {
519
+ return sum?.startsWith("h1:") ? `sha256-${sum.slice(3)}` : undefined;
520
+ }
521
+
33
522
  /**
34
523
  * Builds a Go package component object containing purl, bom-ref, integrity hash,
35
524
  * and optionally license and VCS external reference information.
@@ -38,11 +527,36 @@ function toBase64(hexString) {
38
527
  * @param {string} name Package name (full module path when group is empty)
39
528
  * @param {string} version Package version string
40
529
  * @param {string} hash Integrity hash (e.g. "sha256-…"), used as _integrity
530
+ * @param {Object} [options] Options
531
+ * @param {boolean} [options.skipRegistryLookups] Leave the licence and VCS
532
+ * URL unset instead of asking pkg.go.dev. Used for the main module, which
533
+ * no registry holds.
534
+ * @param {string} [options.dir] The module's directory, as `go list` reports
535
+ * it, where a local licence notice is read first
536
+ * @param {string} [options.projectRoot] Project root holding a `vendor/` tree
537
+ * the module may be vendored in
538
+ * @param {string} [options.vendorPath] The path the module is vendored under,
539
+ * when it differs from `name`
41
540
  * @returns {Promise<Object>} Component object ready for inclusion in a BOM package list
42
541
  */
43
- export async function getGoPkgComponent(group, name, version, hash) {
542
+ export async function getGoPkgComponent(
543
+ group,
544
+ name,
545
+ version,
546
+ hash,
547
+ options = {},
548
+ ) {
44
549
  let license;
45
- if (shouldFetchLicense()) {
550
+ let licenses;
551
+ const registryLookups =
552
+ !options.skipRegistryLookups &&
553
+ isGoRegistryModule(group ? `${group}/${name}` : name, version);
554
+ // Notices held on this machine answer before pkg.go.dev is asked, and the
555
+ // registry is consulted only when there is none.
556
+ const notices = readLocalGoLicenses(name, version, options);
557
+ if (notices.length) {
558
+ licenses = localGoLicenses(notices);
559
+ } else if (shouldFetchLicense() && registryLookups) {
46
560
  if (DEBUG_MODE) {
47
561
  console.log(
48
562
  `About to fetch go package license information for ${group}:${name}`,
@@ -51,6 +565,7 @@ export async function getGoPkgComponent(group, name, version, hash) {
51
565
  license = await getGoPkgLicense({
52
566
  group: group,
53
567
  name: name,
568
+ version: version,
54
569
  });
55
570
  }
56
571
  // Split the full module path into namespace and name, since the caller
@@ -76,8 +591,8 @@ export async function getGoPkgComponent(group, name, version, hash) {
76
591
  version: version || null,
77
592
  })?.replace(/%2F/g, "/") || null;
78
593
  let vcs;
79
- if (shouldFetchVCS()) {
80
- vcs = await getGoPkgVCSUrl(group, name);
594
+ if (shouldFetchVCS() && registryLookups) {
595
+ vcs = await getGoPkgVCSUrl(group, name, version);
81
596
  }
82
597
  const packageInfo = {
83
598
  group: group,
@@ -86,6 +601,9 @@ export async function getGoPkgComponent(group, name, version, hash) {
86
601
  _integrity: hash,
87
602
  license: license,
88
603
  };
604
+ if (licenses) {
605
+ packageInfo.licenses = licenses;
606
+ }
89
607
  applyPurl(packageInfo, purlString);
90
608
  if (vcs) {
91
609
  packageInfo.externalReferences = [{ type: "vcs", url: vcs }];
@@ -99,7 +617,8 @@ export async function getGoPkgComponent(group, name, version, hash) {
99
617
  * @param {String} goModData Contents of go.mod file
100
618
  * @param {Object} gosumMap Data from go.sum files
101
619
  *
102
- * @returns {Object} Object containing parent component, rootList and packages list
620
+ * @returns {Object} Object containing parent component, rootList, packages
621
+ * list, and the replace directives as `replacements`
103
622
  */
104
623
  export async function parseGoModData(goModData, gosumMap) {
105
624
  const pkgComponentsList = [];
@@ -108,8 +627,13 @@ export async function parseGoModData(goModData, gosumMap) {
108
627
  if (!goModData) {
109
628
  return {};
110
629
  }
111
- const { parentComponent, entries } = decodeGoModData(goModData, gosumMap);
112
- await prefetchGoPkgMetadata(entries);
630
+ const { parentComponent, entries, replacements } = decodeGoModData(
631
+ goModData,
632
+ gosumMap,
633
+ );
634
+ // A module whose licence notice is read locally is not asked for its
635
+ // licence; getGoPkgComponent reads the same notice for each entry below.
636
+ await prefetchGoPkgMetadata(goModulesToPrefetch(entries));
113
637
  for (const entry of entries) {
114
638
  const component = await getGoPkgComponent(
115
639
  "",
@@ -129,8 +653,6 @@ export async function parseGoModData(goModData, gosumMap) {
129
653
  }
130
654
  pkgComponentsList.push(component);
131
655
  }
132
- // Clear the cache
133
- _clearMetadataCache();
134
656
  return {
135
657
  parentComponent,
136
658
  pkgList: pkgComponentsList.sort((a, b) =>
@@ -141,6 +663,7 @@ export async function parseGoModData(goModData, gosumMap) {
141
663
  (a["bom-ref"] || "").localeCompare(b["bom-ref"] || ""),
142
664
  ),
143
665
  rootList,
666
+ replacements,
144
667
  };
145
668
  }
146
669
 
@@ -153,107 +676,251 @@ export async function parseGoModData(goModData, gosumMap) {
153
676
  *
154
677
  * @param {String} goModData Contents of go.mod file
155
678
  * @param {Object} gosumMap Data from go.sum files
156
- * @returns {{parentComponent: Object, entries: Array<Object>}} Parent component
157
- * and module entries in file order
679
+ * @returns {{parentComponent: Object, entries: Array<Object>, replacements: Array<Object>}}
680
+ * Parent component, module entries in file order, and every replace
681
+ * directive as `path`, `version`, `newPath` and `newVersion`
158
682
  */
159
683
  function decodeGoModData(goModData, gosumMap) {
160
684
  const parentComponent = {};
161
685
  const entries = [];
162
- let isModReplacement = false;
163
- let isTool = false;
164
-
165
- const pkgs = goModData.split("\n");
166
- for (let l of pkgs) {
167
- // Windows of course
168
- l = l.replaceAll("\r", "").replace(/[\t ]+/g, " ");
169
- // Capture the parent component name from the module
170
- if (l.startsWith("module ")) {
171
- parentComponent.name = l.split(" ").pop().trim();
172
- parentComponent.type = "application";
173
- // Single-segment module paths are valid golang purls as of cdx-purl 0.0.4.
174
- applyPurl(
175
- parentComponent,
176
- tryParsePurl(`pkg:golang/${parentComponent.name}`),
177
- );
178
- continue;
179
- }
686
+ const replacements = [];
687
+ // The verb of the parenthesised block being read: undefined outside a block,
688
+ // and "" inside one whose header is not a single verb, which Go rejects.
689
+ let blockVerb;
180
690
 
181
- // The `tool` block dependency relations will be recorded into `require` block(need run `go mod tidy`), just ignore that
182
- if (l.includes("tool (")) {
183
- isTool = !l.includes(")");
184
- continue;
185
- }
186
- if (l.includes(")")) {
187
- isTool = false;
691
+ for (const line of goModData.split("\n")) {
692
+ const { tokens, comment } = tokenizeGoModLine(line);
693
+ if (!tokens.length) {
188
694
  continue;
189
695
  }
190
- if (l.includes("tool ") || isTool) {
696
+ let verb;
697
+ let args;
698
+ if (blockVerb !== undefined) {
699
+ if (tokens.length === 1 && tokens[0] === ")") {
700
+ blockVerb = undefined;
701
+ continue;
702
+ }
703
+ verb = blockVerb;
704
+ args = tokens;
705
+ } else if (tokens.at(-1) === "(") {
706
+ // `require (` opens a block whose lines all carry the verb.
707
+ blockVerb = tokens.length === 2 ? tokens[0] : "";
191
708
  continue;
192
- }
193
- if (l.startsWith("toolchain ")) {
194
- const toolchainVer = l.split(" ").pop().trim();
195
- parentComponent.properties = [
196
- { name: "cdx:go:toolchain", value: toolchainVer },
197
- ];
709
+ } else if (tokens.at(-2) === "(" && tokens.at(-1) === ")") {
710
+ // `require ( )` is an empty block.
198
711
  continue;
712
+ } else {
713
+ verb = tokens[0];
714
+ args = tokens.slice(1);
199
715
  }
200
- // Skip go.mod file headers, whitespace, and/or comments
201
- if (
202
- l.startsWith("go ") ||
203
- //TODO: should toolchain be considered as a dependency
204
- l.includes(")") ||
205
- l.trim() === "" ||
206
- l.trim().startsWith("//")
207
- ) {
208
- continue;
716
+ switch (verb) {
717
+ case "module":
718
+ if (args.length) {
719
+ parentComponent.name = unquoteGoModToken(args[0]);
720
+ parentComponent.type = "application";
721
+ // Single-segment module paths are valid golang purls as of cdx-purl 0.0.4.
722
+ applyPurl(
723
+ parentComponent,
724
+ tryParsePurl(`pkg:golang/${parentComponent.name}`),
725
+ );
726
+ }
727
+ break;
728
+ case "toolchain":
729
+ if (args.length) {
730
+ parentComponent.properties = [
731
+ { name: "cdx:go:toolchain", value: args[0] },
732
+ ];
733
+ }
734
+ break;
735
+ case "require": {
736
+ // module/path v1.2.3
737
+ const name = unquoteGoModToken(args[0]);
738
+ const version = args[1];
739
+ if (!name || !version) {
740
+ break;
741
+ }
742
+ entries.push({
743
+ group: "",
744
+ name,
745
+ version,
746
+ hash: gosumMap[`${name}@${version}`],
747
+ indirect: isGoModIndirectComment(comment),
748
+ replacement: false,
749
+ });
750
+ break;
751
+ }
752
+ case "replace": {
753
+ // module/path [v1.2.3] => other/module v1.4.5
754
+ // module/path [v1.2.3] => ../local/directory
755
+ // Only the first form names a module version; a directory
756
+ // replacement leaves the required module as the component.
757
+ const arrow = args[1] === "=>" ? 1 : 2;
758
+ if (
759
+ args[arrow] !== "=>" ||
760
+ (args.length !== arrow + 2 && args.length !== arrow + 3)
761
+ ) {
762
+ break;
763
+ }
764
+ const oldPath = unquoteGoModToken(args[0]);
765
+ const name = unquoteGoModToken(args[arrow + 1]);
766
+ const version = args[arrow + 2];
767
+ if (oldPath && name) {
768
+ replacements.push({
769
+ path: oldPath,
770
+ version: arrow === 2 ? args[1] : undefined,
771
+ newPath: name,
772
+ newVersion: version,
773
+ });
774
+ }
775
+ if (!name || args.length !== arrow + 3) {
776
+ break;
777
+ }
778
+ entries.push({
779
+ group: "",
780
+ name,
781
+ version,
782
+ hash: gosumMap[`${name}@${version}`],
783
+ indirect: false,
784
+ replacement: true,
785
+ });
786
+ break;
787
+ }
788
+ // `go`, `godebug`, `exclude`, `retract`, `ignore` and `tool` name no
789
+ // dependency: an excluded or retracted version is kept out of the build,
790
+ // and a tool's module is listed under `require` once the module is tidy.
791
+ default:
792
+ break;
209
793
  }
794
+ }
795
+ return { parentComponent, entries, replacements };
796
+ }
210
797
 
211
- // Handle required modules separately from replacement modules to ensure accuracy when parsing component data.
212
- if (l.includes("require (")) {
213
- isModReplacement = false;
214
- continue;
215
- }
216
- if (l.includes("replace (")) {
217
- isModReplacement = true;
218
- continue;
219
- }
220
- if (l.includes("replace ")) {
221
- // If this is an inline replacement, drop the word replace
222
- // (eg; "replace google.golang.org/grpc => google.golang.org/grpc v1.21.0" becomes " google.golang.org/grpc => google.golang.org/grpc v1.21.0")
223
- l = l.replace("replace", "");
224
- isModReplacement = true;
225
- }
226
- // require google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17
227
- if (l.startsWith("require ")) {
228
- l = l.replace("require ", "");
229
- isModReplacement = false;
230
- }
231
- const tmpA = l.trim().split(" ");
232
- if (!isModReplacement) {
233
- // Add group, name and version component properties for required modules
234
- const version = tmpA[1];
235
- entries.push({
236
- group: "",
237
- name: tmpA[0],
238
- version,
239
- hash: gosumMap[`${tmpA[0]}@${version}`],
240
- indirect: l.endsWith("// indirect"),
241
- replacement: false,
242
- });
798
+ // Characters the go.mod lexer always reads as a token of their own.
799
+ const GO_MOD_PUNCTUATION = "()[]{},";
800
+
801
+ /**
802
+ * Split one go.mod line into tokens the way the Go toolchain's modfile lexer
803
+ * does: whitespace separates tokens, `(`, `)`, `[`, `]`, `{`, `}` and `,`
804
+ * stand alone, a quoted string is one token, and `//` starts a comment that
805
+ * runs to the end of the line.
806
+ *
807
+ * @param {string} line A single line of a go.mod file
808
+ * @returns {{tokens: string[], comment: string}} The tokens, quoted strings
809
+ * still quoted, and the trailing comment text without its `//`
810
+ */
811
+ function tokenizeGoModLine(line) {
812
+ const tokens = [];
813
+ let i = 0;
814
+ while (i < line.length) {
815
+ const c = line[i];
816
+ if (/\s/.test(c)) {
817
+ i++;
818
+ } else if (line.startsWith("//", i)) {
819
+ return { tokens, comment: line.slice(i + 2).trim() };
820
+ } else if (GO_MOD_PUNCTUATION.includes(c)) {
821
+ tokens.push(c);
822
+ i++;
823
+ } else if (c === '"' || c === "`") {
824
+ let end = i + 1;
825
+ while (end < line.length && line[end] !== c) {
826
+ // A double-quoted string escapes its quote with a backslash.
827
+ end += c === '"' && line[end] === "\\" ? 2 : 1;
828
+ }
829
+ tokens.push(line.slice(i, end + 1));
830
+ i = end + 1;
243
831
  } else {
244
- // Add group, name and version component properties for replacement modules
245
- const version = tmpA[3];
246
- entries.push({
247
- group: "",
248
- name: tmpA[2],
249
- version,
250
- hash: gosumMap[`${tmpA[2]}@${version}`],
251
- indirect: false,
252
- replacement: true,
253
- });
832
+ let end = i;
833
+ while (
834
+ end < line.length &&
835
+ !/\s/.test(line[end]) &&
836
+ !GO_MOD_PUNCTUATION.includes(line[end]) &&
837
+ !line.startsWith("//", end)
838
+ ) {
839
+ end++;
840
+ }
841
+ tokens.push(line.slice(i, end));
842
+ i = end;
843
+ }
844
+ }
845
+ return { tokens, comment: "" };
846
+ }
847
+
848
+ /**
849
+ * Read a module path or directory token, which go.mod allows in double
850
+ * quotes. Like the Go toolchain, any other quote character makes it invalid.
851
+ *
852
+ * @param {string|undefined} token A go.mod token
853
+ * @returns {string|undefined} The unquoted value, or undefined when invalid
854
+ */
855
+ function unquoteGoModToken(token) {
856
+ if (token?.startsWith('"')) {
857
+ return token.length >= 2 && token.endsWith('"')
858
+ ? token.slice(1, -1)
859
+ : undefined;
860
+ }
861
+ if (/["'`]/.test(token || "")) {
862
+ return undefined;
863
+ }
864
+ return token;
865
+ }
866
+
867
+ /**
868
+ * Whether a require line's comment marks it indirect, as the Go toolchain
869
+ * reads it: `// indirect`, or `// indirect; ` followed by a reason.
870
+ *
871
+ * @param {string} comment Comment text without its `//`
872
+ * @returns {boolean} True for an indirect requirement
873
+ */
874
+ function isGoModIndirectComment(comment) {
875
+ const fields = comment.split(/\s+/).filter(Boolean);
876
+ return (
877
+ (fields.length === 1 && fields[0] === "indirect") ||
878
+ (fields.length > 1 && fields[0] === "indirect;")
879
+ );
880
+ }
881
+
882
+ // A semantic version as golang.org/x/mod/semver accepts it, used to tell a
883
+ // module line's version from the `=>` of a replacement.
884
+ const GO_SEMVER_PATTERN =
885
+ /^v(0|[1-9]\d*)(\.(0|[1-9]\d*)){0,2}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
886
+
887
+ /**
888
+ * Read one `# ` module line of a vendor/modules.txt file the way the Go
889
+ * toolchain does (modload.readVendorList):
890
+ *
891
+ * # module/path v1.2.3
892
+ * # module/path v1.2.3 => other/module v1.4.5
893
+ * # module/path v1.2.3 => ../local/directory
894
+ * # module/path => other/module v1.4.5 (a replacement of every version)
895
+ *
896
+ * @param {string} line A line starting with `# `
897
+ * @returns {Object|undefined} `path`, `version` and, for a replaced module,
898
+ * `newPath` and `newVersion`; undefined for a line Go ignores
899
+ */
900
+ function parseModulesTxtModuleLine(line) {
901
+ const fields = line.split(/\s+/).filter(Boolean);
902
+ if (fields.length < 3) {
903
+ return undefined;
904
+ }
905
+ let rest;
906
+ const module = { path: fields[1] };
907
+ if (GO_SEMVER_PATTERN.test(fields[2])) {
908
+ module.version = fields[2];
909
+ rest = fields.slice(3);
910
+ } else if (fields[2] === "=>") {
911
+ rest = fields.slice(2);
912
+ } else {
913
+ return undefined;
914
+ }
915
+ if (rest[0] === "=>") {
916
+ if (rest.length === 2) {
917
+ module.newPath = rest[1];
918
+ } else if (rest.length === 3 && GO_SEMVER_PATTERN.test(rest[2])) {
919
+ module.newPath = rest[1];
920
+ module.newVersion = rest[2];
254
921
  }
255
922
  }
256
- return { parentComponent, entries };
923
+ return module;
257
924
  }
258
925
 
259
926
  /**
@@ -261,6 +928,12 @@ function decodeGoModData(goModData, gosumMap) {
261
928
  * Go package components. Cross-references the go.sum map for integrity hashes and
262
929
  * sets scope and confidence based on hash availability.
263
930
  *
931
+ * Only the modules the vendor tree holds become components: a module line
932
+ * followed by its `##` annotations or its packages. `go mod vendor` also
933
+ * records every replace directive of go.mod at the end of the file, as a
934
+ * module line with nothing after it, so the consumer can tell that the
935
+ * replacement had no effect; those lines name no vendored module.
936
+ *
264
937
  * @param {string} txtFile Path to the modules.txt file
265
938
  * @param {Object} gosumMap Map of "module@version" keys to sha256 hash values from go.sum
266
939
  * @returns {Promise<Object[]>} List of Go package component objects with evidence
@@ -268,21 +941,66 @@ function decodeGoModData(goModData, gosumMap) {
268
941
  export async function parseGoModulesTxt(txtFile, gosumMap) {
269
942
  const pkgList = [];
270
943
  const txtData = readFileSync(txtFile, { encoding: "utf-8" });
271
- const pkgs = txtData
272
- .split("\n")
273
- .filter((p) => p.trim().replace(/["']/g, "").startsWith("# "));
274
- await prefetchGoPkgMetadata(
275
- // # cel.dev/expr v0.18.0
276
- pkgs.map((l) => ({ group: "", name: l.split(" ")[1] })),
277
- );
278
- for (const l of pkgs) {
279
- const tmpA = l.split(" ");
280
- if (!tmpA.length === 3) {
944
+ // A modules.txt file lives in the vendor/ tree, so the modules it lists are
945
+ // vendored one directory up from it.
946
+ const projectRoot = dirname(dirname(txtFile));
947
+ const records = [];
948
+ let current;
949
+ for (const rawLine of txtData.split("\n")) {
950
+ // A checkout with CRLF line endings must read the same as the LF file
951
+ // go mod vendor wrote.
952
+ const line = rawLine.replaceAll("\r", "");
953
+ if (line.startsWith("# ")) {
954
+ current = parseModulesTxtModuleLine(line);
955
+ if (current) {
956
+ records.push(current);
957
+ }
281
958
  continue;
282
959
  }
283
- const version = tmpA[2];
284
- const gosumHash = gosumMap[`${tmpA[1]}@${version}`];
285
- const component = await getGoPkgComponent("", tmpA[1], tmpA[2], gosumHash);
960
+ if (current && line.trim()) {
961
+ current.vendored = true;
962
+ }
963
+ }
964
+ const modules = [];
965
+ for (const record of records) {
966
+ // A replacement record of every version names no module version, and a
967
+ // record with nothing after it was not vendored.
968
+ if (!record.version || !record.vendored) {
969
+ continue;
970
+ }
971
+ const replacement = goReplacement(
972
+ record.path,
973
+ record.version,
974
+ record.newPath,
975
+ record.newVersion,
976
+ );
977
+ modules.push({
978
+ group: "",
979
+ name: replacement?.name ?? record.path,
980
+ version: replacement ? replacement.version : record.version,
981
+ replacement,
982
+ projectRoot,
983
+ // Go vendors a replaced module under the path it replaces.
984
+ vendorPath: record.path,
985
+ skipRegistryLookups: !!replacement?.local,
986
+ });
987
+ }
988
+ // A module whose vendored licence notice is read is not asked for its
989
+ // licence; getGoPkgComponent reads the same file. Local code is never
990
+ // asked about.
991
+ await prefetchGoPkgMetadata(goModulesToPrefetch(modules));
992
+ for (const m of modules) {
993
+ const gosumHash = m.version
994
+ ? gosumMap[`${m.name}@${m.version}`]
995
+ : undefined;
996
+ const component = await getGoPkgComponent(
997
+ "",
998
+ m.name,
999
+ m.version,
1000
+ gosumHash,
1001
+ m,
1002
+ );
1003
+ addGoReplacementProperties(component, m.replacement);
286
1004
  let confidence = 0.7;
287
1005
  if (gosumHash) {
288
1006
  component.scope = "required";
@@ -294,119 +1012,160 @@ export async function parseGoModulesTxt(txtFile, gosumMap) {
294
1012
  return pkgList;
295
1013
  }
296
1014
 
1015
+ /**
1016
+ * Whether two paths name the same directory. `go list` prints the real path
1017
+ * of a directory, which differs from the one cdxgen was given when that runs
1018
+ * through a symbolic link (macOS /tmp is /private/tmp).
1019
+ *
1020
+ * @param {string} [a] A directory
1021
+ * @param {string} [b] Another directory
1022
+ * @returns {boolean}
1023
+ */
1024
+ function isSameGoDir(a, b) {
1025
+ if (!a || !b) {
1026
+ return false;
1027
+ }
1028
+ if (resolve(a) === resolve(b)) {
1029
+ return true;
1030
+ }
1031
+ try {
1032
+ return realpathSync(a) === realpathSync(b);
1033
+ } catch {
1034
+ return false;
1035
+ }
1036
+ }
1037
+
297
1038
  /**
298
1039
  * Parse go list output
299
1040
  *
1041
+ * Each row is printed by the template in createGoBom:
1042
+ * `Path|Version|Indirect|GoMod|GoVersion|Main|Time|Deprecated|GoModSum|Dir|Sum|
1043
+ * Replace.Path|Replace.Version|Replace.Sum`. Rows from older templates end
1044
+ * after `Dir`.
1045
+ *
1046
+ * A replaced module is reported as the module Go builds (see
1047
+ * {@link goReplacement}), with the checksum of the replacement. A go.work
1048
+ * workspace makes every member a main module, so the parent is the main
1049
+ * module in `options.mainModuleDir`, and the other members are returned as
1050
+ * `workspaceModules`.
1051
+ *
300
1052
  * @param {string} rawOutput Output from go list invocation
301
1053
  * @param {Object} gosumMap go.sum data
302
- * @returns Object with parent component and List of packages
1054
+ * @param {Object} [options] Options
1055
+ * @param {string} [options.mainModuleDir] Directory `go list` ran in, whose
1056
+ * main module is the parent component
1057
+ * @returns Object with parent component, list of packages, and the other main
1058
+ * modules of a workspace
303
1059
  */
304
- export async function parseGoListDep(rawOutput, gosumMap) {
1060
+ export async function parseGoListDep(rawOutput, gosumMap, options = {}) {
305
1061
  let parentComponent = {};
306
1062
  const deps = [];
1063
+ const workspaceModules = [];
307
1064
  if (typeof rawOutput === "string") {
308
- const keys_cache = {};
309
- const pkgs = rawOutput
310
- .split("\n")
311
- .filter((p) => p.trim().replace(/["']/g, "").length);
312
- // Same shape and same duplicate filter as the loop below, so the batch
313
- // covers every module the loop resolves and nothing else.
314
- const prefetchSeen = new Set();
315
- const prefetchModules = [];
316
- for (const l of pkgs) {
1065
+ const rows = [];
1066
+ const seen = new Set();
1067
+ for (const l of rawOutput.split("\n")) {
317
1068
  const verArr = l.trim().replace(/["']/g, "").split("|");
318
- if (!verArr || verArr.length < 5) {
1069
+ if (verArr.length < 5) {
319
1070
  continue;
320
1071
  }
1072
+ // Every package of a module repeats the module's row.
321
1073
  const key = `${verArr[0]}-${verArr[1]}`;
322
- if (prefetchSeen.has(key)) {
1074
+ if (seen.has(key)) {
323
1075
  continue;
324
1076
  }
325
- prefetchSeen.add(key);
326
- prefetchModules.push({ group: "", name: verArr[0] });
1077
+ seen.add(key);
1078
+ rows.push(verArr);
327
1079
  }
328
- await prefetchGoPkgMetadata(prefetchModules);
329
- for (const l of pkgs) {
330
- const verArr = l.trim().replace(/["']/g, "").split("|");
331
- if (verArr && verArr.length >= 5) {
332
- const key = `${verArr[0]}-${verArr[1]}`;
333
- // Filter duplicates
334
- if (!keys_cache[key]) {
335
- keys_cache[key] = key;
336
- const version = verArr[1];
337
- let gosumHash = gosumMap[`${verArr[0]}@${version}`];
338
- if (!gosumHash && verArr.length >= 8 && verArr[8]?.length) {
339
- gosumHash = `sha256-${verArr[8].replace("h1:", "")}`;
340
- }
341
- const component = await getGoPkgComponent(
342
- "",
343
- verArr[0],
344
- version,
345
- gosumHash,
346
- );
347
- // This is misusing the scope attribute to represent direct vs indirect
348
- if (verArr[2] === "false") {
349
- component.scope = "required";
350
- } else if (verArr[2] === "true") {
351
- component.scope = "optional";
352
- }
353
- component.properties = [
354
- {
355
- name: "internal:SrcGoMod",
356
- value: verArr[3] || "",
357
- },
358
- {
359
- name: "internal:ModuleGoVersion",
360
- value: verArr[4] || "",
361
- },
362
- {
363
- name: "cdx:go:indirect",
364
- value: verArr[2],
365
- },
366
- ];
367
- if (
368
- verArr.length >= 6 &&
369
- verArr[6]?.length &&
370
- verArr[6] !== "<nil>"
371
- ) {
372
- component.properties.push({
373
- name: "cdx:go:creation_time",
374
- value: verArr[6],
375
- });
376
- }
377
- if (verArr.length >= 7 && verArr[7]?.length) {
378
- component.properties.push({
379
- name: "cdx:go:deprecated",
380
- value: verArr[7],
381
- });
382
- }
383
- if (verArr.length >= 9 && verArr[9]?.length) {
384
- component.properties.push({
385
- name: "cdx:go:local_dir",
386
- value: verArr[9],
387
- });
388
- if (safeExistsSync(join(verArr[9], "LICENSE"))) {
389
- const licenseText = readFileSync(join(verArr[9], "LICENSE"), {
390
- encoding: "utf-8",
391
- });
392
- if (licenseText?.length) {
393
- component.licenses = [
394
- {
395
- license: {
396
- name: "CUSTOM",
397
- text: { contentType: "text/plain", content: licenseText },
398
- },
399
- },
400
- ];
401
- }
402
- }
403
- }
404
- if (verArr.length > 5 && verArr[5] === "true") {
405
- parentComponent = component;
406
- } else {
407
- deps.push(component);
408
- }
409
- }
1080
+ // The main module is the project itself, which no registry holds. In a
1081
+ // go.work workspace every member is a main module, and the one go list
1082
+ // ran in is the parent.
1083
+ const mainRows = rows.filter((verArr) => verArr[5] === "true");
1084
+ const mainRow =
1085
+ mainRows.find((verArr) =>
1086
+ isSameGoDir(verArr[9], options.mainModuleDir),
1087
+ ) || mainRows[0];
1088
+ // The directory of the main module, which is the project root, under
1089
+ // which the vendored copies of the dependencies live.
1090
+ const mainModuleDir = mainRow?.[9] || undefined;
1091
+ const modules = rows.map((verArr) => {
1092
+ const isMainModule = verArr[5] === "true";
1093
+ const replacement = isMainModule
1094
+ ? undefined
1095
+ : goReplacement(verArr[0], verArr[1], verArr[11], verArr[12]);
1096
+ const name = replacement?.name ?? verArr[0];
1097
+ const version = replacement ? replacement.version : verArr[1];
1098
+ return {
1099
+ verArr,
1100
+ isMainModule,
1101
+ replacement,
1102
+ group: "",
1103
+ name,
1104
+ version,
1105
+ // Module.Sum is the checksum of the module content, the hash of what
1106
+ // is built. GoModSum hashes only its go.mod file.
1107
+ hash:
1108
+ (version && gosumMap[`${name}@${version}`]) ||
1109
+ goSumIntegrity(replacement ? verArr[13] : verArr[10]),
1110
+ dir: verArr[9]?.length ? verArr[9] : undefined,
1111
+ projectRoot: mainModuleDir,
1112
+ // Go vendors a replaced module under the path it replaces.
1113
+ vendorPath: verArr[0],
1114
+ skipRegistryLookups: isMainModule || !!replacement?.local,
1115
+ };
1116
+ });
1117
+ // A module whose licence notice is held on this machine is not asked for
1118
+ // its licence: getGoPkgComponent reads the same notices, so the batch
1119
+ // covers exactly what the loop will look up.
1120
+ await prefetchGoPkgMetadata(goModulesToPrefetch(modules));
1121
+ for (const m of modules) {
1122
+ const { verArr } = m;
1123
+ const component = await getGoPkgComponent(
1124
+ "",
1125
+ m.name,
1126
+ m.version,
1127
+ m.hash,
1128
+ m,
1129
+ );
1130
+ // This is misusing the scope attribute to represent direct vs indirect
1131
+ if (verArr[2] === "false") {
1132
+ component.scope = "required";
1133
+ } else if (verArr[2] === "true") {
1134
+ component.scope = "optional";
1135
+ }
1136
+ component.properties = [
1137
+ {
1138
+ name: "internal:SrcGoMod",
1139
+ value: verArr[3] || "",
1140
+ },
1141
+ {
1142
+ name: "internal:ModuleGoVersion",
1143
+ value: verArr[4] || "",
1144
+ },
1145
+ {
1146
+ name: "cdx:go:indirect",
1147
+ value: verArr[2],
1148
+ },
1149
+ ];
1150
+ if (verArr.length >= 6 && verArr[6]?.length && verArr[6] !== "<nil>") {
1151
+ component.properties.push({
1152
+ name: "cdx:go:creation_time",
1153
+ value: verArr[6],
1154
+ });
1155
+ }
1156
+ if (verArr.length >= 7 && verArr[7]?.length) {
1157
+ component.properties.push({
1158
+ name: "cdx:go:deprecated",
1159
+ value: verArr[7],
1160
+ });
1161
+ }
1162
+ addGoReplacementProperties(component, m.replacement);
1163
+ if (verArr === mainRow) {
1164
+ parentComponent = component;
1165
+ } else if (m.isMainModule) {
1166
+ workspaceModules.push(component);
1167
+ } else {
1168
+ deps.push(component);
410
1169
  }
411
1170
  }
412
1171
  }
@@ -419,6 +1178,7 @@ export async function parseGoListDep(rawOutput, gosumMap) {
419
1178
  // be built silently reorders the output.
420
1179
  (a["bom-ref"] || "").localeCompare(b["bom-ref"] || ""),
421
1180
  ),
1181
+ workspaceModules,
422
1182
  };
423
1183
  }
424
1184
 
@@ -465,9 +1225,39 @@ function goModGraphTokenPurl(token) {
465
1225
  return canonical ? Purl.parse(canonical) : null;
466
1226
  }
467
1227
 
1228
+ /**
1229
+ * Split a `go mod graph` token into its module path and version, exactly as
1230
+ * Go printed them. A main module is printed without a version.
1231
+ *
1232
+ * @param {string} token `module/path@version` or `module/path`
1233
+ * @returns {{path: string, version: string|undefined}}
1234
+ */
1235
+ function splitGoModGraphToken(token) {
1236
+ const at = token.indexOf("@");
1237
+ return at < 0
1238
+ ? { path: token, version: undefined }
1239
+ : { path: token.slice(0, at), version: token.slice(at + 1) };
1240
+ }
1241
+
1242
+ /**
1243
+ * Value of the first property with the given name.
1244
+ *
1245
+ * @param {Object} component Component
1246
+ * @param {string} name Property name
1247
+ * @returns {string|undefined}
1248
+ */
1249
+ function goComponentProperty(component, name) {
1250
+ return component?.properties?.find((p) => p.name === name)?.value;
1251
+ }
1252
+
468
1253
  /**
469
1254
  * Parse go mod graph
470
1255
  *
1256
+ * `go mod graph` names every module by the path and version its requirement
1257
+ * names, whatever replaces it. A replaced module is mapped to the module Go
1258
+ * builds: from the components `go list` reported, which record what they
1259
+ * replace, or else from the replace directives of go.mod.
1260
+ *
471
1261
  * @param {string} rawOutput Output from go mod graph invocation
472
1262
  * @param {string} goModFile go.mod file
473
1263
  * @param {Object} gosumMap Hashes from gosum for lookups
@@ -493,9 +1283,41 @@ export async function parseGoModGraph(
493
1283
  let goModPkgMap = {};
494
1284
  // Direct dependencies by manually parsing the go.mod data
495
1285
  const goModDirectDepsMap = {};
1286
+ // The same direct dependencies by module path, which stays correct when
1287
+ // the version the go.mod text names has drifted from the selected one.
1288
+ const goModDirectDepNames = new Set();
496
1289
  // Indirect dependencies by manually parsing the go.mod data
497
1290
  const goModOptionalDepsMap = {};
498
1291
  const excludedRefs = [];
1292
+ // The module versions `go list` reported are the ones MVS selected, and the
1293
+ // only ones the BOM carries components for. `go mod graph` instead prints
1294
+ // the requirement graph, where each edge names the version the source's
1295
+ // go.mod asks for, so the two routinely disagree for the same module. They
1296
+ // are keyed by the module path exactly as Go prints it, which keeps its
1297
+ // upper-case letters; a replaced module is keyed by the path it replaces,
1298
+ // which is the one the graph prints.
1299
+ const selectedVersions = {};
1300
+ // `path@version` as the graph prints it, for every module a replace
1301
+ // directive put another module in place of.
1302
+ const replacedModules = new Map();
1303
+ for (const epkg of epkgList) {
1304
+ const replaces = goComponentProperty(epkg, "cdx:go:replaces");
1305
+ if (replaces) {
1306
+ const { path, version } = splitGoModGraphToken(replaces);
1307
+ replacedModules.set(replaces, {
1308
+ name: epkg.name,
1309
+ version: epkg.version,
1310
+ local: !epkg.version,
1311
+ localDir: goComponentProperty(epkg, "cdx:go:local_dir"),
1312
+ replaces,
1313
+ });
1314
+ if (version) {
1315
+ selectedVersions[path] = version;
1316
+ }
1317
+ } else if (epkg?.name && epkg?.version) {
1318
+ selectedVersions[epkg.name] = epkg.version;
1319
+ }
1320
+ }
499
1321
  if (goModFile) {
500
1322
  goModPkgMap = await parseGoModData(
501
1323
  readFileSync(goModFile, { encoding: "utf-8" }),
@@ -504,6 +1326,11 @@ export async function parseGoModGraph(
504
1326
  if (goModPkgMap?.rootList) {
505
1327
  for (const epkg of goModPkgMap.rootList) {
506
1328
  goModDirectDepsMap[epkg["bom-ref"]] = true;
1329
+ // The bom-ref above names the version the go.mod text requires,
1330
+ // which an untidy module can leave older than the version MVS
1331
+ // selected. Root-edge validation below also asks by module path,
1332
+ // so a drifted direct require still counts as direct (#4497).
1333
+ goModDirectDepNames.add(epkg.name);
507
1334
  }
508
1335
  }
509
1336
  if (goModPkgMap?.pkgList) {
@@ -514,59 +1341,117 @@ export async function parseGoModGraph(
514
1341
  }
515
1342
  }
516
1343
  }
1344
+ // What the replace directives of go.mod make of a module the graph names. A
1345
+ // directive for one version wins over one for every version, as in Go.
1346
+ const goModReplacement = (path, version) => {
1347
+ let wildcard;
1348
+ for (const r of goModPkgMap?.replacements || []) {
1349
+ if (r.path !== path) {
1350
+ continue;
1351
+ }
1352
+ if (r.version === undefined) {
1353
+ wildcard = r;
1354
+ } else if (r.version === version) {
1355
+ return goReplacement(path, version, r.newPath, r.newVersion);
1356
+ }
1357
+ }
1358
+ return wildcard
1359
+ ? goReplacement(path, version, wildcard.newPath, wildcard.newVersion)
1360
+ : undefined;
1361
+ };
1362
+ // The module Go builds for one graph token, with the reference of the
1363
+ // component that stands for it.
1364
+ const graphModule = (path, version) => {
1365
+ const replacement = version
1366
+ ? replacedModules.get(`${path}@${version}`) ||
1367
+ goModReplacement(path, version)
1368
+ : undefined;
1369
+ const name = replacement ? replacement.name : path;
1370
+ const moduleVersion = replacement ? replacement.version : version;
1371
+ const token = moduleVersion ? `${name}@${moduleVersion}` : name;
1372
+ // Some golang modules (e.g. go4.org) have no path separator and
1373
+ // cdx-purl requires a namespace. Use the raw ref when parsing fails.
1374
+ const purl = goModGraphTokenPurl(token);
1375
+ return {
1376
+ path,
1377
+ name,
1378
+ version: moduleVersion,
1379
+ token,
1380
+ replacement,
1381
+ ref: purl ? decodeURIComponent(purl.toString()) : `pkg:golang/${token}`,
1382
+ };
1383
+ };
517
1384
  for (const epkg of epkgList) {
518
1385
  existingPkgMap[epkg["bom-ref"]] = true;
519
1386
  }
520
1387
  if (parentComponent && Object.keys(parentComponent).length) {
521
1388
  existingPkgMap[parentComponent["bom-ref"]] = true;
522
1389
  }
1390
+ const filterByExisting = Object.keys(existingPkgMap).length > 0;
523
1391
  if (typeof rawOutput === "string") {
524
1392
  const lines = rawOutput.split("\n");
525
1393
  // Each line is of the form ref dependsOn
526
1394
  // github.com/spf13/afero@v1.2.2 golang.org/x/text@v0.3.0
527
1395
  for (const l of lines) {
528
- // To keep the parsing logic simple we prefix pkg:golang/
529
- // and let packageurl work out the rest
530
1396
  const tmpA = l.replaceAll("\r", "").split(" ");
531
1397
  if (tmpA && tmpA.length === 2) {
532
1398
  try {
533
- // Some golang modules (e.g. go4.org) have no path separator and
534
- // cdx-purl requires a namespace. Use the raw ref when parsing fails.
535
- let sourceRefString;
536
- let sourceName = tmpA[0].split("@")[0];
537
- const sourcePurl = goModGraphTokenPurl(tmpA[0]);
538
- if (sourcePurl) {
539
- sourceRefString = decodeURIComponent(sourcePurl.toString());
540
- sourceName = `${sourcePurl.namespace ? `${sourcePurl.namespace}/` : ""}${sourcePurl.name}`;
541
- } else {
542
- sourceRefString = `pkg:golang/${tmpA[0]}`;
543
- }
544
- let dependsRefString;
545
- let dependsName = tmpA[1].split("@")[0];
546
- const dependsPurl = goModGraphTokenPurl(tmpA[1]);
547
- if (dependsPurl) {
548
- dependsRefString = decodeURIComponent(dependsPurl.toString());
549
- dependsName = `${dependsPurl.namespace ? `${dependsPurl.namespace}/` : ""}${dependsPurl.name}`;
550
- } else {
551
- dependsRefString = `pkg:golang/${tmpA[1]}`;
1399
+ const sourceToken = splitGoModGraphToken(tmpA[0]);
1400
+ const dependsToken = splitGoModGraphToken(tmpA[1]);
1401
+ const source = graphModule(sourceToken.path, sourceToken.version);
1402
+ let depends = graphModule(dependsToken.path, dependsToken.version);
1403
+ // An edge whose target names a version the build did not select is
1404
+ // rewritten to the selected one rather than dropped: the selected
1405
+ // module is the one actually linked, and dropping the edge silently
1406
+ // disconnects it from every dependent that asked for an older
1407
+ // version (#4497). Only the target is rewritten; an edge from a
1408
+ // version that was not selected describes the requirements of a
1409
+ // module the build never used, so it stays filtered out.
1410
+ if (filterByExisting && !existingPkgMap[depends.ref]) {
1411
+ const selectedVersion = selectedVersions[dependsToken.path];
1412
+ if (selectedVersion && selectedVersion !== dependsToken.version) {
1413
+ const rewritten = graphModule(dependsToken.path, selectedVersion);
1414
+ if (existingPkgMap[rewritten.ref]) {
1415
+ depends = rewritten;
1416
+ }
1417
+ }
552
1418
  }
1419
+ const sourceRefString = source.ref;
1420
+ const dependsRefString = depends.ref;
553
1421
  // Since go mod graph over-reports direct dependencies we use the existing list
554
1422
  // from go deps to filter the result
555
1423
  if (
556
- existingPkgMap &&
557
- Object.keys(existingPkgMap).length &&
1424
+ filterByExisting &&
558
1425
  (!existingPkgMap[sourceRefString] ||
559
1426
  !existingPkgMap[dependsRefString])
560
1427
  ) {
561
1428
  continue;
562
1429
  }
563
1430
  // Add the source and depends to the pkgList
564
- if (!addedPkgs[tmpA[0]] && !excludedRefs.includes(sourceRefString)) {
565
- const component = await getGoPkgComponent(
566
- "",
567
- sourceName,
568
- sourcePurl?.version || tmpA[0].split("@")[1],
569
- gosumMap[tmpA[0]],
1431
+ if (
1432
+ !addedPkgs[sourceRefString] &&
1433
+ !excludedRefs.includes(sourceRefString)
1434
+ ) {
1435
+ // go mod graph prints a main module without a version. A main
1436
+ // module is the project itself, or a go.work member, which no
1437
+ // registry holds, so it is never looked up. It still becomes a
1438
+ // component, because the graph's edges hang off it.
1439
+ const isParentSource =
1440
+ !sourceToken.version ||
1441
+ source.name === goModPkgMap?.parentComponent?.name ||
1442
+ sourceRefString === parentComponent?.["bom-ref"];
1443
+ const component = addGoReplacementProperties(
1444
+ await getGoPkgComponent(
1445
+ "",
1446
+ source.name,
1447
+ source.version,
1448
+ gosumMap[source.token],
1449
+ {
1450
+ skipRegistryLookups:
1451
+ isParentSource || !!source.replacement?.local,
1452
+ },
1453
+ ),
1454
+ source.replacement,
570
1455
  );
571
1456
  let confidence = 0.7;
572
1457
  if (goModOptionalDepsMap[component["bom-ref"]]) {
@@ -578,26 +1463,35 @@ export async function parseGoModGraph(
578
1463
  // These are likely false positives
579
1464
  if (
580
1465
  goModFile &&
581
- !Object.keys(existingPkgMap).length &&
1466
+ !filterByExisting &&
582
1467
  goModPkgMap?.parentComponent?.["bom-ref"] !== sourceRefString &&
583
1468
  !component.scope
584
1469
  ) {
585
1470
  continue;
586
1471
  }
587
- // Don't add the parent component to the package list
588
- if (goModPkgMap?.parentComponent?.["bom-ref"] !== sourceRefString) {
1472
+ // Don't add the parent component to the package list: neither
1473
+ // this go.mod's module nor the caller's parent, which in a go.work
1474
+ // workspace is another member that the graph also prints.
1475
+ if (
1476
+ goModPkgMap?.parentComponent?.["bom-ref"] !== sourceRefString &&
1477
+ parentComponent?.["bom-ref"] !== sourceRefString
1478
+ ) {
589
1479
  pkgList.push(
590
1480
  _addGoComponentEvidence(component, goModFile, confidence),
591
1481
  );
592
1482
  }
593
- addedPkgs[tmpA[0]] = true;
1483
+ addedPkgs[sourceRefString] = true;
594
1484
  }
595
- if (!addedPkgs[tmpA[1]]) {
596
- const component = await getGoPkgComponent(
597
- "",
598
- dependsName,
599
- dependsPurl?.version || tmpA[1].split("@")[1],
600
- gosumMap[tmpA[1]],
1485
+ if (!addedPkgs[dependsRefString]) {
1486
+ const component = addGoReplacementProperties(
1487
+ await getGoPkgComponent(
1488
+ "",
1489
+ depends.name,
1490
+ depends.version,
1491
+ gosumMap[depends.token],
1492
+ { skipRegistryLookups: !!depends.replacement?.local },
1493
+ ),
1494
+ depends.replacement,
601
1495
  );
602
1496
  let confidence = 0.7;
603
1497
  if (goModDirectDepsMap[component["bom-ref"]]) {
@@ -627,7 +1521,7 @@ export async function parseGoModGraph(
627
1521
  if (
628
1522
  goModFile &&
629
1523
  goModPkgMap?.parentComponent?.["bom-ref"] !== sourceRefString &&
630
- !Object.keys(existingPkgMap).length &&
1524
+ !filterByExisting &&
631
1525
  !component.scope
632
1526
  ) {
633
1527
  excludedRefs.push(dependsRefString);
@@ -639,7 +1533,7 @@ export async function parseGoModGraph(
639
1533
  pkgList.push(
640
1534
  _addGoComponentEvidence(component, goModFile, confidence),
641
1535
  );
642
- addedPkgs[tmpA[1]] = true;
1536
+ addedPkgs[dependsRefString] = true;
643
1537
  }
644
1538
  if (!depsMap[sourceRefString]) {
645
1539
  depsMap[sourceRefString] = new Set();
@@ -647,11 +1541,18 @@ export async function parseGoModGraph(
647
1541
  if (!depsMap[dependsRefString]) {
648
1542
  depsMap[dependsRefString] = new Set();
649
1543
  }
650
- // Check if the root is really dependent on this component
1544
+ // Check if the root is really dependent on this component. The
1545
+ // by-path check accepts a direct require whose go.mod version
1546
+ // drifted from the selected one: the edge was already rewritten
1547
+ // to the selected version above, and the by-ref map only knows
1548
+ // the stale bom-ref (#4497). It also accepts a directly required
1549
+ // module that a directory replaces, whose component has no
1550
+ // version.
651
1551
  if (
652
1552
  goModPkgMap?.parentComponent?.["bom-ref"] === sourceRefString &&
653
1553
  Object.keys(goModDirectDepsMap).length &&
654
- !goModDirectDepsMap[dependsRefString]
1554
+ !goModDirectDepsMap[dependsRefString] &&
1555
+ !goModDirectDepNames.has(dependsToken.path)
655
1556
  ) {
656
1557
  // ignore
657
1558
  } else if (!excludedRefs.includes(dependsRefString)) {
@@ -722,33 +1623,73 @@ export function isGoModWhyNotNeeded(rawOutput) {
722
1623
  }
723
1624
 
724
1625
  /**
725
- * Parse go sum data
1626
+ * Read the `module@version` to hash map out of go.sum contents, without
1627
+ * building components or touching the network.
1628
+ *
726
1629
  * @param {string} gosumData Content of go.sum
727
- * @returns package list
1630
+ * @returns {Object} Map of `module@version` keys to the `sha256-…` checksum of
1631
+ * the module content, for every version go.sum holds one for
728
1632
  */
729
- export async function parseGosumData(gosumData) {
730
- const pkgList = [];
1633
+ export function parseGosumHashes(gosumData) {
1634
+ const gosumMap = {};
1635
+ for (const entry of decodeGosumData(gosumData)) {
1636
+ if (entry.hash) {
1637
+ gosumMap[`${entry.name}@${entry.version}`] = entry.hash;
1638
+ }
1639
+ }
1640
+ return gosumMap;
1641
+ }
1642
+
1643
+ /**
1644
+ * Decode a go.sum file into one entry per module version it names.
1645
+ *
1646
+ * go.sum holds up to two lines per module version: `path version h1:…`, the
1647
+ * checksum of the module's content, and `path version/go.mod h1:…`, the
1648
+ * checksum of its go.mod file alone. The content checksum is the hash of the
1649
+ * component. A version with only a go.mod line was needed to resolve the
1650
+ * module graph but never downloaded, so it has no content hash.
1651
+ *
1652
+ * @param {string} gosumData Content of go.sum
1653
+ * @returns {Object[]} Entries with `name`, `version` and `hash`, in the order
1654
+ * go.sum first names them
1655
+ */
1656
+ function decodeGosumData(gosumData) {
1657
+ const entries = new Map();
731
1658
  if (!gosumData) {
732
- return pkgList;
1659
+ return [];
733
1660
  }
734
- // Decoding every line first lets the whole module set be fetched in one
735
- // batched round before any component is built. One decode function feeds both,
736
- // so the batch cannot ask for a module the loop does not resolve.
737
- const entries = [];
738
1661
  for (const l of gosumData.split("\n")) {
739
- const m = l.replaceAll("\r", "");
740
- // look for lines containing go.mod
741
- if (m.indexOf("go.mod") > -1) {
742
- const tmpA = m.split(" ");
743
- entries.push({
744
- group: "",
745
- name: tmpA[0],
746
- version: tmpA[1].replace("/go.mod", ""),
747
- hash: tmpA[tmpA.length - 1].replace("h1:", "sha256-"),
748
- });
1662
+ const fields = l.replaceAll("\r", "").trim().split(/\s+/);
1663
+ if (fields.length !== 3 || !fields[2].startsWith("h1:")) {
1664
+ continue;
1665
+ }
1666
+ const isGoModLine = fields[1].endsWith("/go.mod");
1667
+ const name = fields[0];
1668
+ const version = isGoModLine
1669
+ ? fields[1].slice(0, -"/go.mod".length)
1670
+ : fields[1];
1671
+ const key = `${name}@${version}`;
1672
+ if (!entries.has(key)) {
1673
+ entries.set(key, { group: "", name, version, hash: undefined });
1674
+ }
1675
+ if (!isGoModLine) {
1676
+ entries.get(key).hash = goSumIntegrity(fields[2]);
749
1677
  }
750
1678
  }
751
- await prefetchGoPkgMetadata(entries);
1679
+ return [...entries.values()];
1680
+ }
1681
+
1682
+ /**
1683
+ * Parse go sum data
1684
+ * @param {string} gosumData Content of go.sum
1685
+ * @returns package list
1686
+ */
1687
+ export async function parseGosumData(gosumData) {
1688
+ const pkgList = [];
1689
+ const entries = decodeGosumData(gosumData);
1690
+ // A module whose licence notice is read locally is not asked for its
1691
+ // licence; getGoPkgComponent reads the same notice for each entry below.
1692
+ await prefetchGoPkgMetadata(goModulesToPrefetch(entries));
752
1693
  for (const entry of entries) {
753
1694
  pkgList.push(
754
1695
  await getGoPkgComponent("", entry.name, entry.version, entry.hash),
@@ -775,13 +1716,17 @@ export async function parseGopkgData(gopkgData) {
775
1716
  for (const l of pkgs) {
776
1717
  let key = null;
777
1718
  let value = null;
778
- if (l.indexOf("[[projects]]") > -1) {
1719
+ // Every table header ends the project read so far, and only a
1720
+ // [[projects]] table starts another. The keys of [solve-meta] belong to
1721
+ // no project.
1722
+ if (l.trim().startsWith("[")) {
779
1723
  if (pkg) {
780
- pkgList.push(pkg);
1724
+ pkgList.push(await finalizeGopkgProject(pkg));
781
1725
  }
782
- pkg = {};
1726
+ pkg = l.indexOf("[[projects]]") > -1 ? {} : null;
1727
+ continue;
783
1728
  }
784
- if (l.indexOf("=") > -1) {
1729
+ if (pkg && l.indexOf("=") > -1) {
785
1730
  const tmpA = l.split("=");
786
1731
  key = tmpA[0].trim();
787
1732
  value = tmpA[1].trim().replace(/"/g, "");
@@ -794,12 +1739,6 @@ export async function parseGopkgData(gopkgData) {
794
1739
  case "name":
795
1740
  pkg.group = "";
796
1741
  pkg.name = value;
797
- if (shouldFetchLicense()) {
798
- pkg.license = await getGoPkgLicense({
799
- group: pkg.group,
800
- name: pkg.name,
801
- });
802
- }
803
1742
  break;
804
1743
  case "version":
805
1744
  pkg.version = value;
@@ -811,13 +1750,41 @@ export async function parseGopkgData(gopkgData) {
811
1750
  }
812
1751
  }
813
1752
  }
1753
+ // The last project has no header after it.
1754
+ if (pkg) {
1755
+ pkgList.push(await finalizeGopkgProject(pkg));
1756
+ }
814
1757
  return pkgList;
815
1758
  }
816
1759
 
1760
+ /**
1761
+ * Attach the licence of one Gopkg project once its name and version are both
1762
+ * known, so the pkg.go.dev URL names the pinned release.
1763
+ *
1764
+ * @param {Object} pkg The project entry read so far
1765
+ * @returns {Promise<Object>} The same entry, with a licence when one was found
1766
+ */
1767
+ async function finalizeGopkgProject(pkg) {
1768
+ if (shouldFetchLicense() && pkg.name) {
1769
+ pkg.license = await getGoPkgLicense({
1770
+ group: pkg.group,
1771
+ name: pkg.name,
1772
+ version: pkg.version,
1773
+ });
1774
+ }
1775
+ return pkg;
1776
+ }
1777
+
817
1778
  /**
818
1779
  * Parses the output of `go version -m` (build info) and returns a list of Go
819
1780
  * package components for each "dep" line, including name, version, and integrity hash.
820
1781
  *
1782
+ * A dependency that a replace directive swapped out is printed as its `dep`
1783
+ * line followed by a `=>` line naming the replacement (runtime/debug
1784
+ * BuildInfo.String). The replacement is what the binary holds, so it is
1785
+ * reported as {@link goReplacement} describes, with the replacement's
1786
+ * checksum.
1787
+ *
821
1788
  * @param {string} buildInfoData Raw string output from `go version -m`
822
1789
  * @returns {Promise<Object[]>} List of Go package component objects
823
1790
  */
@@ -827,29 +1794,48 @@ export async function parseGoVersionData(buildInfoData) {
827
1794
  return pkgList;
828
1795
  }
829
1796
  const entries = [];
1797
+ let lastDep;
830
1798
  for (const line of buildInfoData.split("\n")) {
831
- const l = line.trim().replace(/\t/g, " ");
832
- if (!l.startsWith("dep")) {
1799
+ const fields = line.trim().split(/\s+/);
1800
+ if (fields[0] === "=>" && lastDep && fields.length >= 2) {
1801
+ lastDep.replacement = goReplacement(
1802
+ lastDep.name,
1803
+ lastDep.version,
1804
+ fields[1],
1805
+ fields[2],
1806
+ );
1807
+ lastDep.name = lastDep.replacement.name;
1808
+ lastDep.version = lastDep.replacement.version;
1809
+ lastDep.skipRegistryLookups = lastDep.replacement.local;
1810
+ lastDep.hash = goSumIntegrity(fields[3]) || "";
1811
+ lastDep = undefined;
833
1812
  continue;
834
1813
  }
835
- const tmpA = l.split(" ");
836
- if (!tmpA || tmpA.length < 3) {
1814
+ lastDep = undefined;
1815
+ if (fields[0] !== "dep" || fields.length < 3) {
837
1816
  continue;
838
1817
  }
839
- entries.push({
1818
+ lastDep = {
840
1819
  group: "",
841
- name: tmpA[1].trim(),
842
- version: tmpA[2].trim(),
843
- hash:
844
- tmpA.length === 4
845
- ? tmpA[tmpA.length - 1].replace("h1:", "sha256-")
846
- : "",
847
- });
1820
+ name: fields[1],
1821
+ version: fields[2],
1822
+ hash: goSumIntegrity(fields[3]) || "",
1823
+ };
1824
+ entries.push(lastDep);
848
1825
  }
849
- await prefetchGoPkgMetadata(entries);
1826
+ await prefetchGoPkgMetadata(goModulesToPrefetch(entries));
850
1827
  for (const entry of entries) {
851
1828
  pkgList.push(
852
- await getGoPkgComponent("", entry.name, entry.version, entry.hash),
1829
+ addGoReplacementProperties(
1830
+ await getGoPkgComponent(
1831
+ "",
1832
+ entry.name,
1833
+ entry.version,
1834
+ entry.hash,
1835
+ entry,
1836
+ ),
1837
+ entry.replacement,
1838
+ ),
853
1839
  );
854
1840
  }
855
1841
  return pkgList;