@cdxgen/cdxgen 13.1.0 → 13.2.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 (186) hide show
  1. package/README.md +8 -7
  2. package/bin/cdxgen.js +16 -3
  3. package/bin/evinse.js +2 -0
  4. package/data/crypto-oid.json +88 -12
  5. package/data/remediations.json +23 -0
  6. package/data/rules/ai-bom.yaml +78 -0
  7. package/data/rules/cargo-rust.yaml +132 -0
  8. package/data/rules/mcp-servers.yaml +122 -0
  9. package/lib/audit/targets.js +6 -1
  10. package/lib/cli/bomAssembly.js +12 -0
  11. package/lib/cli/index.js +192 -1
  12. package/lib/cli/jsBom.js +59 -2
  13. package/lib/cli/jvmBom.js +22 -0
  14. package/lib/cli/managedBom.js +152 -0
  15. package/lib/cli/nativeBom.js +798 -1
  16. package/lib/core/activity.js +120 -2
  17. package/lib/core/env.js +39 -1
  18. package/lib/core/userAgent.js +151 -0
  19. package/lib/ecosystems/cargoCache.js +364 -0
  20. package/lib/ecosystems/caxa.js +13 -5
  21. package/lib/ecosystems/cppEvidence.js +40 -2
  22. package/lib/ecosystems/ecosystems.js +241 -91
  23. package/lib/ecosystems/golem.js +11 -1
  24. package/lib/ecosystems/gradleutils.js +143 -2
  25. package/lib/ecosystems/kosi.js +1256 -0
  26. package/lib/ecosystems/parsers-crystal.js +179 -0
  27. package/lib/ecosystems/parsers-dotnet.js +69 -39
  28. package/lib/ecosystems/parsers-elm.js +816 -0
  29. package/lib/ecosystems/parsers-gleam.js +1 -1
  30. package/lib/ecosystems/parsers-julia.js +268 -0
  31. package/lib/ecosystems/parsers-lua.js +218 -0
  32. package/lib/ecosystems/parsers-misc.js +172 -0
  33. package/lib/ecosystems/parsers-nim.js +221 -0
  34. package/lib/ecosystems/parsers-opam.js +180 -0
  35. package/lib/ecosystems/parsers-perl.js +277 -0
  36. package/lib/ecosystems/parsers-python.js +1 -1
  37. package/lib/ecosystems/parsers-r.js +148 -0
  38. package/lib/ecosystems/parsers-rebar.js +411 -0
  39. package/lib/ecosystems/parsers-rust.js +932 -17
  40. package/lib/ecosystems/parsers-spack.js +204 -0
  41. package/lib/ecosystems/parsers-stack.js +264 -0
  42. package/lib/ecosystems/parsers-terraform.js +194 -0
  43. package/lib/ecosystems/rusi.js +126 -6
  44. package/lib/evinser/evinser.js +111 -1
  45. package/lib/helpers/agenticToolutils.js +925 -0
  46. package/lib/inventory/agentFormulationParser.js +57 -4
  47. package/lib/inventory/aiCollector.js +504 -10
  48. package/lib/inventory/aiInventory.js +12 -0
  49. package/lib/inventory/analyzer.js +488 -30
  50. package/lib/inventory/astWorker.js +6 -0
  51. package/lib/inventory/auditCategories.js +8 -0
  52. package/lib/inventory/cbomutils.js +30 -0
  53. package/lib/inventory/cdxrs.js +7 -2
  54. package/lib/inventory/communityAiConfigParser.js +215 -2
  55. package/lib/inventory/dosai.js +77 -0
  56. package/lib/inventory/envcontext.js +9 -6
  57. package/lib/inventory/evidenceUtils.js +13 -0
  58. package/lib/inventory/fetchBatch.js +74 -2
  59. package/lib/inventory/fetchRate.js +74 -11
  60. package/lib/inventory/forgeEnricher.js +3 -2
  61. package/lib/inventory/jvmToolEnv.js +6 -5
  62. package/lib/inventory/mcpConfigParser.js +166 -2
  63. package/lib/inventory/mcpDiscovery.js +11 -0
  64. package/lib/inventory/mcpServerManifest.js +389 -0
  65. package/lib/inventory/mcpbBundle.js +419 -0
  66. package/lib/inventory/plugins.js +3 -0
  67. package/lib/inventory/protobom.js +54 -88
  68. package/lib/inventory/protobomLoader.js +4 -0
  69. package/lib/inventory/rustFormulationParser.js +1 -1
  70. package/lib/inventory/source.js +10 -4
  71. package/lib/inventory/stylePackageRefs.js +41 -0
  72. package/lib/inventory/svelteUtils.js +524 -0
  73. package/lib/parsers/huggingfaceUtils.js +15 -7
  74. package/lib/parsers/onnx.js +294 -0
  75. package/lib/parsers/safetensors.js +114 -0
  76. package/lib/stages/postgen/introspection/markers.js +29 -6
  77. package/lib/stages/postgen/postgen.js +169 -1
  78. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  79. package/package.json +25 -25
  80. package/types/lib/audit/targets.d.ts.map +1 -1
  81. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  82. package/types/lib/cli/index.d.ts +2 -2
  83. package/types/lib/cli/index.d.ts.map +1 -1
  84. package/types/lib/cli/jsBom.d.ts +23 -0
  85. package/types/lib/cli/jsBom.d.ts.map +1 -1
  86. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  87. package/types/lib/cli/managedBom.d.ts +33 -0
  88. package/types/lib/cli/managedBom.d.ts.map +1 -1
  89. package/types/lib/cli/nativeBom.d.ts +88 -8
  90. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  91. package/types/lib/core/activity.d.ts.map +1 -1
  92. package/types/lib/core/env.d.ts +21 -0
  93. package/types/lib/core/env.d.ts.map +1 -1
  94. package/types/lib/core/userAgent.d.ts +56 -0
  95. package/types/lib/core/userAgent.d.ts.map +1 -0
  96. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  97. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  98. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  99. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  100. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  101. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  102. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  103. package/types/lib/ecosystems/gradleutils.d.ts +20 -0
  104. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  105. package/types/lib/ecosystems/kosi.d.ts +73 -0
  106. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  107. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  108. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  109. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  110. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  111. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  112. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  113. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  114. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  115. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  116. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  117. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  118. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  120. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  121. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  122. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  123. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  124. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  125. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  126. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  127. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  128. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  129. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  130. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  131. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  132. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  133. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  134. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  135. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  136. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  137. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  138. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  139. package/types/lib/evinser/evinser.d.ts +29 -0
  140. package/types/lib/evinser/evinser.d.ts.map +1 -1
  141. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  142. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  143. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  144. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  145. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  146. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  147. package/types/lib/inventory/auditCategories.d.ts +4 -0
  148. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  149. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  150. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  151. package/types/lib/inventory/dosai.d.ts +23 -0
  152. package/types/lib/inventory/dosai.d.ts.map +1 -1
  153. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  154. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  155. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  156. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  157. package/types/lib/inventory/fetchRate.d.ts +9 -6
  158. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  159. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  160. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  161. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  162. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  163. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  164. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  165. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  166. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  167. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  168. package/types/lib/inventory/plugins.d.ts.map +1 -1
  169. package/types/lib/inventory/protobom.d.ts +18 -13
  170. package/types/lib/inventory/protobom.d.ts.map +1 -1
  171. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  172. package/types/lib/inventory/source.d.ts.map +1 -1
  173. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  174. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  175. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  176. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  177. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  178. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  179. package/types/lib/parsers/onnx.d.ts +28 -0
  180. package/types/lib/parsers/onnx.d.ts.map +1 -0
  181. package/types/lib/parsers/safetensors.d.ts +22 -0
  182. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  183. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  184. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  185. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  186. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -66,11 +66,13 @@ import {
66
66
  prefetchEnabled,
67
67
  prefetchedResponse,
68
68
  prefetchJson,
69
+ withHostRateLimit,
69
70
  } from "../inventory/fetchBatch.js";
