@cyclonedx/cdxgen 12.1.3 → 12.1.5

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 (107) hide show
  1. package/README.md +1 -1
  2. package/bin/cdxgen.js +12 -0
  3. package/bin/repl.js +2 -2
  4. package/lib/cli/index.js +164 -71
  5. package/lib/evinser/evinser.js +3 -4
  6. package/lib/evinser/swiftsem.js +1 -1
  7. package/lib/helpers/caxa.js +1 -1
  8. package/lib/helpers/display.js +6 -10
  9. package/lib/helpers/envcontext.js +5 -5
  10. package/lib/helpers/pythonutils.js +296 -0
  11. package/lib/helpers/pythonutils.poku.js +469 -0
  12. package/lib/helpers/utils.js +303 -95
  13. package/lib/helpers/utils.poku.js +84 -1
  14. package/lib/managers/piptree.js +1 -1
  15. package/lib/parsers/npmrc.js +88 -0
  16. package/lib/parsers/npmrc.poku.js +492 -0
  17. package/lib/server/openapi.yaml +0 -9
  18. package/lib/server/server.js +18 -5
  19. package/lib/stages/pregen/env-audit.js +34 -0
  20. package/lib/stages/pregen/env-audit.poku.js +290 -0
  21. package/lib/third-party/arborist/lib/deepest-nesting-target.js +1 -1
  22. package/lib/third-party/arborist/lib/node.js +3 -3
  23. package/lib/third-party/arborist/lib/shrinkwrap.js +1 -1
  24. package/lib/third-party/arborist/lib/tree-check.js +1 -1
  25. package/package.json +6 -6
  26. package/types/lib/cli/index.d.ts +39 -39
  27. package/types/lib/cli/index.d.ts.map +1 -1
  28. package/types/lib/evinser/evinser.d.ts +19 -19
  29. package/types/lib/evinser/evinser.d.ts.map +1 -1
  30. package/types/lib/evinser/swiftsem.d.ts +14 -14
  31. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  32. package/types/lib/helpers/cbomutils.d.ts +1 -1
  33. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  34. package/types/lib/helpers/db.d.ts +2 -2
  35. package/types/lib/helpers/db.d.ts.map +1 -1
  36. package/types/lib/helpers/display.d.ts +2 -2
  37. package/types/lib/helpers/display.d.ts.map +1 -1
  38. package/types/lib/helpers/envcontext.d.ts +14 -14
  39. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  40. package/types/lib/helpers/logger.d.ts +1 -1
  41. package/types/lib/helpers/logger.d.ts.map +1 -1
  42. package/types/lib/helpers/protobom.d.ts +4 -2
  43. package/types/lib/helpers/protobom.d.ts.map +1 -1
  44. package/types/lib/helpers/pythonutils.d.ts +9 -0
  45. package/types/lib/helpers/pythonutils.d.ts.map +1 -0
  46. package/types/lib/helpers/utils.d.ts +103 -88
  47. package/types/lib/helpers/utils.d.ts.map +1 -1
  48. package/types/lib/managers/binary.d.ts +2 -2
  49. package/types/lib/managers/binary.d.ts.map +1 -1
  50. package/types/lib/managers/docker.d.ts +2 -2
  51. package/types/lib/managers/docker.d.ts.map +1 -1
  52. package/types/lib/managers/oci.d.ts +1 -1
  53. package/types/lib/managers/oci.d.ts.map +1 -1
  54. package/types/lib/managers/piptree.d.ts +1 -1
  55. package/types/lib/managers/piptree.d.ts.map +1 -1
  56. package/types/lib/parsers/iri.d.ts +6 -6
  57. package/types/lib/parsers/iri.d.ts.map +1 -1
  58. package/types/lib/parsers/npmrc.d.ts +23 -0
  59. package/types/lib/parsers/npmrc.d.ts.map +1 -0
  60. package/types/lib/server/server.d.ts +1 -1
  61. package/types/lib/server/server.d.ts.map +1 -1
  62. package/types/lib/stages/postgen/annotator.d.ts +3 -3
  63. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  64. package/types/lib/stages/postgen/postgen.d.ts +5 -5
  65. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  66. package/types/lib/stages/pregen/env-audit.d.ts +2 -0
  67. package/types/lib/stages/pregen/env-audit.d.ts.map +1 -0
  68. package/types/lib/stages/pregen/pregen.d.ts +6 -6
  69. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  70. package/types/lib/third-party/arborist/lib/arborist/index.d.ts +4 -3
  71. package/types/lib/third-party/arborist/lib/arborist/index.d.ts.map +1 -1
  72. package/types/lib/third-party/arborist/lib/can-place-dep.d.ts +5 -5
  73. package/types/lib/third-party/arborist/lib/can-place-dep.d.ts.map +1 -1
  74. package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts +4 -4
  75. package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts.map +1 -1
  76. package/types/lib/third-party/arborist/lib/diff.d.ts +3 -3
  77. package/types/lib/third-party/arborist/lib/diff.d.ts.map +1 -1
  78. package/types/lib/third-party/arborist/lib/edge.d.ts +2 -2
  79. package/types/lib/third-party/arborist/lib/edge.d.ts.map +1 -1
  80. package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts +1 -1
  81. package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts.map +1 -1
  82. package/types/lib/third-party/arborist/lib/inventory.d.ts +3 -2
  83. package/types/lib/third-party/arborist/lib/inventory.d.ts.map +1 -1
  84. package/types/lib/third-party/arborist/lib/link.d.ts +10 -7
  85. package/types/lib/third-party/arborist/lib/link.d.ts.map +1 -1
  86. package/types/lib/third-party/arborist/lib/node.d.ts +8 -8
  87. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  88. package/types/lib/third-party/arborist/lib/optional-set.d.ts +1 -1
  89. package/types/lib/third-party/arborist/lib/optional-set.d.ts.map +1 -1
  90. package/types/lib/third-party/arborist/lib/override-set.d.ts +3 -3
  91. package/types/lib/third-party/arborist/lib/override-set.d.ts.map +1 -1
  92. package/types/lib/third-party/arborist/lib/peer-entry-sets.d.ts +1 -1
  93. package/types/lib/third-party/arborist/lib/peer-entry-sets.d.ts.map +1 -1
  94. package/types/lib/third-party/arborist/lib/place-dep.d.ts +3 -3
  95. package/types/lib/third-party/arborist/lib/place-dep.d.ts.map +1 -1
  96. package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts +7 -7
  97. package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts.map +1 -1
  98. package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts +1 -1
  99. package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts.map +1 -1
  100. package/types/lib/third-party/arborist/lib/yarn-lock.d.ts +4 -3
  101. package/types/lib/third-party/arborist/lib/yarn-lock.d.ts.map +1 -1
  102. package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts +0 -34
  103. package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts.map +0 -1
  104. package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts +0 -24
  105. package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts.map +0 -1
  106. package/types/lib/third-party/arborist/lib/tracker.d.ts +0 -13
  107. package/types/lib/third-party/arborist/lib/tracker.d.ts.map +0 -1
