@cdxgen/cdxgen 13.5.0 → 13.7.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 (380) hide show
  1. package/README.md +17 -10
  2. package/bin/audit.js +7 -4
  3. package/bin/cdxgen.js +260 -37
  4. package/bin/convert.js +146 -22
  5. package/bin/evinse.js +28 -3
  6. package/bin/repl.js +47 -3
  7. package/bin/sign.js +211 -34
  8. package/bin/tracebom.js +18 -6
  9. package/bin/validate.js +24 -3
  10. package/bin/verify.js +113 -5
  11. package/data/behavior-taxonomy.schema.json +1491 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
  13. package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
  14. package/data/license-aliases.json +24 -2
  15. package/data/pqc-migration.json +239 -0
  16. package/data/remediations.json +176 -0
  17. package/data/rules/ai-bom.yaml +1 -1
  18. package/data/rules/brotli-decoder.yaml +107 -0
  19. package/data/rules/cbom-security.yaml +4 -3
  20. package/data/rules/package-integrity.yaml +28 -0
  21. package/data/rules/vscode-extensions.yaml +75 -0
  22. package/data/spdx-license-list.json +121 -7
  23. package/data/spdx-licenses.json +69 -0
  24. package/data/spdx.schema.json +845 -2
  25. package/lib/audit/index.js +25 -3
  26. package/lib/cli/bomAssembly.js +172 -4
  27. package/lib/cli/cliOptions.js +121 -17
  28. package/lib/cli/index.js +720 -902
  29. package/lib/cli/jsBom.js +156 -64
  30. package/lib/cli/jvmBom.js +602 -86
  31. package/lib/cli/managedBom.js +161 -19
  32. package/lib/cli/nativeBom.js +52 -5
  33. package/lib/core/boundedFile.js +344 -0
  34. package/lib/core/brotliProbe.js +182 -0
  35. package/lib/core/env.js +34 -0
  36. package/lib/core/fs.js +138 -3
  37. package/lib/core/globPatterns.js +218 -0
  38. package/lib/core/httpClient.js +98 -1
  39. package/lib/core/lockPaths.js +38 -0
  40. package/lib/core/paths.js +24 -1
  41. package/lib/core/processTree.js +100 -8
  42. package/lib/core/projectConfig.js +332 -25
  43. package/lib/core/toolRegistry.js +184 -0
  44. package/lib/ecosystems/cppEvidence.js +263 -106
  45. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  46. package/lib/ecosystems/denoutils.js +2 -2
  47. package/lib/ecosystems/ecosystems.js +25 -10
  48. package/lib/ecosystems/gemHostProbe.js +301 -0
  49. package/lib/ecosystems/golem.js +3 -8
  50. package/lib/ecosystems/gradleutils.js +1909 -229
  51. package/lib/ecosystems/jsEvidence.js +90 -38
  52. package/lib/ecosystems/kosi.js +3 -5
  53. package/lib/ecosystems/npmVersionResolution.js +376 -0
  54. package/lib/ecosystems/parsers-bazel.js +107 -6
  55. package/lib/ecosystems/parsers-js.js +505 -22
  56. package/lib/ecosystems/parsers-misc.js +209 -20
  57. package/lib/ecosystems/parsers-python.js +178 -30
  58. package/lib/ecosystems/parsers-rust.js +117 -15
  59. package/lib/ecosystems/pythonutils.js +128 -2
  60. package/lib/ecosystems/remote/huggingface.js +22 -0
  61. package/lib/ecosystems/rubyutils.js +538 -31
  62. package/lib/ecosystems/rusi.js +2 -4
  63. package/lib/ecosystems/scalacliutils.js +2 -0
  64. package/lib/ecosystems/scalasem.js +798 -152
  65. package/lib/ecosystems/vendoredCode.js +237 -3
  66. package/lib/ecosystems/venvProbe.js +360 -0
  67. package/lib/evinser/evinser.js +536 -71
  68. package/lib/evinser/scalasem.js +5 -1
  69. package/lib/helpers/agenticToolutils.js +525 -94
  70. package/lib/helpers/bomSigner.js +43 -9
  71. package/lib/helpers/jssSigner.js +1276 -0
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentBlueprint.js +193 -0
  74. package/lib/inventory/agentToolNames.js +145 -0
  75. package/lib/inventory/aiCollector.js +153 -7
  76. package/lib/inventory/aiInventory.js +44 -8
  77. package/lib/inventory/aiProviders.js +324 -0
  78. package/lib/inventory/atomUtils.js +77 -0
  79. package/lib/inventory/bomUtils.js +8 -1
  80. package/lib/inventory/brotliDecoder.js +142 -0
  81. package/lib/inventory/buildToolRateLimit.js +1 -1
  82. package/lib/inventory/cbomutils.js +56 -7
  83. package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
  84. package/lib/inventory/citations.js +31 -2
  85. package/lib/inventory/communityAiConfigParser.js +978 -80
  86. package/lib/inventory/cyclonedx20.js +152 -0
  87. package/lib/inventory/cyclonedx20Common.js +286 -0
  88. package/lib/inventory/cyclonedx20Derived.js +159 -0
  89. package/lib/inventory/cyclonedx20Parties.js +552 -0
  90. package/lib/inventory/cyclonedx20Read.js +768 -0
  91. package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
  92. package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
  93. package/lib/inventory/cyclonedx20ReadContext.js +380 -0
  94. package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
  95. package/lib/inventory/cyclonedx20ReadSections.js +599 -0
  96. package/lib/inventory/cyclonedx20Render.js +766 -0
  97. package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
  98. package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
  99. package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
  100. package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
  101. package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
  102. package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
  103. package/lib/inventory/cyclonedx20RenderSections.js +499 -0
  104. package/lib/inventory/cyclonedx20Tables.js +594 -0
  105. package/lib/inventory/deps.js +141 -13
  106. package/lib/inventory/dosai.js +375 -6
  107. package/lib/inventory/dosaiParsers.js +3 -6
  108. package/lib/inventory/dotnetRuntime.js +367 -0
  109. package/lib/inventory/ecosystemFormulationParsers.js +979 -0
  110. package/lib/inventory/envcontext.js +436 -48
  111. package/lib/inventory/evidenceUtils.js +13 -1
  112. package/lib/inventory/fetchBatch.js +1 -1
  113. package/lib/inventory/formulationParsers.js +124 -14
  114. package/lib/inventory/gtfobins.js +13 -0
  115. package/lib/inventory/hbom.js +45 -6
  116. package/lib/inventory/hbomLoader.js +166 -0
  117. package/lib/inventory/hostRuntimeProbe.js +686 -0
  118. package/lib/inventory/jvmLocalRepos.js +9 -2
  119. package/lib/inventory/jvmToolEnv.js +29 -0
  120. package/lib/inventory/licenseEnhancer.js +12 -0
  121. package/lib/inventory/mavenCentralMirror.js +534 -0
  122. package/lib/inventory/mcpConfigParser.js +5 -0
  123. package/lib/inventory/nodeBuildInfo.js +218 -0
  124. package/lib/inventory/nodeHostProbe.js +202 -0
  125. package/lib/inventory/occurrenceKey.js +90 -0
  126. package/lib/inventory/osPackageResolver.js +182 -0
  127. package/lib/inventory/phpBuildInfo.js +10 -59
  128. package/lib/inventory/phpHostProbe.js +32 -488
  129. package/lib/inventory/pqcMigration.js +426 -0
  130. package/lib/inventory/rubyBuildInfo.js +972 -0
  131. package/lib/inventory/rubyHostProbe.js +218 -0
  132. package/lib/inventory/rustFormulationParser.js +21 -10
  133. package/lib/inventory/scalaCoords.js +4 -3
  134. package/lib/inventory/scalaDedupe.js +440 -0
  135. package/lib/inventory/source.js +114 -8
  136. package/lib/managers/binary.js +337 -3
  137. package/lib/managers/docker.js +79 -0
  138. package/lib/parsers/huggingfaceManifest.js +9 -1
  139. package/lib/parsers/huggingfaceUtils.js +18 -0
  140. package/lib/parsers/kiroPermissions.js +166 -0
  141. package/lib/parsers/largeJson.js +28 -11
  142. package/lib/parsers/vscodeExtensionId.js +183 -0
  143. package/lib/server/server.js +48 -35
  144. package/lib/stages/postgen/postgen.js +41 -1
  145. package/lib/stages/postgen/ruleEngine.js +13 -5
  146. package/lib/stages/postgen/specVersionCompat.js +76 -0
  147. package/lib/stages/pregen/envAudit.js +1 -0
  148. package/lib/stages/pregen/pregen.js +114 -57
  149. package/lib/validator/bomValidator.js +14 -2
  150. package/lib/validator/complianceEngine.js +30 -3
  151. package/lib/validator/complianceRules.js +1416 -4
  152. package/lib/validator/generated/validate-1.6.mjs +1 -1
  153. package/lib/validator/generated/validate-1.7.mjs +1 -1
  154. package/lib/validator/generated/validate-2.0.mjs +57554 -13851
  155. package/lib/validator/index.js +49 -8
  156. package/lib/validator/reporters/json.js +6 -0
  157. package/package.json +11 -10
  158. package/types/bin/repl.d.ts.map +1 -1
  159. package/types/lib/audit/index.d.ts +6 -2
  160. package/types/lib/audit/index.d.ts.map +1 -1
  161. package/types/lib/cli/bomAssembly.d.ts +29 -1
  162. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  163. package/types/lib/cli/cliOptions.d.ts +6 -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 +3 -18
  167. package/types/lib/cli/jsBom.d.ts.map +1 -1
  168. package/types/lib/cli/jvmBom.d.ts +15 -0
  169. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  170. package/types/lib/cli/managedBom.d.ts.map +1 -1
  171. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  172. package/types/lib/core/boundedFile.d.ts +100 -0
  173. package/types/lib/core/boundedFile.d.ts.map +1 -0
  174. package/types/lib/core/brotliProbe.d.ts +37 -0
  175. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  176. package/types/lib/core/env.d.ts +14 -0
  177. package/types/lib/core/env.d.ts.map +1 -1
  178. package/types/lib/core/fs.d.ts +4 -0
  179. package/types/lib/core/fs.d.ts.map +1 -1
  180. package/types/lib/core/globPatterns.d.ts +70 -0
  181. package/types/lib/core/globPatterns.d.ts.map +1 -0
  182. package/types/lib/core/httpClient.d.ts +24 -0
  183. package/types/lib/core/httpClient.d.ts.map +1 -1
  184. package/types/lib/core/lockPaths.d.ts +19 -0
  185. package/types/lib/core/lockPaths.d.ts.map +1 -0
  186. package/types/lib/core/paths.d.ts +9 -0
  187. package/types/lib/core/paths.d.ts.map +1 -1
  188. package/types/lib/core/processTree.d.ts +17 -4
  189. package/types/lib/core/processTree.d.ts.map +1 -1
  190. package/types/lib/core/projectConfig.d.ts +47 -8
  191. package/types/lib/core/projectConfig.d.ts.map +1 -1
  192. package/types/lib/core/toolRegistry.d.ts +36 -0
  193. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  194. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  195. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  196. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  197. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  198. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  199. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  200. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  201. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  202. package/types/lib/ecosystems/gradleutils.d.ts +294 -20
  203. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
  205. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  206. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  207. package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
  208. package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
  209. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  210. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  211. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  212. package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
  213. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  214. package/types/lib/ecosystems/parsers-python.d.ts +10 -0
  215. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  216. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  217. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  218. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  219. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  220. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  221. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  222. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  223. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  224. package/types/lib/ecosystems/scalasem.d.ts +58 -16
  225. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  226. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  227. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  228. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  229. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  230. package/types/lib/evinser/evinser.d.ts +120 -13
  231. package/types/lib/evinser/evinser.d.ts.map +1 -1
  232. package/types/lib/evinser/scalasem.d.ts.map +1 -1
  233. package/types/lib/helpers/agenticToolutils.d.ts +53 -19
  234. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
  235. package/types/lib/helpers/bomSigner.d.ts +20 -3
  236. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  237. package/types/lib/helpers/jssSigner.d.ts +288 -0
  238. package/types/lib/helpers/jssSigner.d.ts.map +1 -0
  239. package/types/lib/helpers/vsixutils.d.ts +303 -19
  240. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  241. package/types/lib/inventory/agentBlueprint.d.ts +70 -0
  242. package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
  243. package/types/lib/inventory/agentToolNames.d.ts +51 -0
  244. package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
  245. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  246. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  247. package/types/lib/inventory/aiProviders.d.ts +85 -0
  248. package/types/lib/inventory/aiProviders.d.ts.map +1 -0
  249. package/types/lib/inventory/atomUtils.d.ts +19 -0
  250. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  251. package/types/lib/inventory/bomUtils.d.ts.map +1 -1
  252. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  253. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  254. package/types/lib/inventory/cbomutils.d.ts +17 -0
  255. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  256. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
  257. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
  258. package/types/lib/inventory/citations.d.ts +5 -0
  259. package/types/lib/inventory/citations.d.ts.map +1 -1
  260. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  261. package/types/lib/inventory/cyclonedx20.d.ts +99 -0
  262. package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
  263. package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
  264. package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
  265. package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
  266. package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
  267. package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
  268. package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
  269. package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
  270. package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
  271. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
  272. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
  273. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
  274. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
  275. package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
  276. package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
  277. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
  278. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
  279. package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
  280. package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
  281. package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
  282. package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
  283. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
  284. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
  285. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
  286. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
  287. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
  288. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
  289. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
  290. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
  291. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
  292. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
  293. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
  294. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
  295. package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
  296. package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
  297. package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
  298. package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
  299. package/types/lib/inventory/deps.d.ts +35 -2
  300. package/types/lib/inventory/deps.d.ts.map +1 -1
  301. package/types/lib/inventory/dosai.d.ts +36 -1
  302. package/types/lib/inventory/dosai.d.ts.map +1 -1
  303. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  304. package/types/lib/inventory/dotnetRuntime.d.ts +91 -0
  305. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  306. package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
  307. package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
  308. package/types/lib/inventory/envcontext.d.ts +74 -12
  309. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  310. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  311. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  312. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  313. package/types/lib/inventory/hbom.d.ts +11 -0
  314. package/types/lib/inventory/hbom.d.ts.map +1 -1
  315. package/types/lib/inventory/hbomLoader.d.ts +54 -0
  316. package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
  317. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  318. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  319. package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
  320. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
  321. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  322. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  323. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  324. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  325. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  326. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  327. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  328. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  329. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  330. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  331. package/types/lib/inventory/occurrenceKey.d.ts +52 -0
  332. package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
  333. package/types/lib/inventory/osPackageResolver.d.ts +38 -0
  334. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  335. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
  336. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
  337. package/types/lib/inventory/pqcMigration.d.ts +84 -0
  338. package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
  339. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  340. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  341. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  342. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  343. package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
  344. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  345. package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
  346. package/types/lib/inventory/scalaDedupe.d.ts +43 -0
  347. package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
  348. package/types/lib/inventory/source.d.ts +25 -0
  349. package/types/lib/inventory/source.d.ts.map +1 -1
  350. package/types/lib/managers/binary.d.ts.map +1 -1
  351. package/types/lib/managers/docker.d.ts +12 -0
  352. package/types/lib/managers/docker.d.ts.map +1 -1
  353. package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
  354. package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
  355. package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
  356. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  357. package/types/lib/parsers/kiroPermissions.d.ts +62 -0
  358. package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
  359. package/types/lib/parsers/largeJson.d.ts +4 -3
  360. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  361. package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
  362. package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
  363. package/types/lib/server/server.d.ts.map +1 -1
  364. package/types/lib/stages/postgen/postgen.d.ts +7 -0
  365. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  366. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  367. package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
  368. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  369. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  370. package/types/lib/stages/pregen/pregen.d.ts +12 -8
  371. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  372. package/types/lib/validator/bomValidator.d.ts +5 -1
  373. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  374. package/types/lib/validator/complianceEngine.d.ts +4 -4
  375. package/types/lib/validator/complianceEngine.d.ts.map +1 -1
  376. package/types/lib/validator/complianceRules.d.ts +14 -2
  377. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  378. package/types/lib/validator/index.d.ts +10 -0
  379. package/types/lib/validator/index.d.ts.map +1 -1
  380. package/types/lib/validator/reporters/json.d.ts.map +1 -1
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import { builtinModules } from "node:module";
2
3
  import path, { basename, join, resolve } from "node:path";
