@cdxgen/cdxgen 13.5.0 → 13.6.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 (182) hide show
  1. package/README.md +6 -6
  2. package/bin/cdxgen.js +4 -0
  3. package/bin/evinse.js +3 -0
  4. package/bin/validate.js +6 -0
  5. package/data/license-aliases.json +24 -2
  6. package/data/remediations.json +124 -0
  7. package/data/rules/ai-bom.yaml +1 -1
  8. package/data/rules/brotli-decoder.yaml +107 -0
  9. package/data/rules/vscode-extensions.yaml +75 -0
  10. package/data/spdx-license-list.json +121 -7
  11. package/data/spdx-licenses.json +69 -0
  12. package/data/spdx.schema.json +845 -2
  13. package/lib/cli/bomAssembly.js +92 -3
  14. package/lib/cli/index.js +108 -1
  15. package/lib/cli/jsBom.js +153 -64
  16. package/lib/cli/jvmBom.js +237 -46
  17. package/lib/cli/managedBom.js +142 -16
  18. package/lib/cli/nativeBom.js +42 -5
  19. package/lib/core/boundedFile.js +344 -0
  20. package/lib/core/brotliProbe.js +182 -0
  21. package/lib/core/env.js +8 -0
  22. package/lib/core/fs.js +12 -0
  23. package/lib/core/httpClient.js +98 -1
  24. package/lib/core/lockPaths.js +38 -0
  25. package/lib/core/paths.js +24 -1
  26. package/lib/core/processTree.js +5 -0
  27. package/lib/core/toolRegistry.js +170 -0
  28. package/lib/ecosystems/cppEvidence.js +263 -106
  29. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  30. package/lib/ecosystems/ecosystems.js +25 -10
  31. package/lib/ecosystems/gemHostProbe.js +301 -0
  32. package/lib/ecosystems/gradleutils.js +828 -224
  33. package/lib/ecosystems/parsers-bazel.js +107 -6
  34. package/lib/ecosystems/parsers-js.js +505 -22
  35. package/lib/ecosystems/parsers-misc.js +209 -20
  36. package/lib/ecosystems/parsers-python.js +89 -2
  37. package/lib/ecosystems/pythonutils.js +128 -2
  38. package/lib/ecosystems/rubyutils.js +538 -31
  39. package/lib/ecosystems/scalacliutils.js +2 -0
  40. package/lib/ecosystems/scalasem.js +796 -151
  41. package/lib/ecosystems/vendoredCode.js +237 -3
  42. package/lib/ecosystems/venvProbe.js +360 -0
  43. package/lib/evinser/evinser.js +346 -43
  44. package/lib/helpers/agenticToolutils.js +413 -93
  45. package/lib/helpers/vsixutils.js +1574 -315
  46. package/lib/inventory/atomUtils.js +77 -0
  47. package/lib/inventory/brotliDecoder.js +142 -0
  48. package/lib/inventory/buildToolRateLimit.js +1 -1
  49. package/lib/inventory/cbomutils.js +56 -7
  50. package/lib/inventory/deps.js +35 -0
  51. package/lib/inventory/dosai.js +139 -5
  52. package/lib/inventory/dotnetRuntime.js +217 -0
  53. package/lib/inventory/envcontext.js +307 -13
  54. package/lib/inventory/evidenceUtils.js +11 -0
  55. package/lib/inventory/fetchBatch.js +1 -1
  56. package/lib/inventory/formulationParsers.js +31 -3
  57. package/lib/inventory/gtfobins.js +13 -0
  58. package/lib/inventory/hostRuntimeProbe.js +686 -0
  59. package/lib/inventory/jvmLocalRepos.js +9 -2
  60. package/lib/inventory/jvmToolEnv.js +29 -0
  61. package/lib/inventory/licenseEnhancer.js +12 -0
  62. package/lib/inventory/mavenCentralMirror.js +534 -0
  63. package/lib/inventory/nodeBuildInfo.js +218 -0
  64. package/lib/inventory/nodeHostProbe.js +202 -0
  65. package/lib/inventory/osPackageResolver.js +182 -0
  66. package/lib/inventory/phpBuildInfo.js +10 -59
  67. package/lib/inventory/phpHostProbe.js +32 -488
  68. package/lib/inventory/rubyBuildInfo.js +972 -0
  69. package/lib/inventory/rubyHostProbe.js +218 -0
  70. package/lib/inventory/scalaCoords.js +4 -3
  71. package/lib/inventory/scalaDedupe.js +440 -0
  72. package/lib/inventory/source.js +49 -2
  73. package/lib/managers/binary.js +337 -3
  74. package/lib/managers/docker.js +79 -0
  75. package/lib/parsers/largeJson.js +28 -11
  76. package/lib/parsers/vscodeExtensionId.js +183 -0
  77. package/lib/server/server.js +12 -1
  78. package/lib/stages/postgen/ruleEngine.js +13 -5
  79. package/lib/stages/pregen/envAudit.js +1 -0
  80. package/lib/stages/pregen/pregen.js +114 -57
  81. package/lib/validator/bomValidator.js +6 -1
  82. package/lib/validator/generated/validate-1.6.mjs +1 -1
  83. package/lib/validator/generated/validate-1.7.mjs +1 -1
  84. package/lib/validator/generated/validate-2.0.mjs +1 -1
  85. package/lib/validator/index.js +1 -1
  86. package/package.json +3 -2
  87. package/types/lib/cli/bomAssembly.d.ts +13 -1
  88. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/cli/jsBom.d.ts +1 -17
  91. package/types/lib/cli/jsBom.d.ts.map +1 -1
  92. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  93. package/types/lib/cli/managedBom.d.ts.map +1 -1
  94. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  95. package/types/lib/core/boundedFile.d.ts +70 -0
  96. package/types/lib/core/boundedFile.d.ts.map +1 -0
  97. package/types/lib/core/brotliProbe.d.ts +37 -0
  98. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  99. package/types/lib/core/env.d.ts +4 -0
  100. package/types/lib/core/env.d.ts.map +1 -1
  101. package/types/lib/core/fs.d.ts.map +1 -1
  102. package/types/lib/core/httpClient.d.ts +24 -0
  103. package/types/lib/core/httpClient.d.ts.map +1 -1
  104. package/types/lib/core/lockPaths.d.ts +19 -0
  105. package/types/lib/core/lockPaths.d.ts.map +1 -0
  106. package/types/lib/core/processTree.d.ts +2 -0
  107. package/types/lib/core/processTree.d.ts.map +1 -1
  108. package/types/lib/core/toolRegistry.d.ts +36 -0
  109. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  110. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  111. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  112. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  113. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  114. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  115. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  116. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  117. package/types/lib/ecosystems/gradleutils.d.ts +47 -13
  118. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  121. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
  123. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-python.d.ts +10 -0
  125. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  126. package/types/lib/ecosystems/pythonutils.d.ts +20 -2
  127. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  128. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  129. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  131. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  132. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  133. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  134. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  135. package/types/lib/evinser/evinser.d.ts +63 -8
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/inventory/atomUtils.d.ts +19 -0
  138. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  139. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  140. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  141. package/types/lib/inventory/cbomutils.d.ts +17 -0
  142. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  143. package/types/lib/inventory/deps.d.ts.map +1 -1
  144. package/types/lib/inventory/dosai.d.ts +17 -1
  145. package/types/lib/inventory/dosai.d.ts.map +1 -1
  146. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  147. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  148. package/types/lib/inventory/envcontext.d.ts +52 -6
  149. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  150. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  151. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  152. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  153. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  154. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  155. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  156. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  157. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  158. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  159. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  160. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  161. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  162. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  163. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  164. package/types/lib/inventory/osPackageResolver.d.ts +38 -0
  165. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  166. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
  167. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
  168. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  169. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  170. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  171. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  172. package/types/lib/inventory/source.d.ts +10 -0
  173. package/types/lib/inventory/source.d.ts.map +1 -1
  174. package/types/lib/managers/binary.d.ts.map +1 -1
  175. package/types/lib/managers/docker.d.ts +12 -0
  176. package/types/lib/managers/docker.d.ts.map +1 -1
  177. package/types/lib/parsers/largeJson.d.ts +4 -3
  178. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  179. package/types/lib/server/server.d.ts.map +1 -1
  180. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/pregen.d.ts +12 -8
  182. package/types/lib/stages/pregen/pregen.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
 
