@cdxgen/cdxgen 13.3.0 → 13.4.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 (255) hide show
  1. package/README.md +9 -6
  2. package/bin/cdxgen.js +90 -26
  3. package/bin/evinse.js +18 -1
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +45 -15
  6. package/bin/tracebom.js +5 -1
  7. package/bin/validate.js +84 -18
  8. package/bin/verify.js +155 -86
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +61 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/index.js +113 -54
  15. package/lib/cli/jsBom.js +50 -29
  16. package/lib/cli/jvmBom.js +665 -138
  17. package/lib/cli/managedBom.js +320 -200
  18. package/lib/cli/nativeBom.js +139 -30
  19. package/lib/core/activity.js +12 -0
  20. package/lib/core/env.js +17 -9
  21. package/lib/core/fs.js +3 -2
  22. package/lib/core/httpClient.js +159 -24
  23. package/lib/core/pluginRun.js +184 -0
  24. package/lib/core/processTree.js +100 -0
  25. package/lib/core/runState.js +39 -0
  26. package/lib/core/toml.js +72 -0
  27. package/lib/ecosystems/asarutils.js +4 -2
  28. package/lib/ecosystems/bunutils.js +5 -7
  29. package/lib/ecosystems/cargoCache.js +231 -8
  30. package/lib/ecosystems/cmakeResolver.js +10 -21
  31. package/lib/ecosystems/cppBuildContext.js +631 -0
  32. package/lib/ecosystems/cppEvidence.js +337 -22
  33. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  34. package/lib/ecosystems/denoutils.js +93 -66
  35. package/lib/ecosystems/ecosystems.js +1967 -788
  36. package/lib/ecosystems/golem.js +28 -12
  37. package/lib/ecosystems/gradleutils.js +314 -30
  38. package/lib/ecosystems/jsEvidence.js +85 -24
  39. package/lib/ecosystems/kosi.js +21 -4
  40. package/lib/ecosystems/npmutils.js +64 -0
  41. package/lib/ecosystems/parsers-dotnet.js +567 -151
  42. package/lib/ecosystems/parsers-elm.js +46 -28
  43. package/lib/ecosystems/parsers-gleam.js +3 -4
  44. package/lib/ecosystems/parsers-go.js +324 -61
  45. package/lib/ecosystems/parsers-js.js +92 -26
  46. package/lib/ecosystems/parsers-julia.js +2 -3
  47. package/lib/ecosystems/parsers-jvm.js +204 -12
  48. package/lib/ecosystems/parsers-misc.js +182 -55
  49. package/lib/ecosystems/parsers-python.js +141 -10
  50. package/lib/ecosystems/parsers-rust.js +4 -4
  51. package/lib/ecosystems/parsers-terraform.js +1573 -19
  52. package/lib/ecosystems/pythonutils.js +102 -6
  53. package/lib/ecosystems/remote/huggingface.js +3 -0
  54. package/lib/ecosystems/rubyutils.js +67 -17
  55. package/lib/ecosystems/rusi.js +22 -9
  56. package/lib/ecosystems/sbtutils.js +243 -187
  57. package/lib/ecosystems/scalacliutils.js +342 -0
  58. package/lib/ecosystems/scalasem.js +2618 -0
  59. package/lib/ecosystems/terraformRegistry.js +288 -0
  60. package/lib/ecosystems/utils.js +2 -0
  61. package/lib/ecosystems/vendoredCode.js +156 -0
  62. package/lib/evinser/evinser.js +578 -69
  63. package/lib/helpers/agenticToolutils.js +2 -2
  64. package/lib/helpers/bomSigner.js +576 -128
  65. package/lib/inventory/aiCollector.js +4 -1
  66. package/lib/inventory/analyzer.js +110 -127
  67. package/lib/inventory/atomUtils.js +171 -48
  68. package/lib/inventory/auditCategories.js +7 -0
  69. package/lib/inventory/buildToolRateLimit.js +144 -0
  70. package/lib/inventory/cacheCommands.js +2 -1
  71. package/lib/inventory/cmakeBuildDirs.js +254 -0
  72. package/lib/inventory/deps.js +501 -124
  73. package/lib/inventory/dosai.js +248 -44
  74. package/lib/inventory/dosaiParsers.js +310 -15
  75. package/lib/inventory/dynamic.js +52 -6
  76. package/lib/inventory/evidenceUtils.js +154 -109
  77. package/lib/inventory/fetchBatch.js +574 -38
  78. package/lib/inventory/fetchDiskCache.js +597 -0
  79. package/lib/inventory/forgeEnricher.js +14 -5
  80. package/lib/inventory/jvmLocalRepos.js +917 -0
  81. package/lib/inventory/mcpConfigParser.js +2 -3
  82. package/lib/inventory/parentSelection.js +146 -0
  83. package/lib/inventory/rustFormulationParser.js +3 -4
  84. package/lib/inventory/scalaCoords.js +153 -0
  85. package/lib/inventory/source.js +8 -8
  86. package/lib/inventory/spdx.js +101 -0
  87. package/lib/inventory/traceRunner.js +88 -7
  88. package/lib/inventory/tracedCrypto.js +294 -0
  89. package/lib/managers/docker.js +33 -32
  90. package/lib/managers/oci.js +24 -2
  91. package/lib/parsers/cmakeCache.js +69 -0
  92. package/lib/parsers/cmakeLists.js +16 -2
  93. package/lib/parsers/cmakePresets.js +400 -0
  94. package/lib/parsers/compileCommands.js +557 -0
  95. package/lib/parsers/strictJson.js +322 -0
  96. package/lib/parsers/terraformConfig.js +1073 -0
  97. package/lib/parsers/terraformSource.js +652 -0
  98. package/lib/server/server.js +23 -0
  99. package/lib/stages/postgen/introspection/markers.js +1 -1
  100. package/lib/stages/postgen/postgen.js +15 -1
  101. package/lib/stages/postgen/specVersionCompat.js +32 -0
  102. package/lib/validator/index.js +25 -10
  103. package/lib/validator/reporters/json.js +1 -0
  104. package/package.json +21 -21
  105. package/types/lib/audit/index.d.ts.map +1 -1
  106. package/types/lib/cli/index.d.ts.map +1 -1
  107. package/types/lib/cli/jsBom.d.ts.map +1 -1
  108. package/types/lib/cli/jvmBom.d.ts +24 -1
  109. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  110. package/types/lib/cli/managedBom.d.ts +7 -4
  111. package/types/lib/cli/managedBom.d.ts.map +1 -1
  112. package/types/lib/cli/nativeBom.d.ts +3 -1
  113. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  114. package/types/lib/core/activity.d.ts.map +1 -1
  115. package/types/lib/core/env.d.ts +9 -0
  116. package/types/lib/core/env.d.ts.map +1 -1
  117. package/types/lib/core/fs.d.ts.map +1 -1
  118. package/types/lib/core/httpClient.d.ts +13 -8
  119. package/types/lib/core/httpClient.d.ts.map +1 -1
  120. package/types/lib/core/pluginRun.d.ts +97 -0
  121. package/types/lib/core/pluginRun.d.ts.map +1 -0
  122. package/types/lib/core/processTree.d.ts +36 -0
  123. package/types/lib/core/processTree.d.ts.map +1 -0
  124. package/types/lib/core/runState.d.ts +29 -0
  125. package/types/lib/core/runState.d.ts.map +1 -0
  126. package/types/lib/core/toml.d.ts +20 -0
  127. package/types/lib/core/toml.d.ts.map +1 -0
  128. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  129. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  131. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  132. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  133. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  134. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  135. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  136. package/types/lib/ecosystems/cppEvidence.d.ts +18 -2
  137. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  139. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  140. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  141. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  143. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  144. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  145. package/types/lib/ecosystems/gradleutils.d.ts +50 -0
  146. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  147. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  148. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  149. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  150. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  151. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  152. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  153. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  154. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  155. package/types/lib/ecosystems/parsers-go.d.ts +44 -2
  156. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  157. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  158. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  159. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  160. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  161. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  162. package/types/lib/ecosystems/parsers-misc.d.ts +3 -1
  163. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  164. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  165. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  166. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  167. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  168. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  169. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  170. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  171. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  172. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  173. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  174. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  175. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  176. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  177. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  178. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  179. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  180. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  181. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  182. package/types/lib/ecosystems/utils.d.ts +2 -2
  183. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  184. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  185. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  186. package/types/lib/evinser/evinser.d.ts +75 -8
  187. package/types/lib/evinser/evinser.d.ts.map +1 -1
  188. package/types/lib/helpers/bomSigner.d.ts +80 -4
  189. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  190. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  191. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  192. package/types/lib/inventory/atomUtils.d.ts +82 -10
  193. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  194. package/types/lib/inventory/auditCategories.d.ts +3 -0
  195. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  196. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  197. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  198. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  199. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  200. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  201. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  202. package/types/lib/inventory/deps.d.ts +59 -1
  203. package/types/lib/inventory/deps.d.ts.map +1 -1
  204. package/types/lib/inventory/dosai.d.ts +57 -8
  205. package/types/lib/inventory/dosai.d.ts.map +1 -1
  206. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  207. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  208. package/types/lib/inventory/dynamic.d.ts +10 -0
  209. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  210. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  211. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  212. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  213. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  214. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  215. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  216. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  217. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  218. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  219. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  220. package/types/lib/inventory/parentSelection.d.ts +34 -0
  221. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  222. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  223. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  224. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  225. package/types/lib/inventory/source.d.ts.map +1 -1
  226. package/types/lib/inventory/spdx.d.ts +9 -0
  227. package/types/lib/inventory/spdx.d.ts.map +1 -1
  228. package/types/lib/inventory/traceRunner.d.ts +12 -0
  229. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  230. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  231. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  232. package/types/lib/managers/docker.d.ts.map +1 -1
  233. package/types/lib/managers/oci.d.ts +3 -1
  234. package/types/lib/managers/oci.d.ts.map +1 -1
  235. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  236. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  237. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  238. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  239. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  240. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  241. package/types/lib/parsers/compileCommands.d.ts +115 -0
  242. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  243. package/types/lib/parsers/strictJson.d.ts +46 -0
  244. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  245. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  246. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  247. package/types/lib/parsers/terraformSource.d.ts +66 -0
  248. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  249. package/types/lib/server/server.d.ts.map +1 -1
  250. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  252. package/types/lib/validator/index.d.ts +8 -0
  253. package/types/lib/validator/index.d.ts.map +1 -1
  254. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  255. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -11,6 +11,10 @@ import {
11
11
  safeSpawnSync,
12
12
  } from "../core/fs.js";
