@cyclonedx/cdxgen 12.3.1 → 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 (87) hide show
  1. package/README.md +6 -0
  2. package/bin/cdxgen.js +1 -2
  3. package/data/rules/ai-agent-governance.yaml +43 -0
  4. package/data/rules/ci-permissions.yaml +132 -0
  5. package/data/rules/dependency-sources.yaml +65 -5
  6. package/data/rules/mcp-servers.yaml +36 -2
  7. package/data/rules/package-integrity.yaml +22 -0
  8. package/lib/cli/index.js +436 -56
  9. package/lib/cli/index.poku.js +875 -2
  10. package/lib/helpers/agentFormulationParser.js +10 -3
  11. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  12. package/lib/helpers/aiInventory.js +262 -0
  13. package/lib/helpers/aiInventory.poku.js +111 -0
  14. package/lib/helpers/analyzer.js +413 -54
  15. package/lib/helpers/analyzer.poku.js +117 -0
  16. package/lib/helpers/auditCategories.js +76 -0
  17. package/lib/helpers/chromextutils.js +25 -3
  18. package/lib/helpers/chromextutils.poku.js +68 -0
  19. package/lib/helpers/ciParsers/githubActions.js +79 -0
  20. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  21. package/lib/helpers/communityAiConfigParser.js +15 -5
  22. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  23. package/lib/helpers/depsUtils.js +5 -0
  24. package/lib/helpers/depsUtils.poku.js +55 -0
  25. package/lib/helpers/display.js +50 -24
  26. package/lib/helpers/display.poku.js +70 -58
  27. package/lib/helpers/formulationParsers.js +26 -6
  28. package/lib/helpers/jsonLike.js +21 -20
  29. package/lib/helpers/jsonLike.poku.js +34 -0
  30. package/lib/helpers/mcpConfigParser.js +32 -16
  31. package/lib/helpers/mcpConfigParser.poku.js +104 -0
  32. package/lib/helpers/mcpDiscovery.js +13 -23
  33. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  34. package/lib/helpers/propertySanitizer.js +121 -0
  35. package/lib/helpers/utils.js +953 -41
  36. package/lib/helpers/utils.poku.js +901 -1
  37. package/lib/managers/binary.js +16 -0
  38. package/lib/managers/binary.poku.js +1 -0
  39. package/lib/managers/docker.js +240 -16
  40. package/lib/managers/docker.poku.js +1142 -2
  41. package/lib/server/server.js +7 -4
  42. package/lib/server/server.poku.js +36 -1
  43. package/lib/stages/postgen/annotator.js +2 -1
  44. package/lib/stages/postgen/annotator.poku.js +15 -0
  45. package/lib/stages/postgen/auditBom.js +12 -6
  46. package/lib/stages/postgen/auditBom.poku.js +755 -6
  47. package/lib/stages/postgen/postgen.js +229 -6
  48. package/lib/stages/postgen/postgen.poku.js +180 -0
  49. package/package.json +2 -1
  50. package/types/lib/cli/index.d.ts +1 -0
  51. package/types/lib/cli/index.d.ts.map +1 -1
  52. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  53. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  54. package/types/lib/helpers/aiInventory.d.ts +23 -0
  55. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  56. package/types/lib/helpers/analyzer.d.ts +5 -0
  57. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  58. package/types/lib/helpers/auditCategories.d.ts +12 -0
  59. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  60. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  61. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  62. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  63. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  64. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  65. package/types/lib/helpers/display.d.ts +1 -0
  66. package/types/lib/helpers/display.d.ts.map +1 -1
  67. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  68. package/types/lib/helpers/jsonLike.d.ts +4 -0
  69. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  70. package/types/lib/helpers/mcp.d.ts +29 -0
  71. package/types/lib/helpers/mcp.d.ts.map +1 -0
  72. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  73. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  74. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  75. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  76. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  77. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  78. package/types/lib/helpers/utils.d.ts +31 -0
  79. package/types/lib/helpers/utils.d.ts.map +1 -1
  80. package/types/lib/managers/binary.d.ts.map +1 -1
  81. package/types/lib/managers/docker.d.ts +3 -0
  82. package/types/lib/managers/docker.d.ts.map +1 -1
  83. package/types/lib/server/server.d.ts +1 -0
  84. package/types/lib/server/server.d.ts.map +1 -1
  85. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  86. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  87. package/types/lib/stages/postgen/postgen.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,