3
4
  import process from "node:process";
4
5
 
@@ -16,6 +17,7 @@ import {
16
17
  safeRmSync,
17
18
  safeWriteSync,
18
19
  } from "../core/fs.js";
20
+ import { thoughtLog } from "../core/logger.js";
19
21
  import { isProjectToolingAllowed } from "../core/projectTooling.js";
20
22
  import { hasCppSources } from "../ecosystems/cppEvidence.js";
21
23
  import {
@@ -41,6 +43,7 @@ import {
41
43
  kosiWorkspaceComponent,
42
44
  mergeKosiEvidence,
43
45
  } from "../ecosystems/kosi.js";
46
+ import { keepResolvedNpmVersions } from "../ecosystems/npmVersionResolution.js";
44
47
  import {
45
48
  analyzeRusiProject,
46
49
  collectRusiEvidence,
@@ -54,6 +57,7 @@ import {
54
57
  isScalasemLanguage,
55
58
  } from "../ecosystems/scalasem.js";
56
59
  import {
60
+ atomBomFileArgs,
57
61
  atomBuildToolArgs,
58
62
  atomCompileCommandsArgs,
59
63
  buildAtomCommandEnv,
@@ -69,6 +73,10 @@ import {
69
73
  findCryptoAlgos,
70
74
  } from "../inventory/cbomutils.js";
71
75
  import { cmakeBuildDirCandidates } from "../inventory/cmakeBuildDirs.js";
76
+ import {
77
+ formatReadLossNotice,
78
+ loadBomForAnalysis,
79
+ } from "../inventory/cyclonedx20.js";
72
80
  import { collectMvnDependencies } from "../inventory/deps.js";
73
81
  import { mergeServices } from "../inventory/depsUtils.js";
74
82
  import {
@@ -84,6 +92,7 @@ import {
84
92
  dosaiOmittedSections,
85
93
  dosaiReportFileOmittedSections,
86
94
  dosaiSectionsCdxgenReads,
95
+ dosaiSlicesFileIsFresh,
87
96
  isDosaiDotnetLanguage,
88
97
  normalizeDosaiServiceMap,
89
98
  persistDosaiSemanticsReport,
@@ -91,6 +100,11 @@ import {
91
100
  readDosaiMethodsReport,
92
101
  } from "../inventory/dosai.js";
93
102
  import { parseOccurrenceEvidenceLocation } from "../inventory/evidenceUtils.js";
103
+ import {
104
+ dropSubsumedOccurrenceKeys,
105
+ occurrenceLocationKey,
106
+ } from "../inventory/occurrenceKey.js";
107
+ import { splitPurlLoose } from "../inventory/purl.js";
94
108
  import { executeOsQuery } from "../managers/binary.js";
95
109
  import { MAX_JSON_TEXT_BYTES, readJsonFile } from "../parsers/largeJson.js";
96
110
  import { postProcess } from "../stages/postgen/postgen.js";
@@ -98,7 +112,8 @@ import { createOrLoad } from "./db.js";
98
112
  import { findPurlLocations } from "./scalasem.js";
99
113
  import { createSemanticsSlices } from "./swiftsem.js";
100
114
 
101
- const typePurlsCache = {};
115
+ // keyed by type names, which can be "constructor", "toString", ...: no prototype
116
+ const typePurlsCache = Object.create(null);
102
117
 
103
118
  function appendUniqueProperty(properties, name, value) {
104
119
  if (value === undefined || value === null || value === "") {
@@ -261,6 +276,7 @@ export function buildEvinseOptions(options, args, input) {
261
276
  force: false,
262
277
  withReachables: options.deep,
263
278
  usagesSlicesFile: options.usagesSlicesFile,
279
+ callsitesSlicesFile: options.callsitesSlicesFile,
264
280
  dataFlowSlicesFile: options.dataFlowSlicesFile,
265
281
  reachablesSlicesFile: options.reachablesSlicesFile,
266
282
  semanticsSlicesFile: options.semanticsSlicesFile,
@@ -293,6 +309,45 @@ export function buildEvinseOptions(options, args, input) {
293
309
  };
294
310
  }
295
311
 
312
+ /**
313
+ * Read the evinse input BOM as cdxgen's 1.x-shaped internal model. A
314
+ * CycloneDX 2.0 document is converted with loadBomForAnalysis; a 1.x document
315
+ * (or an internal model) is returned as is. When `options.inputBomJson` holds
316
+ * a model converted earlier, a copy of it is returned instead of reading
317
+ * `options.input` again, so prepareDB, analyzeProject and createEvinseFile
318
+ * each get their own object, as they did when each parsed the file.
319
+ *
320
+ * @param {Object} options Evinse options with `input` and optional
321
+ * `inputBomJson`
322
+ * @returns {{bomJson: Object, losses: Array<{pointer: string, reason: string}>}}
323
+ * The internal model and the losses of the 2.0 read (none when
324
+ * `inputBomJson` was given)
325
+ */
326
+ export function loadEvinseInputBom(options) {
327
+ if (options?.inputBomJson && typeof options.inputBomJson === "object") {
328
+ return { bomJson: structuredClone(options.inputBomJson), losses: [] };
329
+ }
330
+ const parsedBom = JSON.parse(fs.readFileSync(options.input, "utf8"));
331
+ const { internalBom, losses } = loadBomForAnalysis(parsedBom);
332
+ return { bomJson: internalBom, losses };
333
+ }
334
+
335
+ /**
336
+ * Print the one-line notice for the losses of reading a 2.0 evinse input,
337
+ * with the details in thoughtLog.
338
+ *
339
+ * @param {Array<{pointer: string, reason: string}>} losses Reader losses
340
+ */
341
+ export function reportEvinseInputLosses(losses) {
342
+ if (!losses?.length) {
343
+ return;
344
+ }
345
+ console.log(formatReadLossNotice(losses));
346
+ for (const loss of losses) {
347
+ thoughtLog(`evinse 2.0 loss at ${loss.pointer}: ${loss.reason}`);
348
+ }
349
+ }
350
+
296
351
  /**
297
352
  * Function to create the db for the libraries referred in the sbom.
298
353
  *
@@ -312,7 +367,7 @@ export async function prepareDB(options) {
312
367
  }
313
368
  return;
314
369
  }
315
- const bomJson = JSON.parse(fs.readFileSync(bomJsonFile, "utf8"));
370
+ const { bomJson } = loadEvinseInputBom(options);
316
371
  if (bomJson.specVersion < 1.5) {
317
372
  console.log(
318
373
  "Evinse requires the input SBOM in CycloneDX 1.5 format or above. You can generate one by invoking cdxgen without any --spec-version argument.",
@@ -734,6 +789,7 @@ export async function createSlice(
734
789
  args.push("--slice-depth");
735
790
  args.push(readEnvironmentVariable("ATOM_SLICE_DEPTH"));
736
791
  }
792
+ args.push(...atomBomFileArgs(sliceType, options));
737
793
  args.push(...atomCompileCommandsArgs(filePath, language, options));
738
794
  args.push(...buildToolArgs);
739
795
  args.push(resolve(filePath));
@@ -839,15 +895,18 @@ export function purlToLanguage(purl, filePath) {
839
895
  * Walks the supplied components reading `internal:ImportedModules` (or
840
896
  * `internal:Namespaces` for php/ruby, plus the `internal:ImportedSymbols` the
841
897
  * C/C++ collector records) and `evidence.occurrences` properties to construct
842
- * lookup maps used during slice analysis.
898
+ * lookup maps used during slice analysis. An occurrence that carries `offset`
899
+ * or `symbol` seeds those too, so a re-run over a previous evinse output keys
900
+ * and annotates the same use sites the fresh slice adds.
843
901
  *
844
902
  * @param {Object[]} components CycloneDX components from the input SBOM
845
903
  * @param {string} language Application language used to select the import property name
846
- * @returns {{ purlLocationMap: Object<string, Set>, purlImportsMap: Object<string, string[]> }} Seeded purl location and import maps
904
+ * @returns {{ purlLocationMap: Object<string, Set>, purlImportsMap: Object<string, string[]>, purlSymbolMap: Object<string, Object<string, string>> }} Seeded purl location, import and symbol maps
847
905
  */
848
906
  export function initFromSbom(components, language) {
849
907
  const purlLocationMap = {};
850
908
  const purlImportsMap = {};
909
+ const purlSymbolMap = {};
851
910
  for (const comp of components) {
852
911
  if (!comp?.evidence) {
853
912
  continue;
@@ -880,17 +939,30 @@ export function initFromSbom(components, language) {
880
939
  if (comp.evidence.occurrences) {
881
940
  // Keep the `location#line` shape so a re-run over a cdxgen BOM does not
882
941
  // degrade seeded occurrences to file-only entries and duplicate the
883
- // richer locations the fresh slice merge adds to the same set.
942
+ // richer locations the fresh slice merge adds to the same set. The key
943
+ // function also writes the location with `/`, so a Windows BOM's
944
+ // backslash spelling and a slice's forward-slash spelling of one place
945
+ // are one occurrence.
884
946
  purlLocationMap[comp.purl] = new Set(
885
- comp.evidence.occurrences.map((v) =>
886
- v.line ? `${v.location}#${v.line}` : v.location,
887
- ),
947
+ comp.evidence.occurrences
948
+ .map((v) => occurrenceLocationKey(v.location, v.line, v.offset))
949
+ .filter(Boolean),
888
950
  );
951
+ // The first symbol a key carries wins, so a line two records without a
952
+ // column share keeps one symbol across a re-run over this output.
953
+ const symbols = (purlSymbolMap[comp.purl] ??= {});
954
+ for (const v of comp.evidence.occurrences) {
955
+ const key = occurrenceLocationKey(v.location, v.line, v.offset);
956
+ if (v.symbol && key && symbols[key] === undefined) {
957
+ symbols[key] = v.symbol;
958
+ }
959
+ }
889
960
  }
890
961
  }
891
962
  return {
892
963
  purlLocationMap,
893
964
  purlImportsMap,
965
+ purlSymbolMap,
894
966
  };
895
967
  }
896
968
 
@@ -1244,7 +1316,7 @@ export async function analyzeProject(dbObjMap, options) {
1244
1316
  let retMap = {};
1245
1317
  let userDefinedTypesMap = {};
1246
1318
  const bomFile = options.input;
1247
- const bomJson = JSON.parse(fs.readFileSync(bomFile, "utf8"));
1319
+ const { bomJson } = loadEvinseInputBom(options);
1248
1320
  const components = bomJson.components || [];
1249
1321
  let cryptoComponents = [];
1250
1322
  let aiComponents = [];
@@ -1252,7 +1324,22 @@ export async function analyzeProject(dbObjMap, options) {
1252
1324
  let openapiSpecFile;
1253
1325
  // Load any existing purl-location information from the sbom.
1254
1326
  // For eg: cdxgen populates this information for javascript projects
1255
- let { purlLocationMap, purlImportsMap } = initFromSbom(components, language);
1327
+ let { purlLocationMap, purlImportsMap, purlSymbolMap } = initFromSbom(
1328
+ components,
1329
+ language,
1330
+ );
1331
+ // purls whose input occurrences all belong to another installed version
1332
+ let clearedOccurrencePurls = new Set();
1333
+ // The places the input BOM's own import evidence gives each npm version,
1334
+ // before any slice adds to them: they say which version a file uses.
1335
+ const seededJsLocationMap = JS_LANGUAGES.includes(language)
1336
+ ? Object.fromEntries(
1337
+ Object.entries(purlLocationMap).map(([purl, keys]) => [
1338
+ purl,
1339
+ new Set(keys),
1340
+ ]),
1341
+ )
1342
+ : {};
1256
1343
 
1257
1344
  if (isGolemGoLanguage(language)) {
1258
1345
  const golemReport = analyzeGolemProject(dirPath, options);
@@ -1271,6 +1358,7 @@ export async function analyzeProject(dbObjMap, options) {
1271
1358
  cryptoGeneratePurls: golemEvidence.cryptoGeneratePurls,
1272
1359
  servicesMap,
1273
1360
  userDefinedTypesMap,
1361
+ purlSymbolMap,
1274
1362
  };
1275
1363
  }
1276
1364
  return {
@@ -1278,6 +1366,7 @@ export async function analyzeProject(dbObjMap, options) {
1278
1366
  servicesMap,
1279
1367
  dataFlowFrames,
1280
1368
  userDefinedTypesMap,
1369
+ purlSymbolMap,
1281
1370
  };
1282
1371
  }
1283
1372
 
@@ -1297,6 +1386,7 @@ export async function analyzeProject(dbObjMap, options) {
1297
1386
  cryptoGeneratePurls: rusiEvidence.cryptoGeneratePurls,
1298
1387
  servicesMap,
1299
1388
  userDefinedTypesMap,
1389
+ purlSymbolMap,
1300
1390
  };
1301
1391
  }
1302
1392
  return {
@@ -1304,6 +1394,7 @@ export async function analyzeProject(dbObjMap, options) {
1304
1394
  servicesMap,
1305
1395
  dataFlowFrames,
1306
1396
  userDefinedTypesMap,
1397
+ purlSymbolMap,
1307
1398
  };
1308
1399
  }
1309
1400
 
@@ -1375,6 +1466,7 @@ export async function analyzeProject(dbObjMap, options) {
1375
1466
  servicesMap,
1376
1467
  userDefinedTypesMap,
1377
1468
  workspaceComponent,
1469
+ purlSymbolMap,
1378
1470
  };
1379
1471
  }
1380
1472
  return {
@@ -1382,6 +1474,7 @@ export async function analyzeProject(dbObjMap, options) {
1382
1474
  servicesMap,
1383
1475
  dataFlowFrames,
1384
1476
  userDefinedTypesMap,
1477
+ purlSymbolMap,
1385
1478
  };
1386
1479
  }
1387
1480
 
@@ -1443,6 +1536,17 @@ export async function analyzeProject(dbObjMap, options) {
1443
1536
  `The dosai deps slices file "${options.depsSlicesFile}" omits sections (${omittedSections.join(", ")}); running dosai for the full usages report.`,
1444
1537
  );
1445
1538
  }
1539
+ } else if (
1540
+ !dosaiSlicesFileIsFresh(options.depsSlicesFile, dirPath, options)
1541
+ ) {
1542
+ // A slice older than the sources it describes holds the occurrences
1543
+ // of the sources as they were when it was written, so reusing it
1544
+ // keeps occurrences of call sites that no longer exist (issue 4675).
1545
+ if (DEBUG_MODE) {
1546
+ console.log(
1547
+ `The dosai deps slices file "${options.depsSlicesFile}" is older than the sources it describes; running dosai for a fresh usages report.`,
1548
+ );
1549
+ }
1446
1550
  } else {
1447
1551
  if (DEBUG_MODE) {
1448
1552
  console.log(
@@ -1540,6 +1644,7 @@ export async function analyzeProject(dbObjMap, options) {
1540
1644
  cryptoComponents,
1541
1645
  aiComponents,
1542
1646
  cryptoGeneratePurls,
1647
+ purlSymbolMap,
1543
1648
  };
1544
1649
  }
1545
1650
  // Do reachables first so that usages slicing can reuse the atom file
@@ -1573,6 +1678,7 @@ export async function analyzeProject(dbObjMap, options) {
1573
1678
  cryptoGeneratePurls = retMap.cryptoGeneratePurls;
1574
1679
  }
1575
1680
  // Reuse existing usages slices
1681
+ let usagesAtomFile;
1576
1682
  if (options.usagesSlicesFile && usableSlicesFile(options.usagesSlicesFile)) {
1577
1683
  usageSlice = filterAtomSliceData(
1578
1684
  readSlicesFile(options.usagesSlicesFile, "usages"),
@@ -1582,6 +1688,7 @@ export async function analyzeProject(dbObjMap, options) {
1582
1688
  } else {
1583
1689
  // Generate our own slices
1584
1690
  retMap = await createSlice(language, dirPath, "usages", options);
1691
+ usagesAtomFile = retMap?.atomFile || "";
1585
1692
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
1586
1693
  usageSlice = filterAtomSliceData(
1587
1694
  readSlicesFile(retMap.slicesFile, "usages"),
@@ -1661,6 +1768,48 @@ export async function analyzeProject(dbObjMap, options) {
1661
1768
  servicesMap = retMap.servicesMap;
1662
1769
  userDefinedTypesMap = retMap.userDefinedTypesMap;
1663
1770
  }
1771
+ // Parse the callsites slice (JS/TS): one record per external use site, each
1772
+ // record's own (file, line) is an occurrence of the module's package. The
1773
+ // usages slice above still provides services and endpoints.
1774
+ if (JS_LANGUAGES.includes(language)) {
1775
+ let callsitesSlice;
1776
+ if (
1777
+ options.callsitesSlicesFile &&
1778
+ usableSlicesFile(options.callsitesSlicesFile)
1779
+ ) {
1780
+ callsitesSlice = readSlicesFile(options.callsitesSlicesFile, "callsites");
1781
+ } else if (
1782
+ usagesAtomFile !== undefined &&
1783
+ !safeExistsSync(usagesAtomFile)
1784
+ ) {
1785
+ // The usages run above built no atom (stopped at the timeout, or atom
1786
+ // failed); a callsites run would rebuild the same atom and fail the same way.
1787
+ console.log(
1788
+ "Skipping the callsites slice since atom could not build the atom for the usages slice.",
1789
+ );
1790
+ } else {
1791
+ retMap = await createSlice(language, dirPath, "callsites", options);
1792
+ if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
1793
+ callsitesSlice = readSlicesFile(retMap.slicesFile, "callsites");
1794
+ }
1795
+ }
1796
+ if (callsitesSlice?.callsites?.length) {
1797
+ attributeJsCallsites(
1798
+ callsitesSlice,
1799
+ purlImportsMap,
1800
+ purlLocationMap,
1801
+ purlSymbolMap,
1802
+ );
1803
+ }
1804
+ // A slice record names a package, not a version: keep each occurrence on
1805
+ // the installed version its file uses.
1806
+ clearedOccurrencePurls = keepResolvedNpmVersions(
1807
+ purlLocationMap,
1808
+ purlSymbolMap,
1809
+ seededJsLocationMap,
1810
+ bomJson,
1811
+ );
1812
+ }
1664
1813
  // A component whose calls the slices do not show (code inside the project,
1665
1814
  // such as a vendored library) is still used where its header is included
1666
1815
  for (const [purl, includeLocations] of cIncludeLocations) {
@@ -1734,6 +1883,8 @@ export async function analyzeProject(dbObjMap, options) {
1734
1883
  reachablesSlicesFile,
1735
1884
  semanticsSlicesFile,
1736
1885
  purlLocationMap,
1886
+ purlSymbolMap,
1887
+ clearedOccurrencePurls,
1737
1888
  servicesMap,
1738
1889
  dataFlowFrames,
1739
1890
  tempDir: retMap?.tempDir,
@@ -1840,11 +1991,7 @@ export function attributeCIncludes(
1840
1991
  continue;
1841
1992
  }
1842
1993
  const known = locations.get(purl) || new Set();
1843
- known.add(
1844
- slice.lineNumber
1845
- ? `${slice.fileName}#${slice.lineNumber}`
1846
- : slice.fileName,
1847
- );
1994
+ known.add(occurrenceLocationKey(slice.fileName, slice.lineNumber));
1848
1995
  locations.set(purl, known);
1849
1996
  }
1850
1997
  return { symbols, locations };
@@ -1968,8 +2115,15 @@ export async function parseSliceUsages(
1968
2115
  ) {
1969
2116
  const fileName = slice.fileName;
1970
2117
  const typesToLookup = new Set();
1971
- const lKeyOverrides = {};
1972
- const usages = slice.usages || [];
2118
+ // keyed by type names, which can be "constructor", "toString", ...: no prototype
2119
+ const lKeyOverrides = Object.create(null);
2120
+ // A user defined type has no usages; its fields' types are evidence at the fields' lines.
2121
+ const usages =
2122
+ slice.usages ||
2123
+ (slice.fields || [])
2124
+ .filter((f) => f?.typeFullName)
2125
+ .map((f) => ({ targetObj: f }));
2126
+ const isJs = JS_LANGUAGES.includes(language);
1973
2127
  // What should be the line number to use. slice.lineNumber would be quite coarse and could lead to reports such as
1974
2128
  // #1670. Line numbers under targetObj and definedBy is a safe bet for dynamic languages, but occassionally leads to
1975
2129
  // confusion when inter-procedural tracking works better than expected.
@@ -1992,13 +2146,27 @@ export async function parseSliceUsages(
1992
2146
  sliceLineNumber ||
1993
2147
  ausage?.targetObj?.lineNumber ||
1994
2148
  ausage?.definedBy?.lineNumber;
2149
+ // JavaScript entries name the imported module they resolve to; attribute those by package
2150
+ // and keep the symbol-based matching below for entries without one.
2151
+ if (isJs) {
2152
+ attributeJsModules(ausage, fileName, purlImportsMap, purlLocationMap);
2153
+ }
2154
+ const viaSymbols = (o) => !(isJs && o?.module);
1995
2155
  // First capture the types in the targetObj and definedBy
1996
2156
  for (const atype of [
1997
- [ausage?.targetObj?.isExternal, ausage?.targetObj?.typeFullName],
1998
- [ausage?.targetObj?.isExternal, ausage?.targetObj?.resolvedMethod],
1999
- [ausage?.definedBy?.name?.includes("::"), ausage?.definedBy?.name],
2000
- [ausage?.definedBy?.isExternal, ausage?.definedBy?.typeFullName],
2001
- [ausage?.definedBy?.isExternal, ausage?.definedBy?.resolvedMethod],
2157
+ ...(viaSymbols(ausage?.targetObj)
2158
+ ? [
2159
+ [ausage?.targetObj?.isExternal, ausage?.targetObj?.typeFullName],
2160
+ [ausage?.targetObj?.isExternal, ausage?.targetObj?.resolvedMethod],
2161
+ ]
2162
+ : []),
2163
+ ...(viaSymbols(ausage?.definedBy)
2164
+ ? [
2165
+ [ausage?.definedBy?.name?.includes("::"), ausage?.definedBy?.name],
2166
+ [ausage?.definedBy?.isExternal, ausage?.definedBy?.typeFullName],
2167
+ [ausage?.definedBy?.isExternal, ausage?.definedBy?.resolvedMethod],
2168
+ ]
2169
+ : []),
2002
2170
  ...(ausage?.fields || []).map((f) => [f?.isExternal, f?.typeFullName]),
2003
2171
  ]) {
2004
2172
  if (
@@ -2039,7 +2207,8 @@ export async function parseSliceUsages(
2039
2207
  for (const acall of []
2040
2208
  .concat(ausage?.invokedCalls || [])
2041
2209
  .concat(ausage?.argToCalls || [])
2042
- .concat(ausage?.procedures || [])) {
2210
+ .concat(ausage?.procedures || [])
2211
+ .filter(viaSymbols)) {
2043
2212
  if (
2044
2213
  acall.resolvedMethod?.startsWith("@") ||
2045
2214
  acall?.callName?.includes("::")
@@ -2139,9 +2308,7 @@ export async function parseSliceUsages(
2139
2308
  if (!purlLocationMap[apurl]) {
2140
2309
  purlLocationMap[apurl] = new Set();
2141
2310
  }
2142
- if (lKeyOverrides[atype]) {
2143
- purlLocationMap[apurl].add(...lKeyOverrides[atype]);
2144
- }
2311
+ addAllLocations(purlLocationMap[apurl], lKeyOverrides[atype]);
2145
2312
  }
2146
2313
  }
2147
2314
  } else {
@@ -2149,9 +2316,7 @@ export async function parseSliceUsages(
2149
2316
  if (!purlLocationMap[apurl]) {
2150
2317
  purlLocationMap[apurl] = new Set();
2151
2318
  }
2152
- if (lKeyOverrides[atype]) {
2153
- purlLocationMap[apurl].add(...lKeyOverrides[atype]);
2154
- }
2319
+ addAllLocations(purlLocationMap[apurl], lKeyOverrides[atype]);
2155
2320
  }
2156
2321
  }
2157
2322
  }
@@ -2173,9 +2338,7 @@ export async function parseSliceUsages(
2173
2338
  if (!purlLocationMap[ns.purl]) {
2174
2339
  purlLocationMap[ns.purl] = new Set();
2175
2340
  }
2176
- if (lKeyOverrides[atype]) {
2177
- purlLocationMap[ns.purl].add(...lKeyOverrides[atype]);
2178
- }
2341
+ addAllLocations(purlLocationMap[ns.purl], lKeyOverrides[atype]);
2179
2342
  }
2180
2343
  typePurlsCache[atype] = nsHits;
2181
2344
  } else {
@@ -2231,7 +2394,7 @@ export function parseSemanticSlices(_language, components, semanticsSlice) {
2231
2394
  continue;
2232
2395
  }
2233
2396
  for (const aline of lines) {
2234
- locations.add(`${aswiftFile}#${aline}`);
2397
+ locations.add(occurrenceLocationKey(aswiftFile, aline));
2235
2398
  }
2236
2399
  }
2237
2400
  }
@@ -2241,7 +2404,7 @@ export function parseSemanticSlices(_language, components, semanticsSlice) {
2241
2404
  fileIndexes,
2242
2405
  )) {
2243
2406
  for (const aline of ahit.lineNumbers) {
2244
- locations.add(`${ahit.file}#${aline}`);
2407
+ locations.add(occurrenceLocationKey(ahit.file, aline));
2245
2408
  }
2246
2409
  }
2247
2410
  }
@@ -2583,6 +2746,17 @@ export function detectServicesFromOpenAPI(
2583
2746
  * @param {Array} slice Usages array for each objectSlice
2584
2747
  * @param {Object} servicesMap Existing service map
2585
2748
  */
2749
+ /**
2750
+ * The text routes and endpoints are read from: JavaScript slices keep a call's source text in
2751
+ * `code` and the resolved callee name in `resolvedMethod`; other languages have only the latter.
2752
+ *
2753
+ * @param {Object|undefined} entry slice entry
2754
+ * @returns {string|undefined} text
2755
+ */
2756
+ function sourceTextOf(entry) {
2757
+ return entry?.code || entry?.resolvedMethod;
2758
+ }
2759
+
2586
2760
  export function detectServicesFromUsages(language, slice, servicesMap = {}) {
2587
2761
  const usages = slice.usages;
2588
2762
  if (!usages) {
@@ -2595,30 +2769,30 @@ export function detectServicesFromUsages(language, slice, servicesMap = {}) {
2595
2769
  let authenticated;
2596
2770
  if (language === "ruby" && definedBy?.name?.includes("/")) {
2597
2771
  endpoints = extractEndpoints(language, definedBy.name);
2598
- } else if (targetObj?.resolvedMethod) {
2772
+ } else if (sourceTextOf(targetObj)) {
2599
2773
  if (language !== "php") {
2600
- endpoints = extractEndpoints(language, targetObj?.resolvedMethod);
2774
+ endpoints = extractEndpoints(language, sourceTextOf(targetObj));
2601
2775
  }
2602
- if (targetObj?.resolvedMethod.toLowerCase().includes("auth")) {
2776
+ if (sourceTextOf(targetObj).toLowerCase().includes("auth")) {
2603
2777
  authenticated = true;
2604
2778
  }
2605
- } else if (definedBy?.resolvedMethod) {
2779
+ } else if (sourceTextOf(definedBy)) {
2606
2780
  if (language !== "php") {
2607
- endpoints = extractEndpoints(language, definedBy?.resolvedMethod);
2781
+ endpoints = extractEndpoints(language, sourceTextOf(definedBy));
2608
2782
  }
2609
- if (definedBy?.resolvedMethod.toLowerCase().includes("auth")) {
2783
+ if (sourceTextOf(definedBy).toLowerCase().includes("auth")) {
2610
2784
  authenticated = true;
2611
2785
  }
2612
2786
  }
2613
2787
  if (usage.invokedCalls) {
2614
2788
  for (const acall of usage.invokedCalls) {
2615
- if (acall.resolvedMethod) {
2789
+ if (sourceTextOf(acall)) {
2616
2790
  if (language !== "php") {
2617
2791
  const tmpEndpoints = extractEndpoints(
2618
2792
  language,
2619
- acall.resolvedMethod,
2793
+ sourceTextOf(acall),
2620
2794
  );
2621
- if (acall.resolvedMethod.toLowerCase().includes("auth")) {
2795
+ if (sourceTextOf(acall).toLowerCase().includes("auth")) {
2622
2796
  authenticated = true;
2623
2797
  }
2624
2798
  if (tmpEndpoints?.length) {
@@ -2824,12 +2998,81 @@ export function extractEndpoints(language, code) {
2824
2998
  return endpoints;
2825
2999
  }
2826
3000
 
3001
+ /**
3002
+ * Key naming the package a purl refers to, independent of its version,
3003
+ * qualifiers and subpath. Namespace and name are the decoded purl segments,
3004
+ * so an encoded scope (`%40angular/common`) keys like a plain one.
3005
+ *
3006
+ * @param {string} purl Purl string
3007
+ * @returns {string|undefined} `type/namespace/name`, or undefined for a non-purl
3008
+ */
3009
+ export function versionlessPurlKey(purl) {
3010
+ const parts = splitPurlLoose(purl);
3011
+ if (!parts) {
3012
+ return undefined;
3013
+ }
3014
+ return `${parts.type}/${parts.namespace || ""}/${parts.name}`;
3015
+ }
3016
+
3017
+ /**
3018
+ * Index the data-flow frames recorded under versionless flow purls by
3019
+ * {@link versionlessPurlKey}. A flow purl without a version attaches to every
3020
+ * BOM component of the same package name, whatever its version (maintainer
3021
+ * decision, 2026-10-27); chen leaves the version off exactly when it cannot
3022
+ * pick one between apps that pin the package differently.
3023
+ *
3024
+ * @param {Object} dataFlowFrames Frames keyed by flow purl
3025
+ * @returns {Object} Frames keyed by versionless purl key
3026
+ */
3027
+ export function indexVersionlessPurls(dataFlowFrames) {
3028
+ const index = {};
3029
+ for (const [purl, frames] of Object.entries(dataFlowFrames || {})) {
3030
+ if (splitPurlLoose(purl)?.version === null) {
3031
+ const key = versionlessPurlKey(purl);
3032
+ if (!key) {
3033
+ continue;
3034
+ }
3035
+ index[key] ??= [];
3036
+ index[key].push(...frames);
3037
+ }
3038
+ }
3039
+ return index;
3040
+ }
3041
+
3042
+ /**
3043
+ * Index the crypto generate pools recorded under versionless flow purls by
3044
+ * {@link versionlessPurlKey}, the crypto counterpart of
3045
+ * {@link indexVersionlessPurls}.
3046
+ *
3047
+ * @param {Object} cryptoGeneratePurls Algorithm bom-refs keyed by flow purl
3048
+ * @returns {Object} Algorithm bom-ref sets keyed by versionless purl key
3049
+ */
3050
+ export function indexVersionlessCrypto(cryptoGeneratePurls) {
3051
+ const index = {};
3052
+ for (const [purl, algos] of Object.entries(cryptoGeneratePurls || {})) {
3053
+ if (splitPurlLoose(purl)?.version === null) {
3054
+ const key = versionlessPurlKey(purl);
3055
+ if (!key) {
3056
+ continue;
3057
+ }
3058
+ index[key] ??= new Set();
3059
+ for (const ref of algos || []) {
3060
+ index[key].add(ref);
3061
+ }
3062
+ }
3063
+ }
3064
+ return index;
3065
+ }
3066
+
2827
3067
  /**
2828
3068
  * Method to create the SBOM with evidence file called evinse file.
2829
3069
  *
2830
3070
  * @param {Object} sliceArtefacts Various artefacts from the slice operation
2831
- * @param {Object} options Command line options
2832
- * @returns
3071
+ * @param {Object} options Command line options. With `returnInternalModel`,
3072
+ * a CycloneDX 2.0 run returns the 1.x-shaped internal model instead of the
3073
+ * rendered document; the file written is the rendered document either way.
3074
+ * @returns {Promise<Object>} The BOM written to the output file, or its
3075
+ * internal model when `returnInternalModel` is set
2833
3076
  */
2834
3077
  export async function createEvinseFile(sliceArtefacts, options) {
2835
3078
  const {
@@ -2839,6 +3082,8 @@ export async function createEvinseFile(sliceArtefacts, options) {
2839
3082
  dataFlowSlicesFile,
2840
3083
  reachablesSlicesFile,
2841
3084
  purlLocationMap,
3085
+ purlSymbolMap = {},
3086
+ clearedOccurrencePurls,
2842
3087
  servicesMap,
2843
3088
  dataFlowFrames,
2844
3089
  componentPropertiesMap = {},
@@ -2848,9 +3093,13 @@ export async function createEvinseFile(sliceArtefacts, options) {
2848
3093
  cryptoGeneratePurls,
2849
3094
  workspaceComponent,
2850
3095
  } = sliceArtefacts;
2851
- const bomFile = options.input;
2852
3096
  const evinseOutFile = options.output;
2853
- const bomJson = JSON.parse(fs.readFileSync(bomFile, "utf8"));
3097
+ // A 2.0 input is read into the 1.x internal model; everything below works
3098
+ // on the 1.x shape and the final postProcess renders 2.0 again. A caller
3099
+ // that converted the input already (bin/evinse.js) passes the model as
3100
+ // `inputBomJson` and has printed the losses.
3101
+ const { bomJson, losses: bomLosses } = loadEvinseInputBom(options);
3102
+ reportEvinseInputLosses(bomLosses);
2854
3103
  const components = bomJson.components || [];
2855
3104
  // Clear existing annotations
2856
3105
  bomJson.annotations = [];
@@ -2875,27 +3124,62 @@ export async function createEvinseFile(sliceArtefacts, options) {
2875
3124
  let servicesPresent = false;
2876
3125
  const propertyEvidencePresent =
2877
3126
  metadataProperties.length || Object.keys(componentPropertiesMap).length;
3127
+ // Versionless flow purls attach to every same-name component (see
3128
+ // indexVersionlessPurls), so look both ways: exact purl first, then the
3129
+ // versionless index.
3130
+ const versionlessFrames = indexVersionlessPurls(dataFlowFrames);
3131
+ const versionlessCrypto = indexVersionlessCrypto(cryptoGeneratePurls);
3132
+ const depCryptoPurls =
3133
+ Object.keys(versionlessCrypto).length && cryptoGeneratePurls
3134
+ ? { ...cryptoGeneratePurls }
3135
+ : cryptoGeneratePurls;
2878
3136
  for (const comp of [bomJson.metadata?.component, ...components]) {
2879
3137
  if (!comp?.purl) {
2880
3138
  continue;
2881
3139
  }
2882
3140
  delete comp.signature;
2883
3141
  appendComponentProperties(comp, componentPropertiesMap[comp.purl]);
2884
- const locationOccurrences = Array.from(
2885
- purlLocationMap[comp.purl] || [],
2886
- ).sort();
3142
+ // A key with an offset names a use site; the line-only key of the same
3143
+ // line names no place of its own then, so it is dropped rather than
3144
+ // counted beside the use sites (see dropSubsumedOccurrenceKeys).
3145
+ const locationOccurrences = dropSubsumedOccurrenceKeys(
3146
+ Array.from(purlLocationMap[comp.purl] || [])
3147
+ .filter(Boolean)
3148
+ .sort(),
3149
+ );
2887
3150
  if (locationOccurrences.length) {
2888
3151
  if (!comp.evidence) {
2889
3152
  comp.evidence = {};
2890
3153
  }
2891
3154
  // This step would replace any existing occurrences
2892
3155
  // This is fine as long as the input sbom was also generated by cdxgen
2893
- comp.evidence.occurrences = locationOccurrences
2894
- .filter((l) => !!l)
2895
- .map((l) => parseOccurrenceEvidenceLocation(l));
3156
+ // A callsites occurrence carries its column as `offset` and its member
3157
+ // as `symbol`; an occurrence from any other source has neither.
3158
+ const symbols = purlSymbolMap[comp.purl] || {};
3159
+ comp.evidence.occurrences = locationOccurrences.map((l) =>
3160
+ parseOccurrenceEvidenceLocation(
3161
+ l,
3162
+ symbols[l] ? { symbol: symbols[l] } : {},
3163
+ ),
3164
+ );
2896
3165
  occEvidencePresent = true;
3166
+ } else if (
3167
+ clearedOccurrencePurls?.has(comp.purl) &&
3168
+ comp.evidence?.occurrences
3169
+ ) {
3170
+ // The input BOM credited this version with places another installed
3171
+ // version of the package takes.
3172
+ delete comp.evidence.occurrences;
3173
+ if (!Object.keys(comp.evidence).length) {
3174
+ delete comp.evidence;
3175
+ }
3176
+ }
3177
+ const compKey = versionlessPurlKey(comp.purl);
3178
+ let dfFrames = dataFlowFrames[comp.purl];
3179
+ const extraFrames = compKey ? versionlessFrames[compKey] : undefined;
3180
+ if (extraFrames?.length) {
3181
+ dfFrames = [...(dfFrames || []), ...extraFrames];
2897
3182
  }
2898
- const dfFrames = dataFlowFrames[comp.purl];
2899
3183
  if (dfFrames?.length) {
2900
3184
  if (!comp.evidence) {
2901
3185
  comp.evidence = {};
@@ -2909,10 +3193,20 @@ export async function createEvinseFile(sliceArtefacts, options) {
2909
3193
  }
2910
3194
  }
2911
3195
  // Add crypto tags if this purl offers any generation algorithm
2912
- if (
2913
- cryptoGeneratePurls?.[comp.purl] &&
2914
- Array.from(cryptoGeneratePurls[comp.purl]).length
2915
- ) {
3196
+ let compCryptoPurls = cryptoGeneratePurls?.[comp.purl];
3197
+ const extraCrypto = compKey ? versionlessCrypto[compKey] : undefined;
3198
+ if (extraCrypto?.size) {
3199
+ compCryptoPurls = new Set([...(compCryptoPurls || []), ...extraCrypto]);
3200
+ // The provides section below looks pool entries up by bom-ref, so the
3201
+ // merged set has to be reachable that way too.
3202
+ if (depCryptoPurls) {
3203
+ depCryptoPurls[comp.purl] = compCryptoPurls;
3204
+ if (comp["bom-ref"]) {
3205
+ depCryptoPurls[comp["bom-ref"]] = compCryptoPurls;
3206
+ }
3207
+ }
3208
+ }
3209
+ if (compCryptoPurls && Array.from(compCryptoPurls).length) {
2916
3210
  comp.tags = ["crypto", "crypto-generate"];
2917
3211
  }
2918
3212
  } // for
@@ -2944,14 +3238,14 @@ export async function createEvinseFile(sliceArtefacts, options) {
2944
3238
  }
2945
3239
  // Fix the dependencies section with provides information
2946
3240
  if (
2947
- cryptoGeneratePurls &&
2948
- Object.keys(cryptoGeneratePurls).length &&
3241
+ depCryptoPurls &&
3242
+ Object.keys(depCryptoPurls).length &&
2949
3243
  bomJson.dependencies
2950
3244
  ) {
2951
3245
  const newDependencies = [];
2952
3246
  for (const depObj of bomJson.dependencies) {
2953
- if (depObj.ref && cryptoGeneratePurls[depObj.ref]) {
2954
- const providedAlgos = Array.from(cryptoGeneratePurls[depObj.ref]);
3247
+ if (depObj.ref && depCryptoPurls[depObj.ref]) {
3248
+ const providedAlgos = Array.from(depCryptoPurls[depObj.ref]);
2955
3249
  if (providedAlgos.length) {
2956
3250
  depObj.provides = providedAlgos;
2957
3251
  }
@@ -2993,9 +3287,10 @@ export async function createEvinseFile(sliceArtefacts, options) {
2993
3287
  // Redo post-processing with evinse data. `executeOsQuery` has to be injected
2994
3288
  // because postgen no longer imports it directly (stages is below managers);
2995
3289
  // without it the crypto formulation path receives `undefined` and throws.
3290
+ const { inputBomJson: _inputBomJson, ...postProcessOptions } = options;
2996
3291
  const bomNSData = await postProcess(
2997
3292
  { bomJson },
2998
- { ...options, executeOsQuery },
3293
+ { ...postProcessOptions, executeOsQuery },
2999
3294
  );
3000
3295
  safeWriteSync(
3001
3296
  evinseOutFile,
@@ -3036,6 +3331,9 @@ export async function createEvinseFile(sliceArtefacts, options) {
3036
3331
  if (tempDirOwned && tempDir?.startsWith(getTmpDir())) {
3037
3332
  safeRmSync(tempDir, { recursive: true, force: true });
3038
3333
  }
3334
+ if (options.returnInternalModel && bomNSData?.internalBomJson) {
3335
+ return bomNSData.internalBomJson;
3336
+ }
3039
3337
  return bomNSData?.bomJson;
3040
3338
  }
3041
3339
 
@@ -3196,15 +3494,15 @@ export function collectReachableFrames(_language, reachablesSlice) {
3196
3494
  if (tagStr.includes("crypto")) {
3197
3495
  isCryptoFlow = true;
3198
3496
  }
3497
+ // Join with a space: findCryptoAlgos treats every non-alphanumeric as a
3498
+ // word separator, and a space is the one separator cleanStr keeps.
3199
3499
  if (isCryptoFlow && fnode.code) {
3200
- codeSnippets = `${codeSnippets}\\n${fnode.code}`;
3500
+ codeSnippets = `${codeSnippets} ${fnode.code}`;
3201
3501
  }
3202
- if (
3203
- tagStr.includes("crypto-generate") ||
3204
- fnode.code.includes("encrypt") ||
3205
- fnode.code.includes("decrypt") ||
3206
- fnode.code.includes("sign")
3207
- ) {
3502
+ // Pool membership is tag-driven: a node contributes its pkg: tags only
3503
+ // when the tagger marked it crypto-generate. Code substring tests read
3504
+ // assign/design/signup into encrypt/decrypt/sign and are gone.
3505
+ if (tagStr.includes("crypto-generate")) {
3208
3506
  cpurls = tagStr.split(", ").filter((t) => t.startsWith("pkg:"));
3209
3507
  for (const cpurl of cpurls) {
3210
3508
  if (!cryptoGeneratePurls[cpurl]) {
@@ -3407,9 +3705,176 @@ export function getClassTypeFromSignature(language, typeFullName) {
3407
3705
  return simplifyType(typeFullName);
3408
3706
  }
3409
3707
 
3708
+ const JS_LANGUAGES = ["javascript", "js", "ts", "typescript"];
3709
+
3710
+ /**
3711
+ * Add every location of a set to a purl's locations (Set.add takes a single value).
3712
+ *
3713
+ * @param {Set<string>} target purl location set
3714
+ * @param {Set<string>|undefined} locations locations to add
3715
+ */
3716
+ function addAllLocations(target, locations) {
3717
+ for (const l of locations || []) {
3718
+ target.add(l);
3719
+ }
3720
+ }
3721
+
3722
+ /**
3723
+ * npm package name of a JavaScript module specifier: `@scope/pkg/sub` -> `@scope/pkg`,
3724
+ * `lodash/fp` -> `lodash`. Node.js builtins (`node:fs`, `fs`) are not packages.
3725
+ *
3726
+ * @param {string} module module specifier
3727
+ * @returns {string|undefined} package name
3728
+ */
3729
+ export function jsPackageOfModule(module) {
3730
+ if (!module || module.startsWith("node:") || module.startsWith(".")) {
3731
+ return undefined;
3732
+ }
3733
+ const parts = module.split("/");
3734
+ const pkg = module.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
3735
+ if (isNodeBuiltin(pkg)) {
3736
+ return undefined;
3737
+ }
3738
+ return pkg;
3739
+ }
3740
+
3741
+ function isNodeBuiltin(name) {
3742
+ return NODE_BUILTINS.has(name);
3743
+ }
3744
+
3745
+ const NODE_BUILTINS = new Set(builtinModules);
3746
+
3747
+ const jsPurlsByPackageCache = new WeakMap();
3748
+
3749
+ function jsPurlsByPackage(purlImportsMap) {
3750
+ let byPackage = jsPurlsByPackageCache.get(purlImportsMap);
3751
+ if (!byPackage) {
3752
+ byPackage = {};
3753
+ for (const apurl of Object.keys(purlImportsMap || {})) {
3754
+ try {
3755
+ const purlObj = Purl.parse(apurl);
3756
+ if (purlObj.type !== "npm") {
3757
+ continue;
3758
+ }
3759
+ const name = purlObj.namespace
3760
+ ? `${purlObj.namespace}/${purlObj.name}`
3761
+ : purlObj.name;
3762
+ (byPackage[name] = byPackage[name] || []).push(apurl);
3763
+ } catch (_e) {
3764
+ // not a valid purl
3765
+ }
3766
+ }
3767
+ jsPurlsByPackageCache.set(purlImportsMap, byPackage);
3768
+ }
3769
+ return byPackage;
3770
+ }
3771
+
3772
+ /**
3773
+ * Attribute a JavaScript usage's module-qualified entries to the packages providing them.
3774
+ *
3775
+ * Each call entry with a `module` is a use of that module's package at the call's line. When
3776
+ * the slice's object is an import binding (defined by a require/import naming the module),
3777
+ * every call the object takes part in is a use of that package too.
3778
+ *
3779
+ * @param {Object} ausage usage entry of an object slice
3780
+ * @param {string} fileName file of the slice
3781
+ * @param {Object} purlImportsMap purl -> imported modules and symbols
3782
+ * @param {Object} purlLocationMap purl -> Set of `file#line`
3783
+ */
3784
+ export function attributeJsModules(
3785
+ ausage,
3786
+ fileName,
3787
+ purlImportsMap,
3788
+ purlLocationMap,
3789
+ ) {
3790
+ const byPackage = jsPurlsByPackage(purlImportsMap);
3791
+ const record = (module, line) => {
3792
+ const pkg = jsPackageOfModule(module);
3793
+ if (!pkg || !line) {
3794
+ return;
3795
+ }
3796
+ for (const apurl of byPackage[pkg] || []) {
3797
+ if (!purlLocationMap[apurl]) {
3798
+ purlLocationMap[apurl] = new Set();
3799
+ }
3800
+ purlLocationMap[apurl].add(occurrenceLocationKey(fileName, line));
3801
+ }
3802
+ };
3803
+ const calls = []
3804
+ .concat(ausage?.invokedCalls || [])
3805
+ .concat(ausage?.argToCalls || []);
3806
+ for (const c of [ausage?.targetObj, ausage?.definedBy, ...calls]) {
3807
+ if (c?.module) {
3808
+ record(c.module, c.lineNumber);
3809
+ }
3810
+ }
3811
+ // Only an import binding stands for its module: a value merely returned by a package's call
3812
+ // (`const s = purl.toString()`) is not a use of that package wherever it goes.
3813
+ const definedBy = ausage?.definedBy;
3814
+ const isImportBinding =
3815
+ definedBy?.name === "require" ||
3816
+ definedBy?.name === "import" ||
3817
+ /^(require|import)\(/.test(definedBy?.code || "");
3818
+ const bindingModule = isImportBinding ? definedBy?.module : undefined;
3819
+ if (bindingModule) {
3820
+ for (const c of calls) {
3821
+ record(bindingModule, c?.lineNumber);
3822
+ }
3823
+ }
3824
+ }
3825
+
3826
+ /**
3827
+ * Attribute a callsites slice's records to the packages providing their modules.
3828
+ *
3829
+ * Each record of `{"callsites": [...]}` (atom `callsites` command) is one external use site
3830
+ * carrying its own file, line and column; the record is an occurrence of every purl whose
3831
+ * package matches the record's module. A record with a column (0 or more; atom writes -1 for
3832
+ * none) is keyed `file:line:col`, so two use sites on one line are two occurrences, and its
3833
+ * occurrence carries the column as `offset` and `module` joined with `path` by `.` as
3834
+ * `symbol`. A record without a column keys the line (`file#line`) and its occurrence carries
3835
+ * the `symbol` alone; a record without a `path` leaves the symbol at the bare `module`.
3836
+ *
3837
+ * @param {Object} callsitesSlice callsites slice with a `callsites` array
3838
+ * @param {Object} purlImportsMap purl -> imported modules and symbols
3839
+ * @param {Object} purlLocationMap purl -> Set of occurrence keys
3840
+ * @param {Object} [purlSymbolMap] purl -> occurrence key -> symbol, filled in place
3841
+ */
3842
+ export function attributeJsCallsites(
3843
+ callsitesSlice,
3844
+ purlImportsMap,
3845
+ purlLocationMap,
3846
+ purlSymbolMap = {},
3847
+ ) {
3848
+ const byPackage = jsPurlsByPackage(purlImportsMap);
3849
+ for (const record of callsitesSlice?.callsites || []) {
3850
+ const pkg = jsPackageOfModule(record?.module);
3851
+ if (!pkg || !(record.line > 0)) {
3852
+ continue;
3853
+ }
3854
+ const key = occurrenceLocationKey(record.file, record.line, record.col);
3855
+ const symbol = record.path
3856
+ ? `${record.module}.${record.path}`
3857
+ : record.module;
3858
+ for (const apurl of byPackage[pkg] || []) {
3859
+ if (!purlLocationMap[apurl]) {
3860
+ purlLocationMap[apurl] = new Set();
3861
+ }
3862
+ purlLocationMap[apurl].add(key);
3863
+ // The first symbol a key carries wins, so a re-run over a previous
3864
+ // output, which seeds the symbol the first run wrote, stays stable.
3865
+ if (symbol) {
3866
+ const symbols = (purlSymbolMap[apurl] ??= {});
3867
+ if (symbols[key] === undefined) {
3868
+ symbols[key] = symbol;
3869
+ }
3870
+ }
3871
+ }
3872
+ }
3873
+ }
3874
+
3410
3875
  function addToOverrides(lKeyOverrides, atype, fileName, ausageLineNumber) {
3411
3876
  if (!lKeyOverrides[atype]) {
3412
3877
  lKeyOverrides[atype] = new Set();
3413
3878
  }
3414
- lKeyOverrides[atype].add(`${fileName}#${ausageLineNumber}`);
3879
+ lKeyOverrides[atype].add(occurrenceLocationKey(fileName, ausageLineNumber));
3415
3880
  }