@cyclonedx/cdxgen 12.1.4 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +47 -39
  2. package/bin/cdxgen.js +181 -90
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +3 -3
  5. package/bin/sign.js +102 -0
  6. package/bin/validate.js +233 -0
  7. package/bin/verify.js +69 -28
  8. package/data/queries.json +1 -1
  9. package/data/rules/ci-permissions.yaml +186 -0
  10. package/data/rules/dependency-sources.yaml +123 -0
  11. package/data/rules/package-integrity.yaml +135 -0
  12. package/data/rules/vscode-extensions.yaml +228 -0
  13. package/lib/cli/index.js +484 -440
  14. package/lib/evinser/db.js +137 -0
  15. package/lib/{helpers → evinser}/db.poku.js +2 -6
  16. package/lib/evinser/evinser.js +5 -18
  17. package/lib/evinser/swiftsem.js +1 -1
  18. package/lib/helpers/bomSigner.js +312 -0
  19. package/lib/helpers/bomSigner.poku.js +156 -0
  20. package/lib/helpers/caxa.js +1 -1
  21. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  22. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  23. package/lib/helpers/ciParsers/circleCi.js +286 -0
  24. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  25. package/lib/helpers/ciParsers/common.js +24 -0
  26. package/lib/helpers/ciParsers/githubActions.js +636 -0
  27. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  28. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  29. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  30. package/lib/helpers/ciParsers/jenkins.js +181 -0
  31. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  32. package/lib/helpers/depsUtils.js +203 -0
  33. package/lib/helpers/depsUtils.poku.js +150 -0
  34. package/lib/helpers/display.js +429 -14
  35. package/lib/helpers/envcontext.js +23 -8
  36. package/lib/helpers/formulationParsers.js +351 -0
  37. package/lib/helpers/logger.js +14 -0
  38. package/lib/helpers/protobom.js +9 -9
  39. package/lib/helpers/pythonutils.js +305 -0
  40. package/lib/helpers/pythonutils.poku.js +469 -0
  41. package/lib/helpers/utils.js +970 -528
  42. package/lib/helpers/utils.poku.js +139 -256
  43. package/lib/helpers/versutils.js +202 -0
  44. package/lib/helpers/versutils.poku.js +315 -0
  45. package/lib/helpers/vsixutils.js +1061 -0
  46. package/lib/helpers/vsixutils.poku.js +2247 -0
  47. package/lib/managers/binary.js +19 -19
  48. package/lib/managers/docker.js +108 -1
  49. package/lib/managers/oci.js +10 -0
  50. package/lib/managers/piptree.js +4 -10
  51. package/lib/parsers/npmrc.js +92 -0
  52. package/lib/parsers/npmrc.poku.js +528 -0
  53. package/lib/server/openapi.yaml +1 -10
  54. package/lib/server/server.js +58 -16
  55. package/lib/server/server.poku.js +123 -144
  56. package/lib/stages/postgen/annotator.js +1 -1
  57. package/lib/stages/postgen/auditBom.js +197 -0
  58. package/lib/stages/postgen/auditBom.poku.js +378 -0
  59. package/lib/stages/postgen/postgen.js +54 -1
  60. package/lib/stages/postgen/postgen.poku.js +90 -1
  61. package/lib/stages/postgen/ruleEngine.js +369 -0
  62. package/lib/stages/pregen/envAudit.js +299 -0
  63. package/lib/stages/pregen/envAudit.poku.js +572 -0
  64. package/lib/stages/pregen/pregen.js +12 -8
  65. package/lib/third-party/arborist/lib/deepest-nesting-target.js +1 -1
  66. package/lib/third-party/arborist/lib/node.js +3 -3
  67. package/lib/third-party/arborist/lib/shrinkwrap.js +1 -1
  68. package/lib/third-party/arborist/lib/tree-check.js +1 -1
  69. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  70. package/lib/validator/complianceEngine.js +241 -0
  71. package/lib/validator/complianceEngine.poku.js +168 -0
  72. package/lib/validator/complianceRules.js +1610 -0
  73. package/lib/validator/complianceRules.poku.js +328 -0
  74. package/lib/validator/index.js +222 -0
  75. package/lib/validator/index.poku.js +144 -0
  76. package/lib/validator/reporters/annotations.js +121 -0
  77. package/lib/validator/reporters/console.js +149 -0
  78. package/lib/validator/reporters/index.js +41 -0
  79. package/lib/validator/reporters/json.js +37 -0
  80. package/lib/validator/reporters/sarif.js +184 -0
  81. package/lib/validator/reporters.poku.js +150 -0
  82. package/package.json +8 -8
  83. package/types/bin/sign.d.ts +3 -0
  84. package/types/bin/sign.d.ts.map +1 -0
  85. package/types/bin/validate.d.ts +3 -0
  86. package/types/bin/validate.d.ts.map +1 -0
  87. package/types/helpers/utils.d.ts +0 -1
  88. package/types/lib/cli/index.d.ts +49 -52
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/evinser/db.d.ts +34 -0
  91. package/types/lib/evinser/db.d.ts.map +1 -0
  92. package/types/lib/evinser/evinser.d.ts +63 -16
  93. package/types/lib/evinser/evinser.d.ts.map +1 -1
  94. package/types/lib/helpers/bomSigner.d.ts +27 -0
  95. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  96. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  97. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  98. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  99. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  100. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  101. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  102. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  103. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  104. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  105. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  106. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  107. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  108. package/types/lib/helpers/depsUtils.d.ts +21 -0
  109. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  110. package/types/lib/helpers/display.d.ts +111 -11
  111. package/types/lib/helpers/display.d.ts.map +1 -1
  112. package/types/lib/helpers/envcontext.d.ts +19 -7
  113. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  114. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  115. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  116. package/types/lib/helpers/logger.d.ts +15 -1
  117. package/types/lib/helpers/logger.d.ts.map +1 -1
  118. package/types/lib/helpers/protobom.d.ts +2 -2
  119. package/types/lib/helpers/pythonutils.d.ts +18 -0
  120. package/types/lib/helpers/pythonutils.d.ts.map +1 -0
  121. package/types/lib/helpers/utils.d.ts +532 -128
  122. package/types/lib/helpers/utils.d.ts.map +1 -1
  123. package/types/lib/helpers/versutils.d.ts +8 -0
  124. package/types/lib/helpers/versutils.d.ts.map +1 -0
  125. package/types/lib/helpers/vsixutils.d.ts +130 -0
  126. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  127. package/types/lib/managers/docker.d.ts +12 -31
  128. package/types/lib/managers/docker.d.ts.map +1 -1
  129. package/types/lib/managers/oci.d.ts +11 -1
  130. package/types/lib/managers/oci.d.ts.map +1 -1
  131. package/types/lib/managers/piptree.d.ts.map +1 -1
  132. package/types/lib/parsers/npmrc.d.ts +26 -0
  133. package/types/lib/parsers/npmrc.d.ts.map +1 -0
  134. package/types/lib/server/server.d.ts +21 -2
  135. package/types/lib/server/server.d.ts.map +1 -1
  136. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  137. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  138. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  139. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  140. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  141. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  142. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  143. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  144. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  145. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  146. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  147. package/types/lib/validator/complianceEngine.d.ts +66 -0
  148. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  149. package/types/lib/validator/complianceRules.d.ts +70 -0
  150. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  151. package/types/lib/validator/index.d.ts +70 -0
  152. package/types/lib/validator/index.d.ts.map +1 -0
  153. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  154. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  155. package/types/lib/validator/reporters/console.d.ts +30 -0
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  157. package/types/lib/validator/reporters/index.d.ts +21 -0
  158. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  159. package/types/lib/validator/reporters/json.d.ts +11 -0
  160. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  161. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  162. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  163. package/lib/helpers/db.js +0 -162
  164. package/types/helpers/db.d.ts +0 -35
  165. package/types/helpers/db.d.ts.map +0 -1
  166. package/types/lib/helpers/db.d.ts +0 -35
  167. package/types/lib/helpers/db.d.ts.map +0 -1
  168. package/types/lib/helpers/validator.d.ts.map +0 -1
  169. package/types/managers/binary.d.ts +0 -37
  170. package/types/managers/binary.d.ts.map +0 -1
  171. package/types/managers/docker.d.ts +0 -56
  172. package/types/managers/docker.d.ts.map +0 -1
  173. package/types/managers/oci.d.ts +0 -2
  174. package/types/managers/oci.d.ts.map +0 -1
  175. package/types/managers/piptree.d.ts +0 -2
  176. package/types/managers/piptree.d.ts.map +0 -1
  177. package/types/server/server.d.ts +0 -34
  178. package/types/server/server.d.ts.map +0 -1
  179. package/types/stages/postgen/annotator.d.ts +0 -27
  180. package/types/stages/postgen/annotator.d.ts.map +0 -1
  181. package/types/stages/postgen/postgen.d.ts +0 -51
  182. package/types/stages/postgen/postgen.d.ts.map +0 -1
  183. package/types/stages/pregen/pregen.d.ts +0 -59
  184. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -48,15 +48,16 @@ import {
48
48
  satisfies,
49
49
  valid,
50
50
  } from "semver";
51
- import { v4 as uuidv4 } from "uuid";
52
51
  import { xml2js } from "xml-js";
53
- import { parse as _load } from "yaml";
52
+ import { parse as _load, parseAllDocuments } from "yaml";
54
53
 
55
54
  import { getTreeWithPlugin } from "../managers/piptree.js";
56
55
  import { IriValidationStrategy, validateIri } from "../parsers/iri.js";
57
56
  import Arborist from "../third-party/arborist/lib/index.js";
57
+ import { parseWorkflowFile } from "./ciParsers/githubActions.js";
58
58
  import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
59
59
  import { thoughtLog, traceLog } from "./logger.js";
60
+ import { get_python_command_from_env, getVenvMetadata } from "./pythonutils.js";
60
61
 
61
62
  let url = import.meta?.url;
62
63
  if (url && !url.startsWith("file://")) {
@@ -78,12 +79,6 @@ export const isDeno = globalThis.Deno?.version?.deno !== undefined;
78
79
 
79
80
  export const isWin = platform() === "win32";
80
81
  export const isMac = platform() === "darwin";
81
- export let ATOM_DB = join(homedir(), ".local", "share", ".atomdb");
82
- if (isWin) {
83
- ATOM_DB = join(homedir(), "AppData", "Local", ".atomdb");
84
- } else if (isMac) {
85
- ATOM_DB = join(homedir(), "Library", "Application Support", ".atomdb");
86
- }
87
82
 
88
83
  /**
89
84
  * Safely check if a file path exists without crashing due to a lack of permissions
@@ -123,14 +118,68 @@ export function safeMkdirSync(filePath, options) {
123
118
  }
124
119
 
125
120
  export const commandsExecuted = new Set();
121
+ const ALLOW_COMMANDS = (process.env.CDXGEN_ALLOWED_COMMANDS || "").split(",");
126
122
  function isAllowedCommand(command) {
127
123
  if (!process.env.CDXGEN_ALLOWED_COMMANDS) {
128
124
  return true;
129
125
  }
130
- const allow_commands = (process.env.CDXGEN_ALLOWED_COMMANDS || "").split(",");
131
- return allow_commands.includes(command.trim());
126
+ return ALLOW_COMMANDS.includes(command.trim());
127
+ }
128
+
129
+ const ALLOWED_WRAPPERS = new Set(["gradlew", "mvnw"]);
130
+
131
+ /**
132
+ * Check for Windows CWD executable hijack when shell: true is used.
133
+ * cmd.exe searches CWD before PATH, allowing local files to shadow system commands.
134
+ *
135
+ * @param {string} command The executable to spawn
136
+ * @param {Object} options Options forwarded to spawnSync (e.g. cwd, env, shell)
137
+ *
138
+ * @returns {boolean} true if there is a hijack risk. false otherwise.
139
+ */
140
+ function isWindowsShellHijackRisk(command, options) {
141
+ const cwd = options?.cwd;
142
+ const usesShell = options?.shell === true;
143
+ if (!isWin || !usesShell || !cwd || !command) {
144
+ return false;
145
+ }
146
+ if (/[\/\\]/.test(command)) {
147
+ return false;
148
+ }
149
+ const cmdBase = command.toLowerCase();
150
+ if (ALLOWED_WRAPPERS.has(cmdBase)) {
151
+ return false;
152
+ }
153
+ const pathExt = (
154
+ process.env.PATHEXT ||
155
+ ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC"
156
+ )
157
+ .split(";")
158
+ .filter(Boolean);
159
+ const candidates = [
160
+ cmdBase,
161
+ ...pathExt.map((ext) => cmdBase + ext.toLowerCase()),
162
+ ];
163
+ const absCwd = resolve(cwd);
164
+ for (const candidate of candidates) {
165
+ const candidatePath = path.join(absCwd, candidate);
166
+ if (existsSync(candidatePath)) {
167
+ return true;
168
+ }
169
+ }
170
+ return false;
132
171
  }
133
172
 
173
+ /**
174
+ * Safe wrapper around spawnSync that enforces permission checks, injects default
175
+ * options (maxBuffer, encoding, timeout), warns about unsafe Python and pip/uv
176
+ * invocations, and records every executed command in the commandsExecuted set.
177
+ *
178
+ * @param {string} command The executable to spawn
179
+ * @param {string[]} args Arguments to pass to the command
180
+ * @param {Object} options Options forwarded to spawnSync (e.g. cwd, env, shell)
181
+ * @returns {Object} spawnSync result object with status, stdout, stderr, and error fields
182
+ */
134
183
  export function safeSpawnSync(command, args, options) {
135
184
  if (
136
185
  (isSecureMode && process.permission && !process.permission.has("child")) ||
@@ -146,6 +195,25 @@ export function safeSpawnSync(command, args, options) {
146
195
  error: new Error("No execute permission"),
147
196
  };
148
197
  }
198
+ if (isSecureMode) {
199
+ if (isWindowsShellHijackRisk(command, options)) {
200
+ const blockedReason = `${command} matches local file in cwd (Windows shell hijack risk)`;
201
+ console.warn(`\x1b[1;31mSecurity Alert: ${blockedReason}\x1b[0m`);
202
+ return {
203
+ status: 1,
204
+ stdout: undefined,
205
+ stderr: undefined,
206
+ error: new Error(blockedReason),
207
+ };
208
+ }
209
+ if (options?.cwd && options.cwd !== resolve(options.cwd)) {
210
+ if (DEBUG_MODE) {
211
+ console.log(
212
+ "Executing commands with a relative cwd can cause security issues.",
213
+ );
214
+ }
215
+ }
216
+ }
149
217
  if (!options) {
150
218
  options = {};
151
219
  }
@@ -175,6 +243,40 @@ export function safeSpawnSync(command, args, options) {
175
243
  );
176
244
  }
177
245
  }