13
13
  import { dirNameStr } from "../core/paths.js";
14
+ import {
15
+ reportPluginRunFailure,
16
+ resolvePluginSourceDir,
17
+ } from "../core/pluginRun.js";
14
18
  import { resolvePluginBinary } from "../inventory/plugins.js";
15
19
 
16
20
  const GO_LANGUAGES = new Set(["go", "golang"]);
@@ -1342,10 +1346,13 @@ function normalizedGolemDataFlowMode(options = {}) {
1342
1346
  return dataFlowMode;
1343
1347
  }
1344
1348
 
1345
- function normalizedGolemCallGraphMode(dataFlowMode, options = {}) {
1349
+ function normalizedGolemCallGraphMode(options = {}) {
1346
1350
  let callgraphMode = String(options.golemCallgraph || "static").toLowerCase();
1347
1351
  if (!GOLEM_CALLGRAPH_MODES.has(callgraphMode)) {
1348
- callgraphMode = dataFlowMode || "static";
1352
+ // An unrecognised mode falls back to the default. It used to fall back to
1353
+ // the data-flow mode ("security", "all"), which is not a call-graph mode
1354
+ // and made golem reject the run.
1355
+ callgraphMode = "static";
1349
1356
  }
1350
1357
  return callgraphMode;
1351
1358
  }
@@ -1637,11 +1644,17 @@ export function runGolemAnalysis(src, outputFile, options = {}) {
1637
1644
  return false;
1638
1645
  }
1639
1646
  const dataFlowMode = normalizedGolemDataFlowMode(options);
1640
- const callgraphMode = normalizedGolemCallGraphMode(dataFlowMode, options);
1647
+ const callgraphMode = normalizedGolemCallGraphMode(options);
1648
+ // cdxgen may run with its working directory already inside the project
1649
+ // while the source argument is relative to a repository root; a plain
1650
+ // resolve would join the two into a directory that does not exist. The
1651
+ // helper checks the result and falls back to the working directory when it
1652
+ // is itself the Go module under analysis.
1653
+ const analysisDir = resolvePluginSourceDir(src, ["go.mod", "go.work"]);
1641
1654
  const args = [
1642
1655
  "analyze",
1643
1656
  "--dir",
1644
- resolve(src),
1657
+ analysisDir,
1645
1658
  "--format",
1646
1659
  "json",
1647
1660
  "--callgraph",
@@ -1666,17 +1679,20 @@ export function runGolemAnalysis(src, outputFile, options = {}) {
1666
1679
  console.log("Executing", executable, args.join(" "));
1667
1680
  }
1668
1681
  const result = safeSpawnSync(executable, args, {
1669
- cwd: resolve(src),
1682
+ cwd: analysisDir,
1670
1683
  shell: false,
1671
1684
  });
1672
1685
  if (result?.status !== 0 || result?.error || !safeExistsSync(outputFile)) {
1673
- if (DEBUG_MODE) {
1674
- if (result?.stdout || result?.stderr) {
1675
- console.error(result.stdout, result.stderr);
1676
- } else {
1677
- console.log("Check if the golem plugin was installed successfully.");
1678
- }
1679
- }
1686
+ reportPluginRunFailure({
1687
+ tool: "golem",
1688
+ ecosystem: "go",
1689
+ executable,
1690
+ args,
1691
+ dir: analysisDir,
1692
+ result,
1693
+ outputFile,
1694
+ options,
1695
+ });
1680
1696
  return false;
1681
1697
  }
1682
1698
  return true;
@@ -1,10 +1,11 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { chmodSync, readFileSync } from "node:fs";
2
+ import { chmodSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { homedir, platform } from "node:os";
4
4
  import { basename, join, resolve } from "node:path";
5
+ import process from "node:process";
5
6
 
6
7
  import { build } from "@cdxgen/cdx-purl";
7
- import * as toml from "smol-toml";
8
+ import { valid } from "semver";
8
9
 
9
10
  import {
10
11
  DEBUG_MODE,
@@ -22,6 +23,11 @@ import {
22
23
  import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
23
24
  import { thoughtLog } from "../core/logger.js";
24
25
  import { isWin } from "../core/paths.js";
26
+ import { parseToml } from "../core/toml.js";
27
+ import {
28
+ isBuildToolRateLimited,
29
+ noteBuildToolRateLimit,
30
+ } from "../inventory/buildToolRateLimit.js";
25
31
  import { collectJarNS } from "../inventory/deps.js";
26
32
  import { minimumGradleVersionForJava } from "../inventory/jvmToolEnv.js";
27
33
  import { applyPurl, tryBuildPurl } from "../inventory/purl.js";
@@ -152,6 +158,65 @@ export function getGradleCommand(srcPath, rootPath, options = {}) {
152
158
  return gradleCmd;
153
159
  }
154
160
 
161
+ /**
162
+ * System properties that force the evaluation model cdxgen's Gradle
163
+ * introspection depends on.
164
+ *
165
+ * The bundled init script registers `afterEvaluate` on every project and a
166
+ * `taskGraph.whenReady` listener, so it cannot run under isolated projects
167
+ * (which forbid reaching into another project's model and fail the build at
168
+ * configuration time, see issue #4444) nor under the configuration cache (whose
169
+ * reuse skips init scripts entirely, dropping the `<CDXGEN:repository>` and
170
+ * `<CDXGEN:includedBuild>` markers even when the first run succeeded). Both
171
+ * features are commonly enabled from `gradle.properties` (e.g. okhttp), which
172
+ * command-line `-D` flags override, so every cdxgen invocation turns them
173
+ * off: these are ephemeral introspection builds, not the user's build. The
174
+ * isolated-projects property is spelled `org.gradle.unsafe.isolated-projects`
175
+ * from Gradle 8.6 until Gradle 9.7 renamed it, so both spellings are forced
176
+ * off to cover every Gradle in between. The configuration cache likewise
177
+ * keeps its pre-8.1 `org.gradle.unsafe.configuration-cache` spelling.
178
+ *
179
+ * Each feature is handled as one group. Gradle disables a feature when any of
180
+ * its spellings is explicitly `false`, so if the user already chose a value
181
+ * via `GRADLE_ARGS`, either as a `-D` property under any spelling or as the
182
+ * `--[no-]isolated-projects` / `--[no-]configuration-cache` option (which
183
+ * beats `-D` properties), cdxgen adds no override for that feature at all.
184
+ *
185
+ * @param {string[]} gradleArguments User-supplied general gradle arguments.
186
+ * @returns {string[]} The `-D` flags to inject before the user's arguments.
187
+ */
188
+ function gradleEvaluationOverrides(gradleArguments) {
189
+ const features = [
190
+ {
191
+ option: "isolated-projects",
192
+ properties: [
193
+ "org.gradle.isolated-projects",
194
+ "org.gradle.unsafe.isolated-projects",
195
+ ],
196
+ },
197
+ {
198
+ option: "configuration-cache",
199
+ properties: [
200
+ "org.gradle.configuration-cache",
201
+ "org.gradle.unsafe.configuration-cache",
202
+ ],
203
+ },
204
+ ];
205
+ const overrides = [];
206
+ for (const { option, properties } of features) {
207
+ const userChose = gradleArguments.some(
208
+ (arg) =>
209
+ arg === `--${option}` ||
210
+ arg === `--no-${option}` ||
211
+ properties.some((property) => arg.startsWith(`-D${property}=`)),
212
+ );
213
+ if (!userChose) {
214
+ overrides.push(...properties.map((property) => `-D${property}=false`));
215
+ }
216
+ }
217
+ return overrides;
218
+ }
219
+
155
220
  /**
156
221
  * Method to combine the general gradle arguments, the sub-commands and the sub-commands' arguments in the correct way
157
222
  *
@@ -175,6 +240,7 @@ export function buildGradleCommandArguments(
175
240
  "--no-parallel",
176
241
  ]
177
242
  .concat(getGradleDaemonParameter())
243
+ .concat(gradleEvaluationOverrides(gradleArguments))
178
244
  .concat(gradleArguments);
179
245
  const maxCliArgsLength = isWin
180
246
  ? 7500 - gradleCommandLength - mainGradleArguments.join(" ").length - 2
@@ -217,47 +283,87 @@ function getGradleDaemonParameter() {
217
283
  }
218
284
  }
219
285
 
286
+ /**
287
+ * Parse a Gradle task header line such as `> Task :app:dependencies FAILED`
288
+ * into the path of the project that owns the task and the task's name.
289
+ *
290
+ * @param {string} line A line of Gradle output
291
+ * @returns {{projectPath: string, taskName: string}|undefined} The owning
292
+ * project's path (`""` for a root task such as `:dependencies`) and the task
293
+ * name, or undefined when the line is not a task header
294
+ */
295
+ function parseGradleTaskHeader(line) {
296
+ if (!line.startsWith("> Task :")) {
297
+ return undefined;
298
+ }
299
+ // The path is the first token: a status such as UP-TO-DATE or FAILED may
300
+ // follow it, and a CRLF line keeps its carriage return.
301
+ const taskPath = line.slice("> Task ".length).trim().split(/\s+/)[0];
302
+ const lastColon = taskPath.lastIndexOf(":");
303
+ return {
304
+ projectPath: taskPath.slice(0, lastColon),
305
+ taskName: taskPath.slice(lastColon + 1),
306
+ };
307
+ }
308
+
220
309
  /**
221
310
  * Method to split the output produced by Gradle using parallel processing by project
222
311
  *
312
+ * Gradle groups console output by task and prints a `> Task :path` header
313
+ * before each group, repeating it whenever a task's output resumes after
314
+ * another task's. Each group is therefore keyed by the project in its own
315
+ * header (`> Task :app:dependencies` belongs to `:app`), never by whatever
316
+ * report header happened to precede it, and the groups of one project are
317
+ * concatenated in order, so reordering or interrupting the task reports cannot
318
+ * move a report to another project or overwrite one (issue 4465). The groups
319
+ * of a root task (`:dependencies`) all take the root project's name from the
320
+ * `Root project '…'` header that only a root task prints, so a continuation
321
+ * group, which carries no report header, stays with the rest of the root
322
+ * report. The groups of other tasks are dropped along with their headers.
323
+ *
223
324
  * @param {string} rawOutput Full output produced by Gradle using parallel processing
224
325
  * @param {string[]} relevantTasks The list of gradle tasks whose output need to be considered.
225
326
  * @returns {map} Map with subProject names as keys and corresponding dependency task outputs as values.
226
327
  */
227
328
  export function splitOutputByGradleProjects(rawOutput, relevantTasks) {
228
329
  const outputSplitBySubprojects = new Map();
229
- let subProjectOut = "";
230
- const outSplitByLine = rawOutput.split("\n");
231
- let currentProjectName = "root";
232
- const regexPatternForRelevantTasks = `.*:(${relevantTasks.join("|")})(?=s|\r|$)`;
233
- const regexForRelevantTasks = new RegExp(regexPatternForRelevantTasks);
234
- for (const [i, line] of outSplitByLine.entries()) {
235
- //filter out everything before first task output
236
- if (!line.startsWith("> Task :") && subProjectOut === "") {
237
- continue;
330
+ const relevantTaskNames = new Set(relevantTasks);
331
+ const taskChunks = [];
332
+ // Lines before the first task header (build logs) belong to no task.
333
+ let currentChunk;
334
+ for (const line of rawOutput.split("\n")) {
335
+ const taskHeader = parseGradleTaskHeader(line);
336
+ if (taskHeader) {
337
+ currentChunk = relevantTaskNames.has(taskHeader.taskName)
338
+ ? { projectPath: taskHeader.projectPath, lines: [line] }
339
+ : undefined;
340
+ if (currentChunk) {
341
+ taskChunks.push(currentChunk);
342
+ }
343
+ } else if (currentChunk) {
344
+ currentChunk.lines.push(line);
238
345
  }
239
-
240
- //ignore output of irrelevant tasks
241
- if (line.startsWith("> Task :") && !regexForRelevantTasks.test(line)) {
346
+ }
347
+ let rootProjectName;
348
+ for (const chunk of taskChunks) {
349
+ if (chunk.projectPath) {
242
350
  continue;
243
351
  }
244
-
245
- if (line.startsWith("Root project '") || line.startsWith("Project ':")) {
246
- currentProjectName = line.split("'")[1];
247
- outputSplitBySubprojects.set(currentProjectName, "");
248
- }
249
- // if previous subProject has ended, push to array and reset subProject string
250
- if (line.startsWith("> Task :") && subProjectOut !== "") {
251
- outputSplitBySubprojects.set(currentProjectName, subProjectOut);
252
- subProjectOut = "";
253
- }
254
- //if in subproject block, keep appending to string
255
- subProjectOut += `${line}\n`;
256
- //if end of last dependencies output block, push to array
257
- if (i === outSplitByLine.length - 1) {
258
- outputSplitBySubprojects.set(currentProjectName, subProjectOut);
352
+ const rootHeader = chunk.lines.find((line) =>
353
+ line.startsWith("Root project '"),
354
+ );
355
+ if (rootHeader) {
356
+ rootProjectName = rootHeader.split("'")[1];
357
+ break;
259
358
  }
260
359
  }
360
+ for (const chunk of taskChunks) {
361
+ const projectName = chunk.projectPath || rootProjectName || "root";
362
+ outputSplitBySubprojects.set(
363
+ projectName,
364
+ `${outputSplitBySubprojects.get(projectName) || ""}${chunk.lines.join("\n")}\n`,
365
+ );
366
+ }
261
367
  return outputSplitBySubprojects;
262
368
  }
263
369
 
@@ -397,6 +503,11 @@ export function executeParallelGradleProperties(
397
503
  extraArgs = [],
398
504
  options = {},
399
505
  ) {
506
+ // This runs once per properties round, so a rate limit an earlier round met
507
+ // ends the later rounds too.
508
+ if (isBuildToolRateLimited("gradle")) {
509
+ return "";
510
+ }
400
511
  const gradleCmd = getGradleCommand(dir, null, options);
401
512
  const gradleArgs = buildGradleCommandArguments(
402
513
  extraArgs.concat(
@@ -423,6 +534,22 @@ export function executeParallelGradleProperties(
423
534
  cwd: dir,
424
535
  shell: isWin,
425
536
  });
537
+ // A properties invocation needs the same repository the build does, so a
538
+ // 429 answer here ends every further gradle invocation in this scan. The
539
+ // dependencies task can even exit 0 under a rate limit, so the output is
540
+ // read whatever the status is. The properties this invocation did print
541
+ // are kept.
542
+ if (
543
+ noteBuildToolRateLimit("gradle", commandOutputText(result), {
544
+ command: `${basename(gradleCmd)} ${gradleArg.join(" ")}`,
545
+ exitCode: typeof result.status === "number" ? result.status : undefined,
546
+ })
547
+ ) {
548
+ if (result.stdout) {
549
+ allOutputs.push(result.stdout);
550
+ }
551
+ break;
552
+ }
426
553
  if (result.status !== 0 || result.error) {
427
554
  recordGradleInvocationFailure(result, {
428
555
  command: `${basename(gradleCmd)} ${gradleArg.join(" ")}`,
@@ -1244,6 +1371,163 @@ export function getMillCommand(srcPath, options = {}) {
1244
1371
  return millCmd;
1245
1372
  }
1246
1373
 
1374
+ /**
1375
+ * Determine the Mill version of a build.
1376
+ *
1377
+ * The version is read from the `.mill-version` file, from the
1378
+ * `//| mill-version:` header of the build file, or from the launcher itself.
1379
+ *
1380
+ * @param {string} millRootPath Root of the Mill build
1381
+ * @param {string} millCmd Mill command to use for the fallback probe
1382
+ * @returns {string|null} The Mill version, or null when it cannot be determined
1383
+ */
1384
+ export function getMillVersion(millRootPath, millCmd) {
1385
+ const millVersionFile = join(millRootPath, ".mill-version");
1386
+ if (safeExistsSync(millVersionFile)) {
1387
+ const version = (readFileSync(millVersionFile, "utf-8") || "").trim();
1388
+ if (valid(version)) {
1389
+ return version;
1390
+ }
1391
+ }
1392
+ for (const buildFile of ["build.mill", "build.sc"]) {
1393
+ const buildFilePath = join(millRootPath, buildFile);
1394
+ if (!safeExistsSync(buildFilePath)) {
1395
+ continue;
1396
+ }
1397
+ const headerMatch = /^\/\/[|\s]\s*mill-version:\s*(\S+)/m.exec(
1398
+ readFileSync(buildFilePath, "utf-8"),
1399
+ );
1400
+ if (headerMatch?.[1] && valid(headerMatch[1])) {
1401
+ return headerMatch[1];
1402
+ }
1403
+ }
1404
+ const versionResult = safeSpawnSync(millCmd, ["--no-server", "--version"], {
1405
+ cwd: millRootPath,
1406
+ shell: isWin,
1407
+ });
1408
+ const versionMatch = /version\s+(\S+)/.exec(`${versionResult.stdout || ""}`);
1409
+ return versionMatch?.[1] && valid(versionMatch[1]) ? versionMatch[1] : null;
1410
+ }
1411
+
1412
+ /**
1413
+ * Find the Mill daemons running under a tree.
1414
+ *
1415
+ * Daemons record their process id in `out/mill-daemon*` (Mill 1.x) or
1416
+ * `out/mill-server*` directories, directly or one level below (Mill 0.12
1417
+ * keeps one subdirectory per server). Only processes that are still alive
1418
+ * are returned.
1419
+ *
1420
+ * @param {string} rootPath Tree to search
1421
+ * @returns {Map<string, Set<number>>} Live daemon process ids by the root of
1422
+ * the build they belong to
1423
+ */
1424
+ export function findMillDaemons(rootPath) {
1425
+ const daemons = new Map();
1426
+ const readPid = (dir) => {
1427
+ for (const candidate of [dir, ...safeSubdirs(dir)]) {
1428
+ try {
1429
+ const pid = Number.parseInt(
1430
+ readFileSync(join(candidate, "processId"), "utf-8").trim(),
1431
+ 10,
1432
+ );
1433
+ if (pid > 0 && isProcessAlive(pid)) {
1434
+ return pid;
1435
+ }
1436
+ } catch (_err) {
1437
+ // no process id recorded in this directory
1438
+ }
1439
+ }
1440
+ return undefined;
1441
+ };
1442
+ const visit = (dir, depth) => {
1443
+ if (depth > 4) {
1444
+ return;
1445
+ }
1446
+ let entries;
1447
+ try {
1448
+ entries = readdirSync(dir, { withFileTypes: true });
1449
+ } catch (_err) {
1450
+ return;
1451
+ }
1452
+ for (const entry of entries) {
1453
+ if (!entry.isDirectory()) {
1454
+ continue;
1455
+ }
1456
+ if (entry.name === "out") {
1457
+ for (const name of safeSubdirs(join(dir, entry.name))) {
1458
+ const base = basename(name);
1459
+ if (
1460
+ !base.startsWith("mill-daemon") &&
1461
+ !base.startsWith("mill-server")
1462
+ ) {
1463
+ continue;
1464
+ }
1465
+ const pid = readPid(name);
1466
+ if (pid) {
1467
+ if (!daemons.has(dir)) {
1468
+ daemons.set(dir, new Set());
1469
+ }
1470
+ daemons.get(dir).add(pid);
1471
+ }
1472
+ }
1473
+ } else if (!["target", "node_modules", ".git"].includes(entry.name)) {
1474
+ visit(join(dir, entry.name), depth + 1);
1475
+ }
1476
+ }
1477
+ };
1478
+ visit(rootPath, 0);
1479
+ return daemons;
1480
+ }
1481
+
1482
+ function safeSubdirs(dir) {
1483
+ try {
1484
+ return readdirSync(dir, { withFileTypes: true })
1485
+ .filter((entry) => entry.isDirectory())
1486
+ .map((entry) => join(dir, entry.name));
1487
+ } catch (_err) {
1488
+ return [];
1489
+ }
1490
+ }
1491
+
1492
+ function isProcessAlive(pid) {
1493
+ try {
1494
+ process.kill(pid, 0);
1495
+ return true;
1496
+ } catch (err) {
1497
+ return err?.code === "EPERM";
1498
+ }
1499
+ }
1500
+
1501
+ /**
1502
+ * Stop the Mill daemons that started under the scanned tree during the scan.
1503
+ *
1504
+ * cdxgen's own Mill calls run with --no-server, but a project build cdxgen
1505
+ * runs can shell out to Mill without it and leave a daemon behind. Daemons
1506
+ * that were already running when the scan started belong to the user, for
1507
+ * example to an IDE, and are left alone; every other live daemon is shut
1508
+ * down through the launcher of the build it belongs to.
1509
+ *
1510
+ * @param {string} rootPath Scanned tree
1511
+ * @param {Map<string, Set<number>>} [runningBefore] Result of
1512
+ * {@link findMillDaemons} taken before the scan
1513
+ */
1514
+ export function stopMillDaemons(rootPath, runningBefore = new Map()) {
1515
+ for (const [buildRoot, pids] of findMillDaemons(rootPath)) {
1516
+ const before = runningBefore.get(buildRoot) || new Set();
1517
+ if ([...pids].every((pid) => before.has(pid))) {
1518
+ continue;
1519
+ }
1520
+ const launcher = safeExistsSync(join(buildRoot, "mill"))
1521
+ ? join(buildRoot, "mill")
1522
+ : "mill";
1523
+ console.log("Stopping the Mill daemon the scan started in", buildRoot);
1524
+ safeSpawnSync(launcher, ["shutdown"], {
1525
+ cwd: buildRoot,
1526
+ shell: isWin,
1527
+ });
1528
+ }
1529
+ }
1530
+
1247
1531
  /**
1248
1532
  * Method to return the maven command to use.
1249
1533
  *
@@ -1445,7 +1729,7 @@ export function getMavenCommand(srcPath, rootPath, options = {}) {
1445
1729
  export function parseGradleVersionCatalog(catalogFile) {
1446
1730
  let catalog;
1447
1731
  try {
1448
- catalog = toml.parse(readFileSync(catalogFile, "utf-8"));
1732
+ catalog = parseToml(readFileSync(catalogFile, "utf-8"));
1449
1733
  } catch (error) {
1450
1734
  console.warn(`Failed to parse ${catalogFile}: ${error.message}`);
1451
1735
  return [];
@@ -6,7 +6,7 @@
6
6
  // import parsers-js directly instead of having parsePkgJson threaded in as an
7
7
  // injected helper from the CLI.
8
8
 
9
- import { join, resolve } from "node:path";
9
+ import { dirname, isAbsolute, join, resolve } from "node:path";
10
10
 
11
11
  import { safeExistsSync } from "../core/fs.js";
12
12
  import { createOccurrenceEvidence } from "../inventory/evidenceUtils.js";
@@ -104,6 +104,85 @@ function applyNpmBinCommandEvidence(pkg, allImports) {
104
104
  }
105
105
  }
106
106
 
107
+ /**
108
+ * Full npm package name (`@scope/name` or `name`) of a component or manifest.
109
+ *
110
+ * @param {{group?: string, name?: string}} pkg Component or parsed manifest.
111
+ * @returns {string} The package name npm installs it under.
112
+ */
113
+ function npmPackageName({ group, name }) {
114
+ const scope = group?.startsWith("@") ? group.substring(1) : group;
115
+ return scope ? `@${scope}/${name}` : name;
116
+ }
117
+
118
+ /**
119
+ * Fill in a component's missing npm metadata (description, author, license,
120
+ * homepage, repository) from the package.json of its installed copy.
121
+ *
122
+ * `internal:LocalNodeModulesPath` holds either an absolute install directory
123
+ * (pnpm) or an arborist `node.location`. A location that starts with
124
+ * `node_modules/` is relative to the directory of the component's
125
+ * `internal:SrcFile`. Other relative locations, such as a workspace member or a
126
+ * package nested under one, have no recorded base and are skipped; the deep npm
127
+ * parse reads those manifests from the arborist node itself. The manifest is
128
+ * used only when it names the component's package and version, and it fills
129
+ * only the fields the component lacks.
130
+ *
131
+ * @param {object} pkg Package component to enrich in place.
132
+ */
133
+ async function fillMissingMetadataFromLocalNodeModules(pkg) {
134
+ if (pkg.description || !Array.isArray(pkg.properties)) {
135
+ return;
136
+ }
137
+ const propValue = (propName) =>
138
+ pkg.properties.find((property) => property.name === propName)?.value;
139
+ const localNodeModulesPath = propValue("internal:LocalNodeModulesPath");
140
+ if (!localNodeModulesPath) {
141
+ return;
142
+ }
143
+ let pkgJsonFile;
144
+ if (isAbsolute(localNodeModulesPath)) {
145
+ pkgJsonFile = join(localNodeModulesPath, "package.json");
146
+ } else {
147
+ const srcFile = propValue("internal:SrcFile");
148
+ if (!srcFile || !localNodeModulesPath.startsWith("node_modules/")) {
149
+ return;
150
+ }
151
+ pkgJsonFile = resolve(
152
+ dirname(srcFile),
153
+ localNodeModulesPath,
154
+ "package.json",
155
+ );
156
+ }
157
+ if (!safeExistsSync(pkgJsonFile)) {
158
+ return;
159
+ }
160
+ const lnmPkgList = await parsePkgJson(pkgJsonFile, true);
161
+ const lnmMetadata = lnmPkgList?.length === 1 ? lnmPkgList[0] : undefined;
162
+ if (
163
+ !lnmMetadata ||
164
+ npmPackageName(lnmMetadata) !== npmPackageName(pkg) ||
165
+ lnmMetadata.version !== pkg.version
166
+ ) {
167
+ return;
168
+ }
169
+ if (!pkg.description && lnmMetadata.description) {
170
+ pkg.description = lnmMetadata.description;
171
+ }
172
+ if (!pkg.author && lnmMetadata.author) {
173
+ pkg.author = lnmMetadata.author;
174
+ }
175
+ if (!pkg.license && lnmMetadata.license) {
176
+ pkg.license = lnmMetadata.license;
177
+ }
178
+ if (!pkg.homepage && lnmMetadata.homepage) {
179
+ pkg.homepage = lnmMetadata.homepage;
180
+ }
181
+ if (!pkg.repository && lnmMetadata.repository) {
182
+ pkg.repository = lnmMetadata.repository;
183
+ }
184
+ }
185
+
107
186
  /**
108
187
  * Attach JS/TS import and export usage evidence to matching package components.
109
188
  *
@@ -136,6 +215,11 @@ export async function addEvidenceForImports(
136
215
  if (group === "@types") {
137
216
  continue;
138
217
  }
218
+ // Runs ahead of the alias loop, which a matching import ends with a break,
219
+ // so that every component is enriched whichever alias the source imports.
220
+ if (deep) {
221
+ await fillMissingMetadataFromLocalNodeModules(pkg);
222
+ }
139
223
  const aliases = group?.length
140
224
  ? [name, `${group}/${name}`, `@${group}/${name}`]
141
225
  : [name];
@@ -282,29 +366,6 @@ export async function addEvidenceForImports(
282
366
  }
283
367
  break;
284
368
  }
285
- // Capture metadata such as description from local node_modules in deep mode
286
- if (deep && !pkg.description && pkg.properties) {
287
- let localNodeModulesPath;
288
- for (const aprop of pkg.properties) {
289
- if (aprop.name === "internal:LocalNodeModulesPath") {
290
- localNodeModulesPath = resolve(join(aprop.value, "package.json"));
291
- break;
292
- }
293
- }
294
- if (localNodeModulesPath && safeExistsSync(localNodeModulesPath)) {
295
- const lnmPkgList = await parsePkgJson(localNodeModulesPath, true);
296
- if (lnmPkgList && lnmPkgList.length === 1) {
297
- const lnmMetadata = lnmPkgList[0];
298
- if (lnmMetadata && Object.keys(lnmMetadata).length) {
299
- pkg.description = lnmMetadata.description;
300
- pkg.author = lnmMetadata.author;
301
- pkg.license = lnmMetadata.license;
302
- pkg.homepage = lnmMetadata.homepage;
303
- pkg.repository = lnmMetadata.repository;
304
- }
305
- }
306
- }
307
- }
308
369
  } // for alias
309
370
  // Trim the properties
310
371
  applyNpmBinCommandEvidence(pkg, allImports);
@@ -10,6 +10,7 @@ import {
10
10
  safeRmSync,
11
11
  safeSpawnSync,
12
12
  } from "../core/fs.js";
13
+ import { resolvePluginSourceDir } from "../core/pluginRun.js";
13
14
  import { resolvePluginBinary } from "../inventory/plugins.js";
14
15
  import {
15
16
  parsePurlLoose,
@@ -174,14 +175,25 @@ export function analyzeKosiProject(src, options = {}) {
174
175
  );
175
176
  return undefined;
176
177
  }
178
+ // cdxgen may run with its working directory already inside the project
179
+ // while the source argument is relative to a repository root; a plain
180
+ // resolve would join the two into a directory that does not exist.
181
+ const analysisDir = resolvePluginSourceDir(src, [
182
+ "pom.xml",
183
+ "build.gradle",
184
+ "build.gradle.kts",
185
+ "settings.gradle",
186
+ "settings.gradle.kts",
187
+ "classpath.txt",
188
+ ]);
177
189
  const tempDir = safeMkdtempSync(join(getTmpDir(), "kosi-"));
178
190
  try {
179
191
  const outputFile = join(tempDir, "kosi-all.json");
180
- const allArgs = kosiArgs(kosi, src, outputFile, "all");
192
+ const allArgs = kosiArgs(kosi, analysisDir, outputFile, "all");
181
193
  if (DEBUG_MODE) {
182
194
  console.log("Executing", kosi.cmd, allArgs.join(" "));
183
195
  }
184
- const report = runKosi(kosi, src, allArgs, outputFile, options);
196
+ const report = runKosi(kosi, analysisDir, allArgs, outputFile, options);
185
197
  if (!report) {
186
198
  return undefined;
187
199
  }
@@ -191,10 +203,15 @@ export function analyzeKosiProject(src, options = {}) {
191
203
  // all-pass's. Losing it would silently drop the filtered evidence,
192
204
  // so a failure here names itself but keeps the all-pass evidence.
193
205
  const reachableFile = join(tempDir, "kosi-reachable.json");
194
- const reachableArgs = kosiArgs(kosi, src, reachableFile, "reachable");
206
+ const reachableArgs = kosiArgs(
207
+ kosi,
208
+ analysisDir,
209
+ reachableFile,
210
+ "reachable",
211
+ );
195
212
  const reachableReport = runKosi(
196
213
  kosi,
197
- src,
214
+ analysisDir,
198
215
  reachableArgs,
199
216
  reachableFile,
200
217
  options,