@cyclonedx/cdxgen 11.4.3 → 11.5.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.
@@ -507,6 +507,7 @@ export const PROJECT_TYPE_ALIASES = {
507
507
  oci: ["docker", "oci", "container", "podman"],
508
508
  cocoa: ["cocoa", "cocoapods", "objective-c", "swift", "ios"],
509
509
  scala: ["scala", "scala3", "sbt", "mill"],
510
+ nix: ["nix", "nixos", "flake"],
510
511
  };
511
512
 
512
513
  // Package manager aliases
@@ -580,8 +581,11 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
580
581
  const allProjectTypes = [...projectTypes];
581
582
  // Convert the project types into base types
582
583
  const baseProjectTypes = [];
583
- // Support for arbitray versioned ruby type
584
- if (projectTypes.filter((p) => p.startsWith("ruby")).length) {
584
+ // Support for arbitrary versioned ruby type
585
+ if (
586
+ options.projectType?.length &&
587
+ projectTypes.filter((p) => p.startsWith("ruby")).length
588
+ ) {
585
589
  baseProjectTypes.push("ruby");
586
590
  }
587
591
  const baseExcludeTypes = [];
@@ -692,6 +696,7 @@ export const cdxgenAgent = got.extend({
692
696
  retry: {
693
697
  limit: 0,
694
698
  },
699
+ followRedirect: !isSecureMode,
695
700
  hooks: {
696
701
  beforeRequest: [
697
702
  (options) => {
@@ -725,6 +730,8 @@ export const cdxgenAgent = got.extend({
725
730
  * @param {string} dirPath Root directory for search
726
731
  * @param {string} pattern Glob pattern (eg: *.gradle)
727
732
  * @param {Object} options CLI options
733
+ *
734
+ * @returns {Array[string]} List of matched files
728
735
  */
729
736
  export function getAllFiles(dirPath, pattern, options = {}) {
730
737
  let ignoreList = [
@@ -737,7 +744,7 @@ export function getAllFiles(dirPath, pattern, options = {}) {
737
744
  "**/coverage/**",
738
745
  ];
739
746
  // Only ignore node_modules if the caller is not looking for package.json
740
- if (!pattern.includes("package.json")) {
747
+ if (!pattern.includes("package.json") && !options.includeNodeModulesDir) {
741
748
  ignoreList.push("**/node_modules/**");
742
749
  }
743
750
  // ignore docs only for non-lock file lookups
@@ -752,7 +759,27 @@ export function getAllFiles(dirPath, pattern, options = {}) {
752
759
  if (options?.exclude && Array.isArray(options.exclude)) {
753
760
  ignoreList = ignoreList.concat(options.exclude);
754
761
  }
755
- return getAllFilesWithIgnore(dirPath, pattern, ignoreList);
762
+ const includeDot = pattern.startsWith(".") || options.includeNodeModulesDir;
763
+ const defaultHits = getAllFilesWithIgnore(
764
+ dirPath,
765
+ pattern,
766
+ includeDot,
767
+ ignoreList,
768
+ );
769
+ // Support for specifying the pattern via options
770
+ if (options?.include?.length) {
771
+ const includeOnlyHits = getAllFilesWithIgnore(
772
+ dirPath,
773
+ options.include,
774
+ includeDot,
775
+ ignoreList,
776
+ );
777
+ if (!includeOnlyHits.length) {
778
+ return [];
779
+ }
780
+ return defaultHits.filter((f) => includeOnlyHits.includes(f));
781
+ }
782
+ return defaultHits;
756
783
  }
757
784
 
758
785
  /**
@@ -760,16 +787,24 @@ export function getAllFiles(dirPath, pattern, options = {}) {
760
787
  *
761
788
  * @param {string} dirPath Root directory for search
762
789
  * @param {string} pattern Glob pattern (eg: *.gradle)
790
+ * @param {Boolean} includeDot whether hidden files can be included.
763
791
  * @param {Array} ignoreList Directory patterns to ignore
792
+ *
793
+ * @returns {Array[string]} List of matched files
764
794
  */
765
- export function getAllFilesWithIgnore(dirPath, pattern, ignoreList) {
795
+ export function getAllFilesWithIgnore(
796
+ dirPath,
797
+ pattern,
798
+ includeDot,
799
+ ignoreList,
800
+ ) {
766
801
  try {
767
802
  const files = globSync(pattern, {
768
803
  cwd: dirPath,
769
804
  absolute: true,
770
805
  nocase: true,
771
806
  nodir: true,
772
- dot: pattern.startsWith("."),
807
+ dot: includeDot,
773
808
  follow: false,
774
809
  ignore: ignoreList,
775
810
  });
@@ -2285,6 +2320,164 @@ export function parsePnpmWorkspace(workspaceFile) {
2285
2320
  };
2286
2321
  }
2287
2322
 
2323
+ /**
2324
+ * Helper function to find a package path in pnpm node_modules structure
2325
+ *
2326
+ * @param {string} baseDir Base directory containing node_modules
2327
+ * @param {string} packageName Package name (with or without scope)
2328
+ * @param {string} version Package version
2329
+ * @returns {string|null} Path to the package directory or null if not found
2330
+ */
2331
+ export function findPnpmPackagePath(baseDir, packageName, version) {
2332
+ if (!baseDir || !packageName) {
2333
+ return null;
2334
+ }
2335
+
2336
+ const nodeModulesDir = join(baseDir, "node_modules");
2337
+ if (!safeExistsSync(nodeModulesDir)) {
2338
+ return null;
2339
+ }
2340
+
2341
+ // Try direct node_modules lookup first (for symlinked packages)
2342
+ const directPath = join(nodeModulesDir, packageName);
2343
+ if (
2344
+ safeExistsSync(directPath) &&
2345
+ safeExistsSync(join(directPath, "package.json"))
2346
+ ) {
2347
+ return directPath;
2348
+ }
2349
+
2350
+ // Try pnpm's .pnpm directory structure
2351
+ const pnpmDir = join(nodeModulesDir, ".pnpm");
2352
+ if (safeExistsSync(pnpmDir)) {
2353
+ // pnpm stores packages as {name}@{version} in .pnpm directory
2354
+ const encodedName = packageName.replace("/", "%2f");
2355
+ let pnpmPackagePath;
2356
+
2357
+ // Try different formats that pnpm might use
2358
+ const possiblePaths = [
2359
+ join(pnpmDir, `${encodedName}@${version}`, "node_modules", packageName),
2360
+ join(pnpmDir, `${packageName}@${version}`, "node_modules", packageName),
2361
+ join(pnpmDir, `${encodedName}@${version}`),
2362
+ join(pnpmDir, `${packageName}@${version}`),
2363
+ ];
2364
+
2365
+ for (const possiblePath of possiblePaths) {
2366
+ if (
2367
+ safeExistsSync(possiblePath) &&
2368
+ safeExistsSync(join(possiblePath, "package.json"))
2369
+ ) {
2370
+ pnpmPackagePath = possiblePath;
2371
+ break;
2372
+ }
2373
+ }
2374
+
2375
+ if (pnpmPackagePath) {
2376
+ return pnpmPackagePath;
2377
+ }
2378
+ }
2379
+
2380
+ return null;
2381
+ }
2382
+
2383
+ /**
2384
+ * pnpm packages with metadata from local node_modules
2385
+ *
2386
+ * @param {Array} pkgList Package list to enhance
2387
+ * @param {string} lockFilePath Path to the pnpm-lock.yaml file
2388
+ * @returns {Array} Enhanced package list
2389
+ */
2390
+ export async function pnpmMetadata(pkgList, lockFilePath) {
2391
+ if (!pkgList || !pkgList.length || !lockFilePath) {
2392
+ return pkgList;
2393
+ }
2394
+
2395
+ const baseDir = dirname(lockFilePath);
2396
+ const nodeModulesDir = join(baseDir, "node_modules");
2397
+
2398
+ // Only proceed if node_modules exists
2399
+ if (!safeExistsSync(nodeModulesDir)) {
2400
+ return pkgList;
2401
+ }
2402
+
2403
+ if (DEBUG_MODE) {
2404
+ console.log(
2405
+ `Metadata for ${pkgList.length} pnpm packages using local node_modules at ${nodeModulesDir}`,
2406
+ );
2407
+ }
2408
+
2409
+ let enhancedCount = 0;
2410
+ for (const pkg of pkgList) {
2411
+ // Skip if package already has complete metadata
2412
+ if (pkg.description && pkg.author && pkg.license) {
2413
+ continue;
2414
+ }
2415
+
2416
+ // Find the package path in node_modules
2417
+ const packagePath = findPnpmPackagePath(baseDir, pkg.name, pkg.version);
2418
+ if (!packagePath) {
2419
+ continue;
2420
+ }
2421
+
2422
+ const packageJsonPath = join(packagePath, "package.json");
2423
+ if (!safeExistsSync(packageJsonPath)) {
2424
+ continue;
2425
+ }
2426
+
2427
+ try {
2428
+ // Parse the local package.json to get metadata
2429
+ const localPkgList = await parsePkgJson(packageJsonPath, true);
2430
+ if (localPkgList && localPkgList.length === 1) {
2431
+ const localMetadata = localPkgList[0];
2432
+ if (localMetadata && Object.keys(localMetadata).length) {
2433
+ if (!pkg.description && localMetadata.description) {
2434
+ pkg.description = localMetadata.description;
2435
+ }
2436
+ if (!pkg.author && localMetadata.author) {
2437
+ pkg.author = localMetadata.author;
2438
+ }
2439
+ if (!pkg.license && localMetadata.license) {
2440
+ pkg.license = localMetadata.license;
2441
+ }
2442
+ if (!pkg.homepage && localMetadata.homepage) {
2443
+ pkg.homepage = localMetadata.homepage;
2444
+ }
2445
+ if (!pkg.repository && localMetadata.repository) {
2446
+ pkg.repository = localMetadata.repository;
2447
+ }
2448
+
2449
+ // Add a property to track that we enhanced from local node_modules
2450
+ if (!pkg.properties) {
2451
+ pkg.properties = [];
2452
+ }
2453
+ pkg.properties.push({
2454
+ name: "LocalNodeModulesPath",
2455
+ value: packagePath,
2456
+ });
2457
+
2458
+ enhancedCount++;
2459
+ }
2460
+ }
2461
+ } catch (error) {
2462
+ // Silently ignore parsing errors for individual packages
2463
+ if (DEBUG_MODE) {
2464
+ console.log(
2465
+ `Failed to parse package.json at ${packageJsonPath}:`,
2466
+ error.message,
2467
+ );
2468
+ }
2469
+ }
2470
+ }
2471
+
2472
+ if (DEBUG_MODE && enhancedCount > 0) {
2473
+ console.log(
2474
+ `Enhanced metadata for ${enhancedCount} packages from local node_modules`,
2475
+ );
2476
+ }
2477
+
2478
+ return pkgList;
2479
+ }
2480
+
2288
2481
  /**
2289
2482
  * Parse nodejs pnpm lock file
2290
2483
  *
@@ -2646,6 +2839,21 @@ export async function parsePnpmLock(
2646
2839
  packages[fullName]?.resolution ||
2647
2840
  snapshots[fullName]?.resolution;
2648
2841
  const integrity = resolution?.integrity;
2842
+ const cpu =
2843
+ packages[pkgKeys[k]]?.cpu ||
2844
+ snapshots[pkgKeys[k]]?.cpu ||
2845
+ packages[fullName]?.cpu ||
2846
+ snapshots[fullName]?.cpu;
2847
+ const os =
2848
+ packages[pkgKeys[k]]?.os ||
2849
+ snapshots[pkgKeys[k]]?.os ||
2850
+ packages[fullName]?.os ||
2851
+ snapshots[fullName]?.os;
2852
+ const libc =
2853
+ packages[pkgKeys[k]]?.libc ||
2854
+ snapshots[pkgKeys[k]]?.libc ||
2855
+ packages[fullName]?.libc ||
2856
+ snapshots[fullName]?.libc;
2649
2857
  // In lock file version 9, dependencies is under snapshots
2650
2858
  const deps =
2651
2859
  packages[pkgKeys[k]]?.dependencies ||
@@ -2849,7 +3057,7 @@ export async function parsePnpmLock(
2849
3057
  value: pnpmLock,
2850
3058
  },
2851
3059
  ];
2852
- if (hasBin) {
3060
+ if (hasBin || os || cpu || libc) {
2853
3061
  properties.push({
2854
3062
  name: "cdx:npm:has_binary",
2855
3063
  value: `${hasBin}`,
@@ -2861,6 +3069,14 @@ export async function parsePnpmLock(
2861
3069
  value: deprecatedMessage,
2862
3070
  });
2863
3071
  }
3072
+ const binary_metadata = { os, cpu, libc };
3073
+ Object.entries(binary_metadata).forEach(([key, value]) => {
3074
+ if (!value) return;
3075
+ properties.push({
3076
+ name: `cdx:pnpm:${key}`,
3077
+ value: Array.isArray(value) ? value.join(", ") : value,
3078
+ });
3079
+ });
2864
3080
  if (srcFilesMap[decodeURIComponent(purlString)]) {
2865
3081
  for (const sf of srcFilesMap[decodeURIComponent(purlString)]) {
2866
3082
  properties.push({
@@ -3075,6 +3291,12 @@ export async function parsePnpmLock(
3075
3291
  });
3076
3292
  }
3077
3293
  }
3294
+
3295
+ // Enhance metadata from local node_modules if available
3296
+ if (pkgList?.length) {
3297
+ pkgList = await pnpmMetadata(pkgList, pnpmLock);
3298
+ }
3299
+
3078
3300
  if (shouldFetchLicense() && pkgList && pkgList.length) {
3079
3301
  if (DEBUG_MODE) {
3080
3302
  console.log(
@@ -4725,13 +4947,19 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
4725
4947
  p.name = p.name.split("[")[0];
4726
4948
  }
4727
4949
  let res;
4950
+ let url_addition;
4951
+ if (p.version?.trim().length) {
4952
+ url_addition = `${p.name}/${p.version.trim()}/json`;
4953
+ } else {
4954
+ url_addition = `${p.name}/json`;
4955
+ }
4728
4956
  try {
4729
- res = await cdxgenAgent.get(`${PYPI_URL + p.name}/json`, {
4957
+ res = await cdxgenAgent.get(`${PYPI_URL + url_addition}`, {
4730
4958
  responseType: "json",
4731
4959
  });
4732
4960
  } catch (_err) {
4733
4961
  // retry by prefixing django- to the package name
4734
- res = await cdxgenAgent.get(`${PYPI_URL}django-${p.name}/json`, {
4962
+ res = await cdxgenAgent.get(`${PYPI_URL}django-${url_addition}`, {
4735
4963
  responseType: "json",
4736
4964
  });
4737
4965
  p.name = `django-${p.name}`;
@@ -4774,6 +5002,12 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
4774
5002
  p.license.push(licenseId);
4775
5003
  }
4776
5004
  }
5005
+ if (body.info.license_expression) {
5006
+ const licenseId = findLicenseId(body.info.license_expression);
5007
+ if (licenseId && !p.license.includes(licenseId)) {
5008
+ p.license.push(licenseId);
5009
+ }
5010
+ }
4777
5011
  if (body.info.home_page) {
4778
5012
  if (body.info.home_page.includes("git")) {
4779
5013
  p.repository = { url: body.info.home_page };
@@ -5611,20 +5845,66 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
5611
5845
  }
5612
5846
 
5613
5847
  /**
5614
- * Method to parse requirements.txt data. This must be replaced with atom parsedeps.
5848
+ * Method to parse requirements.txt file. This must be replaced with atom parsedeps.
5615
5849
  *
5616
- * @param {Object} reqData Requirements.txt data
5850
+ * @param {String} reqFile Requirements.txt file
5617
5851
  * @param {Boolean} fetchDepsInfo Fetch dependencies info from pypi
5852
+ *
5853
+ * @returns {Promise[Array<Object>]} List of direct dependencies from the requirements file
5618
5854
  */
5619
- export async function parseReqFile(reqData, fetchDepsInfo) {
5855
+ export async function parseReqFile(reqFile, fetchDepsInfo = false) {
5856
+ return await parseReqData(reqFile, null, fetchDepsInfo);
5857
+ }
5858
+
5859
+ /**
5860
+ * Method to parse requirements.txt file. Must only be used internally.
5861
+ *
5862
+ * @param {String} reqFile Requirements.txt file
5863
+ * @param {Object} reqData Requirements.txt data for internal invocations from setup.py file etc.
5864
+ *
5865
+ * @param {Boolean} fetchDepsInfo Fetch dependencies info from pypi
5866
+ *
5867
+ * @returns {Promise[Array<Object>]} List of direct dependencies from the requirements file
5868
+ */
5869
+ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
5620
5870
  const pkgList = [];
5621
5871
  let compScope;
5872
+ if (!reqFile && !reqData) {
5873
+ console.warn(
5874
+ "Either the requirements file or the data needs to be provided for parsing.",
5875
+ );
5876
+ return pkgList;
5877
+ }
5878
+ reqData = reqData || readFileSync(reqFile, { encoding: "utf-8" });
5879
+ const evidence = reqFile
5880
+ ? {
5881
+ identity: {
5882
+ field: "purl",
5883
+ confidence: 0.5,
5884
+ methods: [
5885
+ {
5886
+ technique: "manifest-analysis",
5887
+ confidence: 0.5,
5888
+ value: reqFile,
5889
+ },
5890
+ ],
5891
+ },
5892
+ }
5893
+ : undefined;
5622
5894
  reqData
5623
5895
  .replace(/\r/g, "")
5624
5896
  .replace(/ [\\]\n/g, "")
5625
5897
  .replace(/ {4}/g, " ")
5626
5898
  .split("\n")
5627
5899
  .forEach((l) => {
5900
+ const properties = reqFile
5901
+ ? [
5902
+ {
5903
+ name: "SrcFile",
5904
+ value: reqFile,
5905
+ },
5906
+ ]
5907
+ : [];
5628
5908
  l = l.trim();
5629
5909
  let markers;
5630
5910
  if (l.includes(" ; ")) {
@@ -5659,11 +5939,11 @@ export async function parseReqFile(reqData, fetchDepsInfo) {
5659
5939
  const name = tmpA[0].trim().replace(";", "");
5660
5940
  const versionSpecifiers = l.replace(name, "");
5661
5941
  if (!PYTHON_STD_MODULES.includes(name)) {
5662
- const properties = [];
5663
5942
  const apkg = {
5664
5943
  name,
5665
5944
  version: versionStr,
5666
5945
  scope: compScope,
5946
+ evidence,
5667
5947
  };
5668
5948
  if (
5669
5949
  versionSpecifiers?.length > 0 &&
@@ -5695,7 +5975,9 @@ export async function parseReqFile(reqData, fetchDepsInfo) {
5695
5975
  name,
5696
5976
  version: undefined,
5697
5977
  scope: compScope,
5978
+ evidence,
5698
5979
  properties: [
5980
+ ...properties,
5699
5981
  {
5700
5982
  name: "cdx:pypi:versionSpecifiers",
5701
5983
  value: versionSpecifiers?.length
@@ -5718,7 +6000,9 @@ export async function parseReqFile(reqData, fetchDepsInfo) {
5718
6000
  name,
5719
6001
  version: undefined,
5720
6002
  scope: compScope,
6003
+ evidence,
5721
6004
  properties: [
6005
+ ...properties,
5722
6006
  {
5723
6007
  name: "cdx:pypi:versionSpecifiers",
5724
6008
  value: versionSpecifiers?.length
@@ -5743,6 +6027,7 @@ export async function parseReqFile(reqData, fetchDepsInfo) {
5743
6027
  name,
5744
6028
  version: undefined,
5745
6029
  scope: compScope,
6030
+ evidence,
5746
6031
  properties: [
5747
6032
  {
5748
6033
  name: "cdx:pypi:versionSpecifiers",
@@ -5761,6 +6046,7 @@ export async function parseReqFile(reqData, fetchDepsInfo) {
5761
6046
  name,
5762
6047
  version: null,
5763
6048
  scope: compScope,
6049
+ evidence,
5764
6050
  properties: [
5765
6051
  {
5766
6052
  name: "cdx:pypi:versionSpecifiers",
@@ -5884,7 +6170,7 @@ export async function parseSetupPyFile(setupPyData) {
5884
6170
  lines = lines.concat(tmpA);
5885
6171
  }
5886
6172
  });
5887
- return await parseReqFile(lines.join("\n"), false);
6173
+ return await parseReqData(null, lines.join("\n"), false);
5888
6174
  }
5889
6175
 
5890
6176
  /**
@@ -7323,6 +7609,7 @@ export async function parseGemspecData(gemspecData, gemspecFile) {
7323
7609
  pkg?.version?.includes("File.") ||
7324
7610
  pkg?.version?.includes("::")
7325
7611
  ) {
7612
+ const origVersion = pkg.version;
7326
7613
  pkg.version = undefined;
7327
7614
  // Can we find the version from the directory name?
7328
7615
  if (gemspecFile) {
@@ -7333,10 +7620,17 @@ export async function parseGemspecData(gemspecData, gemspecFile) {
7333
7620
  }
7334
7621
  }
7335
7622
  if (!versionHackMatch && !pkg.version) {
7336
- console.log(
7337
- "Unable to identify the package version by parsing the file",
7338
- gemspecFile,
7339
- );
7623
+ if (origVersion?.toLowerCase().includes("version")) {
7624
+ if (DEBUG_MODE) {
7625
+ console.log(
7626
+ `Unable to identify the version for '${pkg.name}' from the string '${origVersion}'. Spec file: ${gemspecFile}`,
7627
+ );
7628
+ }
7629
+ } else {
7630
+ console.log(
7631
+ `Unable to identify the version for '${pkg.name}'. Spec file: ${gemspecFile}`,
7632
+ );
7633
+ }
7340
7634
  }
7341
7635
  }
7342
7636
  for (const aprop of ["authors", "licenses"]) {
@@ -9236,15 +9530,35 @@ export function parseConanLockData(conanLockData) {
9236
9530
  if (!conanLockData) {
9237
9531
  return pkgList;
9238
9532
  }
9239
- const graphLock = JSON.parse(conanLockData);
9240
- if (!graphLock || !graphLock.graph_lock || !graphLock.graph_lock.nodes) {
9533
+ const lockFile = JSON.parse(conanLockData);
9534
+ if (
9535
+ (!lockFile || !lockFile.graph_lock || !lockFile.graph_lock.nodes) &&
9536
+ !lockFile.requires
9537
+ ) {
9241
9538
  return pkgList;
9242
9539
  }
9243
- const nodes = graphLock.graph_lock.nodes;
9244
- for (const nk of Object.keys(nodes)) {
9245
- if (nodes[nk].ref) {
9540
+ if (lockFile.graph_lock?.nodes) {
9541
+ const depends = lockFile.graph_lock.nodes;
9542
+ for (const nk of Object.keys(depends)) {
9543
+ if (depends[nk].ref) {
9544
+ const [purl, name, version] =
9545
+ mapConanPkgRefToPurlStringAndNameAndVersion(depends[nk].ref);
9546
+ if (purl !== null) {
9547
+ pkgList.push({
9548
+ name,
9549
+ version,
9550
+ purl,
9551
+ "bom-ref": decodeURIComponent(purl),
9552
+ });
9553
+ }
9554
+ }
9555
+ }
9556
+ } else if (lockFile.requires) {
9557
+ const depends = lockFile.requires;
9558
+ for (const nk of Object.keys(depends)) {
9559
+ depends[nk] = depends[nk].split("%").shift();
9246
9560
  const [purl, name, version] = mapConanPkgRefToPurlStringAndNameAndVersion(
9247
- nodes[nk].ref,
9561
+ depends[nk],
9248
9562
  );
9249
9563
  if (purl !== null) {
9250
9564
  pkgList.push({
@@ -9397,6 +9711,218 @@ export async function parseNupkg(nupkgFile) {
9397
9711
  return parseNuspecData(nupkgFile, nuspecData);
9398
9712
  }
9399
9713
 
9714
+ /**
9715
+ * Method to parse flake.nix files
9716
+ *
9717
+ * @param {String} flakeNixFile flake.nix file to parse
9718
+ * @returns {Object} Object containing package information
9719
+ */
9720
+ export function parseFlakeNix(flakeNixFile) {
9721
+ const pkgList = [];
9722
+ const dependencies = [];
9723
+
9724
+ if (!existsSync(flakeNixFile)) {
9725
+ return { pkgList, dependencies };
9726
+ }
9727
+
9728
+ try {
9729
+ const flakeContent = readFileSync(flakeNixFile, "utf-8");
9730
+
9731
+ // Extract inputs from flake.nix using regex
9732
+ const inputsRegex = /inputs\s*=\s*\{[^}]*\}/g;
9733
+ let match;
9734
+ while ((match = inputsRegex.exec(flakeContent)) !== null) {
9735
+ const inputBlock = match[0];
9736
+
9737
+ // Match different input patterns including nested inputs
9738
+ const inputPatterns = [
9739
+ /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
9740
+ /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*\}/gs,
9741
+ ];
9742
+
9743
+ const addedPackages = new Set();
9744
+
9745
+ for (const pattern of inputPatterns) {
9746
+ let subMatch;
9747
+ pattern.lastIndex = 0;
9748
+ while ((subMatch = pattern.exec(inputBlock)) !== null) {
9749
+ const name = subMatch[1];
9750
+ const url = subMatch[2] || subMatch[3];
9751
+
9752
+ if (name && url && !addedPackages.has(name)) {
9753
+ addedPackages.add(name);
9754
+ const pkg = {
9755
+ name: name,
9756
+ version: "latest",
9757
+ description: `Nix flake input: ${name}`,
9758
+ scope: "required",
9759
+ properties: [
9760
+ {
9761
+ name: "SrcFile",
9762
+ value: flakeNixFile,
9763
+ },
9764
+ {
9765
+ name: "cdx:nix:input_url",
9766
+ value: url,
9767
+ },
9768
+ ],
9769
+ evidence: {
9770
+ identity: {
9771
+ field: "purl",
9772
+ confidence: 0.8,
9773
+ methods: [
9774
+ {
9775
+ technique: "manifest-analysis",
9776
+ confidence: 0.8,
9777
+ value: flakeNixFile,
9778
+ },
9779
+ ],
9780
+ },
9781
+ },
9782
+ };
9783
+
9784
+ pkg.purl = generateNixPurl(name, "latest");
9785
+ pkg["bom-ref"] = pkg.purl;
9786
+
9787
+ pkgList.push(pkg);
9788
+ }
9789
+ }
9790
+ }
9791
+ }
9792
+ } catch (error) {
9793
+ console.warn(`Failed to parse ${flakeNixFile}: ${error.message}`);
9794
+ }
9795
+
9796
+ return { pkgList, dependencies };
9797
+ }
9798
+
9799
+ /**
9800
+ * Method to parse flake.lock files
9801
+ *
9802
+ * @param {String} flakeLockFile flake.lock file to parse
9803
+ * @returns {Object} Object containing locked dependency information
9804
+ */
9805
+ export function parseFlakeLock(flakeLockFile) {
9806
+ const pkgList = [];
9807
+ const dependencies = [];
9808
+
9809
+ if (!existsSync(flakeLockFile)) {
9810
+ return { pkgList, dependencies };
9811
+ }
9812
+
9813
+ try {
9814
+ const lockContent = readFileSync(flakeLockFile, "utf-8");
9815
+ const lockData = JSON.parse(lockContent);
9816
+
9817
+ if (lockData.nodes) {
9818
+ for (const [nodeName, nodeData] of Object.entries(lockData.nodes)) {
9819
+ if (nodeName === "root" || !nodeData.locked) continue;
9820
+
9821
+ const locked = nodeData.locked;
9822
+
9823
+ let version = "latest";
9824
+ if (locked.rev) {
9825
+ version = locked.rev.substring(0, 7);
9826
+ } else if (locked.ref) {
9827
+ version = locked.ref;
9828
+ }
9829
+
9830
+ const pkg = {
9831
+ name: nodeName,
9832
+ version: version,
9833
+ description: `Nix flake dependency: ${nodeName}`,
9834
+ scope: "required",
9835
+ properties: [
9836
+ {
9837
+ name: "SrcFile",
9838
+ value: flakeLockFile,
9839
+ },
9840
+ ],
9841
+ evidence: {
9842
+ identity: {
9843
+ field: "purl",
9844
+ confidence: 1.0,
9845
+ methods: [
9846
+ {
9847
+ technique: "manifest-analysis",
9848
+ confidence: 1.0,
9849
+ value: flakeLockFile,
9850
+ },
9851
+ ],
9852
+ },
9853
+ },
9854
+ };
9855
+
9856
+ if (locked.narHash) {
9857
+ pkg.properties.push({
9858
+ name: "cdx:nix:nar_hash",
9859
+ value: locked.narHash,
9860
+ });
9861
+ }
9862
+
9863
+ if (locked.lastModified) {
9864
+ pkg.properties.push({
9865
+ name: "cdx:nix:last_modified",
9866
+ value: locked.lastModified.toString(),
9867
+ });
9868
+ }
9869
+
9870
+ if (locked.rev) {
9871
+ pkg.properties.push({
9872
+ name: "cdx:nix:revision",
9873
+ value: locked.rev,
9874
+ });
9875
+ }
9876
+
9877
+ if (locked.ref) {
9878
+ pkg.properties.push({
9879
+ name: "cdx:nix:ref",
9880
+ value: locked.ref,
9881
+ });
9882
+ }
9883
+
9884
+ pkg.purl = generateNixPurl(nodeName, version);
9885
+ pkg["bom-ref"] = pkg.purl;
9886
+
9887
+ pkgList.push(pkg);
9888
+ }
9889
+
9890
+ // Generate dependency relationships from root inputs
9891
+ if (lockData.nodes?.root?.inputs) {
9892
+ const rootInputs = Object.keys(lockData.nodes.root.inputs);
9893
+ if (rootInputs.length > 0) {
9894
+ dependencies.push({
9895
+ ref: "pkg:nix/flake@latest",
9896
+ dependsOn: rootInputs
9897
+ .map(
9898
+ (input) =>
9899
+ pkgList.find((pkg) => pkg.name === input)?.["bom-ref"],
9900
+ )
9901
+ .filter(Boolean),
9902
+ });
9903
+ }
9904
+ }
9905
+ }
9906
+ } catch (error) {
9907
+ console.warn(`Failed to parse ${flakeLockFile}: ${error.message}`);
9908
+ }
9909
+
9910
+ return { pkgList, dependencies };
9911
+ }
9912
+
9913
+ /**
9914
+ * Generate a Nix PURL from input information
9915
+ *
9916
+ * @param {String} name Package name
9917
+ * @param {String} version Package version
9918
+ * @returns {String} PURL string
9919
+ */
9920
+ function generateNixPurl(name, version) {
9921
+ // For now, use a generic nix PURL type
9922
+ // In the future, this could be more sophisticated based on the source type
9923
+ return `pkg:nix/${name}@${version}`;
9924
+ }
9925
+
9400
9926
  /**
9401
9927
  * Method to parse .nuspec files
9402
9928
  *
@@ -9816,7 +10342,7 @@ export function parseCsProjData(csProjData, projFile, pkgNameVersions = {}) {
9816
10342
  continue;
9817
10343
  }
9818
10344
  pkg.name = pref.Include;
9819
- pkg.version = pref.Version;
10345
+ pkg.version = pref.Version || pkgNameVersions[pkg.name];
9820
10346
  pkg.purl = `pkg:nuget/${pkg.name}@${pkg.version}`;
9821
10347
  pkg["bom-ref"] = pkg.purl;
9822
10348
  if (projFile) {
@@ -9853,9 +10379,11 @@ export function parseCsProjData(csProjData, projFile, pkgNameVersions = {}) {
9853
10379
  pkg.name = incParts[0];
9854
10380
  pkg.properties = [];
9855
10381
  if (incParts.length > 1 && incParts[1].includes("Version")) {
9856
- pkg.version = incParts[1].replace("Version=", "").trim();
10382
+ pkg.version =
10383
+ incParts[1].replace("Version=", "").trim() ||
10384
+ pkgNameVersions[pkg.name];
9857
10385
  }
9858
- const version = pkg.version || pkgNameVersions[pkg.name];
10386
+ const version = pkg.version;
9859
10387
  if (version) {
9860
10388
  pkg.purl = `pkg:nuget/${pkg.name}@${version}`;
9861
10389
  } else {
@@ -9999,25 +10527,27 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
9999
10527
  return { pkgList, dependenciesList };
10000
10528
  }
10001
10529
  csProjData = JSON.parse(csProjData);
10002
- const purlString = new PackageURL(
10003
- "nuget",
10004
- "",
10005
- csProjData.project.restore.projectName,
10006
- csProjData.project.version || "latest",
10007
- null,
10008
- null,
10009
- ).toString();
10010
- rootPkg = {
10011
- group: "",
10012
- name: csProjData.project.restore.projectName,
10013
- version: csProjData.project.version || "latest",
10014
- type: "application",
10015
- purl: purlString,
10016
- "bom-ref": decodeURIComponent(purlString),
10017
- };
10018
- pkgList.push(rootPkg);
10530
+ let purlString;
10531
+ if (csProjData.project?.restore?.projectName) {
10532
+ purlString = new PackageURL(
10533
+ "nuget",
10534
+ "",
10535
+ csProjData.project?.restore?.projectName,
10536
+ csProjData.project.version || "latest",
10537
+ null,
10538
+ null,
10539
+ ).toString();
10540
+ rootPkg = {
10541
+ group: "",
10542
+ name: csProjData.project.restore.projectName,
10543
+ version: csProjData.project.version || "latest",
10544
+ type: "application",
10545
+ purl: purlString,
10546
+ "bom-ref": decodeURIComponent(purlString),
10547
+ };
10548
+ pkgList.push(rootPkg);
10549
+ }
10019
10550
  const rootPkgDeps = new Set();
10020
-
10021
10551
  // create root pkg deps
10022
10552
  if (csProjData.targets && csProjData.projectFileDependencyGroups) {
10023
10553
  for (const frameworkTarget in csProjData.projectFileDependencyGroups) {
@@ -10074,11 +10604,12 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
10074
10604
  rootPkgDeps.add(dpurl);
10075
10605
  }
10076
10606
  }
10077
-
10078
- dependenciesList.push({
10079
- ref: purlString,
10080
- dependsOn: Array.from(rootPkgDeps).sort(),
10081
- });
10607
+ if (purlString && rootPkgDeps.size) {
10608
+ dependenciesList.push({
10609
+ ref: purlString,
10610
+ dependsOn: Array.from(rootPkgDeps).sort(),
10611
+ });
10612
+ }
10082
10613
  }
10083
10614
 
10084
10615
  if (csProjData.libraries && csProjData.targets) {
@@ -10453,7 +10984,10 @@ export function parseComposerJson(composerJsonFile) {
10453
10984
  const composerData = JSON.parse(
10454
10985
  readFileSync(composerJsonFile, { encoding: "utf-8" }),
10455
10986
  );
10456
- const rootRequires = composerData.require;
10987
+ const rootRequires = {
10988
+ ...composerData.require,
10989
+ ...composerData["require-dev"],
10990
+ };
10457
10991
  const pkgName = composerData.name;
10458
10992
  if (pkgName) {
10459
10993
  moduleParent.group = dirname(pkgName);
@@ -11321,7 +11855,7 @@ export async function collectMvnDependencies(
11321
11855
  copyArgs = copyArgs.concat(addArgs);
11322
11856
  }
11323
11857
  if (basePath && basePath !== MAVEN_CACHE_DIR) {
11324
- console.log(`Executing '${mavenCmd} ${copyArgs.join(" ")}' in ${basePath}`);
11858
+ console.log(`Executing '${mavenCmd} in ${basePath}`);
11325
11859
  const result = safeSpawnSync(mavenCmd, copyArgs, {
11326
11860
  cwd: basePath,
11327
11861
  encoding: "utf-8",
@@ -13304,7 +13838,7 @@ export function executeAtom(src, args, extra_env = {}) {
13304
13838
  }
13305
13839
  }
13306
13840
  if (DEBUG_MODE) {
13307
- console.log("Executing", ATOM_BIN, args.join(" "));
13841
+ console.log("Executing", ATOM_BIN);
13308
13842
  }
13309
13843
  const env = {
13310
13844
  ...process.env,
@@ -13606,9 +14140,9 @@ export function createUVLock(basePath, options) {
13606
14140
  * @param {string} tempVenvDir Temp venv dir
13607
14141
  * @param {Object} parentComponent Parent component
13608
14142
  *
13609
- * @returns List of packages from the virtual env
14143
+ * @returns {Object} List of packages from the virtual env
13610
14144
  */
13611
- export function getPipFrozenTree(
14145
+ export async function getPipFrozenTree(
13612
14146
  basePath,
13613
14147
  reqOrSetupFile,
13614
14148
  tempVenvDir,
@@ -13623,6 +14157,20 @@ export function getPipFrozenTree(
13623
14157
  const env = {
13624
14158
  ...process.env,
13625
14159
  };
14160
+
14161
+ // FIX: Create a set of explicit dependencies from requirements.txt to identify root packages.
14162
+ const explicitDeps = new Set();
14163
+ if (reqOrSetupFile?.endsWith(".txt") && safeExistsSync(reqOrSetupFile)) {
14164
+ // We only need the package names, so we pass `false` to avoid fetching full metadata.
14165
+ const tempPkgList = await parseReqFile(reqOrSetupFile, null, false);
14166
+ for (const pkg of tempPkgList) {
14167
+ if (pkg.name) {
14168
+ // Normalize the name (lowercase, hyphenated) for accurate lookups.
14169
+ explicitDeps.add(pkg.name.replace(/_/g, "-").toLowerCase());
14170
+ }
14171
+ }
14172
+ }
14173
+
13626
14174
  /**
13627
14175
  * Let's start with an attempt to create a new temporary virtual environment in case we aren't in one
13628
14176
  *
@@ -13804,7 +14352,7 @@ export function getPipFrozenTree(
13804
14352
  `**PIP**: Trying pip install using the arguments ${pipInstallArgs.join(" ")}`,
13805
14353
  );
13806
14354
  if (DEBUG_MODE) {
13807
- console.log("Executing", python_cmd_for_tree, pipInstallArgs.join(" "));
14355
+ console.log("Executing", python_cmd_for_tree);
13808
14356
  }
13809
14357
  // Attempt to perform pip install
13810
14358
  result = safeSpawnSync(python_cmd_for_tree, pipInstallArgs, {
@@ -13883,7 +14431,6 @@ export function getPipFrozenTree(
13883
14431
  console.info(
13884
14432
  "\nEXPERIMENTAL: Invoke cdxgen with '--feature-flags safe-pip-install' to recover a partial dependency tree for projects with build errors.\n",
13885
14433
  );
13886
- console.log("args used:", pipInstallArgs);
13887
14434
  if (result.stderr) {
13888
14435
  console.log(result.stderr);
13889
14436
  }
@@ -14034,12 +14581,14 @@ export function getPipFrozenTree(
14034
14581
  };
14035
14582
  if (scope !== "excluded") {
14036
14583
  pkgList.push(apkg);
14037
- rootList.push({
14038
- name,
14039
- version,
14040
- purl: purlString,
14041
- "bom-ref": decodeURIComponent(purlString),
14042
- });
14584
+ if (explicitDeps.size === 0 || explicitDeps.has(name)) {
14585
+ rootList.push({
14586
+ name,
14587
+ version,
14588
+ purl: purlString,
14589
+ "bom-ref": decodeURIComponent(purlString),
14590
+ });
14591
+ }
14043
14592
  flattenDeps(dependenciesMap, pkgList, reqOrSetupFile, t);
14044
14593
  } else {
14045
14594
  formulationList.push(apkg);
@@ -15829,3 +16378,154 @@ function collectAllLdConfs(basePath, ldConf, allLdConfDirs, libPaths) {
15829
16378
  }
15830
16379
  }
15831
16380
  }
16381
+
16382
+ /**
16383
+ * Get information about the runtime.
16384
+ *
16385
+ * @returns {Object} Object containing the name and version of the runtime
16386
+ */
16387
+ export function getRuntimeInformation() {
16388
+ const runtimeInfo = {};
16389
+
16390
+ if (typeof globalThis.Deno !== "undefined" && globalThis.Deno.version?.deno) {
16391
+ runtimeInfo.runtime = "Deno";
16392
+ runtimeInfo.version = globalThis.Deno.version.deno;
16393
+ } else if (typeof globalThis.Bun !== "undefined" && globalThis.Bun.version) {
16394
+ runtimeInfo.runtime = "Bun";
16395
+ runtimeInfo.version = globalThis.Bun.version;
16396
+ } else if (
16397
+ typeof globalThis.process !== "undefined" &&
16398
+ globalThis.process.versions?.node
16399
+ ) {
16400
+ runtimeInfo.runtime = "Node.js";
16401
+ runtimeInfo.version = globalThis.process.versions.node;
16402
+ const report = process.report.getReport();
16403
+ const nodeSourceUrl = report?.header?.release?.sourceUrl;
16404
+ // Collect the bundled components in node.js
16405
+ if (report?.header?.componentVersions) {
16406
+ const nodeBundledComponents = [];
16407
+ for (const [name, version] of Object.entries(
16408
+ report.header.componentVersions,
16409
+ )) {
16410
+ if (name === "node") {
16411
+ continue;
16412
+ }
16413
+ const apkg = {
16414
+ name,
16415
+ version,
16416
+ description: `Bundled with Node.js ${runtimeInfo.version}`,
16417
+ type: "library",
16418
+ scope: "excluded",
16419
+ purl: `pkg:generic/${name}@${version}`,
16420
+ "bom-ref": `pkg:generic/${name}@${version}`,
16421
+ };
16422
+ if (nodeSourceUrl) {
16423
+ apkg.externalReferences = [
16424
+ {
16425
+ url: nodeSourceUrl,
16426
+ type: "source-distribution",
16427
+ comment: "Node.js release url",
16428
+ },
16429
+ ];
16430
+ }
16431
+ nodeBundledComponents.push(apkg);
16432
+ }
16433
+ if (nodeBundledComponents.length) {
16434
+ runtimeInfo.components = nodeBundledComponents;
16435
+ }
16436
+ }
16437
+ if (report.sharedObjects) {
16438
+ const osSharedObjects = [];
16439
+ for (const aso of report.sharedObjects) {
16440
+ const name = basename(aso);
16441
+ if (name === "node") {
16442
+ continue;
16443
+ }
16444
+ const apkg = {
16445
+ name,
16446
+ type: "library",
16447
+ scope: "excluded",
16448
+ purl: `pkg:generic/${name}#${aso}`,
16449
+ "bom-ref": `pkg:generic/${name}`,
16450
+ };
16451
+ osSharedObjects.push(apkg);
16452
+ }
16453
+ if (osSharedObjects.length) {
16454
+ runtimeInfo.components = osSharedObjects;
16455
+ }
16456
+ }
16457
+ } else {
16458
+ runtimeInfo.runtime = "Unknown";
16459
+ runtimeInfo.version = "N/A";
16460
+ }
16461
+
16462
+ return runtimeInfo;
16463
+ }
16464
+
16465
+ /**
16466
+ * Checks for dangerous Unicode characters that could enable homograph attacks
16467
+ *
16468
+ * @param {string} str String to check
16469
+ * @returns {boolean} true if dangerous Unicode is found
16470
+ */
16471
+ // biome-ignore-start lint/suspicious/noControlCharactersInRegex: validation
16472
+ export function hasDangerousUnicode(str) {
16473
+ // Check for bidirectional control characters
16474
+ const bidiChars = /[\u202A-\u202E\u2066-\u2069]/;
16475
+ if (bidiChars.test(str)) {
16476
+ return true;
16477
+ }
16478
+
16479
+ // Check for zero-width characters that could be used for obfuscation
16480
+ const zeroWidthChars = /[\u200B-\u200D\uFEFF]/;
16481
+ if (zeroWidthChars.test(str)) {
16482
+ return true;
16483
+ }
16484
+
16485
+ // Check for control characters (except common ones like \n, \r, \t)
16486
+ const controlChars = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/;
16487
+ if (controlChars.test(str)) {
16488
+ return true;
16489
+ }
16490
+
16491
+ return false;
16492
+ }
16493
+ // biome-ignore-end lint/suspicious/noControlCharactersInRegex: validation
16494
+
16495
+ /**
16496
+ * Validates that a root is a legitimate Windows drive letter format
16497
+ *
16498
+ * @param {string} root Root to validate
16499
+ * @returns {boolean} true if valid drive format
16500
+ */
16501
+ export function isValidDriveRoot(root) {
16502
+ // Must be at most 3 characters: letter, colon, backslash
16503
+ if (root.length > 3) {
16504
+ return false;
16505
+ }
16506
+
16507
+ // Check each character individually to prevent Unicode lookalikes
16508
+ const driveLetter = root.charAt(0);
16509
+ const colon = root.charAt(1);
16510
+ const backslash = root.charAt(2);
16511
+
16512
+ // Drive letter must be ASCII A-Z or a-z
16513
+ const charCode = driveLetter.charCodeAt(0);
16514
+ const isAsciiLetter =
16515
+ (charCode >= 65 && charCode <= 90) || (charCode >= 97 && charCode <= 122);
16516
+ if (!isAsciiLetter) {
16517
+ return false;
16518
+ }
16519
+
16520
+ // Colon must be exactly ASCII colon (0x3A)
16521
+ if (colon.charCodeAt(0) !== 0x3a) {
16522
+ return false;
16523
+ }
16524
+
16525
+ // Backslash (optional) must be exactly ASCII backslash (0x5C)
16526
+ if (backslash && backslash.charCodeAt(0) !== 0x5c) {
16527
+ return false;
16528
+ }
16529
+
16530
+ return true;
16531
+ }