70
71
  import {
71
72
  applyPurl,
72
73
  encodeForPurl,
73
74
  genericPurl,
75
+ nugetPurl,
74
76
  pypiPurl,
75
77
  } from "../inventory/purl.js";
76
78
  import {
@@ -80,6 +82,10 @@ import {
80
82
  } from "../inventory/spdx.js";
81
83
  import { extractLicenseText, extractRepoUrl } from "../parsers/htmlExtract.js";
82
84
  import { xml2js } from "../parsers/xml.js";
85
+ import {
86
+ localCargoMetadataEnabled,
87
+ readCargoCacheMetadata,
88
+ } from "./cargoCache.js";
83
89
  import {
84
90
  applyPypiClassifierMetadata,
85
91
  applyPypiModuleNames,
@@ -814,11 +820,13 @@ export async function fetchPomXml({ urlPrefix, group, name, version }) {
814
820
  // A direct POM may have been prefetched by getMvnMetadata's batch round.
815
821
  // Parent POMs (fetched from inside fetchPomXmlAsJson) are not prefetched
816
822
  // because their URL depends on the direct POM's content, so they fall
817
- // through to the serial request unchanged.
823
+ // through to the serial request. That fallback still reaches Maven Central
824
+ // and still counts against the same budget as the batch round, so it queues
825
+ // behind the same per-host gate rather than a second, ungoverned one.
818
826
  try {
819
827
  const res =
820
828
  prefetchedResponse(prefetchedPoms, fullUrl) ||
821
- (await cdxgenAgent.get(fullUrl));
829
+ (await withHostRateLimit(fullUrl, () => cdxgenAgent.get(fullUrl)));
822
830
  return res.body;
823
831
  } catch (_err) {
824
832
  return undefined;
@@ -1792,24 +1800,61 @@ function getGoPkgFullName(group, name) {
1792
1800
  /**
1793
1801
  * Method to retrieve metadata for rust packages by querying crates
1794
1802
  *
1803
+ * The local Cargo registry is consulted first, because crates.io's crawler
1804
+ * policy allows one request per second and a populated `~/.cargo` already
1805
+ * holds the license, description, repository, checksum and yanked flag for
1806
+ * every crate the build compiled.
1807
+ *
1808
+ * What it does not hold is publisher identity, so a caller that needs the
1809
+ * publisher-drift and release-cadence signals — the predictive audit does —
1810
+ * passes `preferLocalCache: false` and takes the slower registry path.
1811
+ *
1795
1812
  * @param {Array} pkgList Package list
1813
+ * @param {Object} [options] Options
1814
+ * @param {boolean} [options.preferLocalCache=true] Answer from the local Cargo
1815
+ * registry where it can, and query crates.io only for the rest.
1796
1816
  */
1797
- export async function getCratesMetadata(pkgList) {
1817
+ export async function getCratesMetadata(pkgList, options = {}) {
1798
1818
  const CRATES_URL =
1799
1819
  readEnvironmentVariable("RUST_CRATES_URL") ||
1800
1820
  "https://crates.io/api/v1/crates/";
1801
1821
  const cdepList = [];
1822
+ // The local Cargo registry answers first. crates.io allows one request per
1823
+ // second, so a populated ~/.cargo turns minutes of waiting into a few file
1824
+ // reads; only the crates it cannot account for reach the network.
1825
+ const servedLocally = new Set();
1826
+ const preferLocalCache = options.preferLocalCache !== false;
1827
+ if (preferLocalCache && localCargoMetadataEnabled()) {
1828
+ for (const p of pkgList) {
1829
+ if (!p?.name || !p?.version || p.version === "workspace") {
1830
+ continue;
1831
+ }
1832
+ if (applyCargoCacheMetadata(p)) {
1833
+ servedLocally.add(p);
1834
+ }
1835
+ }
1836
+ if (DEBUG_MODE && servedLocally.size) {
1837
+ console.log(
1838
+ `Resolved ${servedLocally.size} of ${pkgList.length} crate(s) from the local Cargo registry.`,
1839
+ );
1840
+ }
1841
+ }
1842
+ const needsRegistry = pkgList.filter(
1843
+ (p) =>
1844
+ p?.name &&
1845
+ p?.version &&
1846
+ p.version !== "workspace" &&
1847
+ !servedLocally.has(p),
1848
+ );
1802
1849
  // Two URLs per crate — the crate document and its owners — fetched for the
1803
1850
  // whole list at once. The `workspace` guard below is mirrored here so the
1804
1851
  // batch does not request anything the loop would have skipped.
1805
1852
  const prefetched = await prefetchJson(
1806
1853
  prefetchEnabled()
1807
- ? pkgList
1808
- .filter((p) => p?.name && p?.version && p.version !== "workspace")
1809
- .flatMap((p) => [
1810
- { url: CRATES_URL + p.name },
1811
- { url: `${CRATES_URL + p.name}/owners` },
1812
- ])
1854
+ ? needsRegistry.flatMap((p) => [
1855
+ { url: CRATES_URL + p.name },
1856
+ { url: `${CRATES_URL + p.name}/owners` },
1857
+ ])
1813
1858
  : [],
1814
1859
  );
1815
1860
  for (const p of pkgList) {
@@ -1818,23 +1863,35 @@ export async function getCratesMetadata(pkgList) {
1818
1863
  cdepList.push(p);
1819
1864
  continue;
1820
1865
  }
1866
+ if (servedLocally.has(p)) {
1867
+ cdepList.push(p);
1868
+ continue;
1869
+ }
1821
1870
  if (DEBUG_MODE) {
1822
1871
  console.log(`Querying crates.io for ${p.name}@${p.version}`);
1823
1872
  }
1824
1873
  const crateUrl = CRATES_URL + p.name;
1874
+ // A prefetch miss falls back to a direct request, which must queue
1875
+ // behind the same crates.io gate the batch pool uses: the registry
1876
+ // allows one request per second and does not care which code path in
1877
+ // this process issued it.
1825
1878
  const res =
1826
1879
  prefetchedResponse(prefetched, crateUrl) ||
1827
- (await cdxgenAgent.get(crateUrl, {
1828
- responseType: "json",
1829
- }));
1880
+ (await withHostRateLimit(crateUrl, () =>
1881
+ cdxgenAgent.get(crateUrl, {
1882
+ responseType: "json",
1883
+ }),
1884
+ ));
1830
1885
  let ownersRes;
1831
1886
  try {
1832
1887
  const ownersUrl = `${crateUrl}/owners`;
1833
1888
  ownersRes =
1834
1889
  prefetchedResponse(prefetched, ownersUrl) ||
1835
- (await cdxgenAgent.get(ownersUrl, {
1836
- responseType: "json",
1837
- }));
1890
+ (await withHostRateLimit(ownersUrl, () =>
1891
+ cdxgenAgent.get(ownersUrl, {
1892
+ responseType: "json",
1893
+ }),
1894
+ ));
1838
1895
  } catch (_err) {
1839
1896
  ownersRes = undefined;
1840
1897
  }
@@ -1904,6 +1961,86 @@ export async function getCratesMetadata(pkgList) {
1904
1961
  return cdepList;
1905
1962
  }
1906
1963
 
1964
+ /**
1965
+ * Apply the local Cargo registry's view of a crate to its component.
1966
+ *
1967
+ * Returns whether the component is complete enough to skip crates.io. The bar
1968
+ * is the license: `getCratesMetadata` runs only under `FETCH_LICENSE`, so a
1969
+ * crate whose license the local registry cannot name has not answered the
1970
+ * question that was asked and still needs the network. That happens when the
1971
+ * index knows a version but Cargo never extracted its sources — a crate the
1972
+ * lockfile pins but the build never compiled.
1973
+ *
1974
+ * Publisher identity has no local equivalent, so a component served from here
1975
+ * carries the publish time and yanked flag but none of the publisher-drift
1976
+ * properties; `cdx:cargo:metadataSource` records which it was.
1977
+ *
1978
+ * @param {Object} p Package component to enrich in place.
1979
+ * @returns {boolean} true when crates.io can be skipped for this crate.
1980
+ */
1981
+ function applyCargoCacheMetadata(p) {
1982
+ const local = readCargoCacheMetadata(p.name, p.version);
1983
+ if (!local?.license) {
1984
+ return false;
1985
+ }
1986
+ p.license = local.license;
1987
+ if (local.description) {
1988
+ p.description = local.description;
1989
+ }
1990
+ if (local.repository) {
1991
+ p.repository = { url: local.repository };
1992
+ }
1993
+ if (local.homepage && local.homepage !== local.repository) {
1994
+ p.homepage = { url: local.homepage };
1995
+ }
1996
+ if (!p._integrity && local.checksum) {
1997
+ p._integrity = normalizeCargoIntegrity(local.checksum);
1998
+ }
1999
+ if (!p.properties) {
2000
+ p.properties = [];
2001
+ }
2002
+ p.properties.push({
2003
+ name: "cdx:cargo:metadataSource",
2004
+ value: "local-registry",
2005
+ });
2006
+ if (local.rustVersion) {
2007
+ p.properties.push({
2008
+ name: "cdx:cargo:rust_version",
2009
+ value: local.rustVersion,
2010
+ });
2011
+ }
2012
+ if (local.latestVersion) {
2013
+ p.properties.push({
2014
+ name: "cdx:cargo:latest_version",
2015
+ value: local.latestVersion,
2016
+ });
2017
+ }
2018
+ if (typeof local.yanked === "boolean") {
2019
+ p.properties.push({
2020
+ name: "cdx:cargo:yanked",
2021
+ value: String(local.yanked),
2022
+ });
2023
+ }
2024
+ if (local.publishTime) {
2025
+ p.properties.push({
2026
+ name: "cdx:cargo:publishTime",
2027
+ value: local.publishTime,
2028
+ });
2029
+ }
2030
+ if (local.features && Object.keys(local.features).length) {
2031
+ p.properties.push({
2032
+ name: "cdx:cargo:features",
2033
+ value: JSON.stringify(local.features),
2034
+ });
2035
+ }
2036
+ // The download path is a documented, stable URL shape rather than something
2037
+ // the API had to tell us, so it costs no request to state it.
2038
+ p.distribution = {
2039
+ url: `https://crates.io/api/v1/crates/${p.name}/${p.version}/download`,
2040
+ };
2041
+ return true;
2042
+ }
2043
+
1907
2044
  /**
1908
2045
  * Method to retrieve metadata for dart packages by querying pub.dev
1909
2046
  *
@@ -2168,17 +2305,23 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
2168
2305
  try {
2169
2306
  const sha = await checksumFile("sha1", jf);
2170
2307
  const searchurl = `https://central.sonatype.com/solrsearch/select?q=1:%22${sha}%22&rows=20&wt=json`;
2171
- const res = await cdxgenAgent.get(searchurl, {
2172
- responseType: "json",
2173
- timeout: {
2174
- lookup: 1000,
2175
- connect: 5000,
2176
- secureConnect: 5000,
2177
- socket: 1000,
2178
- send: 10000,
2179
- response: 1000,
2180
- },
2181
- });
2308
+ // One Solr query per unidentified jar, against Central's origin
2309
+ // rather than its CDN. A directory of jars issues these back to
2310
+ // back, so they go through the same per-host gate the batch pool
2311
+ // uses (500 ms, serial) instead of a second, ungoverned path.
2312
+ const res = await withHostRateLimit(searchurl, () =>
2313
+ cdxgenAgent.get(searchurl, {
2314
+ responseType: "json",
2315
+ timeout: {
2316
+ lookup: 1000,
2317
+ connect: 5000,
2318
+ secureConnect: 5000,
2319
+ socket: 1000,
2320
+ send: 10000,
2321
+ response: 1000,
2322
+ },
2323
+ }),
2324
+ );
2182
2325
  const data = res?.body ? res.body["response"] : undefined;
2183
2326
  if (data && data["numFound"] === 1) {
2184
2327
  const jarInfo = data["docs"][0];
@@ -2480,6 +2623,12 @@ async function unresolvedJarComponents(unresolvedJars, jarFile, tempDir) {
2480
2623
  return components;
2481
2624
  }
2482
2625
 
2626
+ /**
2627
+ * Property recording the NuGet release a versionless component's metadata was
2628
+ * read from, so the description and licence in the output can be traced.
2629
+ */
2630
+ const METADATA_VERSION_PROP = "cdx:nuget:metadata_version";
2631
+
2483
2632
  async function getNugetUrl() {
2484
2633
  const req = "https://api.nuget.org/v3/index.json";
2485
2634
  const res = await cdxgenAgent.get(req, {
@@ -2555,60 +2704,46 @@ async function queryNuget(p, NUGET_URL) {
2555
2704
  if (!items?.[0]) {
2556
2705
  return [np, newBody, body];
2557
2706
  }
2558
- if (items[0] && !items[0].items) {
2559
- if (!p.version || p.version === "0.0.0" || p.version === "latest") {
2560
- const upper = items[items.length - 1].upper;
2561
- np.version = setLatestVersion(upper);
2562
- }
2707
+ // A component that states no version at all is not the same as one carrying a
2708
+ // `latest` or `0.0.0` placeholder. The placeholder asks to be replaced, so the
2709
+ // registry's newest release becomes the component's version. A missing version
2710
+ // means the manifest never said which release is in use - a `<Reference>` to an
2711
+ // assembly is the usual source - and the newest release is then only a key for
2712
+ // looking metadata up, never a version to write back. See #4359.
2713
+ const versionStated = p.version && !["0.0.0", "latest"].includes(p.version);
2714
+ let lookupVersion = p.version;
2715
+ if (!versionStated) {
2716
+ lookupVersion = setLatestVersion(items[items.length - 1].upper);
2717
+ np.version = p.version ? lookupVersion : undefined;
2718
+ np.metadataVersion = lookupVersion;
2719
+ }
2720
+ if (lookupVersion) {
2563
2721
  for (const item of items) {
2564
- if (np.version) {
2565
- const lower = compare(
2566
- coerce(item.lower, { loose: true }),
2567
- coerce(np.version, { loose: true }),
2568
- );
2569
- const upper = compare(
2570
- coerce(item.upper, { loose: true }),
2571
- coerce(np.version, { loose: true }),
2572
- );
2573
- if (lower !== 1 && upper !== -1) {
2574
- res = await cdxgenAgent.get(item["@id"], { responseType: "json" });
2575
- for (const i of res.body.items.reverse()) {
2576
- if (
2577
- i.catalogEntry &&
2578
- i.catalogEntry.version === coerceUp(np.version)
2579
- ) {
2580
- newBody.push(i);
2581
- return [np, newBody];
2582
- }
2583
- }
2584
- }
2722
+ const lower = compare(
2723
+ coerce(item.lower, { loose: true }),
2724
+ coerce(lookupVersion, { loose: true }),
2725
+ );
2726
+ const upper = compare(
2727
+ coerce(item.upper, { loose: true }),
2728
+ coerce(lookupVersion, { loose: true }),
2729
+ );
2730
+ if (lower === 1 || upper === -1) {
2731
+ continue;
2585
2732
  }
2586
- }
2587
- } else {
2588
- if (!p.version || p.version === "0.0.0" || p.version === "latest") {
2589
- const upper = items[items.length - 1].upper;
2590
- np.version = setLatestVersion(upper);
2591
- }
2592
- if (np.version) {
2593
- for (const item of items) {
2594
- const lower = compare(
2595
- coerce(item.lower, { loose: true }),
2596
- coerce(np.version, { loose: true }),
2597
- );
2598
- const upper = compare(
2599
- coerce(item.upper, { loose: true }),
2600
- coerce(np.version, { loose: true }),
2601
- );
2602
- if (lower !== 1 && upper !== -1) {
2603
- for (const i of item.items.reverse()) {
2604
- if (
2605
- i.catalogEntry &&
2606
- i.catalogEntry.version === coerceUp(np.version)
2607
- ) {
2608
- newBody.push(i);
2609
- return [np, newBody];
2610
- }
2611
- }
2733
+ // The registration index either inlines the leaves of each page or points
2734
+ // at a page that must be fetched.
2735
+ let leaves = item.items;
2736
+ if (!leaves) {
2737
+ res = await cdxgenAgent.get(item["@id"], { responseType: "json" });
2738
+ leaves = res.body.items;
2739
+ }
2740
+ for (const i of leaves.reverse()) {
2741
+ if (
2742
+ i.catalogEntry &&
2743
+ i.catalogEntry.version === coerceUp(lookupVersion)
2744
+ ) {
2745
+ newBody.push(i);
2746
+ return [np, newBody];
2612
2747
  }
2613
2748
  }
2614
2749
  }
@@ -2673,25 +2808,36 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
2673
2808
  [np, newBody] = await queryNuget(p, NUGET_URL);
2674
2809
  if (p.version !== np.version) {
2675
2810
  const oldRef = p["bom-ref"];
2676
- p["bom-ref"] = decodeURIComponent(
2677
- build({
2678
- type: "nuget",
2679
- namespace: "" || null,
2680
- name: np.name,
2681
- version: np.version || null,
2682
- }),
2683
- );
2684
- depRepList[oldRef] = p["bom-ref"];
2811
+ // The purl is rebuilt alongside the bom-ref. Leaving it behind let a
2812
+ // component state one version in `version` and another - or none - in
2813
+ // its purl, which is what a scanner range matches on. See #4359.
2685
2814
  p.version = np.version;
2815
+ applyPurl(p, nugetPurl(np.name, np.version));
2816
+ if (oldRef && p["bom-ref"] !== oldRef) {
2817
+ depRepList[oldRef] = p["bom-ref"];
2818
+ }
2686
2819
  }
2687
2820
  if (newBody && newBody.length > 0) {
2688
2821
  body = newBody[0];
2689
2822
  }
2690
2823
  if (body) {
2691
2824
  metadata_cache[cacheKey] = body;
2692
- // Set the latest version in case it is missing
2693
- if (!p.version && body.catalogEntry.version) {
2694
- p.version = body.catalogEntry.version;
2825
+ // The component states no version, so the description and licence
2826
+ // below were read from whichever release the lookup fell back to.
2827
+ // That release is recorded rather than adopted as the version: a
2828
+ // version cdxgen chose is not a version the project declared.
2829
+ if (!p.version && np.metadataVersion) {
2830
+ p.properties = p.properties || [];
2831
+ if (
2832
+ !p.properties.some(
2833
+ (prop) => prop.name === METADATA_VERSION_PROP,
2834
+ )
2835
+ ) {
2836
+ p.properties.push({
2837
+ name: METADATA_VERSION_PROP,
2838
+ value: np.metadataVersion,
2839
+ });
2840
+ }
2695
2841
  }
2696
2842
  p.description = body.catalogEntry.description;
2697
2843
  if (body.catalogEntry.authors) {
@@ -2720,8 +2866,12 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
2720
2866
  }
2721
2867
  if (body.catalogEntry.projectUrl) {
2722
2868
  p.repository = { url: body.catalogEntry.projectUrl };
2869
+ // A versionless component gets the package's landing page. Pasting
2870
+ // an undefined version into the path produced a 404 link.
2723
2871
  p.homepage = {
2724
- url: `https://www.nuget.org/packages/${p.name}/${p.version}/`,
2872
+ url: p.version
2873
+ ? `https://www.nuget.org/packages/${p.name}/${p.version}/`
2874
+ : `https://www.nuget.org/packages/${p.name}/`,
2725
2875
  };
2726
2876
  if (
2727
2877
  (!p.license || typeof p.license === "string") &&
@@ -1360,11 +1360,21 @@ function normalizedGolemDataFlowCallGraphMode(options = {}) {
1360
1360
  return callgraphMode;
1361
1361
  }
1362
1362
 
1363
+ function availableCpuCount() {
1364
+ try {
1365
+ return cpus()?.length || 1;
1366
+ } catch {
1367
+ // Deno denies the probe without --allow-sys=cpus; a worker-count hint
1368
+ // must not abort the analysis, so fall back to a single worker.
1369
+ return 1;
1370
+ }
1371
+ }
1372
+
1363
1373
  function appendGolemDataFlowArgs(args, dataFlowMode, options = {}) {
1364
1374
  if (dataFlowMode === "none") {
1365
1375
  return;
1366
1376
  }
1367
- const cpuCount = cpus()?.length || 1;
1377
+ const cpuCount = availableCpuCount();
1368
1378
  const performanceWorkerCount = Math.max(1, Math.min(cpuCount, 4));
1369
1379
  const dataFlowCallgraph = normalizedGolemDataFlowCallGraphMode(options);
1370
1380
  const dataFlowPacks =
@@ -1,9 +1,10 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { chmodSync } from "node:fs";
2
+ import { chmodSync, readFileSync } from "node:fs";
3
3
  import { homedir, platform } from "node:os";
4
4
  import { basename, join, resolve } from "node:path";
5
5
 
6
6
  import { build } from "@cdxgen/cdx-purl";
7
+ import * as toml from "smol-toml";
7
8
 
8
9
  import {
9
10
  DEBUG_MODE,
@@ -23,7 +24,7 @@ import { thoughtLog } from "../core/logger.js";
23
24
  import { isWin } from "../core/paths.js";
24
25
  import { collectJarNS } from "../inventory/deps.js";
25
26
  import { minimumGradleVersionForJava } from "../inventory/jvmToolEnv.js";
26
- import { applyPurl } from "../inventory/purl.js";
27
+ import { applyPurl, tryBuildPurl } from "../inventory/purl.js";
27
28
  import { parsePkgJson } from "./parsers-js.js";
28
29
 
29
30
  /**
@@ -1386,3 +1387,143 @@ export function getMavenCommand(srcPath, rootPath) {
1386
1387
  }
1387
1388
  return mavenCmd;
1388
1389
  }
1390
+
1391
+ /**
1392
+ * Parse a Gradle version catalog (`gradle/libs.versions.toml`).
1393
+ *
1394
+ * A catalog declares coordinates the build scripts then reference by alias.
1395
+ * It is a declaration, not a resolution: entries may go unused, and their
1396
+ * versions may be references into the `[versions]` table. Because of that,
1397
+ * catalog components are emitted only as a fallback when Gradle itself
1398
+ * produced no dependency information, and every component carries a
1399
+ * `cdx:gradle:catalog=true` property so consumers can tell declared catalog
1400
+ * entries from a resolved graph.
1401
+ *
1402
+ * Only libraries whose version resolves to a literal (inline or via
1403
+ * `version.ref`) are emitted; rich or unresolved versions would produce purls
1404
+ * that identify no artifact. Plugins are skipped because they are build-time
1405
+ * only.
1406
+ *
1407
+ * @param {string} catalogFile Path to `libs.versions.toml`
1408
+ * @returns {Object[]} Package records for resolvable library entries
1409
+ */
1410
+ export function parseGradleVersionCatalog(catalogFile) {
1411
+ let catalog;
1412
+ try {
1413
+ catalog = toml.parse(readFileSync(catalogFile, "utf-8"));
1414
+ } catch (error) {
1415
+ console.warn(`Failed to parse ${catalogFile}: ${error.message}`);
1416
+ return [];
1417
+ }
1418
+ const versions =
1419
+ catalog?.versions && typeof catalog.versions === "object"
1420
+ ? catalog.versions
1421
+ : {};
1422
+ const libraries =
1423
+ catalog?.libraries && typeof catalog.libraries === "object"
1424
+ ? catalog.libraries
1425
+ : {};
1426
+ const pkgList = [];
1427
+ for (const entry of Object.values(libraries)) {
1428
+ const coordinate = catalogCoordinate(entry);
1429
+ if (!coordinate) {
1430
+ continue;
1431
+ }
1432
+ const version = resolveCatalogVersion(entry, versions);
1433
+ if (!version) {
1434
+ continue;
1435
+ }
1436
+ const { group, name } = coordinate;
1437
+ // A catalog is authored input, but a malformed coordinate must cost the
1438
+ // one entry, not the whole scan.
1439
+ const purl = tryBuildPurl({
1440
+ type: "maven",
1441
+ namespace: group,
1442
+ name,
1443
+ version,
1444
+ });
1445
+ if (!purl) {
1446
+ continue;
1447
+ }
1448
+ pkgList.push({
1449
+ group,
1450
+ name,
1451
+ version,
1452
+ type: "library",
1453
+ scope: "required",
1454
+ purl,
1455
+ "bom-ref": decodeURIComponent(purl),
1456
+ properties: [
1457
+ { name: "internal:SrcFile", value: catalogFile },
1458
+ { name: "cdx:gradle:catalog", value: "true" },
1459
+ ],
1460
+ evidence: {
1461
+ identity: {
1462
+ field: "purl",
1463
+ confidence: 0.5,
1464
+ methods: [
1465
+ {
1466
+ technique: "source-code-analysis",
1467
+ confidence: 0.5,
1468
+ value: `Filename ${catalogFile}`,
1469
+ },
1470
+ ],
1471
+ },
1472
+ },
1473
+ });
1474
+ }
1475
+ return pkgList;
1476
+ }
1477
+
1478
+ /**
1479
+ * Read the Maven coordinate of one catalog entry.
1480
+ *
1481
+ * A catalog writes the coordinate either as `module = "group:name"` or as a
1482
+ * separate `group` and `name` pair; both forms are equivalent and both appear
1483
+ * in real catalogs.
1484
+ *
1485
+ * @param {object} entry Catalog library entry
1486
+ * @returns {{group: string, name: string}|undefined} Coordinate, or undefined
1487
+ */
1488
+ function catalogCoordinate(entry) {
1489
+ const module = typeof entry?.module === "string" ? entry.module : undefined;
1490
+ if (module?.includes(":")) {
1491
+ const separator = module.indexOf(":");
1492
+ return {
1493
+ group: module.slice(0, separator),
1494
+ name: module.slice(separator + 1),
1495
+ };
1496
+ }
1497
+ if (typeof entry?.group === "string" && typeof entry?.name === "string") {
1498
+ return { group: entry.group, name: entry.name };
1499
+ }
1500
+ return undefined;
1501
+ }
1502
+
1503
+ /**
1504
+ * Resolve the version of one catalog entry.
1505
+ *
1506
+ * An entry may pin `version = "1.2.3"` directly or reference the versions
1507
+ * table with `version.ref = "alias"`. Version references that are missing, or
1508
+ * that carry Gradle rich-version syntax (ranges, `!!`, separators), resolve
1509
+ * to nothing and the entry is skipped.
1510
+ *
1511
+ * @param {object} entry Catalog library entry
1512
+ * @param {object} versions Parsed `[versions]` table
1513
+ * @returns {string|undefined} Concrete version, or undefined
1514
+ */
1515
+ function resolveCatalogVersion(entry, versions) {
1516
+ const raw =
1517
+ typeof entry?.version === "string"
1518
+ ? entry.version
1519
+ : typeof entry?.version?.ref === "string"
1520
+ ? versions[entry.version.ref]
1521
+ : undefined;
1522
+ if (typeof raw !== "string" || !raw) {
1523
+ return undefined;
1524
+ }
1525
+ if (/[[\](),!]|->|\.\./u.test(raw)) {
1526
+ return undefined;
1527
+ }
1528
+ return raw;
1529
+ }