@cyclonedx/cdxgen 12.3.2 → 12.3.3

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 (53) hide show
  1. package/README.md +6 -0
  2. package/data/rules/ci-permissions.yaml +132 -0
  3. package/data/rules/dependency-sources.yaml +65 -5
  4. package/data/rules/package-integrity.yaml +22 -0
  5. package/lib/cli/index.js +141 -39
  6. package/lib/cli/index.poku.js +579 -1
  7. package/lib/helpers/agentFormulationParser.js +6 -2
  8. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  9. package/lib/helpers/analyzer.js +38 -9
  10. package/lib/helpers/analyzer.poku.js +67 -0
  11. package/lib/helpers/chromextutils.js +25 -3
  12. package/lib/helpers/chromextutils.poku.js +68 -0
  13. package/lib/helpers/ciParsers/githubActions.js +79 -0
  14. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  15. package/lib/helpers/communityAiConfigParser.js +15 -5
  16. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  17. package/lib/helpers/depsUtils.js +5 -0
  18. package/lib/helpers/depsUtils.poku.js +55 -0
  19. package/lib/helpers/display.js +45 -22
  20. package/lib/helpers/display.poku.js +47 -60
  21. package/lib/helpers/mcpConfigParser.js +21 -5
  22. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  23. package/lib/helpers/propertySanitizer.js +121 -0
  24. package/lib/helpers/utils.js +951 -40
  25. package/lib/helpers/utils.poku.js +882 -0
  26. package/lib/managers/binary.js +16 -0
  27. package/lib/managers/binary.poku.js +1 -0
  28. package/lib/managers/docker.js +240 -16
  29. package/lib/managers/docker.poku.js +1142 -2
  30. package/lib/server/server.js +7 -4
  31. package/lib/server/server.poku.js +36 -1
  32. package/lib/stages/postgen/auditBom.poku.js +644 -2
  33. package/package.json +2 -1
  34. package/types/lib/cli/index.d.ts.map +1 -1
  35. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  36. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  37. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  38. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  39. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  40. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  41. package/types/lib/helpers/display.d.ts +1 -0
  42. package/types/lib/helpers/display.d.ts.map +1 -1
  43. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  44. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  45. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  46. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  47. package/types/lib/helpers/utils.d.ts +29 -0
  48. package/types/lib/helpers/utils.d.ts.map +1 -1
  49. package/types/lib/managers/binary.d.ts.map +1 -1
  50. package/types/lib/managers/docker.d.ts +3 -0
  51. package/types/lib/managers/docker.d.ts.map +1 -1
  52. package/types/lib/server/server.d.ts +1 -0
  53. package/types/lib/server/server.d.ts.map +1 -1
@@ -11,6 +11,7 @@ import {
11
11
  mkdirSync,
12
12
  mkdtempSync,
13
13
  readFileSync,
14
+ realpathSync,
14
15
  rmSync,
15
16
  unlinkSync,
16
17
  writeFileSync,
@@ -981,7 +982,7 @@ export const PROJECT_TYPE_ALIASES = {
981
982
  dart: ["dart", "flutter", "pub"],
982
983
  haskell: ["haskell", "hackage", "cabal"],
983
984
  elixir: ["elixir", "hex", "mix"],
984
- c: ["c", "cpp", "c++", "conan"],
985
+ c: ["c", "cpp", "c++", "conan", "collider"],
985
986
  clojure: ["clojure", "edn", "clj", "leiningen"],
986
987
  github: ["github", "actions"],
987
988
  os: ["os", "osquery", "windows", "linux", "mac", "macos", "darwin"],
@@ -1014,7 +1015,7 @@ export const PROJECT_TYPE_ALIASES = {
1014
1015
  "visionos",
1015
1016
  ],
1016
1017
  binary: ["binary", "blint"],
1017
- oci: ["docker", "oci", "container", "podman"],
1018
+ oci: ["docker", "oci", "container", "podman", "rootfs", "oci-dir"],
1018
1019
  cocoa: ["cocoa", "cocoapods", "objective-c", "swift", "ios"],
1019
1020
  scala: ["scala", "scala3", "sbt", "mill"],
1020
1021
  nix: ["nix", "nixos", "flake"],
@@ -2585,6 +2586,23 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
2585
2586
  value: "true",
2586
2587
  });
2587
2588
  }
2589
+ const npmManifestSources = collectNpmManifestSources(node);
2590
+ if (npmManifestSources.length) {
2591
+ addComponentProperty(
2592
+ pkg,
2593
+ "cdx:npm:manifestSourceType",
2594
+ npmManifestSources
2595
+ .map((manifestSource) => manifestSource.type)
2596
+ .join(","),
2597
+ );
2598
+ addComponentProperty(
2599
+ pkg,
2600
+ "cdx:npm:manifestSource",
2601
+ npmManifestSources
2602
+ .map((manifestSource) => manifestSource.value)
2603
+ .join(","),
2604
+ );
2605
+ }
2588
2606
  // This getter method could fail with errors at times.
2589
2607
  // Example Error: Invalid tag name "^>=6.0.0" of package "^>=6.0.0": Tags may not have any characters that encodeURIComponent encodes.
2590
2608
  try {
@@ -6811,8 +6829,17 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
6811
6829
  value: origName,
6812
6830
  });
6813
6831
  }