@@ -918,7 +919,6 @@ export const PROJECT_TYPE_ALIASES = {
918
919
  "node20",
919
920
  "node22",
920
921
  "node23",
921
- "mcp",
922
922
  "js",
923
923
  "javascript",
924
924
  "typescript",
@@ -927,6 +927,8 @@ export const PROJECT_TYPE_ALIASES = {
927
927
  "yarn",
928
928
  "rush",
929
929
  ],
930
+ mcp: ["mcp"],
931
+ "ai-skill": ["ai-skill", "skill", "skills"],
930
932
  py: [
931
933
  "py",
932
934
  "python",
@@ -980,7 +982,7 @@ export const PROJECT_TYPE_ALIASES = {
980
982
  dart: ["dart", "flutter", "pub"],
981
983
  haskell: ["haskell", "hackage", "cabal"],
982
984
  elixir: ["elixir", "hex", "mix"],
983
- c: ["c", "cpp", "c++", "conan"],
985
+ c: ["c", "cpp", "c++", "conan", "collider"],
984
986
  clojure: ["clojure", "edn", "clj", "leiningen"],
985
987
  github: ["github", "actions"],
986
988
  os: ["os", "osquery", "windows", "linux", "mac", "macos", "darwin"],
@@ -1013,7 +1015,7 @@ export const PROJECT_TYPE_ALIASES = {
1013
1015
  "visionos",
1014
1016
  ],
1015
1017
  binary: ["binary", "blint"],
1016
- oci: ["docker", "oci", "container", "podman"],
1018
+ oci: ["docker", "oci", "container", "podman", "rootfs", "oci-dir"],
1017
1019
  cocoa: ["cocoa", "cocoapods", "objective-c", "swift", "ios"],
1018
1020
  scala: ["scala", "scala3", "sbt", "mill"],
1019
1021
  nix: ["nix", "nixos", "flake"],
@@ -2584,6 +2586,23 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
2584
2586
  value: "true",
2585
2587
  });
2586
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
+ }
2587
2606
  // This getter method could fail with errors at times.
2588
2607
  // Example Error: Invalid tag name "^>=6.0.0" of package "^>=6.0.0": Tags may not have any characters that encodeURIComponent encodes.
2589
2608
  try {
@@ -6810,8 +6829,17 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
6810
6829
  value: origName,
6811
6830
  });
6812
6831
  }
