@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,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFileSync } from "node:fs";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
3
  import {
4
4
  sep as _sep,
5
5
  basename,
@@ -18,6 +18,7 @@ import {
18
18
  hasDangerousUnicode,
19
19
  readEnvironmentVariable,
20
20
  } from "../core/activity.js";
21
+ import { readRegularTextFile } from "../core/boundedFile.js";
21
22
  import { recordDegradation } from "../core/buildLedger.js";
22
23
  import { deferFailOnError } from "../core/deferredExit.js";
23
24
  import { shouldFetchLicense } from "../core/env.js";
@@ -52,7 +53,12 @@ import {
52
53
  tryBuildPurl,
53
54
  } from "../inventory/purl.js";
54
55
  import { getLicenses, spdxLicenses } from "../inventory/spdx.js";
55
- import { cmakeFetchDependencies } from "../parsers/cmakeLists.js";
56
+ import {
57
+ cmakeFetchDependencies,
58
+ cmakeSetVariables,
59
+ expandCmakeVariables,
60
+ parseCmakeCommands,
61
+ } from "../parsers/cmakeLists.js";
56
62
  import { isValidIdnEmail } from "../parsers/email.js";
57
63
  import { submodulePurlCoordinates } from "../parsers/gitmodules.js";
58
64
  import { getDartMetadata } from "./ecosystems.js";
@@ -2784,6 +2790,130 @@ export function cmakeFetchDependencyComponent(dep, srcFile) {
2784
2790
  return component;
2785
2791
  }
2786
2792
 
2793
+ const MAX_MESON_WRAPS = 256;
2794
+ const MAX_MESON_WRAP_BYTES = 64 * 1024;
2795
+
2796
+ function upstreamVersionOf(text) {
2797
+ if (typeof text !== "string") {
2798
+ return undefined;
2799
+ }
2800
+ const value = text.trim().replace(/^v(?=\d)/, "");
2801
+ return /^\d+(\.\d+)*([-+._~][0-9A-Za-z.]+)?$/.test(value) ? value : undefined;
2802
+ }
2803
+
2804
+ // `brotli-1.1.0`, `brotli-1.1.0.tar.gz`: the version after the last `-` that
2805
+ // starts a number.
2806
+ function versionFromArchiveName(name) {
2807
+ if (typeof name !== "string") {
2808
+ return undefined;
2809
+ }
2810
+ const stem = name.replace(/\.(tar\.gz|tar\.xz|tar\.bz2|tgz|zip|tar)$/i, "");
2811
+ const dash = stem.search(/-\d[^-]*$/);
2812
+ return dash === -1 ? undefined : upstreamVersionOf(stem.slice(dash + 1));
2813
+ }
2814
+
2815
+ /**
2816
+ * Read the wrap files of a meson project's subprojects directory: which
2817
+ * dependency names each wrap provides, and the version it pins.
2818
+ * https://mesonbuild.com/Wrap-dependency-system-manual.html
2819
+ *
2820
+ * A WrapDB wrap records `wrapdb_version = <upstream>-<wrap revision>`; any wrap
2821
+ * names its unpacked `directory` and its archive, which carry the upstream
2822
+ * version; a git wrap pins a `revision` that is often a version tag.
2823
+ *
2824
+ * @param {string} subprojectsDir path of the subprojects directory
2825
+ * @returns {Map<string, {wrap: string, version: string, source: string, revision?: string}>}
2826
+ * by provided dependency name, lower-case
2827
+ */
2828
+ export function readMesonWraps(subprojectsDir) {
2829
+ const provided = new Map();
2830
+ let wrapFiles = [];
2831
+ try {
2832
+ wrapFiles = readdirSync(subprojectsDir)
2833
+ .filter((name) => name.endsWith(".wrap"))
2834
+ .sort()
2835
+ .slice(0, MAX_MESON_WRAPS);
2836
+ } catch {
2837
+ return provided;
2838
+ }
2839
+ for (const wrapFile of wrapFiles) {
2840
+ // A regular file only: a FIFO named *.wrap would block a plain read.
2841
+ const text = readRegularTextFile(
2842
+ join(subprojectsDir, wrapFile),
2843
+ MAX_MESON_WRAP_BYTES,
2844
+ );
2845
+ if (text === undefined) {
2846
+ continue;
2847
+ }
2848
+ const sections = {};
2849
+ let section = "";
2850
+ for (const rawLine of text.split(/\r?\n/)) {
2851
+ const line = rawLine.trim();
2852
+ if (!line || line.startsWith("#") || line.startsWith(";")) {
2853
+ continue;
2854
+ }
2855
+ if (line.startsWith("[") && line.endsWith("]")) {
2856
+ section = line.slice(1, -1).trim();
2857
+ sections[section] = sections[section] || {};
2858
+ continue;
2859
+ }
2860
+ const equals = line.indexOf("=");
2861
+ if (equals <= 0 || !section) {
2862
+ continue;
2863
+ }
2864
+ sections[section][line.slice(0, equals).trim()] = line
2865
+ .slice(equals + 1)
2866
+ .trim();
2867
+ }
2868
+ const body = sections["wrap-file"] || sections["wrap-git"] || {};
2869
+ let version;
2870
+ let source;
2871
+ let revision;
2872
+ if (body.wrapdb_version) {
2873
+ const dash = body.wrapdb_version.lastIndexOf("-");
2874
+ version = upstreamVersionOf(
2875
+ dash > 0 ? body.wrapdb_version.slice(0, dash) : body.wrapdb_version,
2876
+ );
2877
+ revision = dash > 0 ? body.wrapdb_version.slice(dash + 1) : undefined;
2878
+ source = "wrapdb_version";
2879
+ }
2880
+ if (!version && sections["wrap-file"]) {
2881
+ version =
2882
+ versionFromArchiveName(body.directory) ||
2883
+ versionFromArchiveName(body.source_filename);
2884
+ source = version ? "directory" : undefined;
2885
+ }
2886
+ if (!version && sections["wrap-git"]) {
2887
+ version = upstreamVersionOf(body.revision);
2888
+ source = version ? "revision" : undefined;
2889
+ }
2890
+ if (!version) {
2891
+ continue;
2892
+ }
2893
+ const wrap = wrapFile.slice(0, -".wrap".length);
2894
+ const entry = { wrap, version, source, ...(revision ? { revision } : {}) };
2895
+ const names = new Set([wrap.toLowerCase()]);
2896
+ const provide = sections.provide || {};
2897
+ for (const [key, value] of Object.entries(provide)) {
2898
+ if (key === "dependency_names" || key === "program_names") {
2899
+ for (const name of value.split(",")) {
2900
+ if (name.trim()) {
2901
+ names.add(name.trim().toLowerCase());
2902
+ }
2903
+ }
2904
+ } else {
2905
+ names.add(key.toLowerCase());
2906
+ }
2907
+ }
2908
+ for (const name of names) {
2909
+ if (!provided.has(name)) {
2910
+ provided.set(name, entry);
2911
+ }
2912
+ }
2913
+ }
2914
+ return provided;
2915
+ }
2916
+
2787
2917
  /**
2788
2918
  * Parse a CMake-like build file (CMakeLists.txt, meson.build, etc.) and extract the
2789
2919
  * parent component and list of dependency packages.
@@ -2801,6 +2931,11 @@ export function cmakeFetchDependencyComponent(dep, srcFile) {
2801
2931
  */
2802
2932
  export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2803
2933
  const rawData = readFileSync(cmakeListFile, { encoding: "utf-8" });
2934
+ // The wraps a meson project ships pin the versions of the subprojects its
2935
+ // dependency() calls fall back to.
2936
+ const mesonWraps = basename(cmakeListFile).startsWith("meson")
2937
+ ? readMesonWraps(join(dirname(cmakeListFile), "subprojects"))
2938
+ : new Map();
2804
2939
  let cmakeListData = rawData;
2805
2940
  const pkgList = [];
2806
2941
  const pkgAddedMap = {};
@@ -2808,6 +2943,25 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2808
2943
  const versionsMap = {};
2809
2944
  let parentComponent = {};
2810
2945
  const templateValues = {};
2946
+ // The parent component is built the same way whether its name and version
2947
+ // came from the parsed project() command or from the line loop below.
2948
+ const toParentComponent = (parentName, parentVersion) => {
2949
+ const component = {
2950
+ group: options.projectGroup || "",
2951
+ name: parentName,
2952
+ version: parentVersion || options.projectVersion || "",
2953
+ type: "application",
2954
+ };
2955
+ component["purl"] = build({
2956
+ type: pkgType,
2957
+ namespace: component.group || null,
2958
+ name: component.name,
2959
+ version: component.version || null,
2960
+ subpath: null,
2961
+ });
2962
+ component["bom-ref"] = decodeURIComponent(component["purl"]);
2963
+ return component;
2964
+ };
2811
2965
  // A dependency fetched at configure time is the same package a find_package()
2812
2966
  // of the same name asks for: the fetched component, with its source and pin,
2813
2967
  // stands for both.
@@ -2823,6 +2977,38 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2823
2977
  fetchedNames.add(dep.name.toLowerCase());
2824
2978
  pkgList.push(component);
2825
2979
  }