@@ -57,6 +57,7 @@ import { IriValidationStrategy, validateIri } from "../parsers/iri.js";
57
57
  import Arborist from "../third-party/arborist/lib/index.js";
58
58
  import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
59
59
  import { thoughtLog, traceLog } from "./logger.js";
60
+ import { get_python_command_from_env, getVenvMetadata } from "./pythonutils.js";
60
61
 
61
62
  let url = import.meta?.url;
62
63
  if (url && !url.startsWith("file://")) {
@@ -175,6 +176,40 @@ export function safeSpawnSync(command, args, options) {
175
176
  );
176
177
  }
177
178
  }
179
+ let isPyPackageInstall = false;
180
+ if (command.includes("pip") && args?.includes("install")) {
181
+ isPyPackageInstall = true;
182
+ } else if (
183
+ command.includes("python") &&
184
+ args?.includes("pip") &&
185
+ args?.includes("install")
186
+ ) {
187
+ isPyPackageInstall = true;
188
+ } else if (
189
+ command.includes("uv") &&
190
+ args?.includes("pip") &&
191
+ args?.includes("install")
192
+ ) {
193
+ isPyPackageInstall = true;
194
+ }
195
+ if (isPyPackageInstall) {
196
+ const hasOnlyBinary = args?.some(
197
+ (arg) => arg === "--only-binary" || arg.startsWith("--only-binary="),
198
+ );
199
+ if (!hasOnlyBinary) {
200
+ if (isSecureMode) {
201
+ console.warn(
202
+ "\x1b[1;31mSecurity Alert: pip/uv install invoked without '--only-binary' argument in secure mode. This is a bug in cdxgen and introduces Arbitrary Code Execution (ACE) risks. Please report with an example repo here https://github.com/cdxgen/cdxgen/issues.\x1b[0m",
203
+ );
204
+ } else if (process.env?.CDXGEN_IN_CONTAINER === "true") {
205
+ console.log("Running pip/uv install without '--only-binary' argument.");
206
+ } else {
207
+ console.warn(
208
+ "\x1b[1;35mNotice: pip/uv install invoked without '--only-binary'. This allows executing untrusted setup.py scripts. Only run cdxgen in trusted directories.\x1b",
209
+ );
210
+ }
211
+ }
212
+ }
178
213
  traceLog("spawn", { command, args, ...options });
179
214
  commandsExecuted.add(command);
180
215
  // Fix for DEP0190 warning