6813
- if (body.releases?.[p.version] && body.releases[p.version].length) {
6814
- 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;
6815
6843
  if (digest["sha256"]) {
6816
6844
  p._integrity = `sha256-${digest["sha256"]}`;
6817
6845
  } else if (digest["md5"]) {
@@ -7042,6 +7070,287 @@ export async function parsePiplockData(lockData) {
7042
7070
  return await getPyMetadata(pkgList, false);
7043
7071
  }
7044
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
+
7045
7354
  /**
7046
7355
  * Method to parse python pyproject.toml file
7047
7356
  *
@@ -7101,6 +7410,7 @@ export function parsePyProjectTomlFile(tomlFile) {
7101
7410
  let tomlData;
7102
7411
  const directDepsKeys = {};
7103
7412
  const groupDepsKeys = {};
7413
+ const dependencySourceMap = {};
7104
7414
  try {
7105
7415
  tomlData = toml.parse(readFileSync(tomlFile, { encoding: "utf-8" }));
7106
7416
  } catch (err) {
@@ -7172,8 +7482,19 @@ export function parsePyProjectTomlFile(tomlFile) {
7172
7482
  }
7173
7483
  if (tomlData?.project?.dependencies) {
7174
7484
  for (const adep of tomlData.project.dependencies) {
7175
- // Example: bcrypt>=4.2.0
7176
- 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
+ }
7177
7498
  }
7178
7499
  }
7179
7500
  if (tomlData["dependency-groups"]) {
@@ -7185,6 +7506,15 @@ export function parsePyProjectTomlFile(tomlFile) {
7185
7506
  groupDepsKeys[pname] = [];
7186
7507
  }
7187
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
+ }
7188
7518
  } else {
7189
7519
  return;
7190
7520
  }
@@ -7205,6 +7535,29 @@ export function parsePyProjectTomlFile(tomlFile) {
7205
7535
  ].includes(adep)
7206
7536
  ) {
7207
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
+ }
7208
7561
  }
7209
7562
  } // for
7210
7563
  if (tomlData?.tool?.poetry?.group) {
@@ -7216,10 +7569,63 @@ export function parsePyProjectTomlFile(tomlFile) {
7216
7569
  groupDepsKeys[adep] = [];
7217
7570
  }
7218
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
+ }
7219
7596
  }
7220
7597
  } // for
7221
7598
  }
7222
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
+ }
7223
7629
  return {
7224
7630
  parentComponent: pkg,
7225
7631
  poetryMode,
@@ -7228,9 +7634,146 @@ export function parsePyProjectTomlFile(tomlFile) {
7228
7634
  workspacePaths,
7229
7635
  directDepsKeys,
7230
7636
  groupDepsKeys,
7637
+ dependencySourceMap,
7231
7638
  };
7232
7639
  }
7233
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
+
7234
7777
  /**
7235
7778
  * Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock, and pylock.toml.
7236
7779
  *
@@ -7247,6 +7790,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7247
7790
  const pkgBomRefMap = {};
7248
7791
  let directDepsKeys = {};
7249
7792
  let groupDepsKeys = {};
7793
+ let dependencySourceMap = {};
7250
7794
  let parentComponent;
7251
7795
  let workspacePaths;
7252
7796
  let workspaceWarningShown = false;
@@ -7254,6 +7798,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7254
7798
  let pyLockProperties = [];
7255
7799
  // Keep track of any workspace components to be added to the parent component
7256
7800
  const workspaceComponentMap = {};
7801
+ const workspaceDependencySourceMap = {};
7257
7802
  const workspacePyProjMap = {};
7258
7803
  const workspaceRefPyProjMap = {};
7259
7804
  const pkgParentMap = {};
@@ -7273,6 +7818,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7273
7818
  const pyProjMap = parsePyProjectTomlFile(pyProjectFile);
7274
7819
  directDepsKeys = pyProjMap.directDepsKeys || {};
7275
7820
  groupDepsKeys = pyProjMap.groupDepsKeys || {};
7821
+ dependencySourceMap = pyProjMap.dependencySourceMap || {};
7276
7822
  parentComponent = pyProjMap.parentComponent;
7277
7823
  workspacePaths = pyProjMap.workspacePaths;
7278
7824
  if (workspacePaths?.length) {
@@ -7336,6 +7882,12 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7336
7882
  }
7337
7883
  }
7338
7884
  const wparentComponentRef = wcompMap.parentComponent["bom-ref"];
7885
+ if (wcompMap?.dependencySourceMap) {
7886
+ Object.assign(
7887
+ workspaceDependencySourceMap,
7888
+ wcompMap.dependencySourceMap,
7889
+ );
7890
+ }
7339
7891
  // Track the parents of workspace direct dependencies
7340
7892
  if (wcompMap?.directDepsKeys) {
7341
7893
  for (const wdd of Object.keys(wcompMap?.directDepsKeys)) {
@@ -7407,6 +7959,11 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7407
7959
  value: workspacePyProjMap[apkg.name] || pyProjectFile,
7408
7960
  });
7409
7961
  }
7962
+ const manifestSource =
7963
+ dependencySourceMap[normalizePythonDependencyKey(apkg.name)] ||
7964
+ workspaceDependencySourceMap[normalizePythonDependencyKey(apkg.name)] ||
7965
+ collectPythonManifestSource(apkg);
7966
+ applyManifestSourceProperties(pkg, "cdx:pypi", manifestSource);
7410
7967
  if (apkg.optional) {
7411
7968
  pkg.scope = "optional";
7412
7969
  }
@@ -7448,6 +8005,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7448
8005
  });
7449
8006
  }
7450
8007
  }
8008
+ mergeExternalReferences(pkg, collectPythonLockDistributionReferences(apkg));
7451
8009
  if (pyLockMode) {
7452
8010
  pkg.properties = pkg.properties.concat(
7453
8011
  collectPyLockPackageProperties(apkg),
@@ -7510,9 +8068,17 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7510
8068
  }
7511
8069
  }
7512
8070
  }
7513
- 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) {
7514
8080
  pkg.components = [];
7515
- for (const afileObj of lockTomlObj.metadata.files[pkg.name]) {
8081
+ for (const afileObj of metadataFileEntries) {
7516
8082
  const hashParts = afileObj?.hash?.split(":");
7517
8083
  let hashes;
7518
8084
  if (hashParts?.length === 2) {
@@ -7546,8 +8112,9 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
7546
8112
  pkg.components = (pkg.components || []).concat(pylockFileComponents);
7547
8113
  }
7548
8114
  }
8115
+ const normalizedPkgName = normalizePythonDependencyKey(pkg.name);
7549
8116
  if (
7550
- directDepsKeys[pkg.name] ||
8117
+ directDepsKeys[normalizedPkgName] ||
7551
8118
  (hasWorkspaces && !Object.keys(workspaceComponentMap).length)
7552
8119
  ) {
7553
8120
  rootList.push(pkg);
@@ -7742,6 +8309,23 @@ export async function parseReqFile(reqFile, fetchDepsInfo = false) {
7742
8309
  const LICENSE_ID_COMMENTS_PATTERN =
7743
8310
  /^(Apache-2\.0|MIT|ISC|GPL-|LGPL-|BSD-[23]-Clause)/i;
7744
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
+
7745
8329
  /**
7746
8330
  * Method to parse requirements.txt file. Must only be used internally.
7747
8331
  *
@@ -7779,11 +8363,16 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7779
8363
  const lines = normalizedData.split("\n");
7780
8364
  for (const line of lines) {
7781
8365
  let l = line.trim();
8366
+ let editableRequirement = false;
7782
8367
  if (l.includes("# Basic requirements")) {
7783
8368
  compScope = "required";
7784
8369
  } else if (l.includes("added by pip freeze")) {
7785
8370
  compScope = undefined;
7786
8371
  }
8372
+ if (l.startsWith("-e ") || l.startsWith("--editable ")) {
8373
+ editableRequirement = true;
8374
+ l = l.replace(/^--editable\s+|^-e\s+/, "").trim();
8375
+ }
7787
8376
  if (l.startsWith("Skipping line") || l.startsWith("(add")) {
7788
8377
  continue;
7789
8378
  }
@@ -7832,6 +8421,45 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7832
8421
  markers = parts.slice(1).join(";").trim();
7833
8422
  structuredMarkers = parseReqEnvMarkers(markers);
7834
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
+ }
7835
8463
 
7836
8464
  // Handle extras (e.g., package[extra1,extra2])
7837
8465
  let extras = null;
@@ -7865,20 +8493,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7865
8493
  if (hashes.length > 0) {
7866
8494
  apkg.hashes = hashes;
7867
8495
  }
7868
- if (comment) {
7869
- apkg.licenses = comment
7870
- .split("/")
7871
- .map((c) => {
7872
- const licenseObj = {};
7873
- const cId = c.trim();
7874
- if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
7875
- licenseObj.id = cId;
7876
- } else {
7877
- return undefined;
7878
- }
7879
- return { license: licenseObj };
7880
- })
7881
- .filter((l) => l !== undefined);
8496
+ const licenses = parseLicenseComment(comment);
8497
+ if (licenses) {
8498
+ apkg.licenses = licenses;
7882
8499
  }
7883
8500
  if (extras && extras.length > 0) {
7884
8501
  properties.push({
@@ -7904,6 +8521,12 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7904
8521
  });
7905
8522
  }
7906
8523
  }
8524
+ if (editableRequirement) {
8525
+ properties.push({
8526
+ name: "cdx:pypi:editable",
8527
+ value: "true",
8528
+ });
8529
+ }
7907
8530
  if (properties.length) {
7908
8531
  apkg.properties = properties;
7909
8532
  }
@@ -7931,20 +8554,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7931
8554
  scope: compScope,
7932
8555
  evidence,
7933
8556
  };
7934
- if (comment) {
7935
- apkg.licenses = comment
7936
- .split("/")
7937
- .map((c) => {
7938
- const licenseObj = {};
7939
- const cId = c.trim();
7940
- if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
7941
- licenseObj.id = cId;
7942
- } else {
7943
- return undefined;
7944
- }
7945
- return { license: licenseObj };
7946
- })
7947
- .filter((l) => l !== undefined);
8557
+ const licenses = parseLicenseComment(comment);
8558
+ if (licenses) {
8559
+ apkg.licenses = licenses;
7948
8560
  }
7949
8561
  if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
7950
8562
  properties.push({
@@ -7964,6 +8576,12 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
7964
8576
  });
7965
8577
  }
7966
8578
  }
8579
+ if (editableRequirement) {
8580
+ properties.push({
8581
+ name: "cdx:pypi:editable",
8582
+ value: "true",
8583
+ });
8584
+ }
7967
8585
  if (properties.length) {
7968
8586
  apkg.properties = properties;
7969
8587
  }
@@ -12664,6 +13282,188 @@ export function parseConanData(conanData) {
12664
13282
  return pkgList;
12665
13283
  }
12666
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
+
12667
13467
  /**
12668
13468
  * Parse Leiningen project.clj data and extract dependency packages.
12669
13469
  *
@@ -18362,6 +19162,92 @@ export function parsePackageJsonName(name) {
18362
19162
  return returnObject;
18363
19163
  }
18364
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
+
18365
19251
  /**
18366
19252
  * Method to add occurrence evidence for components based on import statements. Currently useful for js
18367
19253
  *
@@ -19959,7 +20845,7 @@ export function collectExecutables(basePath, binPaths) {
19959
20845
  if (!binPaths) {
19960
20846
  return [];
19961
20847
  }
19962
- let executables = [];
20848
+ const executablesByResolvedPath = new Map();
19963
20849
  const ignoreList = [
19964
20850
  "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
19965
20851
  "[",
@@ -19975,12 +20861,38 @@ export function collectExecutables(basePath, binPaths) {
19975
20861
  follow: true,
19976
20862
  ignore: ignoreList,
19977
20863
  });
19978
- 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
+ }
19979
20879
  } catch (_err) {
19980
20880
  // ignore
19981
20881
  }
19982
20882
  }
19983
- 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;
19984
20896
  }
19985
20897
 
19986
20898
  /**