2980
+ // A project() call may span several lines, which the line loop below
2981
+ // cannot rejoin, so the parent name and version are read from the parsed
2982
+ // commands instead. The first argument is the project name and the
2983
+ // VERSION keyword is followed by the version.
2984
+ const commands = parseCmakeCommands(rawData);
2985
+ const variables = cmakeSetVariables(commands);
2986
+ const projectCommand = commands.find(
2987
+ (command) => command.name === "project",
2988
+ );
2989
+ if (projectCommand?.args.length) {
2990
+ const args = projectCommand.args.map((arg) =>
2991
+ expandCmakeVariables(arg, variables),
2992
+ );
2993
+ const parentName = args[0].replace(":", "").trim();
2994
+ let parentVersion;
2995
+ const versionIndex = args.findIndex(
2996
+ (arg) => arg.toLowerCase() === "version",
2997
+ );
2998
+ if (versionIndex > -1 && args.length > versionIndex + 1) {
2999
+ parentVersion = args[versionIndex + 1];
3000
+ }
3001
+ // A `${VAR}` that no `set()` in this file defines stays literal. It is
3002
+ // not a version, and percent-encoding it into a purl publishes the
3003
+ // variable name as the version, so the field is left empty instead.
3004
+ // CMakeCache.txt carries the evaluated value when a build tree exists.
3005
+ if (parentVersion?.includes("$")) {
3006
+ parentVersion = undefined;
3007
+ }
3008
+ if (parentName.length && !parentName.includes("$")) {
3009
+ parentComponent = toParentComponent(parentName, parentVersion);
3010
+ }
3011
+ }
2826
3012
  }