@@ -471,7 +506,11 @@ export const PROJECT_TYPE_ALIASES = {
471
506
  "poetry",
472
507
  "uv",
473
508
  "pdm",
509
+ "rye",
474
510
  "hatch",
511
+ "conda",
512
+ "miniconda",
513
+ "pyenv",
475
514
  ],
476
515
  go: ["go", "golang", "gomod", "gopkg"],
477
516
  rust: ["rust", "rust-lang", "cargo"],
@@ -1462,6 +1501,40 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1462
1501
  value: "true",
1463
1502
  });
1464
1503
  }
1504
+ // Detect version spoofing by comparing the version in the lockfile with the version in package.json
1505
+ if (node.path && safeExistsSync(join(node.path, "package.json"))) {
1506
+ try {
1507
+ const diskPkgStr = readFileSync(
1508
+ join(node.path, "package.json"),
1509
+ "utf8",
1510
+ );
1511
+ const diskPkg = JSON.parse(diskPkgStr);
1512
+ if (!diskPkg.name || diskPkg.name !== node.packageName) {
1513
+ console.warn(
1514
+ `\x1b[1;35mWARNING: Package name spoofing detected for ${node.packageName}! Lockfile says ${node.packageName}, but disk says ${diskPkg.name}.\x1b[0m`,
1515
+ );
1516
+ if (diskPkg.name) {
1517
+ pkg.properties.push({
1518
+ name: "cdx:npm:nameMismatchError",
1519
+ value: `${diskPkg.name} used instead of ${node.packageName}`,
1520
+ });
1521
+ }
1522
+ }
1523
+ if (!diskPkg.version || diskPkg.version !== node.version) {
1524
+ console.warn(
1525
+ `\x1b[1;35mWARNING: Package version spoofing detected for ${node.packageName}! Lockfile says ${node.version}, but disk says ${diskPkg.version}.\x1b[0m`,
1526
+ );
1527
+ if (diskPkg.version) {
1528
+ pkg.properties.push({
1529
+ name: "cdx:npm:versionMismatchError",
1530
+ value: `${diskPkg.version} used instead of ${node.version}`,
1531
+ });
1532
+ }
1533
+ }
1534
+ } catch (_err) {
1535
+ // ignore
1536
+ }
1537
+ }
1465
1538
  if (node?.inBundle) {
1466
1539
  pkg.properties.push({
1467
1540
  name: "cdx:npm:inBundle",
@@ -2796,7 +2869,7 @@ export function findPnpmPackagePath(baseDir, packageName, version) {
2796
2869
  * @returns {Array} Enhanced package list
2797
2870
  */
2798
2871
  export async function pnpmMetadata(pkgList, lockFilePath) {
2799
- if (!pkgList || !pkgList.length || !lockFilePath) {
2872
+ if (!pkgList?.length || !lockFilePath) {
2800
2873
  return pkgList;
2801
2874
  }
2802
2875
 
@@ -5062,7 +5135,7 @@ export async function getMvnMetadata(
5062
5135
  const ANDROID_MAVEN_URL =
5063
5136
  process.env.ANDROID_MAVEN_URL || "https://maven.google.com/";
5064
5137
  const cdepList = [];
5065
- if (!pkgList || !pkgList.length) {
5138
+ if (!pkgList?.length) {
5066
5139
  return pkgList;
5067
5140
  }
5068
5141
  if (DEBUG_MODE && shouldFetchLicense()) {
@@ -5356,7 +5429,7 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
5356
5429
  const PYPI_URL = process.env.PYPI_URL || "https://pypi.org/pypi/";
5357
5430
  const cdepList = [];
5358
5431
  for (const p of pkgList) {
5359
- if (!p || !p.name) {
5432
+ if (!p?.name) {
5360
5433
  continue;
5361
5434
  }
5362
5435
  try {
@@ -5440,7 +5513,7 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
5440
5513
  }
5441
5514
  }
5442
5515
  // Use the latest version if none specified
5443
- if (!p.version || !p.version.trim().length) {
5516
+ if (!p.version?.trim().length) {
5444
5517
  let versionSpecifiers;
5445
5518
  if (p.properties?.length) {
5446
5519
  for (const pprop of p.properties) {
@@ -5595,6 +5668,9 @@ export function parseBdistMetadata(mDataFile, rawMetadata = undefined) {
5595
5668
  externalReferences: [],
5596
5669
  properties: [],
5597
5670
  };
5671
+ if (mDataFile) {
5672
+ pkg.properties.push({ name: "SrcFile", value: mDataFile });
5673
+ }
5598
5674
  const lines = mData.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
5599
5675
  let isBody = false;
5600
5676
  for (const line of lines) {
@@ -6433,7 +6509,8 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6433
6509
  },
6434
6510
  }
6435
6511
  : undefined;
6436
- const lines = reqData.replace(/\r/g, "").replace(/\\$/gm, "").split("\n");
6512
+ const normalizedData = reqData.replace(/\r/g, "").replace(/\\\n/g, " ");
6513
+ const lines = normalizedData.split("\n");
6437
6514
  for (const line of lines) {
6438
6515
  let l = line.trim();
6439
6516
  if (l.includes("# Basic requirements")) {
@@ -6461,7 +6538,25 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6461
6538
  },
6462
6539
  ]
6463
6540
  : [];
6464
-
6541
+ const hashes = [];
6542
+ const hashRegex = /--hash=([a-zA-Z0-9\-]+):([a-fA-F0-9]+)/g;
6543
+ let hashMatch;
6544
+ while ((hashMatch = hashRegex.exec(l)) !== null) {
6545
+ let alg = hashMatch[1].toUpperCase();
6546
+ if (alg === "SHA256") alg = "SHA-256";
6547
+ else if (alg === "SHA384") alg = "SHA-384";
6548
+ else if (alg === "SHA512") alg = "SHA-512";
6549
+ else if (alg === "SHA1") alg = "SHA-1";
6550
+ hashes.push({
6551
+ alg: alg,
6552
+ content: hashMatch[2],
6553
+ });
6554
+ }
6555
+ // Strip the hash flags and any residual backslashes
6556
+ l = l
6557
+ .replace(/--hash=[a-zA-Z0-9\-]+:[a-fA-F0-9]+/g, "")
6558
+ .replace(/\\/g, "")
6559
+ .trim();
6465
6560
  // Handle markers
6466
6561
  let markers = null;
6467
6562
  let structuredMarkers = null;
@@ -6501,6 +6596,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6501
6596
  scope: compScope,
6502
6597
  evidence,
6503
6598
  };
6599
+ if (hashes.length > 0) {
6600
+ apkg.hashes = hashes;
6601
+ }
6504
6602
  if (comment) {
6505
6603
  apkg.licenses = comment
6506
6604
  .split("/")
@@ -10492,10 +10590,7 @@ export function parseConanLockData(conanLockData) {
10492
10590
  }
10493
10591
 
10494
10592
  const lockFile = JSON.parse(conanLockData);
10495
- if (
10496
- (!lockFile || !lockFile.graph_lock || !lockFile.graph_lock.nodes) &&
10497
- !lockFile.requires
10498
- ) {
10593
+ if (!lockFile?.graph_lock?.nodes && !lockFile.requires) {
10499
10594
  return { pkgList, dependencies, parentComponentDependencies };
10500
10595
  }
10501
10596
 
@@ -11878,7 +11973,7 @@ export function parseCsPkgLockData(csLockData, pkgLockFile) {
11878
11973
  };
11879
11974
  }
11880
11975
  const assetData = JSON.parse(csLockData);
11881
- if (!assetData || !assetData.dependencies) {
11976
+ if (!assetData?.dependencies) {
11882
11977
  return {
11883
11978
  pkgList,
11884
11979
  dependenciesList,
@@ -12181,7 +12276,7 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12181
12276
  for (const i in packages[compScope]) {
12182
12277
  const pkg = packages[compScope][i];
12183
12278
  // Be extra cautious. Potential fix for #236
12184
- if (!pkg || !pkg.name || !pkg.version) {
12279
+ if (!pkg?.name || !pkg.version) {
12185
12280
  continue;
12186
12281
  }
12187
12282
 
@@ -12279,7 +12374,7 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12279
12374
  for (const compScope in packages) {
12280
12375
  for (const i in packages[compScope]) {
12281
12376
  const pkg = packages[compScope][i];
12282
- if (!pkg || !pkg.name || !pkg.version) {
12377
+ if (!pkg?.name || !pkg.version) {
12283
12378
  continue;
12284
12379
  }
12285
12380
  if (!pkg.require || !Object.keys(pkg.require).length) {
@@ -15232,36 +15327,6 @@ function flattenDeps(dependenciesMap, pkgList, reqOrSetupFile, t) {
15232
15327
  .sort();
15233
15328
  }
15234
15329
 
15235
- function get_python_command_from_env(env) {
15236
- // Virtual environments needs special treatment to use the correct python executable
15237
- // Without this step, the default python is always used resulting in false positives
15238
- const python_exe_name = isWin ? "python.exe" : "python";
15239
- const python3_exe_name = isWin ? "python3.exe" : "python3";
15240
- let python_cmd_to_use = PYTHON_CMD;
15241
- if (env.VIRTUAL_ENV) {
15242
- const bin_dir = isWin ? "Scripts" : "bin";
15243
- if (safeExistsSync(join(env.VIRTUAL_ENV, bin_dir, python_exe_name))) {
15244
- python_cmd_to_use = join(env.VIRTUAL_ENV, bin_dir, python_exe_name);
15245
- } else if (
15246
- safeExistsSync(join(env.VIRTUAL_ENV, bin_dir, python3_exe_name))
15247
- ) {
15248
- python_cmd_to_use = join(env.VIRTUAL_ENV, bin_dir, python3_exe_name);
15249
- }
15250
- } else if (env.CONDA_PREFIX) {
15251
- const bin_dir = isWin ? "" : "bin";
15252
- if (safeExistsSync(join(env.CONDA_PREFIX, bin_dir, python_exe_name))) {
15253
- python_cmd_to_use = join(env.CONDA_PREFIX, bin_dir, python_exe_name);
15254
- } else if (
15255
- safeExistsSync(join(env.CONDA_PREFIX, bin_dir, python3_exe_name))
15256
- ) {
15257
- python_cmd_to_use = join(env.CONDA_PREFIX, bin_dir, python3_exe_name);
15258
- }
15259
- } else if (env.CONDA_PYTHON_EXE) {
15260
- python_cmd_to_use = env.CONDA_PYTHON_EXE;
15261
- }
15262
- return python_cmd_to_use;
15263
- }
15264
-
15265
15330
  /**
15266
15331
  * Create uv.lock file with uv sync command.
15267
15332
  *
@@ -15344,14 +15409,12 @@ export async function getPipFrozenTree(
15344
15409
  ...process.env,
15345
15410
  };
15346
15411
 
15347
- // FIX: Create a set of explicit dependencies from requirements.txt to identify root packages.
15348
15412
  const explicitDeps = new Set();
15349
15413
  if (reqOrSetupFile?.endsWith(".txt") && safeExistsSync(reqOrSetupFile)) {
15350
15414
  // We only need the package names, so we pass `false` to avoid fetching full metadata.
15351
15415
  const tempPkgList = await parseReqFile(reqOrSetupFile, null, false);
15352
15416
  for (const pkg of tempPkgList) {
15353
15417
  if (pkg.name) {
15354
- // Normalize the name (lowercase, hyphenated) for accurate lookups.
15355
15418
  explicitDeps.add(pkg.name.replace(/_/g, "-").toLowerCase());
15356
15419
  }
15357
15420
  }
@@ -15407,17 +15470,26 @@ export async function getPipFrozenTree(
15407
15470
  }
15408
15471
  }
15409
15472
  }
15410
- /**
15411
- * We now have a virtual environment so we can attempt to install the project and perform
15412
- * pip freeze to collect the packages that got installed.
15413
- * Note that we did not create a virtual environment for poetry because poetry will do this when we run the install.
15414
- * This step is accurate but not reproducible since the resulting list could differ based on various factors
15415
- * such as the version of python, pip, os, pypi.org availability (and weather?)
15416
- */
15417
- // Bug #388. Perform pip install in all virtualenv to make the experience consistent
15473
+ const venvMeta = getVenvMetadata(env);
15474
+ const python_cmd_for_tree = get_python_command_from_env(env);
15475
+ // Check if pyproject.toml is actually a uv-configured workspace
15476
+ let hasToolUv = false;
15477
+ let hasToolPoetry = false;
15478
+ if (
15479
+ reqOrSetupFile?.endsWith("pyproject.toml") &&
15480
+ safeExistsSync(reqOrSetupFile)
15481
+ ) {
15482
+ try {
15483
+ const content = readFileSync(reqOrSetupFile, "utf-8");
15484
+ hasToolUv = content.includes("[tool.uv]");
15485
+ hasToolPoetry = content.includes('build-backend = "poetry.core');
15486
+ } catch (_err) {
15487
+ // Ignore read error
15488
+ }
15489
+ }
15418
15490
  if (reqOrSetupFile) {
15419
15491
  // We have a poetry.lock file
15420
- if (reqOrSetupFile.endsWith("poetry.lock")) {
15492
+ if (reqOrSetupFile.endsWith("poetry.lock") || hasToolPoetry) {
15421
15493
  const poetryConfigArgs = [
15422
15494
  "-m",
15423
15495
  "poetry",
@@ -15504,20 +15576,78 @@ export async function getPipFrozenTree(
15504
15576
  )}${_delimiter}${process.env.PATH || ""}`;
15505
15577
  }
15506
15578
  }
15579
+ } else if (reqOrSetupFile.endsWith("pdm.lock") || venvMeta.type === "pdm") {
15580
+ thoughtLog("Performing pdm install");
15581
+ result = safeSpawnSync("pdm", ["install"], {
15582
+ cwd: basePath,
15583
+ shell: isWin,
15584
+ env,
15585
+ });
15586
+ if (result.status !== 0 || result.error) {
15587
+ frozen = false;
15588
+ }
15589
+ } else if (
15590
+ reqOrSetupFile.endsWith("pixi.lock") ||
15591
+ venvMeta.type === "pixi"
15592
+ ) {
15593
+ thoughtLog("Performing pixi install");
15594
+ result = safeSpawnSync("pixi", ["install"], {
15595
+ cwd: basePath,
15596
+ shell: isWin,
15597
+ env,
15598
+ });
15599
+ if (result.status !== 0 || result.error) {
15600
+ frozen = false;
15601
+ }
15602
+ } else if (
15603
+ reqOrSetupFile.endsWith("uv.lock") ||
15604
+ (venvMeta.type === "uv" && hasToolUv)
15605
+ ) {
15606
+ thoughtLog("Performing uv sync");
15607
+ result = safeSpawnSync("uv", ["sync"], {
15608
+ cwd: basePath,
15609
+ shell: isWin,
15610
+ env,
15611
+ });
15612
+ if (result.status !== 0 || result.error) {
15613
+ frozen = false;
15614
+ }
15615
+ } else if (
15616
+ venvMeta.type === "rye" ||
15617
+ reqOrSetupFile.endsWith("requirements.lock")
15618
+ ) {
15619
+ thoughtLog("Performing rye sync");
15620
+ result = safeSpawnSync("rye", ["sync"], {
15621
+ cwd: basePath,
15622
+ shell: isWin,
15623
+ env,
15624
+ });
15625
+ if (result.status !== 0 || result.error) {
15626
+ frozen = false;
15627
+ }
15507
15628
  } else {
15508
- // We are about to do a pip install with the right python command from the virtual environment
15509
- // This step can fail if the correct OS packages and development libraries are not installed
15510
- const python_cmd_for_tree = get_python_command_from_env(env);
15511
- let pipInstallArgs = [
15512
- "-m",
15513
- "pip",
15514
- "install",
15515
- "--disable-pip-version-check",
15516
- ];
15517
- if (isSecureMode) {
15518
- pipInstallArgs.unshift("-S");
15629
+ // General package installation (Handling pip, or uv pip)
15630
+ let installCmd = python_cmd_for_tree;
15631
+ let pipInstallArgs = [];
15632
+ if (venvMeta.type === "uv") {
15633
+ installCmd = "uv";
15634
+ pipInstallArgs = ["pip", "install"];
15635
+ if (isSecureMode) {
15636
+ pipInstallArgs.push("--only-binary");
15637
+ pipInstallArgs.push(":all:");
15638
+ }
15639
+ } else {
15640
+ pipInstallArgs = [
15641
+ "-m",
15642
+ "pip",
15643
+ "install",
15644
+ "--disable-pip-version-check",
15645
+ ];
15646
+ if (isSecureMode) {
15647
+ pipInstallArgs.push("--only-binary=:all:");
15648
+ pipInstallArgs.unshift("-S");
15649
+ }
15519
15650
  }
15520
- // Requirements.txt could be called with any name so best to check for not setup.py and not pyproject.toml
15521
15651
  if (
15522
15652
  !reqOrSetupFile.endsWith("setup.py") &&
15523
15653
  !reqOrSetupFile.endsWith("pyproject.toml")
@@ -15532,20 +15662,17 @@ export async function getPipFrozenTree(
15532
15662
  } else {
15533
15663
  pipInstallArgs.push(resolve(basePath));
15534
15664
  }
15535
- // Support for passing additional arguments to pip
15536
- // Eg: --python-version 3.10 --ignore-requires-python --no-warn-conflicts --only-binary=:all:
15537
15665
  if (process?.env?.PIP_INSTALL_ARGS) {
15538
15666
  const addArgs = process.env.PIP_INSTALL_ARGS.split(" ");
15539
15667
  pipInstallArgs = pipInstallArgs.concat(addArgs);
15540
15668
  }
15541
15669
  thoughtLog(
15542
- `**PIP**: Trying pip install using the arguments ${pipInstallArgs.join(" ")}`,
15670
+ `**INSTALL**: Trying package install using the arguments: ${installCmd} ${pipInstallArgs.join(" ")}`,
15543
15671
  );
15544
15672
  if (DEBUG_MODE) {
15545
- console.log("Executing", python_cmd_for_tree);
15673
+ console.log("Executing", installCmd);
15546
15674
  }
15547
- // Attempt to perform pip install
15548
- result = safeSpawnSync(python_cmd_for_tree, pipInstallArgs, {
15675
+ result = safeSpawnSync(installCmd, pipInstallArgs, {
15549
15676
  cwd: basePath,
15550
15677
  shell: isWin,
15551
15678
  env,
@@ -15570,6 +15697,10 @@ export async function getPipFrozenTree(
15570
15697
  );
15571
15698
  }
15572
15699
  console.log(result.stderr);
15700
+ } else if (result?.stderr?.includes("No module named pip")) {
15701
+ console.log(
15702
+ "Using uv? Ensure 'uv' is in your PATH to allow cdxgen to use `uv pip install` automatically.",
15703
+ );
15573
15704
  } else if (
15574
15705
  process.env.PIP_INSTALL_ARGS &&
15575
15706
  result.stderr?.includes("Cannot set --home and --prefix together")
@@ -15700,18 +15831,42 @@ export async function getPipFrozenTree(
15700
15831
  }
15701
15832
  // Bug #375. Attempt pip freeze on existing and new virtual environments
15702
15833
  if (env.VIRTUAL_ENV?.length || env.CONDA_PREFIX?.length) {
15703
- /**
15704
- * At this point, the previous attempt to do a pip install might have failed and we might have an unclean virtual environment with an incomplete list
15705
- * The position taken by cdxgen is "Some SBOM is better than no SBOM", so we proceed to collecting the dependencies that got installed with pip freeze
15706
- */
15707
- if (DEBUG_MODE) {
15708
- if (reqOrSetupFile) {
15709
- console.log(
15710
- `About to construct the pip dependency tree based on ${reqOrSetupFile}. Please wait ...`,
15711
- );
15834
+ const venvRoot = env.VIRTUAL_ENV || env.CONDA_PREFIX;
15835
+ const binDir = platform() === "win32" ? "Scripts" : "bin";
15836
+ const pipExe = join(
15837
+ venvRoot,
15838
+ binDir,
15839
+ platform() === "win32" ? "pip.exe" : "pip",
15840
+ );
15841
+ if (!safeExistsSync(pipExe)) {
15842
+ thoughtLog(
15843
+ "The 'pip' module is missing in this environment. Bootstrapping it to support piptree extraction.",
15844
+ );
15845
+ if (venvMeta.type === "uv") {
15846
+ safeSpawnSync("uv", ["pip", "install", "pip"], {
15847
+ cwd: basePath,
15848
+ shell: isWin,
15849
+ env,
15850
+ });
15851
+ } else if (venvMeta.type === "rye") {
15852
+ safeSpawnSync("rye", ["run", "pip", "install", "pip"], {
15853
+ cwd: basePath,
15854
+ shell: isWin,
15855
+ env,
15856
+ });
15857
+ } else {
15858
+ safeSpawnSync(python_cmd_for_tree, ["-m", "ensurepip", "--upgrade"], {
15859
+ cwd: basePath,
15860
+ shell: isWin,
15861
+ env,
15862
+ });
15712
15863
  }
15713
15864
  }
15714
- const python_cmd_for_tree = get_python_command_from_env(env);
15865
+ if (DEBUG_MODE && reqOrSetupFile) {
15866
+ console.log(
15867
+ `About to construct the dependency tree based on ${reqOrSetupFile}. Please wait ...`,
15868
+ );
15869
+ }
15715
15870
  // This is a slow step that ideally needs to be invoked only once per venv
15716
15871
  const tree = getTreeWithPlugin(env, python_cmd_for_tree, basePath);
15717
15872
  if (DEBUG_MODE && !tree.length) {
@@ -15862,10 +16017,23 @@ export function getPipTreeForPackages(
15862
16017
  env.PYTHONPATH = undefined;
15863
16018
  }
15864
16019
  }
16020
+ const venvMeta = getVenvMetadata(env);
15865
16021
  const python_cmd_for_tree = get_python_command_from_env(env);
15866
- let pipInstallArgs = ["-m", "pip", "install", "--disable-pip-version-check"];
15867
- if (isSecureMode) {
15868
- pipInstallArgs.unshift("-S");
16022
+ let installCmd = python_cmd_for_tree;
16023
+ let pipInstallArgs = [];
16024
+ if (venvMeta.type === "uv") {
16025
+ installCmd = "uv";
16026
+ pipInstallArgs = ["pip", "install"];
16027
+ if (isSecureMode) {
16028
+ pipInstallArgs.push("--only-binary");
16029
+ pipInstallArgs.push(":all:");
16030
+ }
16031
+ } else {
16032
+ pipInstallArgs = ["-m", "pip", "install", "--disable-pip-version-check"];
16033
+ if (isSecureMode) {
16034
+ pipInstallArgs.push("--only-binary=:all:");
16035
+ pipInstallArgs.unshift("-S");
16036
+ }
15869
16037
  }
15870
16038
  // Support for passing additional arguments to pip
15871
16039
  // Eg: --python-version 3.10 --ignore-requires-python --no-warn-conflicts
@@ -15873,19 +16041,23 @@ export function getPipTreeForPackages(
15873
16041
  const addArgs = process.env.PIP_INSTALL_ARGS.split(" ");
15874
16042
  pipInstallArgs = pipInstallArgs.concat(addArgs);
15875
16043
  } else {
15876
- pipInstallArgs = pipInstallArgs.concat([
15877
- "--ignore-requires-python",
15878
- "--no-compile",
15879
- "--no-warn-script-location",
15880
- "--no-warn-conflicts",
15881
- ]);
16044
+ if (venvMeta.type !== "uv") {
16045
+ pipInstallArgs = pipInstallArgs.concat([
16046
+ "--ignore-requires-python",
16047
+ "--no-compile",
16048
+ "--no-warn-script-location",
16049
+ "--no-warn-conflicts",
16050
+ ]);
16051
+ } else {
16052
+ pipInstallArgs.push("--no-compile");
16053
+ }
15882
16054
  }
15883
16055
  if (DEBUG_MODE) {
15884
16056
  console.log(
15885
16057
  "Installing",
15886
16058
  pkgList.length,
15887
16059
  "packages using the command",
15888
- python_cmd_for_tree,
16060
+ installCmd,
15889
16061
  pipInstallArgs.join(" "),
15890
16062
  );
15891
16063
  }
@@ -15915,7 +16087,7 @@ export function getPipTreeForPackages(
15915
16087
  }
15916
16088
  // Attempt to perform pip install for pkgSpecifier
15917
16089
  const result = safeSpawnSync(
15918
- python_cmd_for_tree,
16090
+ installCmd,
15919
16091
  [...pipInstallArgs, pkgSpecifier],
15920
16092
  {
15921
16093
  cwd: basePath,
@@ -15932,6 +16104,30 @@ export function getPipTreeForPackages(
15932
16104
  }
15933
16105
  // Did any package get installed successfully?
15934
16106
  if (failedPkgList.length < pkgList.length) {
16107
+ const venvRoot = env.VIRTUAL_ENV || env.CONDA_PREFIX;
16108
+ if (venvRoot) {
16109
+ const binDir = platform() === "win32" ? "Scripts" : "bin";
16110
+ const pipExe = join(
16111
+ venvRoot,
16112
+ binDir,
16113
+ platform() === "win32" ? "pip.exe" : "pip",
16114
+ );
16115
+ if (!safeExistsSync(pipExe)) {
16116
+ if (venvMeta.type === "uv") {
16117
+ safeSpawnSync("uv", ["pip", "install", "pip"], {
16118
+ cwd: basePath,
16119
+ shell: isWin,
16120
+ env,
16121
+ });
16122
+ } else {
16123
+ safeSpawnSync(python_cmd_for_tree, ["-m", "ensurepip", "--upgrade"], {
16124
+ cwd: basePath,
16125
+ shell: isWin,
16126
+ env,
16127
+ });
16128
+ }
16129
+ }
16130
+ }
15935
16131
  const dependenciesMap = {};
15936
16132
  const tree = getTreeWithPlugin(env, python_cmd_for_tree, basePath);
15937
16133
  for (const t of tree) {
@@ -16048,6 +16244,7 @@ export async function addEvidenceForImports(
16048
16244
  const aliases = group?.length
16049
16245
  ? [name, `${group}/${name}`, `@${group}/${name}`]
16050
16246
  : [name];
16247
+ let isImported = false;
16051
16248
  for (const alias of aliases) {
16052
16249
  const all_includes = impPkgs.filter(
16053
16250
  (find_pkg) =>
@@ -16096,6 +16293,7 @@ export async function addEvidenceForImports(
16096
16293
  }
16097
16294
  // Identify all the imported modules of a component
16098
16295
  if (impPkgs.includes(alias) || all_includes.length) {
16296
+ isImported = true;
16099
16297
  let importedModules = new Set();
16100
16298
  pkg.scope = "required";
16101
16299
  for (const subevidence of all_includes) {
@@ -16133,6 +16331,16 @@ export async function addEvidenceForImports(
16133
16331
  }
16134
16332
  break;
16135
16333
  }
16334
+ if (
16335
+ impPkgs?.length > 0 &&
16336
+ !isImported &&
16337
+ DEBUG_MODE &&
16338
+ pkg?.scope !== "optional"
16339
+ ) {
16340
+ console.debug(
16341
+ `\x1b[1;35mNotice: Package ${pkg.name} has no usage in code. Check if it is needed.\x1b[0m`,
16342
+ );
16343
+ }
16136
16344
  // Capture metadata such as description from local node_modules in deep mode
16137
16345
  if (deep && !pkg.description && pkg.properties) {
16138
16346
  let localNodeModulesPath;
@@ -16719,7 +16927,7 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
16719
16927
  // Normalize windows separator
16720
16928
  afile = afile.replace("..\\", "").replace(/\\/g, "/");
16721
16929
  const fileName = basename(afile);
16722
- if (!fileName || !fileName.length) {
16930
+ if (!fileName?.length) {
16723
16931
  continue;
16724
16932
  }
16725
16933
  const extn = extname(fileName);
@@ -16956,7 +17164,7 @@ async function queryNuget(p, NUGET_URL) {
16956
17164
  { responseType: "json" },
16957
17165
  );
16958
17166
  const items = res.body.items;
16959
- if (!items || !items[0]) {
17167
+ if (!items?.[0]) {
16960
17168
  return [np, newBody, body];
16961
17169
  }
16962
17170
  if (items[0] && !items[0].items) {