@@ -54,6 +55,7 @@ import {
54
55
  isScalasemLanguage,
55
56
  } from "../ecosystems/scalasem.js";
56
57
  import {
58
+ atomBomFileArgs,
57
59
  atomBuildToolArgs,
58
60
  atomCompileCommandsArgs,
59
61
  buildAtomCommandEnv,
@@ -91,6 +93,7 @@ import {
91
93
  readDosaiMethodsReport,
92
94
  } from "../inventory/dosai.js";
93
95
  import { parseOccurrenceEvidenceLocation } from "../inventory/evidenceUtils.js";
96
+ import { splitPurlLoose } from "../inventory/purl.js";
94
97
  import { executeOsQuery } from "../managers/binary.js";
95
98
  import { MAX_JSON_TEXT_BYTES, readJsonFile } from "../parsers/largeJson.js";
96
99
  import { postProcess } from "../stages/postgen/postgen.js";
@@ -98,7 +101,8 @@ import { createOrLoad } from "./db.js";
98
101
  import { findPurlLocations } from "./scalasem.js";
99
102
  import { createSemanticsSlices } from "./swiftsem.js";
100
103
 
101
- const typePurlsCache = {};
104
+ // keyed by type names, which can be "constructor", "toString", ...: no prototype
105
+ const typePurlsCache = Object.create(null);
102
106
 
103
107
  function appendUniqueProperty(properties, name, value) {
104
108
  if (value === undefined || value === null || value === "") {
@@ -261,6 +265,7 @@ export function buildEvinseOptions(options, args, input) {
261
265
  force: false,
262
266
  withReachables: options.deep,
263
267
  usagesSlicesFile: options.usagesSlicesFile,
268
+ callsitesSlicesFile: options.callsitesSlicesFile,
264
269
  dataFlowSlicesFile: options.dataFlowSlicesFile,
265
270
  reachablesSlicesFile: options.reachablesSlicesFile,
266
271
  semanticsSlicesFile: options.semanticsSlicesFile,
@@ -734,6 +739,7 @@ export async function createSlice(
734
739
  args.push("--slice-depth");
735
740
  args.push(readEnvironmentVariable("ATOM_SLICE_DEPTH"));
736
741
  }
742
+ args.push(...atomBomFileArgs(sliceType, options));
737
743
  args.push(...atomCompileCommandsArgs(filePath, language, options));
738
744
  args.push(...buildToolArgs);
739
745
  args.push(resolve(filePath));
@@ -1573,6 +1579,7 @@ export async function analyzeProject(dbObjMap, options) {
1573
1579
  cryptoGeneratePurls = retMap.cryptoGeneratePurls;
1574
1580
  }
1575
1581
  // Reuse existing usages slices
1582
+ let usagesAtomFile;
1576
1583
  if (options.usagesSlicesFile && usableSlicesFile(options.usagesSlicesFile)) {
1577
1584
  usageSlice = filterAtomSliceData(
1578
1585
  readSlicesFile(options.usagesSlicesFile, "usages"),
@@ -1582,6 +1589,7 @@ export async function analyzeProject(dbObjMap, options) {
1582
1589
  } else {
1583
1590
  // Generate our own slices
1584
1591
  retMap = await createSlice(language, dirPath, "usages", options);
1592
+ usagesAtomFile = retMap?.atomFile || "";
1585
1593
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
1586
1594
  usageSlice = filterAtomSliceData(
1587
1595
  readSlicesFile(retMap.slicesFile, "usages"),
@@ -1661,6 +1669,35 @@ export async function analyzeProject(dbObjMap, options) {
1661
1669
  servicesMap = retMap.servicesMap;
1662
1670
  userDefinedTypesMap = retMap.userDefinedTypesMap;
1663
1671
  }
1672
+ // Parse the callsites slice (JS/TS): one record per external use site, each
1673
+ // record's own (file, line) is an occurrence of the module's package. The
1674
+ // usages slice above still provides services and endpoints.
1675
+ if (JS_LANGUAGES.includes(language)) {
1676
+ let callsitesSlice;
1677
+ if (
1678
+ options.callsitesSlicesFile &&
1679
+ usableSlicesFile(options.callsitesSlicesFile)
1680
+ ) {
1681
+ callsitesSlice = readSlicesFile(options.callsitesSlicesFile, "callsites");
1682
+ } else if (
1683
+ usagesAtomFile !== undefined &&
1684
+ !safeExistsSync(usagesAtomFile)
1685
+ ) {
1686
+ // The usages run above built no atom (stopped at the timeout, or atom
1687
+ // failed); a callsites run would rebuild the same atom and fail the same way.
1688
+ console.log(
1689
+ "Skipping the callsites slice since atom could not build the atom for the usages slice.",
1690
+ );
1691
+ } else {
1692
+ retMap = await createSlice(language, dirPath, "callsites", options);
1693
+ if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
1694
+ callsitesSlice = readSlicesFile(retMap.slicesFile, "callsites");
1695
+ }
1696
+ }
1697
+ if (callsitesSlice?.callsites?.length) {
1698
+ attributeJsCallsites(callsitesSlice, purlImportsMap, purlLocationMap);
1699
+ }
1700
+ }
1664
1701
  // A component whose calls the slices do not show (code inside the project,
1665
1702
  // such as a vendored library) is still used where its header is included
1666
1703
  for (const [purl, includeLocations] of cIncludeLocations) {
@@ -1968,8 +2005,15 @@ export async function parseSliceUsages(
1968
2005
  ) {
1969
2006
  const fileName = slice.fileName;
1970
2007
  const typesToLookup = new Set();
1971
- const lKeyOverrides = {};
1972
- const usages = slice.usages || [];
2008
+ // keyed by type names, which can be "constructor", "toString", ...: no prototype
2009
+ const lKeyOverrides = Object.create(null);
2010
+ // A user defined type has no usages; its fields' types are evidence at the fields' lines.
2011
+ const usages =
2012
+ slice.usages ||
2013
+ (slice.fields || [])
2014
+ .filter((f) => f?.typeFullName)
2015
+ .map((f) => ({ targetObj: f }));
2016
+ const isJs = JS_LANGUAGES.includes(language);
1973
2017
  // What should be the line number to use. slice.lineNumber would be quite coarse and could lead to reports such as
1974
2018
  // #1670. Line numbers under targetObj and definedBy is a safe bet for dynamic languages, but occassionally leads to
1975
2019
  // confusion when inter-procedural tracking works better than expected.
@@ -1992,13 +2036,27 @@ export async function parseSliceUsages(
1992
2036
  sliceLineNumber ||
1993
2037
  ausage?.targetObj?.lineNumber ||
1994
2038
  ausage?.definedBy?.lineNumber;
2039
+ // JavaScript entries name the imported module they resolve to; attribute those by package
2040
+ // and keep the symbol-based matching below for entries without one.
2041
+ if (isJs) {
2042
+ attributeJsModules(ausage, fileName, purlImportsMap, purlLocationMap);
2043
+ }
2044
+ const viaSymbols = (o) => !(isJs && o?.module);
1995
2045
  // First capture the types in the targetObj and definedBy
1996
2046
  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],
2047
+ ...(viaSymbols(ausage?.targetObj)
2048
+ ? [
2049
+ [ausage?.targetObj?.isExternal, ausage?.targetObj?.typeFullName],
2050
+ [ausage?.targetObj?.isExternal, ausage?.targetObj?.resolvedMethod],
2051
+ ]
2052
+ : []),
2053
+ ...(viaSymbols(ausage?.definedBy)
2054
+ ? [
2055
+ [ausage?.definedBy?.name?.includes("::"), ausage?.definedBy?.name],
2056
+ [ausage?.definedBy?.isExternal, ausage?.definedBy?.typeFullName],
2057
+ [ausage?.definedBy?.isExternal, ausage?.definedBy?.resolvedMethod],
2058
+ ]
2059
+ : []),
2002
2060
  ...(ausage?.fields || []).map((f) => [f?.isExternal, f?.typeFullName]),
2003
2061
  ]) {
2004
2062
  if (
@@ -2039,7 +2097,8 @@ export async function parseSliceUsages(
2039
2097
  for (const acall of []
2040
2098
  .concat(ausage?.invokedCalls || [])
2041
2099
  .concat(ausage?.argToCalls || [])
2042
- .concat(ausage?.procedures || [])) {
2100
+ .concat(ausage?.procedures || [])
2101
+ .filter(viaSymbols)) {
2043
2102
  if (
2044
2103
  acall.resolvedMethod?.startsWith("@") ||
2045
2104
  acall?.callName?.includes("::")
@@ -2139,9 +2198,7 @@ export async function parseSliceUsages(
2139
2198
  if (!purlLocationMap[apurl]) {
2140
2199
  purlLocationMap[apurl] = new Set();
2141
2200
  }
2142
- if (lKeyOverrides[atype]) {
2143
- purlLocationMap[apurl].add(...lKeyOverrides[atype]);
2144
- }
2201
+ addAllLocations(purlLocationMap[apurl], lKeyOverrides[atype]);
2145
2202
  }
2146
2203
  }
2147
2204
  } else {
@@ -2149,9 +2206,7 @@ export async function parseSliceUsages(
2149
2206
  if (!purlLocationMap[apurl]) {
2150
2207
  purlLocationMap[apurl] = new Set();
2151
2208
  }
2152
- if (lKeyOverrides[atype]) {
2153
- purlLocationMap[apurl].add(...lKeyOverrides[atype]);
2154
- }
2209
+ addAllLocations(purlLocationMap[apurl], lKeyOverrides[atype]);
2155
2210
  }
2156
2211
  }
2157
2212
  }
@@ -2173,9 +2228,7 @@ export async function parseSliceUsages(
2173
2228
  if (!purlLocationMap[ns.purl]) {
2174
2229
  purlLocationMap[ns.purl] = new Set();
2175
2230
  }
2176
- if (lKeyOverrides[atype]) {
2177
- purlLocationMap[ns.purl].add(...lKeyOverrides[atype]);
2178
- }
2231
+ addAllLocations(purlLocationMap[ns.purl], lKeyOverrides[atype]);
2179
2232
  }
2180
2233
  typePurlsCache[atype] = nsHits;
2181
2234
  } else {
@@ -2583,6 +2636,17 @@ export function detectServicesFromOpenAPI(
2583
2636
  * @param {Array} slice Usages array for each objectSlice
2584
2637
  * @param {Object} servicesMap Existing service map
2585
2638
  */
2639
+ /**
2640
+ * The text routes and endpoints are read from: JavaScript slices keep a call's source text in
2641
+ * `code` and the resolved callee name in `resolvedMethod`; other languages have only the latter.
2642
+ *
2643
+ * @param {Object|undefined} entry slice entry
2644
+ * @returns {string|undefined} text
2645
+ */
2646
+ function sourceTextOf(entry) {
2647
+ return entry?.code || entry?.resolvedMethod;
2648
+ }
2649
+
2586
2650
  export function detectServicesFromUsages(language, slice, servicesMap = {}) {
2587
2651
  const usages = slice.usages;
2588
2652
  if (!usages) {
@@ -2595,30 +2659,30 @@ export function detectServicesFromUsages(language, slice, servicesMap = {}) {
2595
2659
  let authenticated;
2596
2660
  if (language === "ruby" && definedBy?.name?.includes("/")) {
2597
2661
  endpoints = extractEndpoints(language, definedBy.name);
2598
- } else if (targetObj?.resolvedMethod) {
2662
+ } else if (sourceTextOf(targetObj)) {
2599
2663
  if (language !== "php") {
2600
- endpoints = extractEndpoints(language, targetObj?.resolvedMethod);
2664
+ endpoints = extractEndpoints(language, sourceTextOf(targetObj));
2601
2665
  }
2602
- if (targetObj?.resolvedMethod.toLowerCase().includes("auth")) {
2666
+ if (sourceTextOf(targetObj).toLowerCase().includes("auth")) {
2603
2667
  authenticated = true;
2604
2668
  }
2605
- } else if (definedBy?.resolvedMethod) {
2669
+ } else if (sourceTextOf(definedBy)) {
2606
2670
  if (language !== "php") {
2607
- endpoints = extractEndpoints(language, definedBy?.resolvedMethod);
2671
+ endpoints = extractEndpoints(language, sourceTextOf(definedBy));
2608
2672
  }
2609
- if (definedBy?.resolvedMethod.toLowerCase().includes("auth")) {
2673
+ if (sourceTextOf(definedBy).toLowerCase().includes("auth")) {
2610
2674
  authenticated = true;
2611
2675
  }
2612
2676
  }
2613
2677
  if (usage.invokedCalls) {
2614
2678
  for (const acall of usage.invokedCalls) {
2615
- if (acall.resolvedMethod) {
2679
+ if (sourceTextOf(acall)) {
2616
2680
  if (language !== "php") {
2617
2681
  const tmpEndpoints = extractEndpoints(
2618
2682
  language,
2619
- acall.resolvedMethod,
2683
+ sourceTextOf(acall),
2620
2684
  );
2621
- if (acall.resolvedMethod.toLowerCase().includes("auth")) {
2685
+ if (sourceTextOf(acall).toLowerCase().includes("auth")) {
2622
2686
  authenticated = true;
2623
2687
  }
2624
2688
  if (tmpEndpoints?.length) {
@@ -2824,6 +2888,72 @@ export function extractEndpoints(language, code) {
2824
2888
  return endpoints;
2825
2889
  }
2826
2890
 
2891
+ /**
2892
+ * Key naming the package a purl refers to, independent of its version,
2893
+ * qualifiers and subpath. Namespace and name are the decoded purl segments,
2894
+ * so an encoded scope (`%40angular/common`) keys like a plain one.
2895
+ *
2896
+ * @param {string} purl Purl string
2897
+ * @returns {string|undefined} `type/namespace/name`, or undefined for a non-purl
2898
+ */
2899
+ export function versionlessPurlKey(purl) {
2900
+ const parts = splitPurlLoose(purl);
2901
+ if (!parts) {
2902
+ return undefined;
2903
+ }
2904
+ return `${parts.type}/${parts.namespace || ""}/${parts.name}`;
2905
+ }
2906
+
2907
+ /**
2908
+ * Index the data-flow frames recorded under versionless flow purls by
2909
+ * {@link versionlessPurlKey}. A flow purl without a version attaches to every
2910
+ * BOM component of the same package name, whatever its version (maintainer
2911
+ * decision, 2026-10-27); chen leaves the version off exactly when it cannot
2912
+ * pick one between apps that pin the package differently.
2913
+ *
2914
+ * @param {Object} dataFlowFrames Frames keyed by flow purl
2915
+ * @returns {Object} Frames keyed by versionless purl key
2916
+ */
2917
+ export function indexVersionlessPurls(dataFlowFrames) {
2918
+ const index = {};
2919
+ for (const [purl, frames] of Object.entries(dataFlowFrames || {})) {
2920
+ if (splitPurlLoose(purl)?.version === null) {
2921
+ const key = versionlessPurlKey(purl);
2922
+ if (!key) {
2923
+ continue;
2924
+ }
2925
+ index[key] ??= [];
2926
+ index[key].push(...frames);
2927
+ }
2928
+ }
2929
+ return index;
2930
+ }
2931
+
2932
+ /**
2933
+ * Index the crypto generate pools recorded under versionless flow purls by
2934
+ * {@link versionlessPurlKey}, the crypto counterpart of
2935
+ * {@link indexVersionlessPurls}.
2936
+ *
2937
+ * @param {Object} cryptoGeneratePurls Algorithm bom-refs keyed by flow purl
2938
+ * @returns {Object} Algorithm bom-ref sets keyed by versionless purl key
2939
+ */
2940
+ export function indexVersionlessCrypto(cryptoGeneratePurls) {
2941
+ const index = {};
2942
+ for (const [purl, algos] of Object.entries(cryptoGeneratePurls || {})) {
2943
+ if (splitPurlLoose(purl)?.version === null) {
2944
+ const key = versionlessPurlKey(purl);
2945
+ if (!key) {
2946
+ continue;
2947
+ }
2948
+ index[key] ??= new Set();
2949
+ for (const ref of algos || []) {
2950
+ index[key].add(ref);
2951
+ }
2952
+ }
2953
+ }
2954
+ return index;
2955
+ }
2956
+
2827
2957
  /**
2828
2958
  * Method to create the SBOM with evidence file called evinse file.
2829
2959
  *
@@ -2875,6 +3005,15 @@ export async function createEvinseFile(sliceArtefacts, options) {
2875
3005
  let servicesPresent = false;
2876
3006
  const propertyEvidencePresent =
2877
3007
  metadataProperties.length || Object.keys(componentPropertiesMap).length;
3008
+ // Versionless flow purls attach to every same-name component (see
3009
+ // indexVersionlessPurls), so look both ways: exact purl first, then the
3010
+ // versionless index.
3011
+ const versionlessFrames = indexVersionlessPurls(dataFlowFrames);
3012
+ const versionlessCrypto = indexVersionlessCrypto(cryptoGeneratePurls);
3013
+ const depCryptoPurls =
3014
+ Object.keys(versionlessCrypto).length && cryptoGeneratePurls
3015
+ ? { ...cryptoGeneratePurls }
3016
+ : cryptoGeneratePurls;
2878
3017
  for (const comp of [bomJson.metadata?.component, ...components]) {
2879
3018
  if (!comp?.purl) {
2880
3019
  continue;
@@ -2895,7 +3034,12 @@ export async function createEvinseFile(sliceArtefacts, options) {
2895
3034
  .map((l) => parseOccurrenceEvidenceLocation(l));
2896
3035
  occEvidencePresent = true;
2897
3036
  }
2898
- const dfFrames = dataFlowFrames[comp.purl];
3037
+ const compKey = versionlessPurlKey(comp.purl);
3038
+ let dfFrames = dataFlowFrames[comp.purl];
3039
+ const extraFrames = compKey ? versionlessFrames[compKey] : undefined;
3040
+ if (extraFrames?.length) {
3041
+ dfFrames = [...(dfFrames || []), ...extraFrames];
3042
+ }
2899
3043
  if (dfFrames?.length) {
2900
3044
  if (!comp.evidence) {
2901
3045
  comp.evidence = {};
@@ -2909,10 +3053,20 @@ export async function createEvinseFile(sliceArtefacts, options) {
2909
3053
  }
2910
3054
  }
2911
3055
  // Add crypto tags if this purl offers any generation algorithm
2912
- if (
2913
- cryptoGeneratePurls?.[comp.purl] &&
2914
- Array.from(cryptoGeneratePurls[comp.purl]).length
2915
- ) {
3056
+ let compCryptoPurls = cryptoGeneratePurls?.[comp.purl];
3057
+ const extraCrypto = compKey ? versionlessCrypto[compKey] : undefined;
3058
+ if (extraCrypto?.size) {
3059
+ compCryptoPurls = new Set([...(compCryptoPurls || []), ...extraCrypto]);
3060
+ // The provides section below looks pool entries up by bom-ref, so the
3061
+ // merged set has to be reachable that way too.
3062
+ if (depCryptoPurls) {
3063
+ depCryptoPurls[comp.purl] = compCryptoPurls;
3064
+ if (comp["bom-ref"]) {
3065
+ depCryptoPurls[comp["bom-ref"]] = compCryptoPurls;
3066
+ }
3067
+ }
3068
+ }
3069
+ if (compCryptoPurls && Array.from(compCryptoPurls).length) {
2916
3070
  comp.tags = ["crypto", "crypto-generate"];
2917
3071
  }
2918
3072
  } // for
@@ -2944,14 +3098,14 @@ export async function createEvinseFile(sliceArtefacts, options) {
2944
3098
  }
2945
3099
  // Fix the dependencies section with provides information
2946
3100
  if (
2947
- cryptoGeneratePurls &&
2948
- Object.keys(cryptoGeneratePurls).length &&
3101
+ depCryptoPurls &&
3102
+ Object.keys(depCryptoPurls).length &&
2949
3103
  bomJson.dependencies
2950
3104
  ) {
2951
3105
  const newDependencies = [];
2952
3106
  for (const depObj of bomJson.dependencies) {
2953
- if (depObj.ref && cryptoGeneratePurls[depObj.ref]) {
2954
- const providedAlgos = Array.from(cryptoGeneratePurls[depObj.ref]);
3107
+ if (depObj.ref && depCryptoPurls[depObj.ref]) {
3108
+ const providedAlgos = Array.from(depCryptoPurls[depObj.ref]);
2955
3109
  if (providedAlgos.length) {
2956
3110
  depObj.provides = providedAlgos;
2957
3111
  }
@@ -3196,15 +3350,15 @@ export function collectReachableFrames(_language, reachablesSlice) {
3196
3350
  if (tagStr.includes("crypto")) {
3197
3351
  isCryptoFlow = true;
3198
3352
  }
3353
+ // Join with a space: findCryptoAlgos treats every non-alphanumeric as a
3354
+ // word separator, and a space is the one separator cleanStr keeps.
3199
3355
  if (isCryptoFlow && fnode.code) {
3200
- codeSnippets = `${codeSnippets}\\n${fnode.code}`;
3356
+ codeSnippets = `${codeSnippets} ${fnode.code}`;
3201
3357
  }
3202
- if (
3203
- tagStr.includes("crypto-generate") ||
3204
- fnode.code.includes("encrypt") ||
3205
- fnode.code.includes("decrypt") ||
3206
- fnode.code.includes("sign")
3207
- ) {
3358
+ // Pool membership is tag-driven: a node contributes its pkg: tags only
3359
+ // when the tagger marked it crypto-generate. Code substring tests read
3360
+ // assign/design/signup into encrypt/decrypt/sign and are gone.
3361
+ if (tagStr.includes("crypto-generate")) {
3208
3362
  cpurls = tagStr.split(", ").filter((t) => t.startsWith("pkg:"));
3209
3363
  for (const cpurl of cpurls) {
3210
3364
  if (!cryptoGeneratePurls[cpurl]) {
@@ -3407,6 +3561,155 @@ export function getClassTypeFromSignature(language, typeFullName) {
3407
3561
  return simplifyType(typeFullName);
3408
3562
  }
3409
3563
 
3564
+ const JS_LANGUAGES = ["javascript", "js", "ts", "typescript"];
3565
+
3566
+ /**
3567
+ * Add every location of a set to a purl's locations (Set.add takes a single value).
3568
+ *
3569
+ * @param {Set<string>} target purl location set
3570
+ * @param {Set<string>|undefined} locations locations to add
3571
+ */
3572
+ function addAllLocations(target, locations) {
3573
+ for (const l of locations || []) {
3574
+ target.add(l);
3575
+ }
3576
+ }
3577
+
3578
+ /**
3579
+ * npm package name of a JavaScript module specifier: `@scope/pkg/sub` -> `@scope/pkg`,
3580
+ * `lodash/fp` -> `lodash`. Node.js builtins (`node:fs`, `fs`) are not packages.
3581
+ *
3582
+ * @param {string} module module specifier
3583
+ * @returns {string|undefined} package name
3584
+ */
3585
+ export function jsPackageOfModule(module) {
3586
+ if (!module || module.startsWith("node:") || module.startsWith(".")) {
3587
+ return undefined;
3588
+ }
3589
+ const parts = module.split("/");
3590
+ const pkg = module.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
3591
+ if (isNodeBuiltin(pkg)) {
3592
+ return undefined;
3593
+ }
3594
+ return pkg;
3595
+ }
3596
+
3597
+ function isNodeBuiltin(name) {
3598
+ return NODE_BUILTINS.has(name);
3599
+ }
3600
+
3601
+ const NODE_BUILTINS = new Set(builtinModules);
3602
+
3603
+ const jsPurlsByPackageCache = new WeakMap();
3604
+
3605
+ function jsPurlsByPackage(purlImportsMap) {
3606
+ let byPackage = jsPurlsByPackageCache.get(purlImportsMap);
3607
+ if (!byPackage) {
3608
+ byPackage = {};
3609
+ for (const apurl of Object.keys(purlImportsMap || {})) {
3610
+ try {
3611
+ const purlObj = Purl.parse(apurl);
3612
+ if (purlObj.type !== "npm") {
3613
+ continue;
3614
+ }
3615
+ const name = purlObj.namespace
3616
+ ? `${purlObj.namespace}/${purlObj.name}`
3617
+ : purlObj.name;
3618
+ (byPackage[name] = byPackage[name] || []).push(apurl);
3619
+ } catch (_e) {
3620
+ // not a valid purl
3621
+ }
3622
+ }
3623
+ jsPurlsByPackageCache.set(purlImportsMap, byPackage);
3624
+ }
3625
+ return byPackage;
3626
+ }
3627
+
3628
+ /**
3629
+ * Attribute a JavaScript usage's module-qualified entries to the packages providing them.
3630
+ *
3631
+ * Each call entry with a `module` is a use of that module's package at the call's line. When
3632
+ * the slice's object is an import binding (defined by a require/import naming the module),
3633
+ * every call the object takes part in is a use of that package too.
3634
+ *
3635
+ * @param {Object} ausage usage entry of an object slice
3636
+ * @param {string} fileName file of the slice
3637
+ * @param {Object} purlImportsMap purl -> imported modules and symbols
3638
+ * @param {Object} purlLocationMap purl -> Set of `file#line`
3639
+ */
3640
+ export function attributeJsModules(
3641
+ ausage,
3642
+ fileName,
3643
+ purlImportsMap,
3644
+ purlLocationMap,
3645
+ ) {
3646
+ const byPackage = jsPurlsByPackage(purlImportsMap);
3647
+ const record = (module, line) => {
3648
+ const pkg = jsPackageOfModule(module);
3649
+ if (!pkg || !line) {
3650
+ return;
3651
+ }
3652
+ for (const apurl of byPackage[pkg] || []) {
3653
+ if (!purlLocationMap[apurl]) {
3654
+ purlLocationMap[apurl] = new Set();
3655
+ }
3656
+ purlLocationMap[apurl].add(`${fileName}#${line}`);
3657
+ }
3658
+ };
3659
+ const calls = []
3660
+ .concat(ausage?.invokedCalls || [])
3661
+ .concat(ausage?.argToCalls || []);
3662
+ for (const c of [ausage?.targetObj, ausage?.definedBy, ...calls]) {
3663
+ if (c?.module) {
3664
+ record(c.module, c.lineNumber);
3665
+ }
3666
+ }
3667
+ // Only an import binding stands for its module: a value merely returned by a package's call
3668
+ // (`const s = purl.toString()`) is not a use of that package wherever it goes.
3669
+ const definedBy = ausage?.definedBy;
3670
+ const isImportBinding =
3671
+ definedBy?.name === "require" ||
3672
+ definedBy?.name === "import" ||
3673
+ /^(require|import)\(/.test(definedBy?.code || "");
3674
+ const bindingModule = isImportBinding ? definedBy?.module : undefined;
3675
+ if (bindingModule) {
3676
+ for (const c of calls) {
3677
+ record(bindingModule, c?.lineNumber);
3678
+ }
3679
+ }
3680
+ }
3681
+
3682
+ /**
3683
+ * Attribute a callsites slice's records to the packages providing their modules.
3684
+ *
3685
+ * Each record of `{"callsites": [...]}` (atom `callsites` command) is one external use site
3686
+ * carrying its own file and line; the record is an occurrence of every purl whose package
3687
+ * matches the record's module.
3688
+ *
3689
+ * @param {Object} callsitesSlice callsites slice with a `callsites` array
3690
+ * @param {Object} purlImportsMap purl -> imported modules and symbols
3691
+ * @param {Object} purlLocationMap purl -> Set of `file#line`
3692
+ */
3693
+ export function attributeJsCallsites(
3694
+ callsitesSlice,
3695
+ purlImportsMap,
3696
+ purlLocationMap,
3697
+ ) {
3698
+ const byPackage = jsPurlsByPackage(purlImportsMap);
3699
+ for (const record of callsitesSlice?.callsites || []) {
3700
+ const pkg = jsPackageOfModule(record?.module);
3701
+ if (!pkg || !(record.line > 0)) {
3702
+ continue;
3703
+ }
3704
+ for (const apurl of byPackage[pkg] || []) {
3705
+ if (!purlLocationMap[apurl]) {
3706
+ purlLocationMap[apurl] = new Set();
3707
+ }
3708
+ purlLocationMap[apurl].add(`${record.file}#${record.line}`);
3709
+ }
3710
+ }
3711
+ }
3712
+
3410
3713
  function addToOverrides(lKeyOverrides, atype, fileName, ausageLineNumber) {
3411
3714
  if (!lKeyOverrides[atype]) {
3412
3715
  lKeyOverrides[atype] = new Set();