246
+ let isPyPackageInstall = false;
247
+ if (command.includes("pip") && args?.includes("install")) {
248
+ isPyPackageInstall = true;
249
+ } else if (
250
+ command.includes("python") &&
251
+ args?.includes("pip") &&
252
+ args?.includes("install")
253
+ ) {
254
+ isPyPackageInstall = true;
255
+ } else if (
256
+ command.includes("uv") &&
257
+ args?.includes("pip") &&
258
+ args?.includes("install")
259
+ ) {
260
+ isPyPackageInstall = true;
261
+ }
262
+ if (isPyPackageInstall) {
263
+ const hasOnlyBinary = args?.some(
264
+ (arg) => arg === "--only-binary" || arg.startsWith("--only-binary="),
265
+ );
266
+ if (!hasOnlyBinary) {
267
+ if (isSecureMode) {
268
+ console.warn(
269
+ "\x1b[1;31mSecurity Alert: pip/uv install invoked without '--only-binary' argument in secure mode. This is a bug in cdxgen and introduces Arbitrary Code Execution (ACE) risks. Please report with an example repo here https://github.com/cdxgen/cdxgen/issues.\x1b[0m",
270
+ );
271
+ } else if (process.env?.CDXGEN_IN_CONTAINER === "true") {
272
+ console.log("Running pip/uv install without '--only-binary' argument.");
273
+ } else {
274
+ console.warn(
275
+ "\x1b[1;35mNotice: pip/uv install invoked without '--only-binary'. This allows executing untrusted setup.py scripts. Only run cdxgen in trusted directories.\x1b",
276
+ );
277
+ }
278
+ }
279
+ }
178
280
  traceLog("spawn", { command, args, ...options });
179
281
  commandsExecuted.add(command);
180
282
  // Fix for DEP0190 warning
@@ -268,6 +370,12 @@ export const PREFER_MAVEN_DEPS_TREE = !["false", "0"].includes(
268
370
  process.env?.PREFER_MAVEN_DEPS_TREE,
269
371
  );
270
372
 