6814
- if (body.releases?.[p.version] && body.releases[p.version].length) {
6815
- const digest = body.releases[p.version][0].digests;
6832
+ const releaseEntries = body.releases?.[p.version]?.length
6833
+ ? body.releases[p.version]
6834
+ : Array.isArray(body.urls)
6835
+ ? body.urls
6836
+ : [];
6837
+ mergeExternalReferences(
6838
+ p,
6839
+ collectPypiReleaseExternalReferences(releaseEntries),
6840
+ );
6841
+ if (releaseEntries.length) {
6842
+ const digest = releaseEntries[0].digests;
6816
6843
  if (digest["sha256"]) {
6817
6844
  p._integrity = `sha256-${digest["sha256"]}`;
6818
6845
  } else if (digest["md5"]) {
@@ -7043,6 +7070,287 @@ export async function parsePiplockData(lockData) {
7043
7070
  return await getPyMetadata(pkgList, false);
7044
7071
  }
7045
7072
 
7073
+ function addComponentProperty(component, name, value) {
7074
+ if (value === undefined || value === null || value === "" || !component) {
7075
+ return;
7076
+ }
7077
+ component.properties = component.properties || [];
7078
+ if (
7079
+ component.properties.some(
7080
+ (property) => property.name === name && property.value === value,
7081
+ )
7082
+ ) {
7083
+ return;
7084
+ }
7085
+ component.properties.push({
7086
+ name,
7087
+ value,
7088
+ });
7089
+ }
7090
+
7091
+ const PYTHON_DIRECT_REFERENCE_PATTERN =
7092
+ /^([A-Za-z0-9_.-]+)(?:\[[^\]]+\])?\s*@\s*(\S+)$/;
7093
+
7094
+ function isWindowsAbsolutePath(value) {
7095
+ return /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("\\\\");
7096
+ }
7097
+
7098
+ function createExternalReferenceKey(reference) {
7099
+ return JSON.stringify([
7100
+ reference.type,
7101
+ reference.url,
7102
+ reference.comment || "",
7103
+ ]);
7104
+ }
7105
+
7106
+ function classifyNpmManifestSource(spec) {
7107
+ if (typeof spec !== "string" || !spec.trim()) {
7108
+ return undefined;
7109
+ }
7110
+ const normalizedSpec = spec.trim();
7111
+ const lowerSpec = normalizedSpec.toLowerCase();
7112
+ if (
7113
+ lowerSpec.startsWith("git+") ||
7114
+ lowerSpec.startsWith("git://") ||
7115
+ lowerSpec.startsWith("github:") ||
7116
+ lowerSpec.startsWith("gitlab:") ||
7117
+ lowerSpec.startsWith("bitbucket:") ||
7118
+ lowerSpec.startsWith("gist:")
7119
+ ) {
7120
+ return {
7121
+ type: "git",
7122
+ value: normalizedSpec,
7123
+ };
7124
+ }
7125
+ if (lowerSpec.startsWith("http://") || lowerSpec.startsWith("https://")) {
7126
+ return {
7127
+ type: "url",
7128
+ value: normalizedSpec,
7129
+ };
7130
+ }
7131
+ if (
7132
+ lowerSpec.startsWith("file:") ||
7133
+ lowerSpec.startsWith("link:") ||
7134
+ lowerSpec.startsWith("workspace:") ||
7135
+ normalizedSpec.startsWith("./") ||
7136
+ normalizedSpec.startsWith("../") ||
7137
+ normalizedSpec.startsWith("/") ||
7138
+ isWindowsAbsolutePath(normalizedSpec)
7139
+ ) {
7140
+ return {
7141
+ type: "path",
7142
+ value: normalizedSpec,
7143
+ };
7144
+ }
7145
+ return undefined;
7146
+ }
7147
+
7148
+ function collectNpmManifestSources(node) {
7149
+ const manifestSources = [];
7150
+ const seen = new Set();
7151
+ if (!node?.edgesIn) {
7152
+ return manifestSources;
7153
+ }
7154
+ for (const edge of node.edgesIn) {
7155
+ const manifestSource = classifyNpmManifestSource(edge?.spec);
7156
+ if (!manifestSource) {
7157
+ continue;
7158
+ }
7159
+ const dedupeKey = `${manifestSource.type}|${manifestSource.value}`;
7160
+ if (seen.has(dedupeKey)) {
7161
+ continue;
7162
+ }
7163
+ seen.add(dedupeKey);
7164
+ manifestSources.push(manifestSource);
7165
+ }
7166
+ return manifestSources;
7167
+ }
7168
+
7169
+ function normalizePythonDependencyKey(value) {
7170
+ if (typeof value !== "string" || !value.trim()) {
7171
+ return undefined;
7172
+ }
7173
+ return value.trim().toLowerCase().replaceAll("_", "-");
7174
+ }
7175
+
7176
+ function extractPythonDependencyKey(value) {
7177
+ const manifestSource = parsePyProjectDependencySourceString(value);
7178
+ if (manifestSource?.name) {
7179
+ return normalizePythonDependencyKey(manifestSource.name);
7180
+ }
7181
+ const packageMatch =
7182
+ typeof value === "string"
7183
+ ? value.trim().match(/^([A-Za-z0-9_.-]+)(?:\[[^\]]+\])?/)
7184
+ : undefined;
7185
+ return normalizePythonDependencyKey(packageMatch?.[1]);
7186
+ }
7187
+ function classifyPythonManifestSourceValue(value) {
7188
+ if (typeof value !== "string" || !value.trim()) {
7189
+ return undefined;
7190
+ }
7191
+ const normalizedValue = value.trim();
7192
+ const lowerValue = normalizedValue.toLowerCase();
7193
+ if (
7194
+ lowerValue.startsWith("git+") ||
7195
+ lowerValue.startsWith("git://") ||
7196
+ lowerValue.startsWith("git@") ||
7197
+ lowerValue.startsWith("ssh://git@")
7198
+ ) {
7199
+ return {
7200
+ type: "git",
7201
+ value: normalizedValue,
7202
+ };
7203
+ }
7204
+ if (
7205
+ lowerValue.startsWith("http://") ||
7206
+ lowerValue.startsWith("https://") ||
7207
+ lowerValue.startsWith("ftp://")
7208
+ ) {
7209
+ return {
7210
+ type: "url",
7211
+ value: normalizedValue,
7212
+ };
7213
+ }
7214
+ if (
7215
+ lowerValue.startsWith("file:") ||
7216
+ normalizedValue.startsWith("./") ||
7217
+ normalizedValue.startsWith("../") ||
7218
+ normalizedValue.startsWith("/") ||
7219
+ isWindowsAbsolutePath(normalizedValue)
7220
+ ) {
7221
+ return {
7222
+ type: "path",
7223
+ value: normalizedValue,
7224
+ };
7225
+ }
7226
+ return undefined;
7227
+ }
7228
+
7229
+ function applyManifestSourceProperties(
7230
+ component,
7231
+ propertyPrefix,
7232
+ manifestSource,
7233
+ ) {
7234
+ if (!manifestSource?.type || !manifestSource?.value) {
7235
+ return;
7236
+ }
7237
+ addComponentProperty(
7238
+ component,
7239
+ `${propertyPrefix}:manifestSourceType`,
7240
+ manifestSource.type,
7241
+ );
7242
+ addComponentProperty(
7243
+ component,
7244
+ `${propertyPrefix}:manifestSource`,
7245
+ manifestSource.value,
7246
+ );
7247
+ }
7248
+
7249
+ function recordPythonDependencySource(
7250
+ dependencySourceMap,
7251
+ dependencyName,
7252
+ sourceType,
7253
+ sourceValue,
7254
+ ) {
7255
+ const normalizedKey = normalizePythonDependencyKey(dependencyName);
7256
+ if (!normalizedKey || !sourceType || !sourceValue) {
7257
+ return;
7258
+ }
7259
+ dependencySourceMap[normalizedKey] = {
7260
+ type: sourceType,
7261
+ value: sourceValue,
7262
+ };
7263
+ }
7264
+
7265
+ function parsePyProjectDependencySourceString(value) {
7266
+ if (typeof value !== "string" || !value.includes("@")) {
7267
+ return undefined;
7268
+ }
7269
+ const directReferenceMatch = value
7270
+ .trim()
7271
+ .match(PYTHON_DIRECT_REFERENCE_PATTERN);
7272
+ if (!directReferenceMatch) {
7273
+ return undefined;
7274
+ }
7275
+ const manifestSource = classifyPythonManifestSourceValue(
7276
+ directReferenceMatch[2],
7277
+ );
7278
+ if (!manifestSource) {
7279
+ return undefined;
7280
+ }
7281
+ return {
7282
+ name: directReferenceMatch[1],
7283
+ ...manifestSource,
7284
+ };
7285
+ }
7286
+
7287
+ function collectPythonManifestSource(pkg) {
7288
+ const sourceCandidates = [
7289
+ { kind: "git", value: pkg?.source?.git },
7290
+ { kind: "git", value: pkg?.vcs?.git },
7291
+ { kind: "url", value: pkg?.vcs?.url },
7292
+ { kind: "url", value: pkg?.source?.url },
7293
+ { kind: "path", value: pkg?.source?.path },
7294
+ { kind: "path", value: pkg?.source?.editable },
7295
+ { kind: "path", value: pkg?.source?.virtual },
7296
+ ];
7297
+ for (const candidate of sourceCandidates) {
7298
+ if (typeof candidate.value !== "string" || !candidate.value.trim()) {
7299
+ continue;
7300
+ }
7301
+ const normalizedValue = candidate.value.trim();
7302
+ if (candidate.kind === "git") {
7303
+ return {
7304
+ type: "git",
7305
+ value: normalizedValue.startsWith("git+")
7306
+ ? normalizedValue
7307
+ : `git+${normalizedValue}`,
7308
+ };
7309
+ }
7310
+ const manifestSource = classifyPythonManifestSourceValue(normalizedValue);
7311
+ if (manifestSource) {
7312
+ return manifestSource;
7313
+ }
7314
+ return {
7315
+ type: candidate.kind,
7316
+ value: normalizedValue,
7317
+ };
7318
+ }
7319
+ return undefined;
7320
+ }
7321
+
7322
+ function parsePythonRequirementManifestSource(value) {
7323
+ if (typeof value !== "string" || !value.trim()) {
7324
+ return undefined;
7325
+ }
7326
+ const normalizedValue = value.trim();
7327
+ const directReferenceMatch = normalizedValue.match(
7328
+ PYTHON_DIRECT_REFERENCE_PATTERN,
7329
+ );
7330
+ if (directReferenceMatch) {
7331
+ const manifestSource = classifyPythonManifestSourceValue(
7332
+ directReferenceMatch[2],
7333
+ );
7334
+ if (manifestSource) {
7335
+ return {
7336
+ name: directReferenceMatch[1],
7337
+ ...manifestSource,
7338
+ };
7339
+ }
7340
+ }
7341
+ const vcsRequirementMatch = normalizedValue.match(
7342
+ /^(git\+\S+?)(?:#.*egg=([A-Za-z0-9_.-]+))?$/,
7343
+ );
7344
+ if (vcsRequirementMatch?.[2]) {
7345
+ return {
7346
+ name: vcsRequirementMatch[2],
7347
+ type: "git",
7348
+ value: vcsRequirementMatch[1],
7349
+ };
7350
+ }
7351
+ return undefined;
7352
+ }
7353
+
7046
7354
  /**
7047
7355
  * Method to parse python pyproject.toml file
7048
7356
  *
@@ -7102,6 +7410,7 @@ export function parsePyProjectTomlFile(tomlFile) {
7102
7410
  let tomlData;
7103
7411
  const directDepsKeys = {};
7104
7412
  const groupDepsKeys = {};
7413
+ const dependencySourceMap = {};
7105
7414
  try {
7106
7415
  tomlData = toml.parse(readFileSync(tomlFile, { encoding: "utf-8" }));
7107
7416
  } catch (err) {
@@ -7173,8 +7482,19 @@ export function parsePyProjectTomlFile(tomlFile) {
7173
7482
  }
7174
7483
  if (tomlData?.project?.dependencies) {
7175
7484
  for (const adep of tomlData.project.dependencies) {
7176
- // Example: bcrypt>=4.2.0
7177
- directDepsKeys[adep.split(/[\s<>=]/)[0]] = true;
7485
+ const dependencyKey = extractPythonDependencyKey(adep);
7486
+ if (dependencyKey) {
7487
+ directDepsKeys[dependencyKey] = true;
7488
+ }
7489
+ const manifestSource = parsePyProjectDependencySourceString(adep);
7490
+ if (manifestSource) {
7491
+ recordPythonDependencySource(
7492
+ dependencySourceMap,
7493
+ manifestSource.name,
7494
+ manifestSource.type,
7495
+ manifestSource.value,
7496
+ );
7497
+ }
7178
7498
  }
7179
7499
  }
7180
7500
  if (tomlData["dependency-groups"]) {
@@ -7186,6 +7506,15 @@ export function parsePyProjectTomlFile(tomlFile) {
7186
7506
  groupDepsKeys[pname] = [];
7187
7507
  }
7188
7508
  groupDepsKeys[pname].push(agroup);
7509
+ const manifestSource = parsePyProjectDependencySourceString(p);
7510
+ if (manifestSource) {
7511
+ recordPythonDependencySource(
7512
+ dependencySourceMap,
7513
+ manifestSource.name,
7514
+ manifestSource.type,
7515
+ manifestSource.value,
7516
+ );
7517
+ }
7189
7518
  } else {
7190
7519
  return;
7191
7520
  }
@@ -7206,6 +7535,29 @@ export function parsePyProjectTomlFile(tomlFile) {
7206
7535
  ].includes(adep)
7207
7536
  ) {
7208
7537
  directDepsKeys[adep] = true;
7538
+ const poetryDependency = tomlData.tool.poetry.dependencies[adep];
7539
+ if (poetryDependency?.git) {
7540
+ recordPythonDependencySource(
7541
+ dependencySourceMap,
7542
+ adep,
7543
+ "git",
7544
+ poetryDependency.git,
7545
+ );
7546
+ } else if (poetryDependency?.url) {
7547
+ recordPythonDependencySource(
7548
+ dependencySourceMap,
7549
+ adep,
7550
+ "url",
7551
+ poetryDependency.url,
7552
+ );
7553
+ } else if (poetryDependency?.path) {
7554
+ recordPythonDependencySource(
7555
+ dependencySourceMap,
7556
+ adep,
7557
+ "path",
7558
+ poetryDependency.path,
7559
+ );
7560
+ }
7209
7561
  }
7210
7562
  } // for
7211
7563
  if (tomlData?.tool?.poetry?.group) {
@@ -7217,10 +7569,63 @@ export function parsePyProjectTomlFile(tomlFile) {
7217
7569
  groupDepsKeys[adep] = [];
7218
7570
  }
7219
7571
  groupDepsKeys[adep].push(agroup);
7572
+ const poetryDependency =
7573
+ tomlData.tool.poetry.group[agroup]?.dependencies?.[adep];
7574
+ if (poetryDependency?.git) {
7575
+ recordPythonDependencySource(
7576
+ dependencySourceMap,
7577
+ adep,
7578
+ "git",
7579
+ poetryDependency.git,
7580
+ );
7581
+ } else if (poetryDependency?.url) {
7582
+ recordPythonDependencySource(
7583
+ dependencySourceMap,
7584
+ adep,
7585
+ "url",
7586
+ poetryDependency.url,
7587
+ );
7588
+ } else if (poetryDependency?.path) {
7589
+ recordPythonDependencySource(
7590
+ dependencySourceMap,
7591
+ adep,
7592
+ "path",
7593
+ poetryDependency.path,
7594
+ );
7595
+ }
7220
7596
  }
7221
7597
  } // for
7222
7598
  }
7223
7599
  }
7600
+ if (tomlData?.tool?.uv?.sources) {
7601
+ for (const adep of Object.keys(tomlData.tool.uv.sources)) {
7602
+ const uvSource = Array.isArray(tomlData.tool.uv.sources[adep])
7603
+ ? tomlData.tool.uv.sources[adep][0]
7604
+ : tomlData.tool.uv.sources[adep];
7605
+ if (uvSource?.git) {
7606
+ recordPythonDependencySource(
7607
+ dependencySourceMap,
7608
+ adep,
7609
+ "git",
7610
+ uvSource.git,
7611
+ );
7612
+ } else if (uvSource?.url) {
7613
+ recordPythonDependencySource(
7614
+ dependencySourceMap,
7615
+ adep,
7616
+ "url",
7617
+ uvSource.url,
7618
+ );
7619
+ } else if (uvSource?.path) {
7620
+ recordPythonDependencySource(
7621
+ dependencySourceMap,
7622
+ adep,
7623
+ "path",
7624
+ uvSource.path,
7625
+ );
7626
+ }
7627
+ }
7628
+ }
7224
7629
  return {
7225
7630
  parentComponent: pkg,
7226
7631
  poetryMode,
@@ -7229,9 +7634,146 @@ export function parsePyProjectTomlFile(tomlFile) {
7229
7634
  workspacePaths,
7230
7635
  directDepsKeys,
7231
7636
  groupDepsKeys,
7637
+ dependencySourceMap,
7232
7638
  };
7233
7639
  }
7234
7640
 
7641
+ function collectPythonLockDistributionReferences(pkg) {
7642
+ const externalReferences = [];
7643
+ const seen = new Set();
7644
+
7645
+ function addExternalReference(type, url, comment) {
7646
+ if (typeof url !== "string" || !url.trim()) {
7647
+ return;
7648
+ }
7649
+ const normalizedUrl = url.trim();
7650
+ const reference = {
7651
+ type,
7652
+ url: normalizedUrl,
7653
+ comment,
7654
+ };
7655
+ const referenceKey = createExternalReferenceKey(reference);
7656
+ if (seen.has(referenceKey)) {
7657
+ return;
7658
+ }
7659
+ seen.add(referenceKey);
7660
+ externalReferences.push(reference);
7661
+ }
7662
+
7663
+ addExternalReference("distribution", pkg?.archive?.url, "archive");
7664
+ addExternalReference("distribution", pkg?.sdist?.url, "sdist");
7665
+ if (Array.isArray(pkg?.wheels)) {
7666
+ for (const wheel of pkg.wheels) {
7667
+ addExternalReference(
7668
+ "distribution",
7669
+ wheel?.url,
7670
+ wheel?.file || wheel?.name || wheel?.filename || "wheel",
7671
+ );
7672
+ }
7673
+ }
7674
+ const vcsSource = [
7675
+ { kind: "url", value: pkg?.vcs?.url },
7676
+ { kind: "git", value: pkg?.vcs?.git },
7677
+ { kind: "git", value: pkg?.source?.git },
7678
+ ].find(
7679
+ (entry) => typeof entry.value === "string" && entry.value.trim().length > 0,
7680
+ );
7681
+ if (vcsSource) {
7682
+ const vcsUrl = vcsSource.value.trim();
7683
+ const normalizedVcsUrl =
7684
+ vcsSource.kind === "git" && !vcsUrl.startsWith("git+")
7685
+ ? `git+${vcsUrl}`
7686
+ : vcsUrl;
7687
+ addExternalReference("vcs", normalizedVcsUrl, "vcs");
7688
+ }
7689
+ if (pkg?.source?.url) {
7690
+ const manifestSource = classifyPythonManifestSourceValue(pkg.source.url);
7691
+ addExternalReference(
7692
+ manifestSource?.type === "git" ? "vcs" : "distribution",
7693
+ pkg.source.url,
7694
+ "source",
7695
+ );
7696
+ }
7697
+ return externalReferences;
7698
+ }
7699
+
7700
+ function collectPythonLockMetadataFileEntries(lockTomlObj, pkg) {
7701
+ if (!lockTomlObj?.metadata?.files || !pkg?.name) {
7702
+ return [];
7703
+ }
7704
+ const expectedKeys = new Set([normalizePythonDependencyKey(pkg.name)]);
7705
+ if (pkg.version) {
7706
+ expectedKeys.add(
7707
+ `${normalizePythonDependencyKey(pkg.name)} ${`${pkg.version}`.trim().toLowerCase()}`,
7708
+ );
7709
+ }
7710
+ const matchingEntries = [];
7711
+ for (const [entryKey, entryValues] of Object.entries(
7712
+ lockTomlObj.metadata.files,
7713
+ )) {
7714
+ if (!Array.isArray(entryValues)) {
7715
+ continue;
7716
+ }
7717
+ if (expectedKeys.has(normalizePythonDependencyKey(entryKey))) {
7718
+ matchingEntries.push(...entryValues);
7719
+ }
7720
+ }
7721
+ return matchingEntries;
7722
+ }
7723
+
7724
+ function mergeExternalReferences(component, references) {
7725
+ if (!references?.length) {
7726
+ return;
7727
+ }
7728
+ const existingReferences = component.externalReferences || [];
7729
+ const seen = new Set(
7730
+ existingReferences.map((reference) =>
7731
+ createExternalReferenceKey(reference),
7732
+ ),
7733
+ );
7734
+ for (const reference of references) {
7735
+ const dedupeKey = createExternalReferenceKey(reference);
7736
+ if (seen.has(dedupeKey)) {
7737
+ continue;
7738
+ }
7739
+ seen.add(dedupeKey);
7740
+ existingReferences.push(reference);
7741
+ }
7742
+ if (existingReferences.length) {
7743
+ component.externalReferences = existingReferences;
7744
+ }
7745
+ }
7746
+
7747
+ function collectPythonLockMetadataDistributionReferences(fileEntries) {
7748
+ const distributionReferences = [];
7749
+ for (const fileEntry of fileEntries || []) {
7750
+ if (typeof fileEntry?.url !== "string" || !fileEntry.url.trim()) {
7751
+ continue;
7752
+ }
7753
+ distributionReferences.push({
7754
+ type: "distribution",
7755
+ url: fileEntry.url.trim(),
7756
+ comment: fileEntry.file,
7757
+ });
7758
+ }
7759
+ return distributionReferences;
7760
+ }
7761
+
7762
+ function collectPypiReleaseExternalReferences(releaseEntries) {
7763
+ const externalReferences = [];
7764
+ for (const releaseEntry of releaseEntries || []) {
7765
+ if (typeof releaseEntry?.url !== "string" || !releaseEntry.url.trim()) {
7766
+ continue;
7767
+ }
7768
+ externalReferences.push({
7769
+ type: "distribution",
7770
+ url: releaseEntry.url.trim(),
7771
+ comment: releaseEntry.filename || releaseEntry.packagetype,
7772
+ });
7773
+ }
7774
+ return externalReferences;
7775
+ }
7776
+
7235
7777
  /**
7236
7778
  * Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock, and pylock.toml.
7237
7779
  *
@@ -7248,6 +7790,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7248
7790
  const pkgBomRefMap = {};
7249
7791
  let directDepsKeys = {};
7250
7792
  let groupDepsKeys = {};
7793
+ let dependencySourceMap = {};
7251
7794
  let parentComponent;
7252
7795
  let workspacePaths;
7253
7796
  let workspaceWarningShown = false;
@@ -7255,6 +7798,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7255
7798
  let pyLockProperties = [];
7256
7799
  // Keep track of any workspace components to be added to the parent component
7257
7800
  const workspaceComponentMap = {};
7801
+ const workspaceDependencySourceMap = {};
7258
7802
  const workspacePyProjMap = {};
7259
7803
  const workspaceRefPyProjMap = {};
7260
7804
  const pkgParentMap = {};
@@ -7274,6 +7818,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7274
7818
  const pyProjMap = parsePyProjectTomlFile(pyProjectFile);
7275
7819
  directDepsKeys = pyProjMap.directDepsKeys || {};
7276
7820
  groupDepsKeys = pyProjMap.groupDepsKeys || {};
7821
+ dependencySourceMap = pyProjMap.dependencySourceMap || {};
7277
7822
  parentComponent = pyProjMap.parentComponent;
7278
7823
  workspacePaths = pyProjMap.workspacePaths;
7279
7824
  if (workspacePaths?.length) {
@@ -7337,6 +7882,12 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7337
7882
  }
7338
7883
  }
7339
7884
  const wparentComponentRef = wcompMap.parentComponent["bom-ref"];
7885
+ if (wcompMap?.dependencySourceMap) {
7886
+ Object.assign(
7887
+ workspaceDependencySourceMap,
7888
+ wcompMap.dependencySourceMap,
7889
+ );
7890
+ }
7340
7891
  // Track the parents of workspace direct dependencies
7341
7892
  if (wcompMap?.directDepsKeys) {
7342
7893
  for (const wdd of Object.keys(wcompMap?.directDepsKeys)) {
@@ -7408,6 +7959,11 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7408
7959
  value: workspacePyProjMap[apkg.name] || pyProjectFile,
7409
7960
  });
7410
7961
  }
7962
+ const manifestSource =
7963
+ dependencySourceMap[normalizePythonDependencyKey(apkg.name)] ||
7964
+ workspaceDependencySourceMap[normalizePythonDependencyKey(apkg.name)] ||
7965
+ collectPythonManifestSource(apkg);
7966
+ applyManifestSourceProperties(pkg, "cdx:pypi", manifestSource);
7411
7967
  if (apkg.optional) {
7412
7968
  pkg.scope = "optional";
7413
7969
  }
@@ -7449,6 +8005,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7449
8005
  });
7450
8006
  }
7451
8007
  }
8008
+ mergeExternalReferences(pkg, collectPythonLockDistributionReferences(apkg));
7452
8009
  if (pyLockMode) {
7453
8010
  pkg.properties = pkg.properties.concat(
7454
8011
  collectPyLockPackageProperties(apkg),
@@ -7511,9 +8068,17 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7511
8068
  }
7512
8069
  }
7513
8070
  }
7514
- if (lockTomlObj?.metadata?.files?.[pkg.name]?.length) {
8071
+ const metadataFileEntries = collectPythonLockMetadataFileEntries(
8072
+ lockTomlObj,
8073
+ pkg,
8074
+ );
8075
+ mergeExternalReferences(
8076
+ pkg,
8077
+ collectPythonLockMetadataDistributionReferences(metadataFileEntries),
8078
+ );
8079
+ if (metadataFileEntries.length) {
7515
8080
  pkg.components = [];
7516
- for (const afileObj of lockTomlObj.metadata.files[pkg.name]) {
8081
+ for (const afileObj of metadataFileEntries) {
7517
8082
  const hashParts = afileObj?.hash?.split(":");
7518
8083
  let hashes;
7519
8084
  if (hashParts?.length === 2) {
@@ -7547,8 +8112,9 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7547
8112
  pkg.components = (pkg.components || []).concat(pylockFileComponents);
7548
8113
  }
7549
8114
  }
8115
+ const normalizedPkgName = normalizePythonDependencyKey(pkg.name);
7550
8116
  if (
7551
- directDepsKeys[pkg.name] ||
8117
+ directDepsKeys[normalizedPkgName] ||
7552
8118
  (hasWorkspaces && !Object.keys(workspaceComponentMap).length)
7553
8119
  ) {
7554
8120
  rootList.push(pkg);
@@ -7743,6 +8309,23 @@ export async function parseReqFile(reqFile, fetchDepsInfo = false) {
7743
8309
  const LICENSE_ID_COMMENTS_PATTERN =
7744
8310
  /^(Apache-2\.0|MIT|ISC|GPL-|LGPL-|BSD-[23]-Clause)/i;
7745
8311
 
8312
+ function parseLicenseComment(comment) {
8313
+ if (!comment) {
8314
+ return undefined;
8315
+ }
8316
+ const licenses = comment
8317
+ .split("/")
8318
+ .map((value) => {
8319
+ const licenseId = value.trim();
8320
+ if (!licenseId.match(LICENSE_ID_COMMENTS_PATTERN)) {
8321
+ return undefined;
8322
+ }
8323
+ return { license: { id: licenseId } };
8324
+ })
8325
+ .filter((value) => value !== undefined);
8326
+ return licenses.length ? licenses : undefined;
8327
+ }
8328
+
7746
8329
  /**
7747
8330
  * Method to parse requirements.txt file. Must only be used internally.
7748
8331
  *
@@ -7780,11 +8363,16 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7780
8363
  const lines = normalizedData.split("\n");
7781
8364
  for (const line of lines) {
7782
8365
  let l = line.trim();
8366
+ let editableRequirement = false;
7783
8367
  if (l.includes("# Basic requirements")) {
7784
8368
  compScope = "required";
7785
8369
  } else if (l.includes("added by pip freeze")) {
7786
8370
  compScope = undefined;
7787
8371
  }
8372
+ if (l.startsWith("-e ") || l.startsWith("--editable ")) {
8373
+ editableRequirement = true;
8374
+ l = l.replace(/^--editable\s+|^-e\s+/, "").trim();
8375
+ }
7788
8376
  if (l.startsWith("Skipping line") || l.startsWith("(add")) {
7789
8377
  continue;
7790
8378
  }
@@ -7833,6 +8421,45 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7833
8421
  markers = parts.slice(1).join(";").trim();
7834
8422
  structuredMarkers = parseReqEnvMarkers(markers);
7835
8423
  }
8424
+ const requirementManifestSource = parsePythonRequirementManifestSource(l);
8425
+ if (requirementManifestSource?.name) {
8426
+ const apkg = {
8427
+ name: requirementManifestSource.name,
8428
+ version: null,
8429
+ scope: compScope,
8430
+ evidence,
8431
+ };
8432
+ if (hashes.length > 0) {
8433
+ apkg.hashes = hashes;
8434
+ }
8435
+ const licenses = parseLicenseComment(comment);
8436
+ if (licenses) {
8437
+ apkg.licenses = licenses;
8438
+ }
8439
+ applyManifestSourceProperties(
8440
+ apkg,
8441
+ "cdx:pypi",
8442
+ requirementManifestSource,
8443
+ );
8444
+ if (editableRequirement) {
8445
+ addComponentProperty(apkg, "cdx:pypi:editable", "true");
8446
+ }
8447
+ if (markers) {
8448
+ addComponentProperty(apkg, "cdx:pip:markers", markers);
8449
+ if (structuredMarkers?.length > 0) {
8450
+ addComponentProperty(
8451
+ apkg,
8452
+ "cdx:pip:structuredMarkers",
8453
+ JSON.stringify(structuredMarkers),
8454
+ );
8455
+ }
8456
+ }
8457
+ if (reqFile) {
8458
+ addComponentProperty(apkg, "SrcFile", reqFile);
8459
+ }
8460
+ pkgList.push(apkg);
8461
+ continue;
8462
+ }
7836
8463
 
7837
8464
  // Handle extras (e.g., package[extra1,extra2])
7838
8465
  let extras = null;
@@ -7866,20 +8493,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7866
8493
  if (hashes.length > 0) {
7867
8494
  apkg.hashes = hashes;
7868
8495
  }
7869
- if (comment) {
7870
- apkg.licenses = comment
7871
- .split("/")
7872
- .map((c) => {
7873
- const licenseObj = {};
7874
- const cId = c.trim();
7875
- if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
7876
- licenseObj.id = cId;
7877
- } else {
7878
- return undefined;
7879
- }
7880
- return { license: licenseObj };
7881
- })
7882
- .filter((l) => l !== undefined);
8496
+ const licenses = parseLicenseComment(comment);
8497
+ if (licenses) {
8498
+ apkg.licenses = licenses;
7883
8499
  }
7884
8500
  if (extras && extras.length > 0) {
7885
8501
  properties.push({
@@ -7905,6 +8521,12 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7905
8521
  });
7906
8522
  }
7907
8523
  }
8524
+ if (editableRequirement) {
8525
+ properties.push({
8526
+ name: "cdx:pypi:editable",
8527
+ value: "true",
8528
+ });
8529
+ }
7908
8530
  if (properties.length) {
7909
8531
  apkg.properties = properties;
7910
8532
  }
@@ -7932,20 +8554,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7932
8554
  scope: compScope,
7933
8555
  evidence,
7934
8556
  };
7935
- if (comment) {
7936
- apkg.licenses = comment
7937
- .split("/")
7938
- .map((c) => {
7939
- const licenseObj = {};
7940
- const cId = c.trim();
7941
- if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
7942
- licenseObj.id = cId;
7943
- } else {
7944
- return undefined;
7945
- }
7946
- return { license: licenseObj };
7947
- })
7948
- .filter((l) => l !== undefined);
8557
+ const licenses = parseLicenseComment(comment);
8558
+ if (licenses) {
8559
+ apkg.licenses = licenses;
7949
8560
  }
7950
8561
  if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
7951
8562
  properties.push({
@@ -7965,6 +8576,12 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7965
8576
  });
7966
8577
  }
7967
8578
  }
8579
+ if (editableRequirement) {
8580
+ properties.push({
8581
+ name: "cdx:pypi:editable",
8582
+ value: "true",
8583
+ });
8584
+ }
7968
8585
  if (properties.length) {
7969
8586
  apkg.properties = properties;
7970
8587
  }
@@ -12665,6 +13282,188 @@ export function parseConanData(conanData) {
12665
13282
  return pkgList;
12666
13283
  }
12667
13284
 
13285
+ /**
13286
+ * Construct a generic package component object for collider-managed packages.
13287
+ *
13288
+ * @param {string} name Package name
13289
+ * @param {Object} pkgData Locked package entry from collider.lock
13290
+ * @param {string} lockFile Source lock file path
13291
+ * @param {string} dependencyKind Whether the package is direct or transitive
13292
+ * @returns {Object|undefined} Package component
13293
+ */
13294
+ function buildColliderComponent(name, pkgData, lockFile, dependencyKind) {
13295
+ if (!name) {
13296
+ return undefined;
13297
+ }
13298
+ pkgData = pkgData || {};
13299
+ dependencyKind = dependencyKind || "transitive";
13300
+ const version = pkgData?.version || "";
13301
+ const purl = new PackageURL(
13302
+ "generic",
13303
+ "",
13304
+ name,
13305
+ version || undefined,
13306
+ null,
13307
+ null,
13308
+ ).toString();
13309
+ const properties = [
13310
+ {
13311
+ name: "cdx:collider:dependencyKind",
13312
+ value: dependencyKind,
13313
+ },
13314
+ ];
13315
+ if (lockFile) {
13316
+ properties.unshift({
13317
+ name: "SrcFile",
13318
+ value: lockFile,
13319
+ });
13320
+ }
13321
+ const wrapHash =
13322
+ typeof pkgData?.wrap_hash === "string" ? pkgData.wrap_hash.trim() : "";
13323
+ const wrapHashMatch = wrapHash.match(/^sha256:([0-9A-Fa-f]{64})$/);
13324
+ if (wrapHash) {
13325
+ properties.push({
13326
+ name: "cdx:collider:wrapHash",
13327
+ value: wrapHash,
13328
+ });
13329
+ }
13330
+ properties.push({
13331
+ name: "cdx:collider:hasWrapHash",
13332
+ value: wrapHashMatch ? "true" : "false",
13333
+ });
13334
+ if (wrapHash && !wrapHashMatch) {
13335
+ properties.push({
13336
+ name: "cdx:collider:wrapHashInvalid",
13337
+ value: "true",
13338
+ });
13339
+ }
13340
+ let originReference;
13341
+ if (typeof pkgData?.origin === "string" && pkgData.origin.trim()) {
13342
+ try {
13343
+ const originUrl = new URL(pkgData.origin.trim());
13344
+ const originHadSensitiveParts = Boolean(
13345
+ originUrl.username ||
13346
+ originUrl.password ||
13347
+ originUrl.search ||
13348
+ originUrl.hash,
13349
+ );
13350
+ originUrl.username = "";
13351
+ originUrl.password = "";
13352
+ originUrl.search = "";
13353
+ originUrl.hash = "";
13354
+ originReference = originUrl.toString();
13355
+ properties.push({
13356
+ name: "cdx:collider:origin",
13357
+ value: originReference,
13358
+ });
13359
+ properties.push({
13360
+ name: "cdx:collider:originScheme",
13361
+ value: originUrl.protocol.replace(":", ""),
13362
+ });
13363
+ if (originUrl.host) {
13364
+ properties.push({
13365
+ name: "cdx:collider:originHost",
13366
+ value: originUrl.host,
13367
+ });
13368
+ }
13369
+ if (originHadSensitiveParts) {
13370
+ properties.push({
13371
+ name: "cdx:collider:originSanitized",
13372
+ value: "true",
13373
+ });
13374
+ }
13375
+ } catch {
13376
+ thoughtLog("Ignoring invalid Collider origin URL");
13377
+ }
13378
+ }
13379
+ const component = {
13380
+ name,
13381
+ version,
13382
+ purl,
13383
+ "bom-ref": decodeURIComponent(purl),
13384
+ properties,
13385
+ };
13386
+ if (dependencyKind === "direct") {
13387
+ component.scope = "required";
13388
+ }
13389
+ if (wrapHashMatch) {
13390
+ component.hashes = [
13391
+ {
13392
+ alg: "SHA-256",
13393
+ content: wrapHashMatch[1].toLowerCase(),
13394
+ },
13395
+ ];
13396
+ }
13397
+ if (originReference) {
13398
+ component.externalReferences = [
13399
+ {
13400
+ type: "distribution",
13401
+ url: originReference,
13402
+ },
13403
+ ];
13404
+ }
13405
+ return component;
13406
+ }
13407
+
13408
+ /**
13409
+ * Parse Collider lock file data (collider.lock) and return the package list and
13410
+ * parent component dependencies.
13411
+ *
13412
+ * @param {string} colliderLockData Raw JSON string of the Collider lock file
13413
+ * @param {string} lockFile Source lock file path
13414
+ * @returns {{ pkgList: Object[], dependencies: Object, parentComponentDependencies: string[] }}
13415
+ */
13416
+ export function parseColliderLockData(colliderLockData, lockFile) {
13417
+ const pkgList = [];
13418
+ const dependencies = {};
13419
+ const parentComponentDependencies = [];
13420
+ if (!colliderLockData) {
13421
+ return { pkgList, dependencies, parentComponentDependencies };
13422
+ }
13423
+ let parsedLockFile;
13424
+ try {
13425
+ parsedLockFile = JSON.parse(colliderLockData);
13426
+ } catch {
13427
+ return { pkgList, dependencies, parentComponentDependencies };
13428
+ }
13429
+ const addedBomRefs = new Set();
13430
+ const directDependencies = parsedLockFile.dependencies || {};
13431
+ const packages = parsedLockFile.packages || {};
13432
+ for (const [name, pkgData] of Object.entries(directDependencies)) {
13433
+ const component = buildColliderComponent(name, pkgData, lockFile, "direct");
13434
+ if (!component) {
13435
+ continue;
13436
+ }
13437
+ if (!addedBomRefs.has(component["bom-ref"])) {
13438
+ pkgList.push(component);
13439
+ addedBomRefs.add(component["bom-ref"]);
13440
+ }
13441
+ if (!parentComponentDependencies.includes(component["bom-ref"])) {
13442
+ parentComponentDependencies.push(component["bom-ref"]);
13443
+ }
13444
+ if (!(component["bom-ref"] in dependencies)) {
13445
+ dependencies[component["bom-ref"]] = [];
13446
+ }
13447
+ }
13448
+ for (const [name, pkgData] of Object.entries(packages)) {
13449
+ const component = buildColliderComponent(
13450
+ name,
13451
+ pkgData,
13452
+ lockFile,
13453
+ "transitive",
13454
+ );
13455
+ if (!component || addedBomRefs.has(component["bom-ref"])) {
13456
+ continue;
13457
+ }
13458
+ pkgList.push(component);
13459
+ addedBomRefs.add(component["bom-ref"]);
13460
+ if (!(component["bom-ref"] in dependencies)) {
13461
+ dependencies[component["bom-ref"]] = [];
13462
+ }
13463
+ }
13464
+ return { pkgList, dependencies, parentComponentDependencies };
13465
+ }
13466
+
12668
13467
  /**
12669
13468
  * Parse Leiningen project.clj data and extract dependency packages.
12670
13469
  *
@@ -18363,6 +19162,92 @@ export function parsePackageJsonName(name) {
18363
19162
  return returnObject;
18364
19163
  }
18365
19164
 
19165
+ /**
19166
+ * Collect bom-refs from metadata.tools entries.
19167
+ *
19168
+ * @param {Object[]|Object} tools CycloneDX metadata.tools section
19169
+ * @param {Function} predicate Optional filter function
19170
+ * @returns {string[]} Unique tool bom-refs
19171
+ */
19172
+ export function extractToolRefs(tools, predicate) {
19173
+ if (!tools) {
19174
+ return [];
19175
+ }
19176
+ const toolRefs = new Set();
19177
+ const toolList = Array.isArray(tools)
19178
+ ? tools
19179
+ : [...(tools.components || []), ...(tools.services || [])];
19180
+ for (const tool of toolList) {
19181
+ let toolRef = tool?.["bom-ref"];
19182
+ if (!toolRef && tool?.purl) {
19183
+ toolRef = decodeURIComponent(tool.purl);
19184
+ }
19185
+ if (!toolRef && tool?.name) {
19186
+ try {
19187
+ toolRef = new PackageURL(
19188
+ "generic",
19189
+ tool.group || tool.publisher || tool.manufacturer?.name || undefined,
19190
+ tool.name,
19191
+ tool.version || undefined,
19192
+ null,
19193
+ null,
19194
+ ).toString();
19195
+ } catch (_err) {
19196
+ thoughtLog("Unable to derive bom-ref for external tool", {
19197
+ group: tool.group,
19198
+ manufacturer: tool.manufacturer?.name,
19199
+ name: tool.name,
19200
+ publisher: tool.publisher,
19201
+ version: tool.version,
19202
+ });
19203
+ toolRef = undefined;
19204
+ }
19205
+ }
19206
+ if (!toolRef) {
19207
+ continue;
19208
+ }
19209
+ if (!tool["bom-ref"]) {
19210
+ tool["bom-ref"] = toolRef;
19211
+ }
19212
+ if (predicate && !predicate(tool)) {
19213
+ continue;
19214
+ }
19215
+ toolRefs.add(toolRef);
19216
+ }
19217
+ return Array.from(toolRefs);
19218
+ }
19219
+
19220
+ /**
19221
+ * Attach evidence.identity.tools references to the supplied subjects.
19222
+ *
19223
+ * @param {Object|Object[]} subjects Component or service objects
19224
+ * @param {string[]} toolRefs Tool bom-refs
19225
+ * @returns {Object|Object[]} The same mutated subject(s)
19226
+ */
19227
+ export function attachIdentityTools(subjects, toolRefs) {
19228
+ if (!subjects || !toolRefs?.length) {
19229
+ return subjects;
19230
+ }
19231
+ const uniqueToolRefs = Array.from(new Set(toolRefs.filter(Boolean)));
19232
+ if (!uniqueToolRefs.length) {
19233
+ return subjects;
19234
+ }
19235
+ const subjectList = Array.isArray(subjects) ? subjects : [subjects];
19236
+ for (const subject of subjectList) {
19237
+ const identities = Array.isArray(subject?.evidence?.identity)
19238
+ ? subject.evidence.identity
19239
+ : subject?.evidence?.identity
19240
+ ? [subject.evidence.identity]
19241
+ : [];
19242
+ for (const identity of identities) {
19243
+ identity.tools = Array.from(
19244
+ new Set([...(identity.tools || []), ...uniqueToolRefs]),
19245
+ );
19246
+ }
19247
+ }
19248
+ return subjects;
19249
+ }
19250
+
18366
19251
  /**
18367
19252
  * Method to add occurrence evidence for components based on import statements. Currently useful for js
18368
19253
  *
@@ -19960,7 +20845,7 @@ export function collectExecutables(basePath, binPaths) {
19960
20845
  if (!binPaths) {
19961
20846
  return [];
19962
20847
  }
19963
- let executables = [];
20848
+ const executablesByResolvedPath = new Map();
19964
20849
  const ignoreList = [
19965
20850
  "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
19966
20851
  "[",
@@ -19976,12 +20861,38 @@ export function collectExecutables(basePath, binPaths) {
19976
20861
  follow: true,
19977
20862
  ignore: ignoreList,
19978
20863
  });
19979
- executables = executables.concat(files);
20864
+ for (const file of files) {
20865
+ let resolvedFile = file;
20866
+ try {
20867
+ resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
20868
+ } catch (_err) {
20869
+ // Broken symlinks or permission errors can prevent realpath resolution.
20870
+ if (DEBUG_MODE) {
20871
+ console.log(`Unable to resolve executable path alias for ${file}`);
20872
+ }
20873
+ }
20874
+ const existingFile = executablesByResolvedPath.get(resolvedFile);
20875
+ if (shouldPreferUsrMergedExecutablePath(file, existingFile)) {
20876
+ executablesByResolvedPath.set(resolvedFile, file);
20877
+ }
20878
+ }
19980
20879
  } catch (_err) {
19981
20880
  // ignore
19982
20881
  }
19983
20882
  }
19984
- return Array.from(new Set(executables)).sort();
20883
+ return Array.from(executablesByResolvedPath.values()).sort();
20884
+ }
20885
+
20886
+ function shouldPreferUsrMergedExecutablePath(file, existingFile) {
20887
+ if (!existingFile) {
20888
+ return true;
20889
+ }
20890
+ const fileUsesUsrPrefix = file.startsWith("usr/");
20891
+ const existingFileUsesUsrPrefix = existingFile.startsWith("usr/");
20892
+ if (fileUsesUsrPrefix !== existingFileUsesUsrPrefix) {
20893
+ return fileUsesUsrPrefix;
20894
+ }
20895
+ return file < existingFile;
19985
20896
  }
19986
20897
 
19987
20898
  /**