2827
3013
  cmakeListData = cmakeListData
2828
3014
  .replace(/^ {2}/g, "")
@@ -2883,23 +3069,7 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2883
3069
  parentVersion = undefined;
2884
3070
  }
2885
3071
  if (parentName?.length && !parentName.includes("$")) {
2886
- parentComponent = {
2887
- group: options.projectGroup || "",
2888
- name: parentName,
2889
- version: parentVersion || options.projectVersion || "",
2890
- type: "application",
2891
- };
2892
- const safePath = path ? path.replace(/^\/+/, "") : null;
2893
- parentComponent["purl"] = build({
2894
- type: pkgType,
2895
- namespace: parentComponent.group || null,
2896
- name: parentComponent.name,
2897
- version: parentComponent.version || null,
2898
- subpath: safePath,
2899
- });
2900
- parentComponent["bom-ref"] = decodeURIComponent(
2901
- parentComponent["purl"],
2902
- );
3072
+ parentComponent = toParentComponent(parentName, parentVersion);
2903
3073
  }
2904
3074
  } else if (CMAKE_FIND_COMMANDS.has(command)) {
2905
3075
  let tmpB = commandArgs.split(")")[0].split(" ");
@@ -3041,7 +3211,26 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
3041
3211
  // wrap match has since replaced, so the scraped name is the fallback.
3042
3212
  const versionSpecifier =
3043
3213
  versionSpecifiersMap[n] || versionSpecifiersMap[scrapedName];
3044
- const version = versionsMap[n] || versionsMap[scrapedName] || "";
3214
+ let version = versionsMap[n] || versionsMap[scrapedName] || "";
3215
+ const localWrap =
3216
+ mesonWraps.get(scrapedName.toLowerCase()) ||
3217
+ mesonWraps.get(n.toLowerCase());
3218
+ if (localWrap && !version) {
3219
+ version = localWrap.version;
3220
+ props.push({ name: "cdx:meson:wrap", value: localWrap.wrap });
3221
+ props.push({
3222
+ name: "cdx:meson:wrap:versionSource",
3223
+ value: localWrap.source,
3224
+ });
3225
+ if (localWrap.revision) {
3226
+ props.push({
3227
+ name: "cdx:meson:wrap:revision",
3228
+ value: localWrap.revision,
3229
+ });
3230
+ }
3231
+ // A pinned wrap is a stronger identity than a name match alone.
3232
+ confidence = Math.max(confidence, 0.7);
3233
+ }
3045
3234
  if (versionSpecifier) {
3046
3235
  props.push({
3047
3236
  name: "cdx:build:versionSpecifiers",
@@ -888,6 +888,61 @@ export function enrichFromInstalledMetadata(pkgList, installedMetadata) {
888
888
  return enriched;
889
889
  }
890
890
 
891
+ // The interpreter cdxgen assumes for a lock file's environment. A marker that
892
+ // tests the implementation against it is decidable without a target machine.
893
+ const IMPLEMENTATION_CLAUSE_RE =
894
+ /^(platform_python_implementation|implementation_name)\s*(==|!=)\s*(['"])([A-Za-z]+)\3$/;
895
+
896
+ function implementationClauseIsFalse(clause) {
897
+ const match = IMPLEMENTATION_CLAUSE_RE.exec(clause.trim());
898
+ if (!match) {
899
+ return false;
900
+ }
901
+ const expected = match[1] === "implementation_name" ? "cpython" : "CPython";
902
+ const equal = match[4] === expected;
903
+ return match[2] === "==" ? !equal : equal;
904
+ }
905
+
906
+ /**
907
+ * Whether an environment marker is false for every CPython interpreter. Only
908
+ * `platform_python_implementation` and `implementation_name` clauses are
909
+ * decided; anything else, or a marker with parentheses, is treated as possibly
910
+ * true, so the answer errs towards keeping a package.
911
+ *
912
+ * @param {string} marker PEP 508 environment marker
913
+ * @returns {boolean} true when no CPython environment satisfies the marker
914
+ */
915
+ export function markerExcludesCPython(marker) {
916
+ if (typeof marker !== "string" || !marker.trim() || /[()]/.test(marker)) {
917
+ return false;
918
+ }
919
+ return marker
920
+ .split(/\s+or\s+/)
921
+ .every((disjunct) =>
922
+ disjunct.split(/\s+and\s+/).some(implementationClauseIsFalse),
923
+ );
924
+ }
925
+
926
+ function recordMarkerEdge(markerEdges, depKey, apkgDep) {
927
+ if (!depKey) {
928
+ return;
929
+ }
930
+ const edge = markerEdges.get(depKey) || {
931
+ total: 0,
932
+ cpythonExcluded: 0,
933
+ markers: new Set(),
934
+ };
935
+ edge.total += 1;
936
+ const marker = typeof apkgDep === "object" ? apkgDep?.marker : undefined;
937
+ if (typeof marker === "string" && marker.trim()) {
938
+ edge.markers.add(marker.trim());
939
+ if (markerExcludesCPython(marker)) {
940
+ edge.cpythonExcluded += 1;
941
+ }
942
+ }
943
+ markerEdges.set(depKey, edge);
944
+ }
945
+
891
946
  /**
892
947
  * Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock, and pylock.toml.
893
948
  *
@@ -911,6 +966,8 @@ export async function parsePyLockData(
911
966
  const depsMap = {};
912
967
  const existingPkgMap = {};
913
968
  const pkgBomRefMap = {};
969
+ // Incoming edges per dependency, with the environment markers they carry.
970
+ const markerEdges = new Map();
914
971
  let directDepsKeys = {};
915
972
  let groupDepsKeys = {};
916
973
  let dependencySourceMap = {};
@@ -1334,9 +1391,16 @@ export async function parsePyLockData(
1334
1391
  devDependencies.length ||
1335
1392
  optionalDependencies.length
1336
1393
  ) {
1337
- if (Array.isArray(apkg.dependencies)) {
1394
+ // A package whose only dependencies are optional (urllib3 has no
1395
+ // required ones, only the `brotli`, `socks` and `zstd` extras) has no
1396
+ // `dependencies` key in uv.lock, but its extras are still edges.
1397
+ if (
1398
+ Array.isArray(apkg.dependencies) ||
1399
+ (!apkg.dependencies &&
1400
+ (devDependencies.length || optionalDependencies.length))
1401
+ ) {
1338
1402
  // pdm.lock files
1339
- let allDeps = apkg.dependencies;
1403
+ let allDeps = apkg.dependencies || [];
1340
1404
  allDeps = allDeps.concat(devDependencies);
1341
1405
  allDeps = allDeps.concat(optionalDependencies);
1342
1406
  for (const apkgDep of allDeps) {
@@ -1348,6 +1412,7 @@ export async function parsePyLockData(
1348
1412
  const depPkgRef =
1349
1413
  existingPkgMap[nameLower] || existingPkgMap[nameStr];
1350
1414
  depsMap[pkg["bom-ref"]].add(depPkgRef || nameStr);
1415
+ recordMarkerEdge(markerEdges, depPkgRef || nameLower, apkgDep);
1351
1416
  // Propagate the workspace properties to the child components
1352
1417
  if (depPkgRef && pkgBomRefMap[depPkgRef]) {
1353
1418
  const dependentPkg = pkgBomRefMap[depPkgRef];
@@ -1421,6 +1486,28 @@ export async function parsePyLockData(
1421
1486
  }
1422
1487
  }
1423
1488
  }
1489
+ // A package reached only through edges whose environment marker excludes
1490
+ // CPython (brotlicffi is `platform_python_implementation != 'CPython'`) is
1491
+ // not installed by a CPython environment built from this lock, so it is
1492
+ // optional rather than part of the CPython closure. A direct dependency of
1493
+ // the project is left alone: the manifest asked for it.
1494
+ for (const [depRef, edge] of markerEdges) {
1495
+ const dependentPkg = pkgBomRefMap[depRef];
1496
+ if (
1497
+ !dependentPkg ||
1498
+ !edge.total ||
1499
+ edge.cpythonExcluded !== edge.total ||
1500
+ directDepsKeys[normalizePythonDependencyKey(dependentPkg.name)]
1501
+ ) {
1502
+ continue;
1503
+ }
1504
+ dependentPkg.scope = "optional";
1505
+ dependentPkg.properties = dependentPkg.properties || [];
1506
+ dependentPkg.properties.push({
1507
+ name: "cdx:pip:markers",
1508
+ value: Array.from(edge.markers).sort().join(" or "),
1509
+ });
1510
+ }
1424
1511
  // Seed the parent component's first-level edges from the manifest. Lock
1425
1512
  // files describe the resolved closure but do not say which of the locked
1426
1513
  // packages the root project directly depends on; parsePyProjectTomlFile
@@ -11,6 +11,7 @@ import {
11
11
  import process from "node:process";
12
12
 
13
13
  import { build } from "@cdxgen/cdx-purl";
14
+ import { coerce, gte } from "semver";
14
15
 
15
16
  import {
16
17
  DEBUG_MODE,
@@ -25,6 +26,7 @@ import {
25
26
  PYPI_MODULE_PACKAGE_MAPPING,
26
27
  PYTHON_STD_MODULES,
27
28
  } from "../core/state.js";
29
+ import { parseToml } from "../core/toml.js";
28
30
  import { findAppModules } from "../inventory/atomUtils.js";
29
31
  import { flattenDeps } from "../inventory/deps.js";
30
32
  import { pypiBomRef } from "../inventory/purl.js";
@@ -612,6 +614,128 @@ export function createUVLock(basePath, options) {
612
614
  }
613
615
  }
614
616
 
617
+ const CONSTRAINT_OPERATORS = [">=", "<=", "==", "!=", "~=", ">", "<", "^", "="];
618
+ const LOWER_BOUND_OPERATORS = new Set([">=", ">", "==", "~=", "^", "="]);
619
+
620
+ /**
621
+ * Check whether a single version constraint puts the lower bound at version 2
622
+ * or higher.
623
+ *
624
+ * @param {string} constraint Constraint such as ">=2.0.0" or "<3.0.0"
625
+ * @returns {boolean} true when the constraint requires version 2 or newer
626
+ */
627
+ function requiresVersion2(constraint) {
628
+ let operator = "";
629
+ let version = constraint.trim();
630
+ for (const candidate of CONSTRAINT_OPERATORS) {
631
+ if (version.startsWith(candidate)) {
632
+ operator = candidate;
633
+ version = version.slice(candidate.length).trim();
634
+ break;
635
+ }
636
+ }
637
+ if (!operator || !LOWER_BOUND_OPERATORS.has(operator)) {
638
+ return false;
639
+ }
640
+ const coerced = coerce(version);
641
+ return coerced !== null && gte(coerced, "2.0.0");
642
+ }
643
+
644
+ /**
645
+ * Check whether a build requirement names poetry-core with a lower bound of
646
+ * version 2 or higher.
647
+ *
648
+ * @param {string} requirement Build requirement such as "poetry-core>=2.0.0"
649
+ * @returns {boolean} true when poetry-core 2 or newer is required
650
+ */
651
+ function requiresPoetryCore2(requirement) {
652
+ if (typeof requirement !== "string") {
653
+ return false;
654
+ }
655
+ const entry = requirement.trim().toLowerCase();
656
+ if (!entry.startsWith("poetry-core")) {
657
+ return false;
658
+ }
659
+ let spec = entry.slice("poetry-core".length);
660
+ if (spec.startsWith("[")) {
661
+ const extrasEnd = spec.indexOf("]");
662
+ if (extrasEnd === -1) {
663
+ return false;
664
+ }
665
+ spec = spec.slice(extrasEnd + 1);
666
+ }
667
+ const next = spec.charAt(0);
668
+ if (next && next !== " " && next !== "\t" && !"><=!~^".includes(next)) {
669
+ return false;
670
+ }
671
+ return spec
672
+ .trim()
673
+ .split(",")
674
+ .some((constraint) => requiresVersion2(constraint));
675
+ }
676
+
677
+ /**
678
+ * Check whether a pyproject.toml requires poetry-core 2 or newer to build.
679
+ *
680
+ * @param {string} pyprojectFile Path to pyproject.toml
681
+ * @returns {boolean} true when the project needs Poetry 2 or newer
682
+ */
683
+ export function isPoetry2Project(pyprojectFile) {
684
+ if (!pyprojectFile || !safeExistsSync(pyprojectFile)) {
685
+ return false;
686
+ }
687
+ let config;
688
+ try {
689
+ config = parseToml(readFileSync(pyprojectFile, "utf-8"));
690
+ } catch (_err) {
691
+ return false;
692
+ }
693
+ const requires = config?.["build-system"]?.requires;
694
+ if (!Array.isArray(requires)) {
695
+ return false;
696
+ }
697
+ return requires.some((requirement) => requiresPoetryCore2(requirement));
698
+ }
699
+
700
+ /**
701
+ * Find the pyproject.toml belonging to a poetry trigger file.
702
+ *
703
+ * @param {string} triggerFile pyproject.toml or poetry.lock path
704
+ * @returns {string|null} Path to pyproject.toml, or null when there is none
705
+ */
706
+ function findPoetryPyprojectFile(triggerFile) {
707
+ if (!triggerFile) {
708
+ return null;
709
+ }
710
+ if (triggerFile.endsWith("pyproject.toml")) {
711
+ return triggerFile;
712
+ }
713
+ if (triggerFile.endsWith("poetry.lock")) {
714
+ const sibling = join(dirname(triggerFile), "pyproject.toml");
715
+ return safeExistsSync(sibling) ? sibling : null;
716
+ }
717
+ return null;
718
+ }
719
+
720
+ /**
721
+ * Build the poetry install arguments for a poetry project.
722
+ *
723
+ * Poetry 2 installs only the main dependency group and no extras by default,
724
+ * so projects requiring poetry-core 2 or newer also get --all-groups and
725
+ * --all-extras. Callers prepend the python invocation such as -m poetry.
726
+ *
727
+ * @param {string} pyprojectFile pyproject.toml or poetry.lock path of the project
728
+ * @returns {string[]} poetry install arguments
729
+ */
730
+ export function getPoetryInstallArgs(pyprojectFile) {
731
+ const args = ["install", "-n", "--no-root"];
732
+ const resolvedFile = findPoetryPyprojectFile(pyprojectFile);
733
+ if (resolvedFile && isPoetry2Project(resolvedFile)) {
734
+ args.push("--all-groups", "--all-extras");
735
+ }
736
+ return args;
737
+ }
738
+
615
739
  /**
616
740
  * Execute pip freeze by creating a virtual env in a temp directory and construct the dependency tree
617
741
  *
@@ -742,7 +866,9 @@ export async function getPipFrozenTree(
742
866
  shell: isWin,
743
867
  });
744
868
  thoughtLog("Performing poetry install");
745
- let poetryInstallArgs = ["-m", "poetry", "install", "-n", "--no-root"];
869
+ let poetryInstallArgs = ["-m", "poetry"].concat(
870
+ getPoetryInstallArgs(reqOrSetupFile),
871
+ );
746
872
  if (isSecureMode) {
747
873
  poetryInstallArgs.unshift("-S");
748
874
  }
@@ -756,7 +882,7 @@ export async function getPipFrozenTree(
756
882
  thoughtLog(
757
883
  "Hmm, poetry doesn't seem to be available as a module. Perhaps it was installed directly 🤔?",
758
884
  );
759
- poetryInstallArgs = ["install", "-n", "--no-root"];
885
+ poetryInstallArgs = getPoetryInstallArgs(reqOrSetupFile);
760
886
  // Attempt to perform poetry install
761
887
  result = safeSpawnSync("poetry", poetryInstallArgs, {
762
888
  cwd: basePath,