373
+ /**
374
+ * Determines whether license information should be fetched from remote sources,
375
+ * based on the FETCH_LICENSE environment variable.
376
+ *
377
+ * @returns {boolean} True if the FETCH_LICENSE env var is set to "true" or "1"
378
+ */
271
379
  export function shouldFetchLicense() {
272
380
  return (
273
381
  process.env.FETCH_LICENSE &&
@@ -275,6 +383,12 @@ export function shouldFetchLicense() {
275
383
  );
276
384
  }
277
385
 
386
+ /**
387
+ * Determines whether VCS (version control system) information should be fetched
388
+ * for Go packages, based on the GO_FETCH_VCS environment variable.
389
+ *
390
+ * @returns {boolean} True if the GO_FETCH_VCS env var is set to "true" or "1"
391
+ */
278
392
  export function shouldFetchVCS() {
279
393
  return (
280
394
  process.env.GO_FETCH_VCS && ["true", "1"].includes(process.env.GO_FETCH_VCS)
@@ -301,6 +415,12 @@ const MAX_LICENSE_ID_LENGTH = 100;
301
415
 
302
416
  export const JAVA_CMD = getJavaCommand();
303
417
 
418
+ /**
419
+ * Returns the Java executable command to use, resolved in priority order:
420
+ * JAVA_CMD env var > JAVA_HOME/bin/java > "java".
421
+ *
422
+ * @returns {string} Path or name of the Java executable
423
+ */
304
424
  export function getJavaCommand() {
305
425
  let javaCmd = "java";
306
426
  if (process.env.JAVA_CMD) {
@@ -317,6 +437,12 @@ export function getJavaCommand() {
317
437
 
318
438
  export const PYTHON_CMD = getPythonCommand();
319
439
 
440
+ /**
441
+ * Returns the Python executable command to use, resolved in priority order:
442
+ * PYTHON_CMD env var > CONDA_PYTHON_EXE env var > "python".
443
+ *
444
+ * @returns {string} Path or name of the Python executable
445
+ */
320
446
  export function getPythonCommand() {
321
447
  let pythonCmd = "python";
322
448
  if (process.env.PYTHON_CMD) {
@@ -451,7 +577,6 @@ export const PROJECT_TYPE_ALIASES = {
451
577
  "typescript",
452
578
  "ts",
453
579
  "tsx",
454
- "vsix",
455
580
  "yarn",
456
581
  "rush",
457
582
  ],
@@ -471,7 +596,11 @@ export const PROJECT_TYPE_ALIASES = {
471
596
  "poetry",
472
597
  "uv",
473
598
  "pdm",
599
+ "rye",
474
600
  "hatch",
601
+ "conda",
602
+ "miniconda",
603
+ "pyenv",
475
604
  ],
476
605
  go: ["go", "golang", "gomod", "gopkg"],
477
606
  rust: ["rust", "rust-lang", "cargo"],
@@ -542,6 +671,14 @@ export const PROJECT_TYPE_ALIASES = {
542
671
  scala: ["scala", "scala3", "sbt", "mill"],
543
672
  nix: ["nix", "nixos", "flake"],
544
673
  caxa: ["caxa"],
674
+ "vscode-extension": [
675
+ "vscode-extension",
676
+ "vsix",
677
+ "vscode",
678
+ "openvsx",
679
+ "vscode-extensions",
680
+ "ide-extensions",
681
+ ],
545
682
  };
546
683
 
547
684
  // Package manager aliases
@@ -1119,6 +1256,13 @@ export function readLicenseText(licenseFilepath, licenseContentType) {
1119
1256
  return null;
1120
1257
  }
1121
1258
 
1259
+ /**
1260
+ * Fetches license information for a list of Swift packages by querying the
1261
+ * GitHub repository license API for packages hosted on github.com.
1262
+ *
1263
+ * @param {Object[]} pkgList List of Swift package objects with optional repository.url fields
1264
+ * @returns {Promise<Object[]>} Resolved list of package objects, each augmented with a license field where available
1265
+ */
1122
1266
  export async function getSwiftPackageMetadata(pkgList) {
1123
1267
  const cdepList = [];
1124
1268
  for (const p of pkgList) {
@@ -1203,8 +1347,13 @@ export async function getNpmMetadata(pkgList) {
1203
1347
  *
1204
1348
  * @param {string} pkgJsonFile package.json file
1205
1349
  * @param {boolean} simple Return a simpler representation of the component by skipping extended attributes and license fetch.
1350
+ * @param {boolean} securityProps Collect security-related properties
1206
1351
  */
1207
- export async function parsePkgJson(pkgJsonFile, simple = false) {
1352
+ export async function parsePkgJson(
1353
+ pkgJsonFile,
1354
+ simple = false,
1355
+ securityProps = false,
1356
+ ) {
1208
1357
  const pkgList = [];
1209
1358
  if (safeExistsSync(pkgJsonFile)) {
1210
1359
  try {
@@ -1267,6 +1416,107 @@ export async function parsePkgJson(pkgJsonFile, simple = false) {
1267
1416
  },
1268
1417
  };
1269
1418
  }
1419
+ if (securityProps) {
1420
+ if (!apkg.properties) {
1421
+ apkg.properties = [];
1422
+ }
1423
+ // Track executable binaries (potential code execution vectors)
1424
+ if (pkgData.bin) {
1425
+ const binValue =
1426
+ typeof pkgData.bin === "object"
1427
+ ? Object.keys(pkgData.bin).join(", ")
1428
+ : pkgData.bin;
1429
+ apkg.properties.push({
1430
+ name: "cdx:npm:bin",
1431
+ value: binValue,
1432
+ });
1433
+ apkg.properties.push({
1434
+ name: "cdx:npm:has_binary",
1435
+ value: "true",
1436
+ });
1437
+ }
1438
+ // Track lifecycle scripts (preinstall, postinstall, etc. - code execution risk)
1439
+ if (pkgData.scripts && Object.keys(pkgData.scripts).length) {
1440
+ const scriptNames = Object.keys(pkgData.scripts).join(", ");
1441
+ apkg.properties.push({
1442
+ name: "cdx:npm:scripts",
1443
+ value: scriptNames,
1444
+ });
1445
+ // Flag high-risk scripts specifically
1446
+ const riskyScripts = [
1447
+ "preinstall",
1448
+ "install",
1449
+ "postinstall",
1450
+ "prepublish",
1451
+ "prepare",
1452
+ ].filter((script) => pkgData.scripts[script]);
1453
+ if (riskyScripts.length) {
1454
+ apkg.properties.push({
1455
+ name: "cdx:npm:risky_scripts",
1456
+ value: riskyScripts.join(", "),
1457
+ });
1458
+ }
1459
+ }
1460
+ // Track platform/architecture constraints
1461
+ if (pkgData.cpu && Array.isArray(pkgData.cpu) && pkgData.cpu.length) {
1462
+ apkg.properties.push({
1463
+ name: "cdx:npm:cpu",
1464
+ value: pkgData.cpu.join(", "),
1465
+ });
1466
+ }
1467
+ if (pkgData.os && Array.isArray(pkgData.os) && pkgData.os.length) {
1468
+ apkg.properties.push({
1469
+ name: "cdx:npm:os",
1470
+ value: pkgData.os.join(", "),
1471
+ });
1472
+ }
1473
+ if (
1474
+ pkgData.libc &&
1475
+ Array.isArray(pkgData.libc) &&
1476
+ pkgData.libc.length
1477
+ ) {
1478
+ apkg.properties.push({
1479
+ name: "cdx:npm:libc",
1480
+ value: pkgData.libc.join(", "),
1481
+ });
1482
+ }
1483
+ // Track deprecation notices
1484
+ if (pkgData.deprecated) {
1485
+ apkg.properties.push({
1486
+ name: "cdx:npm:deprecation_notice",
1487
+ value: pkgData.deprecated,
1488
+ });
1489
+ }
1490
+ // Track if package uses node-gyp (native C/C++ addons = higher risk)
1491
+ if (
1492
+ pkgData.gypfile === true ||
1493
+ pkgData.files?.some((f) => f.endsWith(".gyp") || f.endsWith(".gypi"))
1494
+ ) {
1495
+ apkg.properties.push({
1496
+ name: "cdx:npm:gypfile",
1497
+ value: "true",
1498
+ });
1499
+ apkg.properties.push({
1500
+ name: "cdx:npm:native_addon",
1501
+ value: "true",
1502
+ });
1503
+ const nativeDeps = [
1504
+ "nan",
1505
+ "node-addon-api",
1506
+ "bindings",
1507
+ "node-gyp-build",
1508
+ ];
1509
+ const foundNativeDeps = Object.keys(
1510
+ pkgData.dependencies || {},
1511
+ ).filter((dep) => nativeDeps.includes(dep));
1512
+ if (foundNativeDeps.length) {
1513
+ apkg.properties.push({
1514
+ name: "cdx:npm:native_deps",
1515
+ value: foundNativeDeps.join(", "),
1516
+ });
1517
+ }
1518
+ }
1519
+ }
1270
1520
  pkgList.push(apkg);
1271
1521
  } catch (_err) {
1272
1522
  // continue regardless of error
@@ -1411,7 +1661,10 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1411
1661
  name: "ResolvedUrl",
1412
1662
  value: node.resolved,
1413
1663
  });
1414
- pkg.distribution = { url: node.resolved };
1664
+ pkg.externalReferences.push({
1665
+ type: "distribution",
1666
+ url: node.resolved,
1667
+ });
1415
1668
  }
1416
1669
  }
1417
1670
  if (node.location) {
@@ -1692,7 +1945,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1692
1945
  if (!targetVersion || !targetName) {
1693
1946
  if (pkgSpecVersionCache[`${edge.name}-${edge.spec}`]) {
1694
1947
  targetVersion = pkgSpecVersionCache[`${edge.name}-${edge.spec}`];
1695
- targetName = edge.name;
1948
+ targetName = edge.name.replace(/-cjs$/, "");
1696
1949
  }
1697
1950
  }
1698
1951
  }
@@ -2735,6 +2988,13 @@ function findMatchingWorkspace(workspacePackages, packageName) {
2735
2988
  );
2736
2989
  }
2737
2990
 
2991
+ /**
2992
+ * Parses the workspaces field from a package.json file and returns the list of
2993
+ * workspace glob patterns. Handles both array and object (with packages key) formats.
2994
+ *
2995
+ * @param {string} packageJsonFile Path to the package.json file to parse
2996
+ * @returns {Object} Object with a packages array of workspace glob patterns, or an empty object on error
2997
+ */
2738
2998
  export function parseYarnWorkspace(packageJsonFile) {
2739
2999
  try {
2740
3000
  const packageData = JSON.parse(readFileSync(packageJsonFile, "utf-8"));
@@ -2830,45 +3090,31 @@ export function findPnpmPackagePath(baseDir, packageName, version) {
2830
3090
  * @returns {Array} Enhanced package list
2831
3091
  */
2832
3092
  export async function pnpmMetadata(pkgList, lockFilePath) {
2833
- if (!pkgList || !pkgList.length || !lockFilePath) {
3093
+ if (!pkgList?.length || !lockFilePath) {
2834
3094
  return pkgList;
2835
3095
  }
2836
-
2837
3096
  const baseDir = dirname(lockFilePath);
2838
3097
  const nodeModulesDir = join(baseDir, "node_modules");
2839
-
2840
- // Only proceed if node_modules exists
2841
3098
  if (!safeExistsSync(nodeModulesDir)) {
2842
3099
  return pkgList;
2843
3100
  }
2844
-
2845
3101
  if (DEBUG_MODE) {
2846
3102
  console.log(
2847
3103
  `Metadata for ${pkgList.length} pnpm packages using local node_modules at ${nodeModulesDir}`,
2848
3104
  );
2849
3105
  }
2850
-
2851
3106
  let enhancedCount = 0;
2852
3107
  for (const pkg of pkgList) {
2853
- // Skip if package already has complete metadata
2854
- if (pkg.description && pkg.author && pkg.license) {
2855
- continue;
2856
- }
2857
-
2858
- // Find the package path in node_modules
2859
3108
  const packagePath = findPnpmPackagePath(baseDir, pkg.name, pkg.version);
2860
3109
  if (!packagePath) {
2861
3110
  continue;
2862
3111
  }
2863
-
2864
3112
  const packageJsonPath = join(packagePath, "package.json");
2865
3113
  if (!safeExistsSync(packageJsonPath)) {
2866
3114
  continue;
2867
3115
  }
2868
-
2869
3116
  try {
2870
- // Parse the local package.json to get metadata
2871
- const localPkgList = await parsePkgJson(packageJsonPath, true);
3117
+ const localPkgList = await parsePkgJson(packageJsonPath, true, true);
2872
3118
  if (localPkgList && localPkgList.length === 1) {
2873
3119
  const localMetadata = localPkgList[0];
2874
3120
  if (localMetadata && Object.keys(localMetadata).length) {
@@ -2887,16 +3133,27 @@ export async function pnpmMetadata(pkgList, lockFilePath) {
2887
3133
  if (!pkg.repository && localMetadata.repository) {
2888
3134
  pkg.repository = localMetadata.repository;
2889
3135
  }
2890
-
2891
- // Add a property to track that we enhanced from local node_modules
2892
3136
  if (!pkg.properties) {
2893
3137
  pkg.properties = [];
2894
3138
  }
3139
+ if (localMetadata?.properties?.length) {
3140
+ const seenProperties = new Set(
3141
+ pkg.properties.map(
3142
+ (prop) => `${String(prop?.name)}\u0000${String(prop?.value)}`,
3143
+ ),
3144
+ );
3145
+ for (const prop of localMetadata.properties) {
3146
+ const propertyKey = `${String(prop?.name)}\u0000${String(prop?.value)}`;
3147
+ if (!seenProperties.has(propertyKey)) {
3148
+ pkg.properties.push(prop);
3149
+ seenProperties.add(propertyKey);
3150
+ }
3151
+ }
3152
+ }
2895
3153
  pkg.properties.push({
2896
3154
  name: "LocalNodeModulesPath",
2897
3155
  value: packagePath,
2898
3156
  });
2899
-
2900
3157
  enhancedCount++;
2901
3158
  }
2902
3159
  }
@@ -2910,13 +3167,11 @@ export async function pnpmMetadata(pkgList, lockFilePath) {
2910
3167
  }
2911
3168
  }
2912
3169
  }
2913
-
2914
3170
  if (DEBUG_MODE && enhancedCount > 0) {
2915
3171
  console.log(
2916
3172
  `Enhanced metadata for ${enhancedCount} packages from local node_modules`,
2917
3173
  );
2918
3174
  }
2919
-
2920
3175
  return pkgList;
2921
3176
  }
2922
3177
 
@@ -2979,10 +3234,18 @@ export async function parsePnpmLock(
2979
3234
  }
2980
3235
  if (safeExistsSync(pnpmLock)) {
2981
3236
  const lockData = readFileSync(pnpmLock, "utf8");
2982
- const yamlObj = _load(lockData);
3237
+ let yamlObj = parseAllDocuments(lockData);
2983
3238
  if (!yamlObj) {
2984
3239
  return {};
2985
3240
  }
3241
+ if (Array.isArray(yamlObj)) {
3242
+ try {
3243
+ yamlObj = yamlObj[yamlObj.length - 1].toJS();
3244
+ } catch (_e) {
3245
+ console.log(`Unable to parse the pnpm lock file ${pnpmLock}.`);
3246
+ return {};
3247
+ }
3248
+ }
2986
3249
  lockfileVersion = yamlObj.lockfileVersion;
2987
3250
  try {
2988
3251
  lockfileVersion = Number.parseFloat(lockfileVersion, 10);
@@ -3281,6 +3544,7 @@ export async function parsePnpmLock(
3281
3544
  packages[fullName]?.resolution ||
3282
3545
  snapshots[fullName]?.resolution;
3283
3546
  const integrity = resolution?.integrity;
3547
+ const tarball = resolution?.tarball;
3284
3548
  const cpu =
3285
3549
  packages[pkgKeys[k]]?.cpu ||
3286
3550
  snapshots[pkgKeys[k]]?.cpu ||
@@ -3499,10 +3763,10 @@ export async function parsePnpmLock(
3499
3763
  value: pnpmLock,
3500
3764
  },
3501
3765
  ];
3502
- if (hasBin || os || cpu || libc) {
3766
+ if (hasBin) {
3503
3767
  properties.push({
3504
3768
  name: "cdx:npm:has_binary",
3505
- value: `${hasBin}`,
3769
+ value: "true",
3506
3770
  });
3507
3771
  }
3508
3772
  if (deprecatedMessage) {
@@ -3515,7 +3779,7 @@ export async function parsePnpmLock(
3515
3779
  Object.entries(binary_metadata).forEach(([key, value]) => {
3516
3780
  if (!value) return;
3517
3781
  properties.push({
3518
- name: `cdx:pnpm:${key}`,
3782
+ name: `cdx:npm:${key}`,
3519
3783
  value: Array.isArray(value) ? value.join(", ") : value,
3520
3784
  });
3521
3785
  });
@@ -3613,6 +3877,14 @@ export async function parsePnpmLock(
3613
3877
  },
3614
3878
  },
3615
3879
  };
3880
+ if (tarball) {
3881
+ thePkg.externalReferences = [
3882
+ {
3883
+ type: "distribution",
3884
+ url: tarball,
3885
+ },
3886
+ ];
3887
+ }
3616
3888
  // Don't add internal workspace packages to the components list
3617
3889
  if (thePkg.type !== "application") {
3618
3890
  pkgList.push(thePkg);
@@ -4646,6 +4918,15 @@ export function parseLeinDep(rawOutput) {
4646
4918
  return [];
4647
4919
  }
4648
4920
 
4921
+ /**
4922
+ * Recursively walks a parsed EDN map node produced by the Leiningen dependency
4923
+ * tree and collects unique dependency entries into the deps array.
4924
+ *
4925
+ * @param {Object} node Parsed EDN node (expected to have a "map" property)
4926
+ * @param {Object} keys_cache Cache object used to deduplicate entries by group-name-version key
4927
+ * @param {Object[]} deps Accumulator array of dependency objects with group, name, and version fields
4928
+ * @returns {Object[]} The populated deps array
4929
+ */
4649
4930
  export function parseLeinMap(node, keys_cache, deps) {
4650
4931
  if (node["map"]) {
4651
4932
  for (const n of node["map"]) {
@@ -5096,7 +5377,7 @@ export async function getMvnMetadata(
5096
5377
  const ANDROID_MAVEN_URL =
5097
5378
  process.env.ANDROID_MAVEN_URL || "https://maven.google.com/";
5098
5379
  const cdepList = [];
5099
- if (!pkgList || !pkgList.length) {
5380
+ if (!pkgList?.length) {
5100
5381
  return pkgList;
5101
5382
  }
5102
5383
  if (DEBUG_MODE && shouldFetchLicense()) {
@@ -5390,7 +5671,7 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
5390
5671
  const PYPI_URL = process.env.PYPI_URL || "https://pypi.org/pypi/";
5391
5672
  const cdepList = [];
5392
5673
  for (const p of pkgList) {
5393
- if (!p || !p.name) {
5674
+ if (!p?.name) {
5394
5675
  continue;
5395
5676
  }
5396
5677
  try {
@@ -5474,7 +5755,7 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
5474
5755
  }
5475
5756
  }
5476
5757
  // Use the latest version if none specified
5477
- if (!p.version || !p.version.trim().length) {
5758
+ if (!p.version?.trim().length) {
5478
5759
  let versionSpecifiers;
5479
5760
  if (p.properties?.length) {
5480
5761
  for (const pprop of p.properties) {
@@ -5629,6 +5910,9 @@ export function parseBdistMetadata(mDataFile, rawMetadata = undefined) {
5629
5910
  externalReferences: [],
5630
5911
  properties: [],
5631
5912
  };
5913
+ if (mDataFile) {
5914
+ pkg.properties.push({ name: "SrcFile", value: mDataFile });
5915
+ }
5632
5916
  const lines = mData.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
5633
5917
  let isBody = false;
5634
5918
  for (const line of lines) {
@@ -6467,7 +6751,8 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6467
6751
  },
6468
6752
  }
6469
6753
  : undefined;
6470
- const lines = reqData.replace(/\r/g, "").replace(/\\$/gm, "").split("\n");
6754
+ const normalizedData = reqData.replace(/\r/g, "").replace(/\\\n/g, " ");
6755
+ const lines = normalizedData.split("\n");
6471
6756
  for (const line of lines) {
6472
6757
  let l = line.trim();
6473
6758
  if (l.includes("# Basic requirements")) {
@@ -6495,7 +6780,25 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6495
6780
  },
6496
6781
  ]
6497
6782
  : [];
6498
-
6783
+ const hashes = [];
6784
+ const hashRegex = /--hash=([a-zA-Z0-9\-]+):([a-fA-F0-9]+)/g;
6785
+ let hashMatch;
6786
+ while ((hashMatch = hashRegex.exec(l)) !== null) {
6787
+ let alg = hashMatch[1].toUpperCase();
6788
+ if (alg === "SHA256") alg = "SHA-256";
6789
+ else if (alg === "SHA384") alg = "SHA-384";
6790
+ else if (alg === "SHA512") alg = "SHA-512";
6791
+ else if (alg === "SHA1") alg = "SHA-1";
6792
+ hashes.push({
6793
+ alg: alg,
6794
+ content: hashMatch[2],
6795
+ });
6796
+ }
6797
+ // Strip the hash flags and any residual backslashes
6798
+ l = l
6799
+ .replace(/--hash=[a-zA-Z0-9\-]+:[a-fA-F0-9]+/g, "")
6800
+ .replace(/\\/g, "")
6801
+ .trim();
6499
6802
  // Handle markers
6500
6803
  let markers = null;
6501
6804
  let structuredMarkers = null;
@@ -6535,6 +6838,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6535
6838
  scope: compScope,
6536
6839
  evidence,
6537
6840
  };
6841
+ if (hashes.length > 0) {
6842
+ apkg.hashes = hashes;
6843
+ }
6538
6844
  if (comment) {
6539
6845
  apkg.licenses = comment
6540
6846
  .split("/")
@@ -7238,6 +7544,16 @@ async function getGoPkgVCSUrl(group, name) {
7238
7544
  return undefined;
7239
7545
  }
7240
7546
 
7547
+ /**
7548
+ * Builds a Go package component object containing purl, bom-ref, integrity hash,
7549
+ * and optionally license and VCS external reference information.
7550
+ *
7551
+ * @param {string} group Package group (module path prefix, may be empty)
7552
+ * @param {string} name Package name (full module path when group is empty)
7553
+ * @param {string} version Package version string
7554
+ * @param {string} hash Integrity hash (e.g. "sha256-…"), used as _integrity
7555
+ * @returns {Promise<Object>} Component object ready for inclusion in a BOM package list
7556
+ */
7241
7557
  export async function getGoPkgComponent(group, name, version, hash) {
7242
7558
  let license;
7243
7559
  if (shouldFetchLicense()) {
@@ -7421,6 +7737,15 @@ export async function parseGoModData(goModData, gosumMap) {
7421
7737
  };
7422
7738
  }
7423
7739
 
7740
+ /**
7741
+ * Parses a Go modules text file (e.g. vendor/modules.txt) and returns a list of
7742
+ * Go package components. Cross-references the go.sum map for integrity hashes and
7743
+ * sets scope and confidence based on hash availability.
7744
+ *
7745
+ * @param {string} txtFile Path to the modules.txt file
7746
+ * @param {Object} gosumMap Map of "module@version" keys to sha256 hash values from go.sum
7747
+ * @returns {Promise<Object[]>} List of Go package component objects with evidence
7748
+ */
7424
7749
  export async function parseGoModulesTxt(txtFile, gosumMap) {
7425
7750
  const pkgList = [];
7426
7751
  const txtData = readFileSync(txtFile, { encoding: "utf-8" });
@@ -7824,6 +8149,14 @@ export async function parseGosumData(gosumData) {
7824
8149
  return pkgList;
7825
8150
  }
7826
8151
 
8152
+ /**
8153
+ * Parses the contents of a Gopkg.lock or Gopkg.toml file (dep tool format) and
8154
+ * returns a list of Go package components. Optionally fetches license information
8155
+ * for each package when FETCH_LICENSE is enabled.
8156
+ *
8157
+ * @param {string} gopkgData Raw string contents of the Gopkg lock/toml file
8158
+ * @returns {Promise<Object[]>} List of Go package component objects
8159
+ */
7827
8160
  export async function parseGopkgData(gopkgData) {
7828
8161
  const pkgList = [];
7829
8162
  if (!gopkgData) {
@@ -7873,6 +8206,13 @@ export async function parseGopkgData(gopkgData) {
7873
8206
  return pkgList;
7874
8207
  }
7875
8208
 
8209
+ /**
8210
+ * Parses the output of `go version -m` (build info) and returns a list of Go
8211
+ * package components for each "dep" line, including name, version, and integrity hash.
8212
+ *
8213
+ * @param {string} buildInfoData Raw string output from `go version -m`
8214
+ * @returns {Promise<Object[]>} List of Go package component objects
8215
+ */
7876
8216
  export async function parseGoVersionData(buildInfoData) {
7877
8217
  const pkgList = [];
7878
8218
  if (!buildInfoData) {
@@ -9199,6 +9539,13 @@ export async function parseCargoData(
9199
9539
  return pkgList;
9200
9540
  }
9201
9541
 
9542
+ /**
9543
+ * Parses a Cargo.lock file's TOML data and returns a flat dependency graph as an
9544
+ * array of objects mapping each package purl to the purls it directly depends on.
9545
+ *
9546
+ * @param {string} cargoLockData Raw TOML string contents of a Cargo.lock file
9547
+ * @returns {Object[]} Array of dependency relationship objects with ref and dependsOn fields
9548
+ */
9202
9549
  export function parseCargoDependencyData(cargoLockData) {
9203
9550
  const purlFromPackageInfo = (pkg) =>
9204
9551
  decodeURIComponent(
@@ -9258,6 +9605,14 @@ export function parseCargoDependencyData(cargoLockData) {
9258
9605
  return result;
9259
9606
  }
9260
9607
 
9608
+ /**
9609
+ * Parses tab-separated cargo-auditable binary metadata output and returns a list
9610
+ * of Rust package components. Optionally fetches crates.io metadata when
9611
+ * FETCH_LICENSE is enabled.
9612
+ *
9613
+ * @param {string} cargoData Tab-separated string output from cargo-auditable or similar tool
9614
+ * @returns {Promise<Object[]>} List of Rust package component objects with group, name, and version
9615
+ */
9261
9616
  export async function parseCargoAuditableData(cargoData) {
9262
9617
  const pkgList = [];
9263
9618
  if (!cargoData) {
@@ -9360,6 +9715,13 @@ export async function parsePubLockData(pubLockData, lockFile) {
9360
9715
  return { rootList, pkgList };
9361
9716
  }
9362
9717
 
9718
+ /**
9719
+ * Parses a Dart pub package's pubspec.yaml content and returns a list containing
9720
+ * a single component object with name, description, version, homepage, and purl.
9721
+ *
9722
+ * @param {string} pubYamlData Raw YAML string contents of a pubspec.yaml file
9723
+ * @returns {Object[]} List containing a single Dart package component object
9724
+ */
9363
9725
  export function parsePubYamlData(pubYamlData) {
9364
9726
  const pkgList = [];
9365
9727
  let yamlObj;
@@ -9386,6 +9748,14 @@ export function parsePubYamlData(pubYamlData) {
9386
9748
  return pkgList;
9387
9749
  }
9388
9750
 
9751
+ /**
9752
+ * Parses Helm chart YAML data (Chart.yaml or repository index.yaml) and returns
9753
+ * a list of Helm chart component objects including the chart itself and any
9754
+ * declared dependencies or index entries.
9755
+ *
9756
+ * @param {string} helmData Raw YAML string contents of a Helm Chart.yaml or index.yaml file
9757
+ * @returns {Object[]} List of Helm chart component objects with name, version, and optional homepage/repository
9758
+ */
9389
9759
  export function parseHelmYamlData(helmData) {
9390
9760
  const pkgList = [];
9391
9761
  let yamlObj;
@@ -9451,6 +9821,17 @@ export function parseHelmYamlData(helmData) {
9451
9821
  return pkgList;
9452
9822
  }
9453
9823
 
9824
+ /**
9825
+ * Recursively walks a parsed YAML/JSON object structure to find container image
9826
+ * references stored under common keys (image, repository, dockerImage, etc.) and
9827
+ * appends discovered image and service entries to pkgList while tracking seen
9828
+ * images in imgList to avoid duplicates.
9829
+ *
9830
+ * @param {Object|Array|string} keyValueObj The object, array, or string node to inspect
9831
+ * @param {Object[]} pkgList Accumulator array that receives {image} and {service} entries
9832
+ * @param {string[]} imgList Accumulator array of image name strings already seen
9833
+ * @returns {string[]} The updated imgList
9834
+ */
9454
9835
  export function recurseImageNameLookup(keyValueObj, pkgList, imgList) {
9455
9836
  if (typeof keyValueObj === "string" || keyValueObj instanceof String) {
9456
9837
  return imgList;
@@ -9536,6 +9917,14 @@ function substituteBuildArgs(statement, buildArgs) {
9536
9917
  return statement;
9537
9918
  }
9538
9919
 
9920
+ /**
9921
+ * Parses the contents of a Dockerfile or Containerfile and returns a list of
9922
+ * base image objects referenced by FROM instructions, substituting ARG default
9923
+ * values where possible and skipping multi-stage build alias references.
9924
+ *
9925
+ * @param {string} fileContents Raw string contents of the Dockerfile/Containerfile
9926
+ * @returns {Object[]} Array of objects with an image property for each unique base image
9927
+ */
9539
9928
  export function parseContainerFile(fileContents) {
9540
9929
  const buildArgs = new Map();
9541
9930
  const imagesSet = new Set();
@@ -9603,6 +9992,13 @@ export function parseContainerFile(fileContents) {
9603
9992
  });
9604
9993
  }
9605
9994
 
9995
+ /**
9996
+ * Parses a Bitbucket Pipelines YAML file and extracts all Docker image references
9997
+ * used as build environments and pipe references (docker:// pipes are normalized).
9998
+ *
9999
+ * @param {string} fileContents Raw string contents of the bitbucket-pipelines.yml file
10000
+ * @returns {Object[]} Array of objects with an image property for each referenced image or pipe
10001
+ */
9606
10002
  export function parseBitbucketPipelinesFile(fileContents) {
9607
10003
  const imgList = [];
9608
10004
 
@@ -9664,6 +10060,14 @@ export function parseBitbucketPipelinesFile(fileContents) {
9664
10060
  return imgList;
9665
10061
  }
9666
10062
 
10063
+ /**
10064
+ * Parses container specification data such as Docker Compose files, Kubernetes
10065
+ * manifests, Tekton tasks, Skaffold configs, or Kustomize overlays (YAML, possibly
10066
+ * multi-document) and returns a list of image, service, and OCI spec entries.
10067
+ *
10068
+ * @param {string} dcData Raw YAML string contents of the container spec file
10069
+ * @returns {Object[]} Array of objects with image, service, or ociSpec properties
10070
+ */
9667
10071
  export function parseContainerSpecData(dcData) {
9668
10072
  const pkgList = [];
9669
10073
  const imgList = [];
@@ -9731,6 +10135,14 @@ export function parseContainerSpecData(dcData) {
9731
10135
  return pkgList;
9732
10136
  }
9733
10137
 
10138
+ /**
10139
+ * Identifies the data flow direction of a Privado processing object based on its
10140
+ * sinkId value: "write" sinks map to "inbound", "read" sinks to "outbound", and
10141
+ * HTTP/gRPC sinks to "bi-directional".
10142
+ *
10143
+ * @param {Object} processingObj Privado processing object, expected to have a sinkId property
10144
+ * @returns {string} Flow direction string: "inbound", "outbound", "bi-directional", or "unknown"
10145
+ */
9734
10146
  export function identifyFlow(processingObj) {
9735
10147
  let flow = "unknown";
9736
10148
  if (processingObj.sinkId) {
@@ -9757,6 +10169,14 @@ function convertProcessing(processing_list) {
9757
10169
  return data_list;
9758
10170
  }
9759
10171
 
10172
+ /**
10173
+ * Parses a Privado data flow JSON file and returns a list of service objects
10174
+ * enriched with data classifications, endpoints, trust-boundary flag, violations,
10175
+ * and git metadata properties extracted from the scan result.
10176
+ *
10177
+ * @param {string} f Path to the Privado scan result JSON file
10178
+ * @returns {Object[]} List of service component objects suitable for a SaaSBOM
10179
+ */
9760
10180
  export function parsePrivadoFile(f) {
9761
10181
  const pData = readFileSync(f, { encoding: "utf-8" });
9762
10182
  const servlist = [];
@@ -9838,6 +10258,15 @@ export function parsePrivadoFile(f) {
9838
10258
  return servlist;
9839
10259
  }
9840
10260
 
10261
+ /**
10262
+ * Parses an OpenAPI specification (JSON or YAML string) and returns a list
10263
+ * containing a single service object with name, version, endpoints, and
10264
+ * authentication flag derived from the spec's info, servers, paths, and
10265
+ * securitySchemes sections.
10266
+ *
10267
+ * @param {string} oaData Raw JSON or YAML string contents of an OpenAPI specification
10268
+ * @returns {Object[]} List containing a single service component object
10269
+ */
9841
10270
  export function parseOpenapiSpecData(oaData) {
9842
10271
  const servlist = [];
9843
10272
  if (!oaData) {
@@ -9890,6 +10319,13 @@ export function parseOpenapiSpecData(oaData) {
9890
10319
  return servlist;
9891
10320
  }
9892
10321
 
10322
+ /**
10323
+ * Parses Haskell Cabal freeze file content and extracts package name and version
10324
+ * pairs from constraint lines (lines containing " ==").
10325
+ *
10326
+ * @param {string} cabalData Raw string contents of a Cabal freeze file
10327
+ * @returns {Object[]} List of package objects with name and version fields
10328
+ */
9893
10329
  export function parseCabalData(cabalData) {
9894
10330
  const pkgList = [];
9895
10331
  if (!cabalData) {
@@ -9918,6 +10354,13 @@ export function parseCabalData(cabalData) {
9918
10354
  return pkgList;
9919
10355
  }
9920
10356
 
10357
+ /**
10358
+ * Parses an Elixir mix.lock file and extracts Hex package name and version pairs
10359
+ * from lines containing ":hex".
10360
+ *
10361
+ * @param {string} mixData Raw string contents of a mix.lock file
10362
+ * @returns {Object[]} List of package objects with name and version fields
10363
+ */
9921
10364
  export function parseMixLockData(mixData) {
9922
10365
  const pkgList = [];
9923
10366
  if (!mixData) {
@@ -9945,404 +10388,55 @@ export function parseMixLockData(mixData) {
9945
10388
  return pkgList;
9946
10389
  }
9947
10390
 
10391
+ /**
10392
+ * Parses a GitHub Actions workflow YAML file and returns a list of action
10393
+ * components for each step that uses an external action (steps with a "uses"
10394
+ * field). Each component captures the action name, group, version/commit SHA,
10395
+ * version pinning type, job context (runner, permissions, environment), and
10396
+ * workflow-level metadata (triggers, concurrency, write permissions).
10397
+ *
10398
+ * @param {string} f Path to the GitHub Actions workflow YAML file
10399
+ * @returns {Object[]} List of action component objects with purl, properties, and evidence
10400
+ */
9948
10401
  export function parseGitHubWorkflowData(f) {
10402
+ const { components } = parseWorkflowFile(f);
10403
+ return components.filter((c) => c.scope === "required");
10404
+ }
10405
+
10406
+ /**
10407
+ * Parse Google Cloud Build YAML data and extract container image steps as packages.
10408
+ *
10409
+ * @param {string} cbwData Raw YAML string of a Cloud Build configuration file
10410
+ * @returns {Object[]} Array of package objects parsed from the build steps
10411
+ */
10412
+ export function parseCloudBuildData(cbwData) {
9949
10413
  const pkgList = [];
9950
- if (!f) {
9951
- return pkgList;
9952
- }
9953
- const ghwData = readFileSync(f, { encoding: "utf-8" });
9954
10414
  const keys_cache = {};
9955
- if (!ghwData) {
10415
+ if (!cbwData) {
9956
10416
  return pkgList;
9957
10417
  }
9958
- const yamlObj = _load(ghwData);
10418
+ const yamlObj = _load(cbwData);
9959
10419
  if (!yamlObj) {
9960
10420
  return pkgList;
9961
10421
  }
9962
- const lines = ghwData.split("\n");
9963
- // workflow-related values
9964
- const workflowName = yamlObj.name || path.basename(f, path.extname(f));
9965
- const workflowTriggers = yamlObj.on || yamlObj.true;
9966
- const workflowPermissions = yamlObj.permissions || {};
9967
- let hasWritePermissions = analyzePermissions(workflowPermissions);
9968
- // GitHub of course supports strings such as "write-all" to make it easy to create supply-chain attacks.
9969
- if (
9970
- (typeof workflowPermissions === "string" ||
9971
- workflowPermissions instanceof String) &&
9972
- workflowPermissions.includes("write")
9973
- ) {
9974
- hasWritePermissions = true;
9975
- }
9976
- const workflowConcurrency = yamlObj.concurrency || {};
9977
- const _workflowEnv = yamlObj.env || {};
9978
- const hasIdTokenWrite = workflowPermissions?.["id-token"] === "write";
9979
- for (const jobName of Object.keys(yamlObj.jobs)) {
9980
- const job = yamlObj.jobs[jobName];
9981
- if (!job.steps) {
9982
- continue;
9983
- }
9984
- // job-related values
9985
- const jobRunner = job["runs-on"] || "unknown";
9986
- const jobEnvironment = job.environment?.name || job.environment || "";
9987
- const jobTimeout = job["timeout-minutes"] || null;
9988
- const jobPermissions = job.permissions || {};
9989
- const jobServices = job.services ? Object.keys(job.services) : [];
9990
- let jobNeeds = job.needs || [];
9991
- if (!Array.isArray(jobNeeds)) {
9992
- jobNeeds = [jobNeeds];
9993
- }
9994
- const _jobIf = job.if || "";
9995
- const _jobStrategy = job.strategy ? JSON.stringify(job.strategy) : "";
9996
- const jobHasWritePermissions = analyzePermissions(jobPermissions);
9997
- for (const step of job.steps) {
9998
- if (step.uses) {
9999
- const tmpA = step.uses.split("@");
10000
- if (tmpA.length !== 2) {
10001
- continue;
10002
- }
10003
- const groupName = tmpA[0];
10004
- let name = groupName;
10005
- let group = "";
10006
- const tagOrCommit = tmpA[1];
10007
- let version = tagOrCommit;
10008
- const tmpB = groupName.split("/");
10009
- if (tmpB.length >= 2) {
10010
- name = tmpB.pop();
10011
- group = tmpB.join("/");
10012
- } else if (tmpB.length === 1) {
10013
- name = tmpB[0];
10014
- group = "";
10015
- }
10016
- const versionPinningType = getVersionPinningType(tagOrCommit);
10017
- const isShaPinned = versionPinningType === "sha";
10018
- const _isTagPinned = versionPinningType === "tag";
10019
- const _isBranchRef = versionPinningType === "branch";
10020
- let lineNum = -1;
10021
- const stepLineMatch = ghwData.indexOf(step.uses);
10022
- if (stepLineMatch >= 0) {
10023
- lineNum = ghwData.substring(0, stepLineMatch).split("\n").length - 1;
10024
- }
10025
- if (lineNum >= 0 && lines[lineNum]) {
10026
- const line = lines[lineNum];
10027
- const commentMatch = line.match(/#\s*v?([0-9]+(?:\.[0-9]+)*)/);
10028
- if (commentMatch?.[1]) {
10029
- version = commentMatch[1];
10030
- }
10031
- }
10032
- const key = `${group}-${name}-${version}`;
10033
- let confidence = 0.6;
10034
- if (!keys_cache[key] && name && version) {
10035
- keys_cache[key] = key;
10036
- let fullName = name;
10037
- if (group.length) {
10038
- fullName = `${group}/${name}`;
10039
- }
10040
- let purl = `pkg:github/${fullName}@${version}`;
10041
- if (tagOrCommit && version !== tagOrCommit) {
10042
- const qualifierDesc = tagOrCommit.startsWith("v")
10043
- ? "tag"
10044
- : "commit";
10045
- purl = `${purl}?${qualifierDesc}=${tagOrCommit}`;
10046
- confidence = 0.7;
10047
- }
10048
- const properties = [
10049
- { name: "SrcFile", value: f },
10050
- { name: "cdx:github:workflow:name", value: workflowName },
10051
- { name: "cdx:github:job:name", value: jobName },
10052
- {
10053
- name: "cdx:github:job:runner",
10054
- value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
10055
- },
10056
- { name: "cdx:github:action:uses", value: step.uses },
10057
- {
10058
- name: "cdx:github:action:versionPinningType",
10059
- value: versionPinningType,
10060
- },
10061
- {
10062
- name: "cdx:github:action:isShaPinned",
10063
- value: isShaPinned.toString(),
10064
- },
10065
- ];
10066
- if (step.name) {
10067
- properties.push({ name: "cdx:github:step:name", value: step.name });
10068
- }
10069
- if (step.if) {
10070
- properties.push({
10071
- name: "cdx:github:step:condition",
10072
- value: step.if,
10073
- });
10074
- }
10075
- if (step["continue-on-error"]) {
10076
- properties.push({
10077
- name: "cdx:github:step:continueOnError",
10078
- value: "true",
10079
- });
10422
+ if (yamlObj.steps) {
10423
+ for (const step of yamlObj.steps) {
10424
+ if (step.name) {
10425
+ const tmpA = step.name.split(":");
10426
+ if (tmpA.length === 2) {
10427
+ let group = dirname(tmpA[0]);
10428
+ const name = basename(tmpA[0]);
10429
+ if (group === ".") {
10430
+ group = "";
10080
10431
  }
10081
- if (step.timeout) {
10082
- properties.push({
10083
- name: "cdx:github:step:timeout",
10084
- value: step.timeout.toString(),
10085
- });
10086
- }
10087
- if (jobEnvironment) {
10088
- properties.push({
10089
- name: "cdx:github:job:environment",
10090
- value: jobEnvironment,
10091
- });
10092
- }
10093
- if (jobTimeout) {
10094
- properties.push({
10095
- name: "cdx:github:job:timeoutMinutes",
10096
- value: jobTimeout.toString(),
10097
- });
10098
- }
10099
- if (jobHasWritePermissions) {
10100
- properties.push({
10101
- name: "cdx:github:job:hasWritePermissions",
10102
- value: "true",
10103
- });
10104
- }
10105
- if (jobServices.length > 0) {
10106
- properties.push({
10107
- name: "cdx:github:job:services",
10108
- value: jobServices.join(","),
10109
- });
10110
- }
10111
- if (jobNeeds.length > 0) {
10112
- properties.push({
10113
- name: "cdx:github:job:needs",
10114
- value: jobNeeds.join(","),
10115
- });
10116
- }
10117
- if (hasWritePermissions) {
10118
- properties.push({
10119
- name: "cdx:github:workflow:hasWritePermissions",
10120
- value: "true",
10121
- });
10122
- }
10123
- if (hasIdTokenWrite) {
10124
- properties.push({
10125
- name: "cdx:github:workflow:hasIdTokenWrite",
10126
- value: "true",
10127
- });
10128
- }
10129
- if (workflowConcurrency?.group) {
10130
- properties.push({
10131
- name: "cdx:github:workflow:concurrencyGroup",
10132
- value: workflowConcurrency.group,
10133
- });
10134
- }
10135
- if (group?.startsWith("github/") || group === "actions") {
10136
- properties.push({ name: "cdx:actions:isOfficial", value: "true" });
10137
- }
10138
- if (group?.startsWith("github/")) {
10139
- properties.push({ name: "cdx:actions:isVerified", value: "true" });
10140
- }
10141
- if (workflowTriggers) {
10142
- const triggers =
10143
- typeof workflowTriggers === "string"
10144
- ? workflowTriggers
10145
- : Object.keys(workflowTriggers).join(",");
10146
- properties.push({
10147
- name: "cdx:github:workflow:triggers",
10148
- value: triggers,
10149
- });
10150
- }
10151
- pkgList.push({
10152
- group,
10153
- name,
10154
- version,
10155
- purl,
10156
- properties,
10157
- evidence: {
10158
- identity: {
10159
- field: "purl",
10160
- confidence,
10161
- methods: [
10162
- {
10163
- technique: "source-code-analysis",
10164
- confidence,
10165
- value: f,
10166
- },
10167
- ],
10168
- },
10169
- },
10170
- });
10171
- }
10172
- }
10173
- if (step.run) {
10174
- const runLineNum = ghwData.indexOf(step.run);
10175
- const runLine =
10176
- runLineNum >= 0
10177
- ? ghwData.substring(0, runLineNum).split("\n").length
10178
- : -1;
10179
- const pkgCommands = extractPackageManagerCommands(step.run);
10180
- for (const pkgCmd of pkgCommands) {
10181
- const key = `run-${pkgCmd.name}-${pkgCmd.version || "latest"}`;
10182
- if (!keys_cache[key]) {
10183
- keys_cache[key] = key;
10184
- pkgList.push({
10185
- group: "",
10186
- name: pkgCmd.name,
10187
- version: pkgCmd.version || undefined,
10188
- scope: "excluded",
10189
- "bom-ref": uuidv4(),
10190
- description: key,
10191
- properties: [
10192
- { name: "SrcFile", value: f },
10193
- { name: "cdx:github:workflow:name", value: workflowName },
10194
- { name: "cdx:github:job:name", value: jobName },
10195
- { name: "cdx:github:step:type", value: "run" },
10196
- { name: "cdx:github:step:command", value: pkgCmd.command },
10197
- { name: "cdx:github:run:line", value: runLine.toString() },
10198
- ],
10199
- evidence: {
10200
- identity: {
10201
- field: "purl",
10202
- confidence: 0.5,
10203
- methods: [
10204
- {
10205
- technique: "source-code-analysis",
10206
- confidence: 0.5,
10207
- value: f,
10208
- },
10209
- ],
10210
- },
10211
- },
10212
- });
10213
- }
10214
- }
10215
- }
10216
- }
10217
- }
10218
- return pkgList;
10219
- }
10220
-
10221
- /**
10222
- * Analyze permissions for write access.
10223
- *
10224
- * Refer to https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions
10225
- */
10226
- function analyzePermissions(permissions) {
10227
- if (!permissions || typeof permissions !== "object") {
10228
- return false;
10229
- }
10230
- const writePermissions = [
10231
- "actions",
10232
- "artifact-metadata",
10233
- "attestations",
10234
- "checks",
10235
- "contents",
10236
- "deployments",
10237
- "id-token",
10238
- "models",
10239
- "discussions",
10240
- "packages",
10241
- "pages",
10242
- "actions",
10243
- "deployments",
10244
- "issues",
10245
- "pull-requests",
10246
- "security-events",
10247
- "statuses",
10248
- ];
10249
- for (const perm of writePermissions) {
10250
- if (permissions[perm] === "write") {
10251
- return true;
10252
- }
10253
- }
10254
- return false;
10255
- }
10256
-
10257
- /**
10258
- * Determine version pinning type for security assessment
10259
- */
10260
- function getVersionPinningType(versionRef) {
10261
- if (!versionRef) {
10262
- return "unknown";
10263
- }
10264
- if (/^[a-f0-9]{40}$/.test(versionRef)) {
10265
- return "sha";
10266
- }
10267
- if (/^[a-f0-9]{7,}$/.test(versionRef)) {
10268
- return "sha";
10269
- }
10270
- if (
10271
- versionRef === "main" ||
10272
- versionRef === "master" ||
10273
- versionRef.includes("/")
10274
- ) {
10275
- return "branch";
10276
- }
10277
- return "tag";
10278
- }
10279
-
10280
- /**
10281
- * Extract package manager commands from run steps
10282
- */
10283
- function extractPackageManagerCommands(runScript) {
10284
- const commands = [];
10285
- if (!runScript) {
10286
- return commands;
10287
- }
10288
- const patterns = [
10289
- { regex: /npm\s+(install|i|ci)\s+([^&\n|;]+)/g, name: "npm", type: "npm" },
10290
- {
10291
- regex: /yarn\s+(add|install)\s+([^&\n|;]+)/g,
10292
- name: "yarn",
10293
- type: "yarn",
10294
- },
10295
- { regex: /pip\s+(install)\s+([^&\n|;]+)/g, name: "pip", type: "pip" },
10296
- { regex: /pip3\s+(install)\s+([^&\n|;]+)/g, name: "pip3", type: "pip" },
10297
- { regex: /gem\s+(install)\s+([^&\n|;]+)/g, name: "gem", type: "gem" },
10298
- { regex: /go\s+(get|install)\s+([^&\n|;]+)/g, name: "go", type: "go" },
10299
- {
10300
- regex: /cargo\s+(install|add)\s+([^&\n|;]+)/g,
10301
- name: "cargo",
10302
- type: "cargo",
10303
- },
10304
- ];
10305
- for (const pattern of patterns) {
10306
- let match;
10307
- while ((match = pattern.regex.exec(runScript)) !== null) {
10308
- commands.push({
10309
- name: pattern.name,
10310
- command: match[0],
10311
- version: null,
10312
- });
10313
- }
10314
- }
10315
- return commands;
10316
- }
10317
-
10318
- export function parseCloudBuildData(cbwData) {
10319
- const pkgList = [];
10320
- const keys_cache = {};
10321
- if (!cbwData) {
10322
- return pkgList;
10323
- }
10324
- const yamlObj = _load(cbwData);
10325
- if (!yamlObj) {
10326
- return pkgList;
10327
- }
10328
- if (yamlObj.steps) {
10329
- for (const step of yamlObj.steps) {
10330
- if (step.name) {
10331
- const tmpA = step.name.split(":");
10332
- if (tmpA.length === 2) {
10333
- let group = dirname(tmpA[0]);
10334
- const name = basename(tmpA[0]);
10335
- if (group === ".") {
10336
- group = "";
10337
- }
10338
- const version = tmpA[1];
10339
- const key = `${group}-${name}-${version}`;
10340
- if (!keys_cache[key] && name && version) {
10341
- keys_cache[key] = key;
10342
- pkgList.push({
10343
- group,
10344
- name,
10345
- version,
10432
+ const version = tmpA[1];
10433
+ const key = `${group}-${name}-${version}`;
10434
+ if (!keys_cache[key] && name && version) {
10435
+ keys_cache[key] = key;
10436
+ pkgList.push({
10437
+ group,
10438
+ name,
10439
+ version,
10346
10440
  });
10347
10441
  }
10348
10442
  }
@@ -10391,6 +10485,16 @@ function untilFirst(separator, inputStr) {
10391
10485
  ];
10392
10486
  }
10393
10487
 
10488
+ /**
10489
+ * Map a Conan package reference string to a PackageURL string, name, and version.
10490
+ *
10491
+ * Parses a full Conan package reference of the form
10492
+ * `name/version@user/channel#recipe_revision:package_id#package_revision`
10493
+ * and returns the equivalent purl string together with the extracted name and version.
10494
+ *
10495
+ * @param {string} conanPkgRef Conan package reference string
10496
+ * @returns {Array} Tuple of [purlString, name, version], or [null, null, null] on parse failure
10497
+ */
10394
10498
  export function mapConanPkgRefToPurlStringAndNameAndVersion(conanPkgRef) {
10395
10499
  // A full Conan package reference may be composed of the following segments:
10396
10500
  // conanPkgRef = "name/version@user/channel#recipe_revision:package_id#package_revision"
@@ -10516,6 +10620,16 @@ export function mapConanPkgRefToPurlStringAndNameAndVersion(conanPkgRef) {
10516
10620
  return [purl, info.name, info.version];
10517
10621
  }
10518
10622
 
10623
+ /**
10624
+ * Parse Conan lock file data (conan.lock) and return the package list, dependency map,
10625
+ * and parent component dependencies.
10626
+ *
10627
+ * Supports both the legacy `graph_lock.nodes` format (Conan 1.x) and the newer
10628
+ * `requires` format (Conan 2.x).
10629
+ *
10630
+ * @param {string} conanLockData Raw JSON string of the Conan lock file
10631
+ * @returns {{ pkgList: Object[], dependencies: Object, parentComponentDependencies: string[] }}
10632
+ */
10519
10633
  export function parseConanLockData(conanLockData) {
10520
10634
  const pkgList = [];
10521
10635
  const dependencies = {};
@@ -10526,10 +10640,7 @@ export function parseConanLockData(conanLockData) {
10526
10640
  }
10527
10641
 
10528
10642
  const lockFile = JSON.parse(conanLockData);
10529
- if (
10530
- (!lockFile || !lockFile.graph_lock || !lockFile.graph_lock.nodes) &&
10531
- !lockFile.requires
10532
- ) {
10643
+ if (!lockFile?.graph_lock?.nodes && !lockFile.requires) {
10533
10644
  return { pkgList, dependencies, parentComponentDependencies };
10534
10645
  }
10535
10646
 
@@ -10602,6 +10713,12 @@ export function parseConanLockData(conanLockData) {
10602
10713
  return { pkgList, dependencies, parentComponentDependencies };
10603
10714
  }
10604
10715
 
10716
+ /**
10717
+ * Parse a Conan conanfile.txt and extract required and optional packages.
10718
+ *
10719
+ * @param {string} conanData Raw text contents of a conanfile.txt
10720
+ * @returns {Object[]} Array of package objects with purl, name, version, and scope
10721
+ */
10605
10722
  export function parseConanData(conanData) {
10606
10723
  const pkgList = [];
10607
10724
  if (!conanData) {
@@ -10637,6 +10754,12 @@ export function parseConanData(conanData) {
10637
10754
  return pkgList;
10638
10755
  }
10639
10756
 
10757
+ /**
10758
+ * Parse Leiningen project.clj data and extract dependency packages.
10759
+ *
10760
+ * @param {string} leinData Raw text contents of a Leiningen project.clj file
10761
+ * @returns {Object[]} Array of package objects with group, name, and version
10762
+ */
10640
10763
  export function parseLeiningenData(leinData) {
10641
10764
  const pkgList = [];
10642
10765
  if (!leinData) {
@@ -10671,6 +10794,14 @@ export function parseLeiningenData(leinData) {
10671
10794
  return pkgList;
10672
10795
  }
10673
10796
 
10797
+ /**
10798
+ * Parse EDN (Extensible Data Notation) deps.edn data and extract dependency packages.
10799
+ *
10800
+ * Handles Clojure deps.edn files, extracting packages listed under the `:deps` key.
10801
+ *
10802
+ * @param {string} rawEdnData Raw EDN text contents of a deps.edn file
10803
+ * @returns {Object[]} Array of package objects with group, name, and version
10804
+ */
10674
10805
  export function parseEdnData(rawEdnData) {
10675
10806
  const pkgList = [];
10676
10807
  if (!rawEdnData) {
@@ -10750,7 +10881,7 @@ export function parseFlakeNix(flakeNixFile) {
10750
10881
  const pkgList = [];
10751
10882
  const dependencies = [];
10752
10883
 
10753
- if (!existsSync(flakeNixFile)) {
10884
+ if (!safeExistsSync(flakeNixFile)) {
10754
10885
  return { pkgList, dependencies };
10755
10886
  }
10756
10887
 
@@ -10835,7 +10966,7 @@ export function parseFlakeLock(flakeLockFile) {
10835
10966
  const pkgList = [];
10836
10967
  const dependencies = [];
10837
10968
 
10838
- if (!existsSync(flakeLockFile)) {
10969
+ if (!safeExistsSync(flakeLockFile)) {
10839
10970
  return { pkgList, dependencies };
10840
10971
  }
10841
10972
 
@@ -11117,6 +11248,13 @@ export function parseNuspecData(nupkgFile, nuspecData) {
11117
11248
  };
11118
11249
  }
11119
11250
 
11251
+ /**
11252
+ * Parse a C# packages.config XML file and return a list of NuGet package components.
11253
+ *
11254
+ * @param {string} pkgData Raw XML string of a packages.config file
11255
+ * @param {string} pkgFile Path to the packages.config file, used for evidence properties
11256
+ * @returns {Object[]} Array of NuGet package objects with purl, name, and version
11257
+ */
11120
11258
  export function parseCsPkgData(pkgData, pkgFile) {
11121
11259
  const pkgList = [];
11122
11260
  if (!pkgData) {
@@ -11649,6 +11787,17 @@ export function parseCsProjData(
11649
11787
  };
11650
11788
  }
11651
11789
 
11790
+ /**
11791
+ * Parse a .NET project.assets.json file and return the package list and dependency tree.
11792
+ *
11793
+ * Extracts NuGet packages and their transitive dependency relationships from the
11794
+ * `libraries` and `targets` sections of a project.assets.json file produced by
11795
+ * the .NET restore process.
11796
+ *
11797
+ * @param {string} csProjData Raw JSON string of the project.assets.json file
11798
+ * @param {string} assetsJsonFile Path to the project.assets.json file, used for evidence properties
11799
+ * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
11800
+ */
11652
11801
  export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
11653
11802
  // extract name, operator, version from .NET package representation
11654
11803
  // like "NLog >= 4.5.0"
@@ -11899,6 +12048,14 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
11899
12048
  };
11900
12049
  }
11901
12050
 
12051
+ /**
12052
+ * Parse a .NET packages.lock.json file and return the package list, dependency tree,
12053
+ * and list of direct/root dependencies.
12054
+ *
12055
+ * @param {string} csLockData Raw JSON string of the packages.lock.json file
12056
+ * @param {string} pkgLockFile Path to the packages.lock.json file, used for evidence properties
12057
+ * @returns {{ pkgList: Object[], dependenciesList: Object[], rootList: Object[] }}
12058
+ */
11902
12059
  export function parseCsPkgLockData(csLockData, pkgLockFile) {
11903
12060
  const pkgList = [];
11904
12061
  const dependenciesList = [];
@@ -11912,7 +12069,7 @@ export function parseCsPkgLockData(csLockData, pkgLockFile) {
11912
12069
  };
11913
12070
  }
11914
12071
  const assetData = JSON.parse(csLockData);
11915
- if (!assetData || !assetData.dependencies) {
12072
+ if (!assetData?.dependencies) {
11916
12073
  return {
11917
12074
  pkgList,
11918
12075
  dependenciesList,
@@ -12017,6 +12174,14 @@ export function parseCsPkgLockData(csLockData, pkgLockFile) {
12017
12174
  };
12018
12175
  }
12019
12176
 
12177
+ /**
12178
+ * Parse a Paket dependency manager lock file (paket.lock) and return the package list
12179
+ * and dependency tree.
12180
+ *
12181
+ * @param {string} paketLockData Raw text contents of the paket.lock file
12182
+ * @param {string} pkgLockFile Path to the paket.lock file, used for evidence properties
12183
+ * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
12184
+ */
12020
12185
  export function parsePaketLockData(paketLockData, pkgLockFile) {
12021
12186
  const pkgList = [];
12022
12187
  const dependenciesList = [];
@@ -12191,6 +12356,16 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12191
12356
  const rootRequiresMap = {};
12192
12357
  if (rootRequires) {
12193
12358
  for (const rr of Object.keys(rootRequires)) {
12359
+ // Skip platform requirements (php, hhvm, ext-*, lib-*) — they are never
12360
+ // Composer package names and must not be used to identify root packages.
12361
+ if (
12362
+ rr === "php" ||
12363
+ rr === "hhvm" ||
12364
+ rr.startsWith("ext-") ||
12365
+ rr.startsWith("lib-")
12366
+ ) {
12367
+ continue;
12368
+ }
12194
12369
  rootRequiresMap[rr] = true;
12195
12370
  }
12196
12371
  }
@@ -12215,7 +12390,7 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12215
12390
  for (const i in packages[compScope]) {
12216
12391
  const pkg = packages[compScope][i];
12217
12392
  // Be extra cautious. Potential fix for #236
12218
- if (!pkg || !pkg.name || !pkg.version) {
12393
+ if (!pkg?.name || !pkg.version) {
12219
12394
  continue;
12220
12395
  }
12221
12396
 
@@ -12313,7 +12488,7 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12313
12488
  for (const compScope in packages) {
12314
12489
  for (const i in packages[compScope]) {
12315
12490
  const pkg = packages[compScope][i];
12316
- if (!pkg || !pkg.name || !pkg.version) {
12491
+ if (!pkg?.name || !pkg.version) {
12317
12492
  continue;
12318
12493
  }
12319
12494
  if (!pkg.require || !Object.keys(pkg.require).length) {
@@ -12342,6 +12517,15 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12342
12517
  };
12343
12518
  }
12344
12519
 
12520
+ /**
12521
+ * Parse an sbt dependency tree output file and return the package list and dependency tree.
12522
+ *
12523
+ * Reads a file produced by the sbt `dependencyTree` command and extracts Maven artifact
12524
+ * coordinates, building a hierarchical dependency graph. Evicted packages and ranges are ignored.
12525
+ *
12526
+ * @param {string} sbtTreeFile Path to the sbt dependency tree output file
12527
+ * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
12528
+ */
12345
12529
  export function parseSbtTree(sbtTreeFile) {
12346
12530
  const pkgList = [];
12347
12531
  const dependenciesList = [];
@@ -12658,6 +12842,10 @@ export function convertOSQueryResults(
12658
12842
  if (publisher === "null") {
12659
12843
  publisher = "";
12660
12844
  }
12845
+ // For vscode-extension purl type, the publisher is used as the namespace
12846
+ if (queryObj.purlType === "vscode-extension" && publisher) {
12847
+ group = publisher.toLowerCase();
12848
+ }
12661
12849
  let scope;
12662
12850
  const compScope = res.priority;
12663
12851
  if (["required", "optional", "excluded"].includes(compScope)) {
@@ -12758,6 +12946,17 @@ export function convertOSQueryResults(
12758
12946
  return pkgList;
12759
12947
  }
12760
12948
 
12949
+ /**
12950
+ * Create a PackageURL object from a repository URL string, package type, and version.
12951
+ *
12952
+ * Supports HTTPS URLs, SSH `git@` URLs, Bitbucket SSH URLs, and local paths.
12953
+ * Extracts the namespace (host + path prefix) and repository name from the URL.
12954
+ *
12955
+ * @param {string} type PackageURL type (e.g. `"swift"`, `"generic"`)
12956
+ * @param {string} repoUrl Repository URL string
12957
+ * @param {string} version Package version
12958
+ * @returns {PackageURL|undefined} PackageURL object, or undefined for unsupported URL formats
12959
+ */
12761
12960
  export function purlFromUrlString(type, repoUrl, version) {
12762
12961
  let namespace = "";
12763
12962
  let name;
@@ -13050,6 +13249,20 @@ export async function collectMvnDependencies(
13050
13249
  return jarNSMapping;
13051
13250
  }
13052
13251
 
13252
+ /**
13253
+ * Collect Gradle project dependencies by scanning the Gradle cache directory for JAR files
13254
+ * and their associated POM files.
13255
+ *
13256
+ * Uses the `GRADLE_CACHE_DIR` or `GRADLE_USER_HOME` environment variables to locate the
13257
+ * Gradle files-2.1 cache, then delegates to {@link collectJarNS} to extract namespace
13258
+ * and purl information from those JARs.
13259
+ *
13260
+ * @param {string} _gradleCmd Gradle command (unused; reserved for future use)
13261
+ * @param {string} _basePath Base project path (unused; reserved for future use)
13262
+ * @param {boolean} _cleanup Whether to clean up temporary files (unused; reserved for future use)
13263
+ * @param {boolean} _includeCacheDir Whether to include cache directory (unused; reserved for future use)
13264
+ * @returns {Promise<Object>} JAR namespace mapping object returned by collectJarNS
13265
+ */
13053
13266
  export async function collectGradleDependencies(
13054
13267
  _gradleCmd,
13055
13268
  _basePath,
@@ -13263,6 +13476,16 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
13263
13476
  return jarNSMapping;
13264
13477
  }
13265
13478
 
13479
+ /**
13480
+ * Convert a JAR namespace mapping (produced by {@link collectJarNS}) into an array
13481
+ * of CycloneDX package component objects.
13482
+ *
13483
+ * Each entry in the mapping is resolved to a component with name, group, version,
13484
+ * purl, hashes, namespace properties, and source file evidence.
13485
+ *
13486
+ * @param {Object} jarNSMapping Map of purl string to `{ jarFile, pom, namespaces, hashes }`
13487
+ * @returns {Promise<Object[]>} Array of component objects derived from the JAR mapping
13488
+ */
13266
13489
  export async function convertJarNSToPackages(jarNSMapping) {
13267
13490
  const pkgList = [];
13268
13491
  for (const purl of Object.keys(jarNSMapping)) {
@@ -13366,6 +13589,12 @@ export function parsePomXml(pomXmlData) {
13366
13589
  return undefined;
13367
13590
  }
13368
13591
 
13592
+ /**
13593
+ * Parse a JAR MANIFEST.MF file and return its key-value pairs as an object.
13594
+ *
13595
+ * @param {string} jarMetadata Raw text contents of a MANIFEST.MF file
13596
+ * @returns {Object} Key-value pairs extracted from the manifest
13597
+ */
13369
13598
  export function parseJarManifest(jarMetadata) {
13370
13599
  const metadata = {};
13371
13600
  if (!jarMetadata) {
@@ -13383,6 +13612,12 @@ export function parseJarManifest(jarMetadata) {
13383
13612
  return metadata;
13384
13613
  }
13385
13614
 
13615
+ /**
13616
+ * Parse a Maven pom.properties file and return its key-value pairs as an object.
13617
+ *
13618
+ * @param {string} pomProperties Raw text contents of a pom.properties file
13619
+ * @returns {Object} Key-value pairs extracted from the properties file
13620
+ */
13386
13621
  export function parsePomProperties(pomProperties) {
13387
13622
  const properties = {};
13388
13623
  if (!pomProperties) {
@@ -13400,6 +13635,13 @@ export function parsePomProperties(pomProperties) {
13400
13635
  return properties;
13401
13636
  }
13402
13637
 
13638
+ /**
13639
+ * Encode a string for safe inclusion in a PackageURL, percent-encoding special characters
13640
+ * while preserving already-encoded `%40` sequences and keeping `:` and `/` unencoded.
13641
+ *
13642
+ * @param {string} s String to encode
13643
+ * @returns {string} Encoded string suitable for use in a PackageURL component
13644
+ */
13403
13645
  export function encodeForPurl(s) {
13404
13646
  return s && !s.includes("%40")
13405
13647
  ? encodeURIComponent(s).replace(/%3A/g, ":").replace(/%2F/g, "/")
@@ -14303,10 +14545,10 @@ export async function parsePodfileLock(podfileLock, projectPath) {
14303
14545
  },
14304
14546
  ];
14305
14547
  let podspec = join(projectLocation, `${podName}.podspec`);
14306
- if (!existsSync(podspec)) {
14548
+ if (!safeExistsSync(podspec)) {
14307
14549
  podspec = `${podspec}.json`;
14308
14550
  }
14309
- if (existsSync(podspec)) {
14551
+ if (safeExistsSync(podspec)) {
14310
14552
  dependency.metadata.properties.push({
14311
14553
  name: "cdx:pods:podspecLocation",
14312
14554
  value: podspec,
@@ -15051,6 +15293,19 @@ export function getAtomCommand() {
15051
15293
  return "atom";
15052
15294
  }
15053
15295
 
15296
+ /**
15297
+ * Execute the atom tool against a source directory or file with the given arguments.
15298
+ *
15299
+ * Resolves the atom binary via `getAtomCommand`, sets up the required environment
15300
+ * (including `JAVA_HOME` from `ATOM_JAVA_HOME` if set), and spawns the process.
15301
+ * Logs diagnostic messages for common failure modes such as unsupported Java versions,
15302
+ * missing `astgen`, and JVM crashes.
15303
+ *
15304
+ * @param {string} src Path to the source directory or file to analyse
15305
+ * @param {string[]} args Arguments to pass to the atom command
15306
+ * @param {Object} extra_env Additional environment variables to merge into the process environment
15307
+ * @returns {boolean} `true` if atom executed successfully and the language is supported; `false` otherwise
15308
+ */
15054
15309
  export function executeAtom(src, args, extra_env = {}) {
15055
15310
  const cwd =
15056
15311
  safeExistsSync(src) && lstatSync(src).isDirectory() ? src : dirname(src);
@@ -15266,36 +15521,6 @@ function flattenDeps(dependenciesMap, pkgList, reqOrSetupFile, t) {
15266
15521
  .sort();
15267
15522
  }
15268
15523
 
15269
- function get_python_command_from_env(env) {
15270
- // Virtual environments needs special treatment to use the correct python executable
15271
- // Without this step, the default python is always used resulting in false positives
15272
- const python_exe_name = isWin ? "python.exe" : "python";
15273
- const python3_exe_name = isWin ? "python3.exe" : "python3";
15274
- let python_cmd_to_use = PYTHON_CMD;
15275
- if (env.VIRTUAL_ENV) {
15276
- const bin_dir = isWin ? "Scripts" : "bin";
15277
- if (safeExistsSync(join(env.VIRTUAL_ENV, bin_dir, python_exe_name))) {
15278
- python_cmd_to_use = join(env.VIRTUAL_ENV, bin_dir, python_exe_name);
15279
- } else if (
15280
- safeExistsSync(join(env.VIRTUAL_ENV, bin_dir, python3_exe_name))
15281
- ) {
15282
- python_cmd_to_use = join(env.VIRTUAL_ENV, bin_dir, python3_exe_name);
15283
- }
15284
- } else if (env.CONDA_PREFIX) {
15285
- const bin_dir = isWin ? "" : "bin";
15286
- if (safeExistsSync(join(env.CONDA_PREFIX, bin_dir, python_exe_name))) {
15287
- python_cmd_to_use = join(env.CONDA_PREFIX, bin_dir, python_exe_name);
15288
- } else if (
15289
- safeExistsSync(join(env.CONDA_PREFIX, bin_dir, python3_exe_name))
15290
- ) {
15291
- python_cmd_to_use = join(env.CONDA_PREFIX, bin_dir, python3_exe_name);
15292
- }
15293
- } else if (env.CONDA_PYTHON_EXE) {
15294
- python_cmd_to_use = env.CONDA_PYTHON_EXE;
15295
- }
15296
- return python_cmd_to_use;
15297
- }
15298
-
15299
15524
  /**
15300
15525
  * Create uv.lock file with uv sync command.
15301
15526
  *
@@ -15378,14 +15603,12 @@ export async function getPipFrozenTree(
15378
15603
  ...process.env,
15379
15604
  };
15380
15605
 
15381
- // FIX: Create a set of explicit dependencies from requirements.txt to identify root packages.
15382
15606
  const explicitDeps = new Set();
15383
15607
  if (reqOrSetupFile?.endsWith(".txt") && safeExistsSync(reqOrSetupFile)) {
15384
15608
  // We only need the package names, so we pass `false` to avoid fetching full metadata.
15385
15609
  const tempPkgList = await parseReqFile(reqOrSetupFile, null, false);
15386
15610
  for (const pkg of tempPkgList) {
15387
15611
  if (pkg.name) {
15388
- // Normalize the name (lowercase, hyphenated) for accurate lookups.
15389
15612
  explicitDeps.add(pkg.name.replace(/_/g, "-").toLowerCase());
15390
15613
  }
15391
15614
  }
@@ -15441,17 +15664,26 @@ export async function getPipFrozenTree(
15441
15664
  }
15442
15665
  }
15443
15666
  }
15444
- /**
15445
- * We now have a virtual environment so we can attempt to install the project and perform
15446
- * pip freeze to collect the packages that got installed.
15447
- * Note that we did not create a virtual environment for poetry because poetry will do this when we run the install.
15448
- * This step is accurate but not reproducible since the resulting list could differ based on various factors
15449
- * such as the version of python, pip, os, pypi.org availability (and weather?)
15450
- */
15451
- // Bug #388. Perform pip install in all virtualenv to make the experience consistent
15667
+ const venvMeta = getVenvMetadata(env);
15668
+ const python_cmd_for_tree = get_python_command_from_env(env);
15669
+ // Check if pyproject.toml is actually a uv-configured workspace
15670
+ let hasToolUv = false;
15671
+ let hasToolPoetry = false;
15672
+ if (
15673
+ reqOrSetupFile?.endsWith("pyproject.toml") &&
15674
+ safeExistsSync(reqOrSetupFile)
15675
+ ) {
15676
+ try {
15677
+ const content = readFileSync(reqOrSetupFile, "utf-8");
15678
+ hasToolUv = content.includes("[tool.uv]");
15679
+ hasToolPoetry = content.includes('build-backend = "poetry.core');
15680
+ } catch (_err) {
15681
+ // Ignore read error
15682
+ }
15683
+ }
15452
15684
  if (reqOrSetupFile) {
15453
15685
  // We have a poetry.lock file
15454
- if (reqOrSetupFile.endsWith("poetry.lock")) {
15686
+ if (reqOrSetupFile.endsWith("poetry.lock") || hasToolPoetry) {
15455
15687
  const poetryConfigArgs = [
15456
15688
  "-m",
15457
15689
  "poetry",
@@ -15538,20 +15770,78 @@ export async function getPipFrozenTree(
15538
15770
  )}${_delimiter}${process.env.PATH || ""}`;
15539
15771
  }
15540
15772
  }
15773
+ } else if (reqOrSetupFile.endsWith("pdm.lock") || venvMeta.type === "pdm") {
15774
+ thoughtLog("Performing pdm install");
15775
+ result = safeSpawnSync("pdm", ["install"], {
15776
+ cwd: basePath,
15777
+ shell: isWin,
15778
+ env,
15779
+ });
15780
+ if (result.status !== 0 || result.error) {
15781
+ frozen = false;
15782
+ }
15783
+ } else if (
15784
+ reqOrSetupFile.endsWith("pixi.lock") ||
15785
+ venvMeta.type === "pixi"
15786
+ ) {
15787
+ thoughtLog("Performing pixi install");
15788
+ result = safeSpawnSync("pixi", ["install"], {
15789
+ cwd: basePath,
15790
+ shell: isWin,
15791
+ env,
15792
+ });
15793
+ if (result.status !== 0 || result.error) {
15794
+ frozen = false;
15795
+ }
15796
+ } else if (
15797
+ reqOrSetupFile.endsWith("uv.lock") ||
15798
+ (venvMeta.type === "uv" && hasToolUv)
15799
+ ) {
15800
+ thoughtLog("Performing uv sync");
15801
+ result = safeSpawnSync("uv", ["sync"], {
15802
+ cwd: basePath,
15803
+ shell: isWin,
15804
+ env,
15805
+ });
15806
+ if (result.status !== 0 || result.error) {
15807
+ frozen = false;
15808
+ }
15809
+ } else if (
15810
+ venvMeta.type === "rye" ||
15811
+ reqOrSetupFile.endsWith("requirements.lock")
15812
+ ) {
15813
+ thoughtLog("Performing rye sync");
15814
+ result = safeSpawnSync("rye", ["sync"], {
15815
+ cwd: basePath,
15816
+ shell: isWin,
15817
+ env,
15818
+ });
15819
+ if (result.status !== 0 || result.error) {
15820
+ frozen = false;
15821
+ }
15541
15822
  } else {
15542
- // We are about to do a pip install with the right python command from the virtual environment
15543
- // This step can fail if the correct OS packages and development libraries are not installed
15544
- const python_cmd_for_tree = get_python_command_from_env(env);
15545
- let pipInstallArgs = [
15546
- "-m",
15547
- "pip",
15548
- "install",
15549
- "--disable-pip-version-check",
15550
- ];
15551
- if (isSecureMode) {
15552
- pipInstallArgs.unshift("-S");
15823
+ // General package installation (Handling pip, or uv pip)
15824
+ let installCmd = python_cmd_for_tree;
15825
+ let pipInstallArgs = [];
15826
+ if (venvMeta.type === "uv") {
15827
+ installCmd = "uv";
15828
+ pipInstallArgs = ["pip", "install"];
15829
+ if (isSecureMode) {
15830
+ pipInstallArgs.push("--only-binary");
15831
+ pipInstallArgs.push(":all:");
15832
+ }
15833
+ } else {
15834
+ pipInstallArgs = [
15835
+ "-m",
15836
+ "pip",
15837
+ "install",
15838
+ "--disable-pip-version-check",
15839
+ ];
15840
+ if (isSecureMode) {
15841
+ pipInstallArgs.push("--only-binary=:all:");
15842
+ pipInstallArgs.unshift("-S");
15843
+ }
15553
15844
  }
15554
- // Requirements.txt could be called with any name so best to check for not setup.py and not pyproject.toml
15555
15845
  if (
15556
15846
  !reqOrSetupFile.endsWith("setup.py") &&
15557
15847
  !reqOrSetupFile.endsWith("pyproject.toml")
@@ -15566,20 +15856,17 @@ export async function getPipFrozenTree(
15566
15856
  } else {
15567
15857
  pipInstallArgs.push(resolve(basePath));
15568
15858
  }
15569
- // Support for passing additional arguments to pip
15570
- // Eg: --python-version 3.10 --ignore-requires-python --no-warn-conflicts --only-binary=:all:
15571
15859
  if (process?.env?.PIP_INSTALL_ARGS) {
15572
15860
  const addArgs = process.env.PIP_INSTALL_ARGS.split(" ");
15573
15861
  pipInstallArgs = pipInstallArgs.concat(addArgs);
15574
15862
  }
15575
15863
  thoughtLog(
15576
- `**PIP**: Trying pip install using the arguments ${pipInstallArgs.join(" ")}`,
15864
+ `**INSTALL**: Trying package install using the arguments: ${installCmd} ${pipInstallArgs.join(" ")}`,
15577
15865
  );
15578
15866
  if (DEBUG_MODE) {
15579
- console.log("Executing", python_cmd_for_tree);
15867
+ console.log("Executing", installCmd);
15580
15868
  }
15581
- // Attempt to perform pip install
15582
- result = safeSpawnSync(python_cmd_for_tree, pipInstallArgs, {
15869
+ result = safeSpawnSync(installCmd, pipInstallArgs, {
15583
15870
  cwd: basePath,
15584
15871
  shell: isWin,
15585
15872
  env,
@@ -15604,6 +15891,10 @@ export async function getPipFrozenTree(
15604
15891
  );
15605
15892
  }
15606
15893
  console.log(result.stderr);
15894
+ } else if (result?.stderr?.includes("No module named pip")) {
15895
+ console.log(
15896
+ "Using uv? Ensure 'uv' is in your PATH to allow cdxgen to use `uv pip install` automatically.",
15897
+ );
15607
15898
  } else if (
15608
15899
  process.env.PIP_INSTALL_ARGS &&
15609
15900
  result.stderr?.includes("Cannot set --home and --prefix together")
@@ -15734,18 +16025,42 @@ export async function getPipFrozenTree(
15734
16025
  }
15735
16026
  // Bug #375. Attempt pip freeze on existing and new virtual environments
15736
16027
  if (env.VIRTUAL_ENV?.length || env.CONDA_PREFIX?.length) {
15737
- /**
15738
- * At this point, the previous attempt to do a pip install might have failed and we might have an unclean virtual environment with an incomplete list
15739
- * The position taken by cdxgen is "Some SBOM is better than no SBOM", so we proceed to collecting the dependencies that got installed with pip freeze
15740
- */
15741
- if (DEBUG_MODE) {
15742
- if (reqOrSetupFile) {
15743
- console.log(
15744
- `About to construct the pip dependency tree based on ${reqOrSetupFile}. Please wait ...`,
15745
- );
16028
+ const venvRoot = env.VIRTUAL_ENV || env.CONDA_PREFIX;
16029
+ const binDir = platform() === "win32" ? "Scripts" : "bin";
16030
+ const pipExe = join(
16031
+ venvRoot,
16032
+ binDir,
16033
+ platform() === "win32" ? "pip.exe" : "pip",
16034
+ );
16035
+ if (!safeExistsSync(pipExe)) {
16036
+ thoughtLog(
16037
+ "The 'pip' module is missing in this environment. Bootstrapping it to support piptree extraction.",
16038
+ );
16039
+ if (venvMeta.type === "uv") {
16040
+ safeSpawnSync("uv", ["pip", "install", "pip"], {
16041
+ cwd: basePath,
16042
+ shell: isWin,
16043
+ env,
16044
+ });
16045
+ } else if (venvMeta.type === "rye") {
16046
+ safeSpawnSync("rye", ["run", "pip", "install", "pip"], {
16047
+ cwd: basePath,
16048
+ shell: isWin,
16049
+ env,
16050
+ });
16051
+ } else {
16052
+ safeSpawnSync(python_cmd_for_tree, ["-m", "ensurepip", "--upgrade"], {
16053
+ cwd: basePath,
16054
+ shell: isWin,
16055
+ env,
16056
+ });
15746
16057
  }
15747
16058
  }
15748
- const python_cmd_for_tree = get_python_command_from_env(env);
16059
+ if (DEBUG_MODE && reqOrSetupFile) {
16060
+ console.log(
16061
+ `About to construct the dependency tree based on ${reqOrSetupFile}. Please wait ...`,
16062
+ );
16063
+ }
15749
16064
  // This is a slow step that ideally needs to be invoked only once per venv
15750
16065
  const tree = getTreeWithPlugin(env, python_cmd_for_tree, basePath);
15751
16066
  if (DEBUG_MODE && !tree.length) {
@@ -15896,10 +16211,23 @@ export function getPipTreeForPackages(
15896
16211
  env.PYTHONPATH = undefined;
15897
16212
  }
15898
16213
  }
16214
+ const venvMeta = getVenvMetadata(env);
15899
16215
  const python_cmd_for_tree = get_python_command_from_env(env);
15900
- let pipInstallArgs = ["-m", "pip", "install", "--disable-pip-version-check"];
15901
- if (isSecureMode) {
15902
- pipInstallArgs.unshift("-S");
16216
+ let installCmd = python_cmd_for_tree;
16217
+ let pipInstallArgs = [];
16218
+ if (venvMeta.type === "uv") {
16219
+ installCmd = "uv";
16220
+ pipInstallArgs = ["pip", "install"];
16221
+ if (isSecureMode) {
16222
+ pipInstallArgs.push("--only-binary");
16223
+ pipInstallArgs.push(":all:");
16224
+ }
16225
+ } else {
16226
+ pipInstallArgs = ["-m", "pip", "install", "--disable-pip-version-check"];
16227
+ if (isSecureMode) {
16228
+ pipInstallArgs.push("--only-binary=:all:");
16229
+ pipInstallArgs.unshift("-S");
16230
+ }
15903
16231
  }
15904
16232
  // Support for passing additional arguments to pip
15905
16233
  // Eg: --python-version 3.10 --ignore-requires-python --no-warn-conflicts
@@ -15907,19 +16235,23 @@ export function getPipTreeForPackages(
15907
16235
  const addArgs = process.env.PIP_INSTALL_ARGS.split(" ");
15908
16236
  pipInstallArgs = pipInstallArgs.concat(addArgs);
15909
16237
  } else {
15910
- pipInstallArgs = pipInstallArgs.concat([
15911
- "--ignore-requires-python",
15912
- "--no-compile",
15913
- "--no-warn-script-location",
15914
- "--no-warn-conflicts",
15915
- ]);
16238
+ if (venvMeta.type !== "uv") {
16239
+ pipInstallArgs = pipInstallArgs.concat([
16240
+ "--ignore-requires-python",
16241
+ "--no-compile",
16242
+ "--no-warn-script-location",
16243
+ "--no-warn-conflicts",
16244
+ ]);
16245
+ } else {
16246
+ pipInstallArgs.push("--no-compile");
16247
+ }
15916
16248
  }
15917
16249
  if (DEBUG_MODE) {
15918
16250
  console.log(
15919
16251
  "Installing",
15920
16252
  pkgList.length,
15921
16253
  "packages using the command",
15922
- python_cmd_for_tree,
16254
+ installCmd,
15923
16255
  pipInstallArgs.join(" "),
15924
16256
  );
15925
16257
  }
@@ -15949,7 +16281,7 @@ export function getPipTreeForPackages(
15949
16281
  }
15950
16282
  // Attempt to perform pip install for pkgSpecifier
15951
16283
  const result = safeSpawnSync(
15952
- python_cmd_for_tree,
16284
+ installCmd,
15953
16285
  [...pipInstallArgs, pkgSpecifier],
15954
16286
  {
15955
16287
  cwd: basePath,
@@ -15966,6 +16298,30 @@ export function getPipTreeForPackages(
15966
16298
  }
15967
16299
  // Did any package get installed successfully?
15968
16300
  if (failedPkgList.length < pkgList.length) {
16301
+ const venvRoot = env.VIRTUAL_ENV || env.CONDA_PREFIX;
16302
+ if (venvRoot) {
16303
+ const binDir = platform() === "win32" ? "Scripts" : "bin";
16304
+ const pipExe = join(
16305
+ venvRoot,
16306
+ binDir,
16307
+ platform() === "win32" ? "pip.exe" : "pip",
16308
+ );
16309
+ if (!safeExistsSync(pipExe)) {
16310
+ if (venvMeta.type === "uv") {
16311
+ safeSpawnSync("uv", ["pip", "install", "pip"], {
16312
+ cwd: basePath,
16313
+ shell: isWin,
16314
+ env,
16315
+ });
16316
+ } else {
16317
+ safeSpawnSync(python_cmd_for_tree, ["-m", "ensurepip", "--upgrade"], {
16318
+ cwd: basePath,
16319
+ shell: isWin,
16320
+ env,
16321
+ });
16322
+ }
16323
+ }
16324
+ }
15969
16325
  const dependenciesMap = {};
15970
16326
  const tree = getTreeWithPlugin(env, python_cmd_for_tree, basePath);
15971
16327
  for (const t of tree) {
@@ -16033,6 +16389,13 @@ export function getPipTreeForPackages(
16033
16389
  }
16034
16390
 
16035
16391
  // taken from a very old package https://github.com/keithamus/parse-packagejson-name/blob/master/index.js
16392
+ /**
16393
+ * Parse a package.json `name` field (or a plain string) and extract its scope,
16394
+ * full name, project name, and module name components.
16395
+ *
16396
+ * @param {string|Object} name The package name string or an object with a `name` property
16397
+ * @returns {{ scope: string|null, fullName: string, projectName: string|null, moduleName: string|null }}
16398
+ */
16036
16399
  export function parsePackageJsonName(name) {
16037
16400
  const nameRegExp = /^(?:@([^/]+)\/)?(([^.]+)(?:\.(.*))?)$/;
16038
16401
  const returnObject = {
@@ -16169,7 +16532,12 @@ export async function addEvidenceForImports(
16169
16532
  }
16170
16533
  break;
16171
16534
  }
16172
- if (impPkgs?.length > 0 && !isImported && DEBUG_MODE) {
16535
+ if (
16536
+ impPkgs?.length > 0 &&
16537
+ !isImported &&
16538
+ DEBUG_MODE &&
16539
+ pkg?.scope !== "optional"
16540
+ ) {
16173
16541
  console.debug(
16174
16542
  `\x1b[1;35mNotice: Package ${pkg.name} has no usage in code. Check if it is needed.\x1b[0m`,
16175
16543
  );
@@ -16206,6 +16574,16 @@ export async function addEvidenceForImports(
16206
16574
  return pkgList;
16207
16575
  }
16208
16576
 
16577
+ /**
16578
+ * Comparator function for sorting CycloneDX component objects.
16579
+ *
16580
+ * Compares components by `bom-ref`, then `purl`, then `name`, using locale-aware
16581
+ * string comparison on the first available key.
16582
+ *
16583
+ * @param {Object|string} a First component to compare
16584
+ * @param {Object|string} b Second component to compare
16585
+ * @returns {number} Negative, zero, or positive integer as required by Array.sort
16586
+ */
16209
16587
  export function componentSorter(a, b) {
16210
16588
  if (a && b) {
16211
16589
  for (const k of ["bom-ref", "purl", "name"]) {
@@ -16217,6 +16595,19 @@ export function componentSorter(a, b) {
16217
16595
  return a.localeCompare(b);
16218
16596
  }
16219
16597
 
16598
+ /**
16599
+ * Parse a CMake-generated dot/graphviz file and extract components and their dependency
16600
+ * relationships.
16601
+ *
16602
+ * The first `digraph` entry becomes the parent component. Subsequent `node` entries
16603
+ * with a `label` attribute are treated as direct dependencies, while commented
16604
+ * `node -> node` relationships are used to construct the dependency graph.
16605
+ *
16606
+ * @param {string} dotFile Path to the CMake-generated dot file
16607
+ * @param {string} pkgType PackageURL type to assign to extracted packages (e.g. `"generic"`)
16608
+ * @param {Object} options CLI options; may contain `projectGroup`, `projectName`, and `projectVersion`
16609
+ * @returns {{ parentComponent: Object, pkgList: Object[], dependenciesList: Object[] }}
16610
+ */
16220
16611
  export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
16221
16612
  const dotGraphData = readFileSync(dotFile, { encoding: "utf-8" });
16222
16613
  const pkgList = [];
@@ -16326,6 +16717,19 @@ export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
16326
16717
  };
16327
16718
  }
16328
16719
 
16720
+ /**
16721
+ * Parse a CMake-like build file (CMakeLists.txt, meson.build, etc.) and extract the
16722
+ * parent component and list of dependency packages.
16723
+ *
16724
+ * Handles `set`, `project`, `find_package`, `find_library`, `find_dependency`,
16725
+ * `find_file`, `FetchContent_MakeAvailable`, and `dependency()` directives.
16726
+ * Uses the MesonWrapDB to improve name resolution confidence.
16727
+ *
16728
+ * @param {string} cmakeListFile Path to the CMake-like build file
16729
+ * @param {string} pkgType PackageURL type to assign to extracted packages (e.g. `"generic"`)
16730
+ * @param {Object} options CLI options; may contain `projectGroup`, `projectName`, and `projectVersion`
16731
+ * @returns {{ parentComponent: Object, pkgList: Object[] }}
16732
+ */
16329
16733
  export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
16330
16734
  let cmakeListData = readFileSync(cmakeListFile, { encoding: "utf-8" });
16331
16735
  const pkgList = [];
@@ -16579,6 +16983,14 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
16579
16983
  };
16580
16984
  }
16581
16985
 
16986
+ /**
16987
+ * Find the OS package component that provides a given file, by searching the
16988
+ * `PkgProvides` property of each package in the OS package list.
16989
+ *
16990
+ * @param {string} afile Filename or path to look up (matched case-insensitively)
16991
+ * @param {Object[]} osPkgsList Array of OS package component objects to search
16992
+ * @returns {Object|undefined} The matching OS package component, or undefined if not found
16993
+ */
16582
16994
  export function getOSPackageForFile(afile, osPkgsList) {
16583
16995
  for (const ospkg of osPkgsList) {
16584
16996
  for (const props of ospkg.properties || []) {
@@ -16760,7 +17172,7 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
16760
17172
  // Normalize windows separator
16761
17173
  afile = afile.replace("..\\", "").replace(/\\/g, "/");
16762
17174
  const fileName = basename(afile);
16763
- if (!fileName || !fileName.length) {
17175
+ if (!fileName?.length) {
16764
17176
  continue;
16765
17177
  }
16766
17178
  const extn = extname(fileName);
@@ -16997,7 +17409,7 @@ async function queryNuget(p, NUGET_URL) {
16997
17409
  { responseType: "json" },
16998
17410
  );
16999
17411
  const items = res.body.items;
17000
- if (!items || !items[0]) {
17412
+ if (!items?.[0]) {
17001
17413
  return [np, newBody, body];
17002
17414
  }
17003
17415
  if (items[0] && !items[0].items) {
@@ -17192,6 +17604,18 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
17192
17604
  };
17193
17605
  }
17194
17606
 
17607
+ /**
17608
+ * Enrich .NET package components with occurrence evidence and imported module/method
17609
+ * information from a dosai dependency slices file.
17610
+ *
17611
+ * Builds a mapping of DLL filenames to purls using the `PackageFiles` property of each
17612
+ * package, then reads the slices file to add occurrence locations, imported modules,
17613
+ * called methods, and assembly version information where available.
17614
+ *
17615
+ * @param {Object[]} pkgList Array of .NET package component objects to enrich
17616
+ * @param {string} slicesFile Path to the dosai dependency slices JSON file
17617
+ * @returns {Object[]} The enriched package list (same array, mutated in place)
17618
+ */
17195
17619
  export function addEvidenceForDotnet(pkgList, slicesFile) {
17196
17620
  // We need two datastructures.
17197
17621
  // dll to purl mapping from the pkgList
@@ -17626,7 +18050,7 @@ export function collectSharedLibs(
17626
18050
  }
17627
18051
 
17628
18052
  function collectAllLdConfs(basePath, ldConf, allLdConfDirs, libPaths) {
17629
- if (ldConf && existsSync(join(basePath, ldConf))) {
18053
+ if (ldConf && safeExistsSync(join(basePath, ldConf))) {
17630
18054
  const ldConfData = readFileSync(join(basePath, ldConf), "utf-8");
17631
18055
  for (let line of ldConfData.split("\n")) {
17632
18056
  line = line.replace("\r", "").trim();
@@ -17812,6 +18236,14 @@ export function retrieveCdxgenVersion() {
17812
18236
  return `\x1b[1mCycloneDX Generator ${packageJson.version}\x1b[0m\nRuntime: ${runtimeInfo.runtime}, Version: ${runtimeInfo.version}`;
17813
18237
  }
17814
18238
 
18239
+ /**
18240
+ * Retrieve the version of the cdxgen plugins binary package from package.json.
18241
+ *
18242
+ * Reads the local package.json and searches the `optionalDependencies` for a package
18243
+ * whose name starts with `@cdxgen/cdxgen-plugins-bin`, returning its declared version.
18244
+ *
18245
+ * @returns {string|undefined} Version string of the plugins binary package, or undefined if not found
18246
+ */
17815
18247
  export function retrieveCdxgenPluginVersion() {
17816
18248
  const packageJsonAsString = readFileSync(
17817
18249
  join(dirNameStr, "package.json"),
@@ -17876,3 +18308,13 @@ export function splitCommandArgs(commandString) {
17876
18308
  }
17877
18309
  return args;
17878
18310
  }
18311
+
18312
+ /**
18313
+ * Convert hyphenated strings to camel case.
18314
+ *
18315
+ * @param {String} str String to convert
18316
+ * @returns {String} camelCased string
18317
+ */
18318
+ export function toCamel(str) {
18319
+ return str.replace(/-([a-z])/g, (_, g) => g.toUpperCase());
18320
+ }