@cyclonedx/cdxgen 12.1.5 → 12.2.1

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 (193) hide show
  1. package/README.md +51 -40
  2. package/bin/cdxgen.js +194 -97
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +1 -1
  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 +449 -429
  14. package/lib/cli/index.poku.js +117 -0
  15. package/lib/evinser/db.js +137 -0
  16. package/lib/{helpers → evinser}/db.poku.js +2 -6
  17. package/lib/evinser/evinser.js +2 -14
  18. package/lib/helpers/analyzer.js +606 -3
  19. package/lib/helpers/analyzer.poku.js +230 -0
  20. package/lib/helpers/bomSigner.js +312 -0
  21. package/lib/helpers/bomSigner.poku.js +156 -0
  22. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  23. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  24. package/lib/helpers/ciParsers/circleCi.js +286 -0
  25. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  26. package/lib/helpers/ciParsers/common.js +24 -0
  27. package/lib/helpers/ciParsers/githubActions.js +636 -0
  28. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  29. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  30. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  31. package/lib/helpers/ciParsers/jenkins.js +181 -0
  32. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  33. package/lib/helpers/depsUtils.js +219 -0
  34. package/lib/helpers/depsUtils.poku.js +207 -0
  35. package/lib/helpers/display.js +426 -5
  36. package/lib/helpers/envcontext.js +18 -3
  37. package/lib/helpers/formulationParsers.js +351 -0
  38. package/lib/helpers/logger.js +14 -0
  39. package/lib/helpers/protobom.js +9 -9
  40. package/lib/helpers/pythonutils.js +9 -0
  41. package/lib/helpers/remote/dependency-track.js +84 -0
  42. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  43. package/lib/helpers/table.js +384 -0
  44. package/lib/helpers/table.poku.js +186 -0
  45. package/lib/helpers/utils.js +865 -416
  46. package/lib/helpers/utils.poku.js +172 -265
  47. package/lib/helpers/versutils.js +202 -0
  48. package/lib/helpers/versutils.poku.js +315 -0
  49. package/lib/helpers/vsixutils.js +1061 -0
  50. package/lib/helpers/vsixutils.poku.js +2247 -0
  51. package/lib/managers/binary.js +19 -19
  52. package/lib/managers/docker.js +108 -1
  53. package/lib/managers/oci.js +10 -0
  54. package/lib/managers/piptree.js +3 -9
  55. package/lib/parsers/npmrc.js +17 -13
  56. package/lib/parsers/npmrc.poku.js +41 -5
  57. package/lib/server/openapi.yaml +34 -1
  58. package/lib/server/server.js +50 -13
  59. package/lib/server/server.poku.js +332 -144
  60. package/lib/stages/postgen/annotator.js +1 -1
  61. package/lib/stages/postgen/auditBom.js +196 -0
  62. package/lib/stages/postgen/auditBom.poku.js +378 -0
  63. package/lib/stages/postgen/postgen.js +54 -1
  64. package/lib/stages/postgen/postgen.poku.js +90 -1
  65. package/lib/stages/postgen/ruleEngine.js +369 -0
  66. package/lib/stages/pregen/envAudit.js +299 -0
  67. package/lib/stages/pregen/envAudit.poku.js +572 -0
  68. package/lib/stages/pregen/pregen.js +12 -8
  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 -9
  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/analyzer.d.ts.map +1 -1
  95. package/types/lib/helpers/bomSigner.d.ts +27 -0
  96. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  97. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  98. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  99. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  100. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  101. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  102. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  103. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  104. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  105. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  106. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  107. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  108. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  109. package/types/lib/helpers/depsUtils.d.ts +21 -0
  110. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  111. package/types/lib/helpers/display.d.ts +111 -11
  112. package/types/lib/helpers/display.d.ts.map +1 -1
  113. package/types/lib/helpers/envcontext.d.ts +19 -7
  114. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  115. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  116. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  117. package/types/lib/helpers/logger.d.ts +15 -1
  118. package/types/lib/helpers/logger.d.ts.map +1 -1
  119. package/types/lib/helpers/protobom.d.ts +2 -2
  120. package/types/lib/helpers/pythonutils.d.ts +10 -1
  121. package/types/lib/helpers/pythonutils.d.ts.map +1 -1
  122. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  123. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  124. package/types/lib/helpers/table.d.ts +6 -0
  125. package/types/lib/helpers/table.d.ts.map +1 -0
  126. package/types/lib/helpers/utils.d.ts +533 -128
  127. package/types/lib/helpers/utils.d.ts.map +1 -1
  128. package/types/lib/helpers/versutils.d.ts +8 -0
  129. package/types/lib/helpers/versutils.d.ts.map +1 -0
  130. package/types/lib/helpers/vsixutils.d.ts +130 -0
  131. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  132. package/types/lib/managers/docker.d.ts +12 -31
  133. package/types/lib/managers/docker.d.ts.map +1 -1
  134. package/types/lib/managers/oci.d.ts +11 -1
  135. package/types/lib/managers/oci.d.ts.map +1 -1
  136. package/types/lib/managers/piptree.d.ts.map +1 -1
  137. package/types/lib/parsers/npmrc.d.ts +4 -1
  138. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  139. package/types/lib/server/server.d.ts +22 -2
  140. package/types/lib/server/server.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  142. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  144. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  145. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  146. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  147. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  148. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  149. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  150. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  151. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  152. package/types/lib/validator/complianceEngine.d.ts +66 -0
  153. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  154. package/types/lib/validator/complianceRules.d.ts +70 -0
  155. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  156. package/types/lib/validator/index.d.ts +70 -0
  157. package/types/lib/validator/index.d.ts.map +1 -0
  158. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  159. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  160. package/types/lib/validator/reporters/console.d.ts +30 -0
  161. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  162. package/types/lib/validator/reporters/index.d.ts +21 -0
  163. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  164. package/types/lib/validator/reporters/json.d.ts +11 -0
  165. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  166. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  167. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  168. package/lib/helpers/db.js +0 -162
  169. package/lib/stages/pregen/env-audit.js +0 -34
  170. package/lib/stages/pregen/env-audit.poku.js +0 -290
  171. package/types/helpers/db.d.ts +0 -35
  172. package/types/helpers/db.d.ts.map +0 -1
  173. package/types/lib/helpers/db.d.ts +0 -35
  174. package/types/lib/helpers/db.d.ts.map +0 -1
  175. package/types/lib/helpers/validator.d.ts.map +0 -1
  176. package/types/lib/stages/pregen/env-audit.d.ts +0 -2
  177. package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
  178. package/types/managers/binary.d.ts +0 -37
  179. package/types/managers/binary.d.ts.map +0 -1
  180. package/types/managers/docker.d.ts +0 -56
  181. package/types/managers/docker.d.ts.map +0 -1
  182. package/types/managers/oci.d.ts +0 -2
  183. package/types/managers/oci.d.ts.map +0 -1
  184. package/types/managers/piptree.d.ts +0 -2
  185. package/types/managers/piptree.d.ts.map +0 -1
  186. package/types/server/server.d.ts +0 -34
  187. package/types/server/server.d.ts.map +0 -1
  188. package/types/stages/postgen/annotator.d.ts +0 -27
  189. package/types/stages/postgen/annotator.d.ts.map +0 -1
  190. package/types/stages/postgen/postgen.d.ts +0 -51
  191. package/types/stages/postgen/postgen.d.ts.map +0 -1
  192. package/types/stages/pregen/pregen.d.ts +0 -59
  193. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -48,13 +48,13 @@ 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
60
  import { get_python_command_from_env, getVenvMetadata } from "./pythonutils.js";
@@ -79,12 +79,6 @@ export const isDeno = globalThis.Deno?.version?.deno !== undefined;
79
79
 
80
80
  export const isWin = platform() === "win32";
81
81
  export const isMac = platform() === "darwin";
82
- export let ATOM_DB = join(homedir(), ".local", "share", ".atomdb");
83
- if (isWin) {
84
- ATOM_DB = join(homedir(), "AppData", "Local", ".atomdb");
85
- } else if (isMac) {
86
- ATOM_DB = join(homedir(), "Library", "Application Support", ".atomdb");
87
- }
88
82
 
89
83
  /**
90
84
  * Safely check if a file path exists without crashing due to a lack of permissions
@@ -124,14 +118,68 @@ export function safeMkdirSync(filePath, options) {
124
118
  }
125
119
 
126
120
  export const commandsExecuted = new Set();
121
+ const ALLOW_COMMANDS = (process.env.CDXGEN_ALLOWED_COMMANDS || "").split(",");
127
122
  function isAllowedCommand(command) {
128
123
  if (!process.env.CDXGEN_ALLOWED_COMMANDS) {
129
124
  return true;
130
125
  }
131
- const allow_commands = (process.env.CDXGEN_ALLOWED_COMMANDS || "").split(",");
132
- return allow_commands.includes(command.trim());
126
+ return ALLOW_COMMANDS.includes(command.trim());
133
127
  }
134
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;
171
+ }
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
+ */
135
183
  export function safeSpawnSync(command, args, options) {
136
184
  if (
137
185
  (isSecureMode && process.permission && !process.permission.has("child")) ||
@@ -147,6 +195,25 @@ export function safeSpawnSync(command, args, options) {
147
195
  error: new Error("No execute permission"),
148
196
  };
149
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
+ }
150
217
  if (!options) {
151
218
  options = {};
152
219
  }
@@ -270,6 +337,13 @@ export const DEBUG_MODE =
270
337
  ["debug", "verbose"].includes(process.env.CDXGEN_DEBUG_MODE) ||
271
338
  process.env.SCAN_DEBUG_MODE === "debug";
272
339
 
340
+ // Table border style for console output.
341
+ export const TABLE_BORDER_STYLE = ["ascii", "unicode", "auto"].includes(
342
+ `${process.env.CDXGEN_TABLE_BORDER || ""}`.toLowerCase(),
343
+ )
344
+ ? `${process.env.CDXGEN_TABLE_BORDER}`.toLowerCase()
345
+ : "auto";
346
+
273
347
  // Timeout milliseconds. Default 20 mins
274
348
  export const TIMEOUT_MS =
275
349
  Number.parseInt(process.env.CDXGEN_TIMEOUT_MS, 10) || 20 * 60 * 1000;
@@ -303,6 +377,12 @@ export const PREFER_MAVEN_DEPS_TREE = !["false", "0"].includes(
303
377
  process.env?.PREFER_MAVEN_DEPS_TREE,
304
378
  );
305
379
 
380
+ /**
381
+ * Determines whether license information should be fetched from remote sources,
382
+ * based on the FETCH_LICENSE environment variable.
383
+ *
384
+ * @returns {boolean} True if the FETCH_LICENSE env var is set to "true" or "1"
385
+ */
306
386
  export function shouldFetchLicense() {
307
387
  return (
308
388
  process.env.FETCH_LICENSE &&
@@ -310,6 +390,12 @@ export function shouldFetchLicense() {
310
390
  );
311
391
  }
312
392
 
393
+ /**
394
+ * Determines whether VCS (version control system) information should be fetched
395
+ * for Go packages, based on the GO_FETCH_VCS environment variable.
396
+ *
397
+ * @returns {boolean} True if the GO_FETCH_VCS env var is set to "true" or "1"
398
+ */
313
399
  export function shouldFetchVCS() {
314
400
  return (
315
401
  process.env.GO_FETCH_VCS && ["true", "1"].includes(process.env.GO_FETCH_VCS)
@@ -336,6 +422,12 @@ const MAX_LICENSE_ID_LENGTH = 100;
336
422
 
337
423
  export const JAVA_CMD = getJavaCommand();
338
424
 
425
+ /**
426
+ * Returns the Java executable command to use, resolved in priority order:
427
+ * JAVA_CMD env var > JAVA_HOME/bin/java > "java".
428
+ *
429
+ * @returns {string} Path or name of the Java executable
430
+ */
339
431
  export function getJavaCommand() {
340
432
  let javaCmd = "java";
341
433
  if (process.env.JAVA_CMD) {
@@ -352,6 +444,12 @@ export function getJavaCommand() {
352
444
 
353
445
  export const PYTHON_CMD = getPythonCommand();
354
446
 
447
+ /**
448
+ * Returns the Python executable command to use, resolved in priority order:
449
+ * PYTHON_CMD env var > CONDA_PYTHON_EXE env var > "python".
450
+ *
451
+ * @returns {string} Path or name of the Python executable
452
+ */
355
453
  export function getPythonCommand() {
356
454
  let pythonCmd = "python";
357
455
  if (process.env.PYTHON_CMD) {
@@ -486,7 +584,6 @@ export const PROJECT_TYPE_ALIASES = {
486
584
  "typescript",
487
585
  "ts",
488
586
  "tsx",
489
- "vsix",
490
587
  "yarn",
491
588
  "rush",
492
589
  ],
@@ -581,6 +678,14 @@ export const PROJECT_TYPE_ALIASES = {
581
678
  scala: ["scala", "scala3", "sbt", "mill"],
582
679
  nix: ["nix", "nixos", "flake"],
583
680
  caxa: ["caxa"],
681
+ "vscode-extension": [
682
+ "vscode-extension",
683
+ "vsix",
684
+ "vscode",
685
+ "openvsx",
686
+ "vscode-extensions",
687
+ "ide-extensions",
688
+ ],
584
689
  };
585
690
 
586
691
  // Package manager aliases
@@ -1022,7 +1127,44 @@ export function getLicenses(pkg) {
1022
1127
  licenseContent.name = l;
1023
1128
  }
1024
1129
  } else if (Object.keys(l).length) {
1025
- licenseContent = l;
1130
+ licenseContent = { ...l };
1131
+ if (
1132
+ licenseContent.type &&
1133
+ !licenseContent.id &&
1134
+ !licenseContent.name &&
1135
+ !licenseContent.expression
1136
+ ) {
1137
+ if (spdxLicenses.includes(licenseContent.type)) {
1138
+ licenseContent.id = licenseContent.type;
1139
+ } else if (isSpdxLicenseExpression(licenseContent.type)) {
1140
+ licenseContent.expression = licenseContent.type;
1141
+ } else {
1142
+ licenseContent.name = licenseContent.type;
1143
+ }
1144
+ }
1145
+ if (
1146
+ !licenseContent.id &&
1147
+ !licenseContent.name &&
1148
+ !licenseContent.expression &&
1149
+ licenseContent.url?.startsWith("http")
1150
+ ) {
1151
+ const knownLicense = getKnownLicense(licenseContent.url, pkg);
1152
+ if (knownLicense) {
1153
+ if (knownLicense.id) {
1154
+ licenseContent.id = knownLicense.id;
1155
+ } else if (knownLicense.name) {
1156
+ licenseContent.name = knownLicense.name;
1157
+ }
1158
+ }
1159
+ }
1160
+ if (
1161
+ !licenseContent.id &&
1162
+ !licenseContent.name &&
1163
+ !licenseContent.expression
1164
+ ) {
1165
+ licenseContent.name = "CUSTOM";
1166
+ }
1167
+ delete licenseContent.type;
1026
1168
  } else {
1027
1169
  return undefined;
1028
1170
  }
@@ -1158,6 +1300,13 @@ export function readLicenseText(licenseFilepath, licenseContentType) {
1158
1300
  return null;
1159
1301
  }
1160
1302
 
1303
+ /**
1304
+ * Fetches license information for a list of Swift packages by querying the
1305
+ * GitHub repository license API for packages hosted on github.com.
1306
+ *
1307
+ * @param {Object[]} pkgList List of Swift package objects with optional repository.url fields
1308
+ * @returns {Promise<Object[]>} Resolved list of package objects, each augmented with a license field where available
1309
+ */
1161
1310
  export async function getSwiftPackageMetadata(pkgList) {
1162
1311
  const cdepList = [];
1163
1312
  for (const p of pkgList) {
@@ -1242,8 +1391,13 @@ export async function getNpmMetadata(pkgList) {
1242
1391
  *
1243
1392
  * @param {string} pkgJsonFile package.json file
1244
1393
  * @param {boolean} simple Return a simpler representation of the component by skipping extended attributes and license fetch.
1394
+ * @param {boolean} securityProps Collect security-related properties
1245
1395
  */
1246
- export async function parsePkgJson(pkgJsonFile, simple = false) {
1396
+ export async function parsePkgJson(
1397
+ pkgJsonFile,
1398
+ simple = false,
1399
+ securityProps = false,
1400
+ ) {
1247
1401
  const pkgList = [];
1248
1402
  if (safeExistsSync(pkgJsonFile)) {
1249
1403
  try {
@@ -1306,6 +1460,107 @@ export async function parsePkgJson(pkgJsonFile, simple = false) {
1306
1460
  },
1307
1461
  };
1308
1462
  }
1463
+ if (securityProps) {
1464
+ if (!apkg.properties) {
1465
+ apkg.properties = [];
1466
+ }
1467
+ // Track executable binaries (potential code execution vectors)
1468
+ if (pkgData.bin) {
1469
+ const binValue =
1470
+ typeof pkgData.bin === "object"
1471
+ ? Object.keys(pkgData.bin).join(", ")
1472
+ : pkgData.bin;
1473
+ apkg.properties.push({
1474
+ name: "cdx:npm:bin",
1475
+ value: binValue,
1476
+ });
1477
+ apkg.properties.push({
1478
+ name: "cdx:npm:has_binary",
1479
+ value: "true",
1480
+ });
1481
+ }
1482
+ // Track lifecycle scripts (preinstall, postinstall, etc. - code execution risk)
1483
+ if (pkgData.scripts && Object.keys(pkgData.scripts).length) {
1484
+ const scriptNames = Object.keys(pkgData.scripts).join(", ");
1485
+ apkg.properties.push({
1486
+ name: "cdx:npm:scripts",
1487
+ value: scriptNames,
1488
+ });
1489
+ // Flag high-risk scripts specifically
1490
+ const riskyScripts = [
1491
+ "preinstall",
1492
+ "install",
1493
+ "postinstall",
1494
+ "prepublish",
1495
+ "prepare",
1496
+ ].filter((script) => pkgData.scripts[script]);
1497
+ if (riskyScripts.length) {
1498
+ apkg.properties.push({
1499
+ name: "cdx:npm:risky_scripts",
1500
+ value: riskyScripts.join(", "),
1501
+ });
1502
+ }
1503
+ }
1504
+ // Track platform/architecture constraints
1505
+ if (pkgData.cpu && Array.isArray(pkgData.cpu) && pkgData.cpu.length) {
1506
+ apkg.properties.push({
1507
+ name: "cdx:npm:cpu",
1508
+ value: pkgData.cpu.join(", "),
1509
+ });
1510
+ }
1511
+ if (pkgData.os && Array.isArray(pkgData.os) && pkgData.os.length) {
1512
+ apkg.properties.push({
1513
+ name: "cdx:npm:os",
1514
+ value: pkgData.os.join(", "),
1515
+ });
1516
+ }
1517
+ if (
1518
+ pkgData.libc &&
1519
+ Array.isArray(pkgData.libc) &&
1520
+ pkgData.libc.length
1521
+ ) {
1522
+ apkg.properties.push({
1523
+ name: "cdx:npm:libc",
1524
+ value: pkgData.libc.join(", "),
1525
+ });
1526
+ }
1527
+ // Track deprecation notices
1528
+ if (pkgData.deprecated) {
1529
+ apkg.properties.push({
1530
+ name: "cdx:npm:deprecation_notice",
1531
+ value: pkgData.deprecated,
1532
+ });
1533
+ }
1534
+ // Track if package uses node-gyp (native C/C++ addons = higher risk)
1535
+ if (
1536
+ pkgData.gypfile === true ||
1537
+ pkgData.files?.some((f) => f.endsWith(".gyp") || f.endsWith(".gypi"))
1538
+ ) {
1539
+ apkg.properties.push({
1540
+ name: "cdx:npm:gypfile",
1541
+ value: "true",
1542
+ });
1543
+ apkg.properties.push({
1544
+ name: "cdx:npm:native_addon",
1545
+ value: "true",
1546
+ });
1547
+ const nativeDeps = [
1548
+ "nan",
1549
+ "node-addon-api",
1550
+ "bindings",
1551
+ "node-gyp-build",
1552
+ ];
1553
+ const foundNativeDeps = Object.keys(
1554
+ pkgData.dependencies || {},
1555
+ ).filter((dep) => nativeDeps.includes(dep));
1556
+ if (foundNativeDeps.length) {
1557
+ apkg.properties.push({
1558
+ name: "cdx:npm:native_deps",
1559
+ value: foundNativeDeps.join(", "),
1560
+ });
1561
+ }
1562
+ }
1563
+ }
1309
1564
  pkgList.push(apkg);
1310
1565
  } catch (_err) {
1311
1566
  // continue regardless of error
@@ -1361,7 +1616,8 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1361
1616
  const srcFilePath = node.path.includes(`${_sep}node_modules`)
1362
1617
  ? node.path.split(`${_sep}node_modules`)[0]
1363
1618
  : node.path;
1364
- const scope = node.dev === true ? "optional" : undefined;
1619
+ const scope =
1620
+ node.dev === true || node.optional === true ? "optional" : undefined;
1365
1621
  const integrity = node.integrity ? node.integrity : undefined;
1366
1622
 
1367
1623
  let pkg;
@@ -1434,6 +1690,9 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1434
1690
  purl: purlString,
1435
1691
  "bom-ref": decodeURIComponent(purlString),
1436
1692
  };
1693
+ if (node.dev === true) {
1694
+ _setNpmDevelopmentProperty(pkg);
1695
+ }
1437
1696
  if (node.resolved) {
1438
1697
  if (node.resolved.startsWith("file:")) {
1439
1698
  pkg.properties.push({
@@ -1450,7 +1709,10 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1450
1709
  name: "ResolvedUrl",
1451
1710
  value: node.resolved,
1452
1711
  });
1453
- pkg.distribution = { url: node.resolved };
1712
+ pkg.externalReferences.push({
1713
+ type: "distribution",
1714
+ url: node.resolved,
1715
+ });
1454
1716
  }
1455
1717
  }
1456
1718
  if (node.location) {
@@ -1731,7 +1993,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1731
1993
  if (!targetVersion || !targetName) {
1732
1994
  if (pkgSpecVersionCache[`${edge.name}-${edge.spec}`]) {
1733
1995
  targetVersion = pkgSpecVersionCache[`${edge.name}-${edge.spec}`];
1734
- targetName = edge.name;
1996
+ targetName = edge.name.replace(/-cjs$/, "");
1735
1997
  }
1736
1998
  }
1737
1999
  }
@@ -2612,6 +2874,51 @@ function _markTreeOptional(
2612
2874
  }
2613
2875
  }
2614
2876
 
2877
+ function _markTreeDevelopment(
2878
+ dbomRef,
2879
+ dependenciesMap,
2880
+ possibleDevelopmentDeps,
2881
+ visited,
2882
+ ) {
2883
+ // Production-required packages set this map entry to false, and that wins
2884
+ // over any later attempt to propagate a development-only marking.
2885
+ if (possibleDevelopmentDeps[dbomRef] === undefined) {
2886
+ possibleDevelopmentDeps[dbomRef] = true;
2887
+ }
2888
+ if (dependenciesMap[dbomRef] && !visited[dbomRef]) {
2889
+ visited[dbomRef] = true;
2890
+ for (const eachDep of dependenciesMap[dbomRef]) {
2891
+ // Undefined means we have not classified this dependency yet, so we
2892
+ // continue propagating the dev-only marking unless it was already proven
2893
+ // to be non-development via a false entry.
2894
+ if (possibleDevelopmentDeps[eachDep] !== false) {
2895
+ _markTreeDevelopment(
2896
+ eachDep,
2897
+ dependenciesMap,
2898
+ possibleDevelopmentDeps,
2899
+ visited,
2900
+ );
2901
+ }
2902
+ }
2903
+ }
2904
+ }
2905
+
2906
+ function _setNpmDevelopmentProperty(pkg) {
2907
+ if (!pkg.properties) {
2908
+ pkg.properties = [];
2909
+ }
2910
+ if (
2911
+ !pkg.properties.some((property) => {
2912
+ return property.name === "cdx:npm:package:development";
2913
+ })
2914
+ ) {
2915
+ pkg.properties.push({
2916
+ name: "cdx:npm:package:development",
2917
+ value: "true",
2918
+ });
2919
+ }
2920
+ }
2921
+
2615
2922
  function _setTreeWorkspaceRef(
2616
2923
  dependenciesMap,
2617
2924
  depref,
@@ -2774,6 +3081,13 @@ function findMatchingWorkspace(workspacePackages, packageName) {
2774
3081
  );
2775
3082
  }
2776
3083
 
3084
+ /**
3085
+ * Parses the workspaces field from a package.json file and returns the list of
3086
+ * workspace glob patterns. Handles both array and object (with packages key) formats.
3087
+ *
3088
+ * @param {string} packageJsonFile Path to the package.json file to parse
3089
+ * @returns {Object} Object with a packages array of workspace glob patterns, or an empty object on error
3090
+ */
2777
3091
  export function parseYarnWorkspace(packageJsonFile) {
2778
3092
  try {
2779
3093
  const packageData = JSON.parse(readFileSync(packageJsonFile, "utf-8"));
@@ -2872,42 +3186,28 @@ export async function pnpmMetadata(pkgList, lockFilePath) {
2872
3186
  if (!pkgList?.length || !lockFilePath) {
2873
3187
  return pkgList;
2874
3188
  }
2875
-
2876
3189
  const baseDir = dirname(lockFilePath);
2877
3190
  const nodeModulesDir = join(baseDir, "node_modules");
2878
-
2879
- // Only proceed if node_modules exists
2880
3191
  if (!safeExistsSync(nodeModulesDir)) {
2881
3192
  return pkgList;
2882
3193
  }
2883
-
2884
3194
  if (DEBUG_MODE) {
2885
3195
  console.log(
2886
3196
  `Metadata for ${pkgList.length} pnpm packages using local node_modules at ${nodeModulesDir}`,
2887
3197
  );
2888
3198
  }
2889
-
2890
3199
  let enhancedCount = 0;
2891
3200
  for (const pkg of pkgList) {
2892
- // Skip if package already has complete metadata
2893
- if (pkg.description && pkg.author && pkg.license) {
2894
- continue;
2895
- }
2896
-
2897
- // Find the package path in node_modules
2898
3201
  const packagePath = findPnpmPackagePath(baseDir, pkg.name, pkg.version);
2899
3202
  if (!packagePath) {
2900
3203
  continue;
2901
3204
  }
2902
-
2903
3205
  const packageJsonPath = join(packagePath, "package.json");
2904
3206
  if (!safeExistsSync(packageJsonPath)) {
2905
3207
  continue;
2906
3208
  }
2907
-
2908
3209
  try {
2909
- // Parse the local package.json to get metadata
2910
- const localPkgList = await parsePkgJson(packageJsonPath, true);
3210
+ const localPkgList = await parsePkgJson(packageJsonPath, true, true);
2911
3211
  if (localPkgList && localPkgList.length === 1) {
2912
3212
  const localMetadata = localPkgList[0];
2913
3213
  if (localMetadata && Object.keys(localMetadata).length) {
@@ -2926,16 +3226,27 @@ export async function pnpmMetadata(pkgList, lockFilePath) {
2926
3226
  if (!pkg.repository && localMetadata.repository) {
2927
3227
  pkg.repository = localMetadata.repository;
2928
3228
  }
2929
-
2930
- // Add a property to track that we enhanced from local node_modules
2931
3229
  if (!pkg.properties) {
2932
3230
  pkg.properties = [];
2933
3231
  }
3232
+ if (localMetadata?.properties?.length) {
3233
+ const seenProperties = new Set(
3234
+ pkg.properties.map(
3235
+ (prop) => `${String(prop?.name)}\u0000${String(prop?.value)}`,
3236
+ ),
3237
+ );
3238
+ for (const prop of localMetadata.properties) {
3239
+ const propertyKey = `${String(prop?.name)}\u0000${String(prop?.value)}`;
3240
+ if (!seenProperties.has(propertyKey)) {
3241
+ pkg.properties.push(prop);
3242
+ seenProperties.add(propertyKey);
3243
+ }
3244
+ }
3245
+ }
2934
3246
  pkg.properties.push({
2935
3247
  name: "LocalNodeModulesPath",
2936
3248
  value: packagePath,
2937
3249
  });
2938
-
2939
3250
  enhancedCount++;
2940
3251
  }
2941
3252
  }
@@ -2949,13 +3260,11 @@ export async function pnpmMetadata(pkgList, lockFilePath) {
2949
3260
  }
2950
3261
  }
2951
3262
  }
2952
-
2953
3263
  if (DEBUG_MODE && enhancedCount > 0) {
2954
3264
  console.log(
2955
3265
  `Enhanced metadata for ${enhancedCount} packages from local node_modules`,
2956
3266
  );
2957
3267
  }
2958
-
2959
3268
  return pkgList;
2960
3269
  }
2961
3270
 
@@ -2985,6 +3294,7 @@ export async function parsePnpmLock(
2985
3294
  // See: #1163
2986
3295
  // Moreover, we have changed >= 9 for >= 6
2987
3296
  // See: discussion #1359
3297
+ const possibleDevelopmentDeps = {};
2988
3298
  const possibleOptionalDeps = {};
2989
3299
  const dependenciesMap = {};
2990
3300
  let ppurl = "";
@@ -3018,10 +3328,18 @@ export async function parsePnpmLock(
3018
3328
  }
3019
3329
  if (safeExistsSync(pnpmLock)) {
3020
3330
  const lockData = readFileSync(pnpmLock, "utf8");
3021
- const yamlObj = _load(lockData);
3331
+ let yamlObj = parseAllDocuments(lockData);
3022
3332
  if (!yamlObj) {
3023
3333
  return {};
3024
3334
  }
3335
+ if (Array.isArray(yamlObj)) {
3336
+ try {
3337
+ yamlObj = yamlObj[yamlObj.length - 1].toJS();
3338
+ } catch (_e) {
3339
+ console.log(`Unable to parse the pnpm lock file ${pnpmLock}.`);
3340
+ return {};
3341
+ }
3342
+ }
3025
3343
  lockfileVersion = yamlObj.lockfileVersion;
3026
3344
  try {
3027
3345
  lockfileVersion = Number.parseFloat(lockfileVersion, 10);
@@ -3069,7 +3387,7 @@ export async function parsePnpmLock(
3069
3387
  null,
3070
3388
  null,
3071
3389
  ).toString();
3072
- possibleOptionalDeps[decodeURIComponent(dpurl)] = true;
3390
+ possibleDevelopmentDeps[decodeURIComponent(dpurl)] = true;
3073
3391
  }
3074
3392
  // Find the root optional and peer dependencies
3075
3393
  for (const rdk of Object.keys({ ...rootOptionalDeps, ...rootPeerDeps })) {
@@ -3093,6 +3411,7 @@ export async function parsePnpmLock(
3093
3411
  null,
3094
3412
  ).toString();
3095
3413
  possibleOptionalDeps[decodeURIComponent(dpurl)] = true;
3414
+ possibleDevelopmentDeps[decodeURIComponent(dpurl)] = false;
3096
3415
  }
3097
3416
  // Find the root direct dependencies
3098
3417
  for (const dk of Object.keys(rootDirectDeps)) {
@@ -3120,6 +3439,7 @@ export async function parsePnpmLock(
3120
3439
  // These are direct dependencies so cannot be optional
3121
3440
  possibleOptionalDeps[decodeURIComponent(dpurl)] = false;
3122
3441
  }
3442
+ possibleDevelopmentDeps[decodeURIComponent(dpurl)] = false;
3123
3443
  }
3124
3444
  // pnpm-lock.yaml contains more than root dependencies in importers
3125
3445
  // we do what we did above but for all the other components
@@ -3245,6 +3565,7 @@ export async function parsePnpmLock(
3245
3565
  // This is a definite dependency of this component
3246
3566
  comDepList.add(depRef);
3247
3567
  possibleOptionalDeps[depRef] = false;
3568
+ possibleDevelopmentDeps[depRef] = false;
3248
3569
  // Track the package.json files
3249
3570
  if (pkgSrcFile) {
3250
3571
  if (!srcFilesMap[depRef]) {
@@ -3264,7 +3585,7 @@ export async function parsePnpmLock(
3264
3585
  null,
3265
3586
  ).toString();
3266
3587
  const devDpRef = decodeURIComponent(dpurl);
3267
- possibleOptionalDeps[devDpRef] = true;
3588
+ possibleDevelopmentDeps[devDpRef] = true;
3268
3589
  // This is also a dependency of this component
3269
3590
  comDepList.add(devDpRef);
3270
3591
  }
@@ -3282,6 +3603,7 @@ export async function parsePnpmLock(
3282
3603
  null,
3283
3604
  ).toString();
3284
3605
  possibleOptionalDeps[decodeURIComponent(dpurl)] = true;
3606
+ possibleDevelopmentDeps[decodeURIComponent(dpurl)] = false;
3285
3607
  }
3286
3608
  dependenciesList.push({
3287
3609
  ref: decodeURIComponent(compPurl),
@@ -3320,6 +3642,7 @@ export async function parsePnpmLock(
3320
3642
  packages[fullName]?.resolution ||
3321
3643
  snapshots[fullName]?.resolution;
3322
3644
  const integrity = resolution?.integrity;
3645
+ const tarball = resolution?.tarball;
3323
3646
  const cpu =
3324
3647
  packages[pkgKeys[k]]?.cpu ||
3325
3648
  snapshots[pkgKeys[k]]?.cpu ||
@@ -3483,7 +3806,21 @@ export async function parsePnpmLock(
3483
3806
  null,
3484
3807
  ).toString();
3485
3808
  const bomRef = decodeURIComponent(purlString);
3809
+ if (
3810
+ packageNode.dev === true &&
3811
+ possibleDevelopmentDeps[bomRef] === undefined
3812
+ ) {
3813
+ possibleDevelopmentDeps[bomRef] = true;
3814
+ }
3486
3815
  const isBaseOptional = possibleOptionalDeps[bomRef];
3816
+ // optionalDependencies are tracked separately because they may still
3817
+ // be runtime-relevant and should keep the CycloneDX optional scope.
3818
+ // packageNode.dev captures explicit dev-only packages from the lock
3819
+ // entry, while possibleDevelopmentDeps lets that marking propagate to
3820
+ // transitive dependencies discovered through the dependency graph.
3821
+ const isBaseDevelopment =
3822
+ packageNode.dev === true ||
3823
+ possibleDevelopmentDeps[bomRef] === true;
3487
3824
  const deplist = [];
3488
3825
  for (let dpkgName of Object.keys(deps)) {
3489
3826
  let vers = deps[dpkgName];
@@ -3526,6 +3863,18 @@ export async function parsePnpmLock(
3526
3863
  {},
3527
3864
  );
3528
3865
  }
3866
+ if (
3867
+ isBaseDevelopment &&
3868
+ possibleDevelopmentDeps[dbomRef] === undefined
3869
+ ) {
3870
+ possibleDevelopmentDeps[dbomRef] = true;
3871
+ _markTreeDevelopment(
3872
+ dbomRef,
3873
+ dependenciesMap,
3874
+ possibleDevelopmentDeps,
3875
+ {},
3876
+ );
3877
+ }
3529
3878
  }
3530
3879
  if (!dependenciesMap[bomRef]) {
3531
3880
  dependenciesMap[bomRef] = [];
@@ -3538,10 +3887,10 @@ export async function parsePnpmLock(
3538
3887
  value: pnpmLock,
3539
3888
  },
3540
3889
  ];
3541
- if (hasBin || os || cpu || libc) {
3890
+ if (hasBin) {
3542
3891
  properties.push({
3543
3892
  name: "cdx:npm:has_binary",
3544
- value: `${hasBin}`,
3893
+ value: "true",
3545
3894
  });
3546
3895
  }
3547
3896
  if (deprecatedMessage) {
@@ -3554,7 +3903,7 @@ export async function parsePnpmLock(
3554
3903
  Object.entries(binary_metadata).forEach(([key, value]) => {
3555
3904
  if (!value) return;
3556
3905
  properties.push({
3557
- name: `cdx:pnpm:${key}`,
3906
+ name: `cdx:npm:${key}`,
3558
3907
  value: Array.isArray(value) ? value.join(", ") : value,
3559
3908
  });
3560
3909
  });
@@ -3652,6 +4001,14 @@ export async function parsePnpmLock(
3652
4001
  },
3653
4002
  },
3654
4003
  };
4004
+ if (tarball) {
4005
+ thePkg.externalReferences = [
4006
+ {
4007
+ type: "distribution",
4008
+ url: tarball,
4009
+ },
4010
+ ];
4011
+ }
3655
4012
  // Don't add internal workspace packages to the components list
3656
4013
  if (thePkg.type !== "application") {
3657
4014
  pkgList.push(thePkg);
@@ -3677,6 +4034,19 @@ export async function parsePnpmLock(
3677
4034
  }
3678
4035
  }
3679
4036
  }
4037
+ // Repeat development dependency detection after the dependency graph is fully
4038
+ // built, since a single package iteration can encounter a dev-only component
4039
+ // before its own dependency list has been captured in dependenciesMap.
4040
+ for (const dependencyRef of Object.keys(possibleDevelopmentDeps)) {
4041
+ if (possibleDevelopmentDeps[dependencyRef] === true) {
4042
+ _markTreeDevelopment(
4043
+ dependencyRef,
4044
+ dependenciesMap,
4045
+ possibleDevelopmentDeps,
4046
+ {},
4047
+ );
4048
+ }
4049
+ }
3680
4050
 
3681
4051
  // Problem: We might have over aggressively marked a package as optional even it is both required and optional
3682
4052
  // The below loops ensure required packages continue to stay required
@@ -3707,6 +4077,15 @@ export async function parsePnpmLock(
3707
4077
  if (requiredDependencies[apkg["bom-ref"]]) {
3708
4078
  apkg.scope = undefined;
3709
4079
  }
4080
+ if (
4081
+ !requiredDependencies[apkg["bom-ref"]] &&
4082
+ possibleDevelopmentDeps[apkg["bom-ref"]]
4083
+ ) {
4084
+ if (!apkg.scope) {
4085
+ apkg.scope = "optional";
4086
+ }
4087
+ _setNpmDevelopmentProperty(apkg);
4088
+ }
3710
4089
  if (possibleAliasesRefs[apkg["bom-ref"]]) {
3711
4090
  apkg.properties.push({
3712
4091
  name: "cdx:pnpm:alias",
@@ -4685,6 +5064,15 @@ export function parseLeinDep(rawOutput) {
4685
5064
  return [];
4686
5065
  }
4687
5066
 
5067
+ /**
5068
+ * Recursively walks a parsed EDN map node produced by the Leiningen dependency
5069
+ * tree and collects unique dependency entries into the deps array.
5070
+ *
5071
+ * @param {Object} node Parsed EDN node (expected to have a "map" property)
5072
+ * @param {Object} keys_cache Cache object used to deduplicate entries by group-name-version key
5073
+ * @param {Object[]} deps Accumulator array of dependency objects with group, name, and version fields
5074
+ * @returns {Object[]} The populated deps array
5075
+ */
4688
5076
  export function parseLeinMap(node, keys_cache, deps) {
4689
5077
  if (node["map"]) {
4690
5078
  for (const n of node["map"]) {
@@ -7302,6 +7690,16 @@ async function getGoPkgVCSUrl(group, name) {
7302
7690
  return undefined;
7303
7691
  }
7304
7692
 
7693
+ /**
7694
+ * Builds a Go package component object containing purl, bom-ref, integrity hash,
7695
+ * and optionally license and VCS external reference information.
7696
+ *
7697
+ * @param {string} group Package group (module path prefix, may be empty)
7698
+ * @param {string} name Package name (full module path when group is empty)
7699
+ * @param {string} version Package version string
7700
+ * @param {string} hash Integrity hash (e.g. "sha256-…"), used as _integrity
7701
+ * @returns {Promise<Object>} Component object ready for inclusion in a BOM package list
7702
+ */
7305
7703
  export async function getGoPkgComponent(group, name, version, hash) {
7306
7704
  let license;
7307
7705
  if (shouldFetchLicense()) {
@@ -7485,6 +7883,15 @@ export async function parseGoModData(goModData, gosumMap) {
7485
7883
  };
7486
7884
  }
7487
7885
 
7886
+ /**
7887
+ * Parses a Go modules text file (e.g. vendor/modules.txt) and returns a list of
7888
+ * Go package components. Cross-references the go.sum map for integrity hashes and
7889
+ * sets scope and confidence based on hash availability.
7890
+ *
7891
+ * @param {string} txtFile Path to the modules.txt file
7892
+ * @param {Object} gosumMap Map of "module@version" keys to sha256 hash values from go.sum
7893
+ * @returns {Promise<Object[]>} List of Go package component objects with evidence
7894
+ */
7488
7895
  export async function parseGoModulesTxt(txtFile, gosumMap) {
7489
7896
  const pkgList = [];
7490
7897
  const txtData = readFileSync(txtFile, { encoding: "utf-8" });
@@ -7888,6 +8295,14 @@ export async function parseGosumData(gosumData) {
7888
8295
  return pkgList;
7889
8296
  }
7890
8297
 
8298
+ /**
8299
+ * Parses the contents of a Gopkg.lock or Gopkg.toml file (dep tool format) and
8300
+ * returns a list of Go package components. Optionally fetches license information
8301
+ * for each package when FETCH_LICENSE is enabled.
8302
+ *
8303
+ * @param {string} gopkgData Raw string contents of the Gopkg lock/toml file
8304
+ * @returns {Promise<Object[]>} List of Go package component objects
8305
+ */
7891
8306
  export async function parseGopkgData(gopkgData) {
7892
8307
  const pkgList = [];
7893
8308
  if (!gopkgData) {
@@ -7937,6 +8352,13 @@ export async function parseGopkgData(gopkgData) {
7937
8352
  return pkgList;
7938
8353
  }
7939
8354
 
8355
+ /**
8356
+ * Parses the output of `go version -m` (build info) and returns a list of Go
8357
+ * package components for each "dep" line, including name, version, and integrity hash.
8358
+ *
8359
+ * @param {string} buildInfoData Raw string output from `go version -m`
8360
+ * @returns {Promise<Object[]>} List of Go package component objects
8361
+ */
7940
8362
  export async function parseGoVersionData(buildInfoData) {
7941
8363
  const pkgList = [];
7942
8364
  if (!buildInfoData) {
@@ -9263,6 +9685,13 @@ export async function parseCargoData(
9263
9685
  return pkgList;
9264
9686
  }
9265
9687
 
9688
+ /**
9689
+ * Parses a Cargo.lock file's TOML data and returns a flat dependency graph as an
9690
+ * array of objects mapping each package purl to the purls it directly depends on.
9691
+ *
9692
+ * @param {string} cargoLockData Raw TOML string contents of a Cargo.lock file
9693
+ * @returns {Object[]} Array of dependency relationship objects with ref and dependsOn fields
9694
+ */
9266
9695
  export function parseCargoDependencyData(cargoLockData) {
9267
9696
  const purlFromPackageInfo = (pkg) =>
9268
9697
  decodeURIComponent(
@@ -9322,6 +9751,14 @@ export function parseCargoDependencyData(cargoLockData) {
9322
9751
  return result;
9323
9752
  }
9324
9753
 
9754
+ /**
9755
+ * Parses tab-separated cargo-auditable binary metadata output and returns a list
9756
+ * of Rust package components. Optionally fetches crates.io metadata when
9757
+ * FETCH_LICENSE is enabled.
9758
+ *
9759
+ * @param {string} cargoData Tab-separated string output from cargo-auditable or similar tool
9760
+ * @returns {Promise<Object[]>} List of Rust package component objects with group, name, and version
9761
+ */
9325
9762
  export async function parseCargoAuditableData(cargoData) {
9326
9763
  const pkgList = [];
9327
9764
  if (!cargoData) {
@@ -9424,6 +9861,13 @@ export async function parsePubLockData(pubLockData, lockFile) {
9424
9861
  return { rootList, pkgList };
9425
9862
  }
9426
9863
 
9864
+ /**
9865
+ * Parses a Dart pub package's pubspec.yaml content and returns a list containing
9866
+ * a single component object with name, description, version, homepage, and purl.
9867
+ *
9868
+ * @param {string} pubYamlData Raw YAML string contents of a pubspec.yaml file
9869
+ * @returns {Object[]} List containing a single Dart package component object
9870
+ */
9427
9871
  export function parsePubYamlData(pubYamlData) {
9428
9872
  const pkgList = [];
9429
9873
  let yamlObj;
@@ -9450,6 +9894,14 @@ export function parsePubYamlData(pubYamlData) {
9450
9894
  return pkgList;
9451
9895
  }
9452
9896
 
9897
+ /**
9898
+ * Parses Helm chart YAML data (Chart.yaml or repository index.yaml) and returns
9899
+ * a list of Helm chart component objects including the chart itself and any
9900
+ * declared dependencies or index entries.
9901
+ *
9902
+ * @param {string} helmData Raw YAML string contents of a Helm Chart.yaml or index.yaml file
9903
+ * @returns {Object[]} List of Helm chart component objects with name, version, and optional homepage/repository
9904
+ */
9453
9905
  export function parseHelmYamlData(helmData) {
9454
9906
  const pkgList = [];
9455
9907
  let yamlObj;
@@ -9515,6 +9967,17 @@ export function parseHelmYamlData(helmData) {
9515
9967
  return pkgList;
9516
9968
  }
9517
9969
 
9970
+ /**
9971
+ * Recursively walks a parsed YAML/JSON object structure to find container image
9972
+ * references stored under common keys (image, repository, dockerImage, etc.) and
9973
+ * appends discovered image and service entries to pkgList while tracking seen
9974
+ * images in imgList to avoid duplicates.
9975
+ *
9976
+ * @param {Object|Array|string} keyValueObj The object, array, or string node to inspect
9977
+ * @param {Object[]} pkgList Accumulator array that receives {image} and {service} entries
9978
+ * @param {string[]} imgList Accumulator array of image name strings already seen
9979
+ * @returns {string[]} The updated imgList
9980
+ */
9518
9981
  export function recurseImageNameLookup(keyValueObj, pkgList, imgList) {
9519
9982
  if (typeof keyValueObj === "string" || keyValueObj instanceof String) {
9520
9983
  return imgList;
@@ -9600,6 +10063,14 @@ function substituteBuildArgs(statement, buildArgs) {
9600
10063
  return statement;
9601
10064
  }
9602
10065
 
10066
+ /**
10067
+ * Parses the contents of a Dockerfile or Containerfile and returns a list of
10068
+ * base image objects referenced by FROM instructions, substituting ARG default
10069
+ * values where possible and skipping multi-stage build alias references.
10070
+ *
10071
+ * @param {string} fileContents Raw string contents of the Dockerfile/Containerfile
10072
+ * @returns {Object[]} Array of objects with an image property for each unique base image
10073
+ */
9603
10074
  export function parseContainerFile(fileContents) {
9604
10075
  const buildArgs = new Map();
9605
10076
  const imagesSet = new Set();
@@ -9667,6 +10138,13 @@ export function parseContainerFile(fileContents) {
9667
10138
  });
9668
10139
  }
9669
10140
 
10141
+ /**
10142
+ * Parses a Bitbucket Pipelines YAML file and extracts all Docker image references
10143
+ * used as build environments and pipe references (docker:// pipes are normalized).
10144
+ *
10145
+ * @param {string} fileContents Raw string contents of the bitbucket-pipelines.yml file
10146
+ * @returns {Object[]} Array of objects with an image property for each referenced image or pipe
10147
+ */
9670
10148
  export function parseBitbucketPipelinesFile(fileContents) {
9671
10149
  const imgList = [];
9672
10150
 
@@ -9728,6 +10206,14 @@ export function parseBitbucketPipelinesFile(fileContents) {
9728
10206
  return imgList;
9729
10207
  }
9730
10208
 
10209
+ /**
10210
+ * Parses container specification data such as Docker Compose files, Kubernetes
10211
+ * manifests, Tekton tasks, Skaffold configs, or Kustomize overlays (YAML, possibly
10212
+ * multi-document) and returns a list of image, service, and OCI spec entries.
10213
+ *
10214
+ * @param {string} dcData Raw YAML string contents of the container spec file
10215
+ * @returns {Object[]} Array of objects with image, service, or ociSpec properties
10216
+ */
9731
10217
  export function parseContainerSpecData(dcData) {
9732
10218
  const pkgList = [];
9733
10219
  const imgList = [];
@@ -9795,6 +10281,14 @@ export function parseContainerSpecData(dcData) {
9795
10281
  return pkgList;
9796
10282
  }
9797
10283
 
10284
+ /**
10285
+ * Identifies the data flow direction of a Privado processing object based on its
10286
+ * sinkId value: "write" sinks map to "inbound", "read" sinks to "outbound", and
10287
+ * HTTP/gRPC sinks to "bi-directional".
10288
+ *
10289
+ * @param {Object} processingObj Privado processing object, expected to have a sinkId property
10290
+ * @returns {string} Flow direction string: "inbound", "outbound", "bi-directional", or "unknown"
10291
+ */
9798
10292
  export function identifyFlow(processingObj) {
9799
10293
  let flow = "unknown";
9800
10294
  if (processingObj.sinkId) {
@@ -9821,6 +10315,14 @@ function convertProcessing(processing_list) {
9821
10315
  return data_list;
9822
10316
  }
9823
10317
 
10318
+ /**
10319
+ * Parses a Privado data flow JSON file and returns a list of service objects
10320
+ * enriched with data classifications, endpoints, trust-boundary flag, violations,
10321
+ * and git metadata properties extracted from the scan result.
10322
+ *
10323
+ * @param {string} f Path to the Privado scan result JSON file
10324
+ * @returns {Object[]} List of service component objects suitable for a SaaSBOM
10325
+ */
9824
10326
  export function parsePrivadoFile(f) {
9825
10327
  const pData = readFileSync(f, { encoding: "utf-8" });
9826
10328
  const servlist = [];
@@ -9902,6 +10404,15 @@ export function parsePrivadoFile(f) {
9902
10404
  return servlist;
9903
10405
  }
9904
10406
 
10407
+ /**
10408
+ * Parses an OpenAPI specification (JSON or YAML string) and returns a list
10409
+ * containing a single service object with name, version, endpoints, and
10410
+ * authentication flag derived from the spec's info, servers, paths, and
10411
+ * securitySchemes sections.
10412
+ *
10413
+ * @param {string} oaData Raw JSON or YAML string contents of an OpenAPI specification
10414
+ * @returns {Object[]} List containing a single service component object
10415
+ */
9905
10416
  export function parseOpenapiSpecData(oaData) {
9906
10417
  const servlist = [];
9907
10418
  if (!oaData) {
@@ -9954,6 +10465,13 @@ export function parseOpenapiSpecData(oaData) {
9954
10465
  return servlist;
9955
10466
  }
9956
10467
 
10468
+ /**
10469
+ * Parses Haskell Cabal freeze file content and extracts package name and version
10470
+ * pairs from constraint lines (lines containing " ==").
10471
+ *
10472
+ * @param {string} cabalData Raw string contents of a Cabal freeze file
10473
+ * @returns {Object[]} List of package objects with name and version fields
10474
+ */
9957
10475
  export function parseCabalData(cabalData) {
9958
10476
  const pkgList = [];
9959
10477
  if (!cabalData) {
@@ -9982,6 +10500,13 @@ export function parseCabalData(cabalData) {
9982
10500
  return pkgList;
9983
10501
  }
9984
10502
 
10503
+ /**
10504
+ * Parses an Elixir mix.lock file and extracts Hex package name and version pairs
10505
+ * from lines containing ":hex".
10506
+ *
10507
+ * @param {string} mixData Raw string contents of a mix.lock file
10508
+ * @returns {Object[]} List of package objects with name and version fields
10509
+ */
9985
10510
  export function parseMixLockData(mixData) {
9986
10511
  const pkgList = [];
9987
10512
  if (!mixData) {
@@ -10009,376 +10534,27 @@ export function parseMixLockData(mixData) {
10009
10534
  return pkgList;
10010
10535
  }
10011
10536
 
10012
- export function parseGitHubWorkflowData(f) {
10013
- const pkgList = [];
10014
- if (!f) {
10015
- return pkgList;
10016
- }
10017
- const ghwData = readFileSync(f, { encoding: "utf-8" });
10018
- const keys_cache = {};
10019
- if (!ghwData) {
10020
- return pkgList;
10021
- }
10022
- const yamlObj = _load(ghwData);
10023
- if (!yamlObj) {
10024
- return pkgList;
10025
- }
10026
- const lines = ghwData.split("\n");
10027
- // workflow-related values
10028
- const workflowName = yamlObj.name || path.basename(f, path.extname(f));
10029
- const workflowTriggers = yamlObj.on || yamlObj.true;
10030
- const workflowPermissions = yamlObj.permissions || {};
10031
- let hasWritePermissions = analyzePermissions(workflowPermissions);
10032
- // GitHub of course supports strings such as "write-all" to make it easy to create supply-chain attacks.
10033
- if (
10034
- (typeof workflowPermissions === "string" ||
10035
- workflowPermissions instanceof String) &&
10036
- workflowPermissions.includes("write")
10037
- ) {
10038
- hasWritePermissions = true;
10039
- }
10040
- const workflowConcurrency = yamlObj.concurrency || {};
10041
- const _workflowEnv = yamlObj.env || {};
10042
- const hasIdTokenWrite = workflowPermissions?.["id-token"] === "write";
10043
- for (const jobName of Object.keys(yamlObj.jobs)) {
10044
- const job = yamlObj.jobs[jobName];
10045
- if (!job.steps) {
10046
- continue;
10047
- }
10048
- // job-related values
10049
- const jobRunner = job["runs-on"] || "unknown";
10050
- const jobEnvironment = job.environment?.name || job.environment || "";
10051
- const jobTimeout = job["timeout-minutes"] || null;
10052
- const jobPermissions = job.permissions || {};
10053
- const jobServices = job.services ? Object.keys(job.services) : [];
10054
- let jobNeeds = job.needs || [];
10055
- if (!Array.isArray(jobNeeds)) {
10056
- jobNeeds = [jobNeeds];
10057
- }
10058
- const _jobIf = job.if || "";
10059
- const _jobStrategy = job.strategy ? JSON.stringify(job.strategy) : "";
10060
- const jobHasWritePermissions = analyzePermissions(jobPermissions);
10061
- for (const step of job.steps) {
10062
- if (step.uses) {
10063
- const tmpA = step.uses.split("@");
10064
- if (tmpA.length !== 2) {
10065
- continue;
10066
- }
10067
- const groupName = tmpA[0];
10068
- let name = groupName;
10069
- let group = "";
10070
- const tagOrCommit = tmpA[1];
10071
- let version = tagOrCommit;
10072
- const tmpB = groupName.split("/");
10073
- if (tmpB.length >= 2) {
10074
- name = tmpB.pop();
10075
- group = tmpB.join("/");
10076
- } else if (tmpB.length === 1) {
10077
- name = tmpB[0];
10078
- group = "";
10079
- }
10080
- const versionPinningType = getVersionPinningType(tagOrCommit);
10081
- const isShaPinned = versionPinningType === "sha";
10082
- const _isTagPinned = versionPinningType === "tag";
10083
- const _isBranchRef = versionPinningType === "branch";
10084
- let lineNum = -1;
10085
- const stepLineMatch = ghwData.indexOf(step.uses);
10086
- if (stepLineMatch >= 0) {
10087
- lineNum = ghwData.substring(0, stepLineMatch).split("\n").length - 1;
10088
- }
10089
- if (lineNum >= 0 && lines[lineNum]) {
10090
- const line = lines[lineNum];
10091
- const commentMatch = line.match(/#\s*v?([0-9]+(?:\.[0-9]+)*)/);
10092
- if (commentMatch?.[1]) {
10093
- version = commentMatch[1];
10094
- }
10095
- }
10096
- const key = `${group}-${name}-${version}`;
10097
- let confidence = 0.6;
10098
- if (!keys_cache[key] && name && version) {
10099
- keys_cache[key] = key;
10100
- let fullName = name;
10101
- if (group.length) {
10102
- fullName = `${group}/${name}`;
10103
- }
10104
- let purl = `pkg:github/${fullName}@${version}`;
10105
- if (tagOrCommit && version !== tagOrCommit) {
10106
- const qualifierDesc = tagOrCommit.startsWith("v")
10107
- ? "tag"
10108
- : "commit";
10109
- purl = `${purl}?${qualifierDesc}=${tagOrCommit}`;
10110
- confidence = 0.7;
10111
- }
10112
- const properties = [
10113
- { name: "SrcFile", value: f },
10114
- { name: "cdx:github:workflow:name", value: workflowName },
10115
- { name: "cdx:github:job:name", value: jobName },
10116
- {
10117
- name: "cdx:github:job:runner",
10118
- value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
10119
- },
10120
- { name: "cdx:github:action:uses", value: step.uses },
10121
- {
10122
- name: "cdx:github:action:versionPinningType",
10123
- value: versionPinningType,
10124
- },
10125
- {
10126
- name: "cdx:github:action:isShaPinned",
10127
- value: isShaPinned.toString(),
10128
- },
10129
- ];
10130
- if (step.name) {
10131
- properties.push({ name: "cdx:github:step:name", value: step.name });
10132
- }
10133
- if (step.if) {
10134
- properties.push({
10135
- name: "cdx:github:step:condition",
10136
- value: step.if,
10137
- });
10138
- }
10139
- if (step["continue-on-error"]) {
10140
- properties.push({
10141
- name: "cdx:github:step:continueOnError",
10142
- value: "true",
10143
- });
10144
- }
10145
- if (step.timeout) {
10146
- properties.push({
10147
- name: "cdx:github:step:timeout",
10148
- value: step.timeout.toString(),
10149
- });
10150
- }
10151
- if (jobEnvironment) {
10152
- properties.push({
10153
- name: "cdx:github:job:environment",
10154
- value: jobEnvironment,
10155
- });
10156
- }
10157
- if (jobTimeout) {
10158
- properties.push({
10159
- name: "cdx:github:job:timeoutMinutes",
10160
- value: jobTimeout.toString(),
10161
- });
10162
- }
10163
- if (jobHasWritePermissions) {
10164
- properties.push({
10165
- name: "cdx:github:job:hasWritePermissions",
10166
- value: "true",
10167
- });
10168
- }
10169
- if (jobServices.length > 0) {
10170
- properties.push({
10171
- name: "cdx:github:job:services",
10172
- value: jobServices.join(","),
10173
- });
10174
- }
10175
- if (jobNeeds.length > 0) {
10176
- properties.push({
10177
- name: "cdx:github:job:needs",
10178
- value: jobNeeds.join(","),
10179
- });
10180
- }
10181
- if (hasWritePermissions) {
10182
- properties.push({
10183
- name: "cdx:github:workflow:hasWritePermissions",
10184
- value: "true",
10185
- });
10186
- }
10187
- if (hasIdTokenWrite) {
10188
- properties.push({
10189
- name: "cdx:github:workflow:hasIdTokenWrite",
10190
- value: "true",
10191
- });
10192
- }
10193
- if (workflowConcurrency?.group) {
10194
- properties.push({
10195
- name: "cdx:github:workflow:concurrencyGroup",
10196
- value: workflowConcurrency.group,
10197
- });
10198
- }
10199
- if (group?.startsWith("github/") || group === "actions") {
10200
- properties.push({ name: "cdx:actions:isOfficial", value: "true" });
10201
- }
10202
- if (group?.startsWith("github/")) {
10203
- properties.push({ name: "cdx:actions:isVerified", value: "true" });
10204
- }
10205
- if (workflowTriggers) {
10206
- const triggers =
10207
- typeof workflowTriggers === "string"
10208
- ? workflowTriggers
10209
- : Object.keys(workflowTriggers).join(",");
10210
- properties.push({
10211
- name: "cdx:github:workflow:triggers",
10212
- value: triggers,
10213
- });
10214
- }
10215
- pkgList.push({
10216
- group,
10217
- name,
10218
- version,
10219
- purl,
10220
- properties,
10221
- evidence: {
10222
- identity: {
10223
- field: "purl",
10224
- confidence,
10225
- methods: [
10226
- {
10227
- technique: "source-code-analysis",
10228
- confidence,
10229
- value: f,
10230
- },
10231
- ],
10232
- },
10233
- },
10234
- });
10235
- }
10236
- }
10237
- if (step.run) {
10238
- const runLineNum = ghwData.indexOf(step.run);
10239
- const runLine =
10240
- runLineNum >= 0
10241
- ? ghwData.substring(0, runLineNum).split("\n").length
10242
- : -1;
10243
- const pkgCommands = extractPackageManagerCommands(step.run);
10244
- for (const pkgCmd of pkgCommands) {
10245
- const key = `run-${pkgCmd.name}-${pkgCmd.version || "latest"}`;
10246
- if (!keys_cache[key]) {
10247
- keys_cache[key] = key;
10248
- pkgList.push({
10249
- group: "",
10250
- name: pkgCmd.name,
10251
- version: pkgCmd.version || undefined,
10252
- scope: "excluded",
10253
- "bom-ref": uuidv4(),
10254
- description: key,
10255
- properties: [
10256
- { name: "SrcFile", value: f },
10257
- { name: "cdx:github:workflow:name", value: workflowName },
10258
- { name: "cdx:github:job:name", value: jobName },
10259
- { name: "cdx:github:step:type", value: "run" },
10260
- { name: "cdx:github:step:command", value: pkgCmd.command },
10261
- { name: "cdx:github:run:line", value: runLine.toString() },
10262
- ],
10263
- evidence: {
10264
- identity: {
10265
- field: "purl",
10266
- confidence: 0.5,
10267
- methods: [
10268
- {
10269
- technique: "source-code-analysis",
10270
- confidence: 0.5,
10271
- value: f,
10272
- },
10273
- ],
10274
- },
10275
- },
10276
- });
10277
- }
10278
- }
10279
- }
10280
- }
10281
- }
10282
- return pkgList;
10283
- }
10284
-
10285
10537
  /**
10286
- * Analyze permissions for write access.
10538
+ * Parses a GitHub Actions workflow YAML file and returns a list of action
10539
+ * components for each step that uses an external action (steps with a "uses"
10540
+ * field). Each component captures the action name, group, version/commit SHA,
10541
+ * version pinning type, job context (runner, permissions, environment), and
10542
+ * workflow-level metadata (triggers, concurrency, write permissions).
10287
10543
  *
10288
- * Refer to https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions
10289
- */
10290
- function analyzePermissions(permissions) {
10291
- if (!permissions || typeof permissions !== "object") {
10292
- return false;
10293
- }
10294
- const writePermissions = [
10295
- "actions",
10296
- "artifact-metadata",
10297
- "attestations",
10298
- "checks",
10299
- "contents",
10300
- "deployments",
10301
- "id-token",
10302
- "models",
10303
- "discussions",
10304
- "packages",
10305
- "pages",
10306
- "actions",
10307
- "deployments",
10308
- "issues",
10309
- "pull-requests",
10310
- "security-events",
10311
- "statuses",
10312
- ];
10313
- for (const perm of writePermissions) {
10314
- if (permissions[perm] === "write") {
10315
- return true;
10316
- }
10317
- }
10318
- return false;
10319
- }
10320
-
10321
- /**
10322
- * Determine version pinning type for security assessment
10544
+ * @param {string} f Path to the GitHub Actions workflow YAML file
10545
+ * @returns {Object[]} List of action component objects with purl, properties, and evidence
10323
10546
  */
10324
- function getVersionPinningType(versionRef) {
10325
- if (!versionRef) {
10326
- return "unknown";
10327
- }
10328
- if (/^[a-f0-9]{40}$/.test(versionRef)) {
10329
- return "sha";
10330
- }
10331
- if (/^[a-f0-9]{7,}$/.test(versionRef)) {
10332
- return "sha";
10333
- }
10334
- if (
10335
- versionRef === "main" ||
10336
- versionRef === "master" ||
10337
- versionRef.includes("/")
10338
- ) {
10339
- return "branch";
10340
- }
10341
- return "tag";
10547
+ export function parseGitHubWorkflowData(f) {
10548
+ const { components } = parseWorkflowFile(f);
10549
+ return components.filter((c) => c.scope === "required");
10342
10550
  }
10343
10551
 
10344
10552
  /**
10345
- * Extract package manager commands from run steps
10553
+ * Parse Google Cloud Build YAML data and extract container image steps as packages.
10554
+ *
10555
+ * @param {string} cbwData Raw YAML string of a Cloud Build configuration file
10556
+ * @returns {Object[]} Array of package objects parsed from the build steps
10346
10557
  */
10347
- function extractPackageManagerCommands(runScript) {
10348
- const commands = [];
10349
- if (!runScript) {
10350
- return commands;
10351
- }
10352
- const patterns = [
10353
- { regex: /npm\s+(install|i|ci)\s+([^&\n|;]+)/g, name: "npm", type: "npm" },
10354
- {
10355
- regex: /yarn\s+(add|install)\s+([^&\n|;]+)/g,
10356
- name: "yarn",
10357
- type: "yarn",
10358
- },
10359
- { regex: /pip\s+(install)\s+([^&\n|;]+)/g, name: "pip", type: "pip" },
10360
- { regex: /pip3\s+(install)\s+([^&\n|;]+)/g, name: "pip3", type: "pip" },
10361
- { regex: /gem\s+(install)\s+([^&\n|;]+)/g, name: "gem", type: "gem" },
10362
- { regex: /go\s+(get|install)\s+([^&\n|;]+)/g, name: "go", type: "go" },
10363
- {
10364
- regex: /cargo\s+(install|add)\s+([^&\n|;]+)/g,
10365
- name: "cargo",
10366
- type: "cargo",
10367
- },
10368
- ];
10369
- for (const pattern of patterns) {
10370
- let match;
10371
- while ((match = pattern.regex.exec(runScript)) !== null) {
10372
- commands.push({
10373
- name: pattern.name,
10374
- command: match[0],
10375
- version: null,
10376
- });
10377
- }
10378
- }
10379
- return commands;
10380
- }
10381
-
10382
10558
  export function parseCloudBuildData(cbwData) {
10383
10559
  const pkgList = [];
10384
10560
  const keys_cache = {};
@@ -10455,6 +10631,16 @@ function untilFirst(separator, inputStr) {
10455
10631
  ];
10456
10632
  }
10457
10633
 
10634
+ /**
10635
+ * Map a Conan package reference string to a PackageURL string, name, and version.
10636
+ *
10637
+ * Parses a full Conan package reference of the form
10638
+ * `name/version@user/channel#recipe_revision:package_id#package_revision`
10639
+ * and returns the equivalent purl string together with the extracted name and version.
10640
+ *
10641
+ * @param {string} conanPkgRef Conan package reference string
10642
+ * @returns {Array} Tuple of [purlString, name, version], or [null, null, null] on parse failure
10643
+ */
10458
10644
  export function mapConanPkgRefToPurlStringAndNameAndVersion(conanPkgRef) {
10459
10645
  // A full Conan package reference may be composed of the following segments:
10460
10646
  // conanPkgRef = "name/version@user/channel#recipe_revision:package_id#package_revision"
@@ -10580,6 +10766,16 @@ export function mapConanPkgRefToPurlStringAndNameAndVersion(conanPkgRef) {
10580
10766
  return [purl, info.name, info.version];
10581
10767
  }
10582
10768
 
10769
+ /**
10770
+ * Parse Conan lock file data (conan.lock) and return the package list, dependency map,
10771
+ * and parent component dependencies.
10772
+ *
10773
+ * Supports both the legacy `graph_lock.nodes` format (Conan 1.x) and the newer
10774
+ * `requires` format (Conan 2.x).
10775
+ *
10776
+ * @param {string} conanLockData Raw JSON string of the Conan lock file
10777
+ * @returns {{ pkgList: Object[], dependencies: Object, parentComponentDependencies: string[] }}
10778
+ */
10583
10779
  export function parseConanLockData(conanLockData) {
10584
10780
  const pkgList = [];
10585
10781
  const dependencies = {};
@@ -10663,6 +10859,12 @@ export function parseConanLockData(conanLockData) {
10663
10859
  return { pkgList, dependencies, parentComponentDependencies };
10664
10860
  }
10665
10861
 
10862
+ /**
10863
+ * Parse a Conan conanfile.txt and extract required and optional packages.
10864
+ *
10865
+ * @param {string} conanData Raw text contents of a conanfile.txt
10866
+ * @returns {Object[]} Array of package objects with purl, name, version, and scope
10867
+ */
10666
10868
  export function parseConanData(conanData) {
10667
10869
  const pkgList = [];
10668
10870
  if (!conanData) {
@@ -10698,6 +10900,12 @@ export function parseConanData(conanData) {
10698
10900
  return pkgList;
10699
10901
  }
10700
10902
 
10903
+ /**
10904
+ * Parse Leiningen project.clj data and extract dependency packages.
10905
+ *
10906
+ * @param {string} leinData Raw text contents of a Leiningen project.clj file
10907
+ * @returns {Object[]} Array of package objects with group, name, and version
10908
+ */
10701
10909
  export function parseLeiningenData(leinData) {
10702
10910
  const pkgList = [];
10703
10911
  if (!leinData) {
@@ -10732,6 +10940,14 @@ export function parseLeiningenData(leinData) {
10732
10940
  return pkgList;
10733
10941
  }
10734
10942
 
10943
+ /**
10944
+ * Parse EDN (Extensible Data Notation) deps.edn data and extract dependency packages.
10945
+ *
10946
+ * Handles Clojure deps.edn files, extracting packages listed under the `:deps` key.
10947
+ *
10948
+ * @param {string} rawEdnData Raw EDN text contents of a deps.edn file
10949
+ * @returns {Object[]} Array of package objects with group, name, and version
10950
+ */
10735
10951
  export function parseEdnData(rawEdnData) {
10736
10952
  const pkgList = [];
10737
10953
  if (!rawEdnData) {
@@ -10811,7 +11027,7 @@ export function parseFlakeNix(flakeNixFile) {
10811
11027
  const pkgList = [];
10812
11028
  const dependencies = [];
10813
11029
 
10814
- if (!existsSync(flakeNixFile)) {
11030
+ if (!safeExistsSync(flakeNixFile)) {
10815
11031
  return { pkgList, dependencies };
10816
11032
  }
10817
11033
 
@@ -10896,7 +11112,7 @@ export function parseFlakeLock(flakeLockFile) {
10896
11112
  const pkgList = [];
10897
11113
  const dependencies = [];
10898
11114
 
10899
- if (!existsSync(flakeLockFile)) {
11115
+ if (!safeExistsSync(flakeLockFile)) {
10900
11116
  return { pkgList, dependencies };
10901
11117
  }
10902
11118
 
@@ -11178,6 +11394,13 @@ export function parseNuspecData(nupkgFile, nuspecData) {
11178
11394
  };
11179
11395
  }
11180
11396
 
11397
+ /**
11398
+ * Parse a C# packages.config XML file and return a list of NuGet package components.
11399
+ *
11400
+ * @param {string} pkgData Raw XML string of a packages.config file
11401
+ * @param {string} pkgFile Path to the packages.config file, used for evidence properties
11402
+ * @returns {Object[]} Array of NuGet package objects with purl, name, and version
11403
+ */
11181
11404
  export function parseCsPkgData(pkgData, pkgFile) {
11182
11405
  const pkgList = [];
11183
11406
  if (!pkgData) {
@@ -11710,6 +11933,17 @@ export function parseCsProjData(
11710
11933
  };
11711
11934
  }
11712
11935
 
11936
+ /**
11937
+ * Parse a .NET project.assets.json file and return the package list and dependency tree.
11938
+ *
11939
+ * Extracts NuGet packages and their transitive dependency relationships from the
11940
+ * `libraries` and `targets` sections of a project.assets.json file produced by
11941
+ * the .NET restore process.
11942
+ *
11943
+ * @param {string} csProjData Raw JSON string of the project.assets.json file
11944
+ * @param {string} assetsJsonFile Path to the project.assets.json file, used for evidence properties
11945
+ * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
11946
+ */
11713
11947
  export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
11714
11948
  // extract name, operator, version from .NET package representation
11715
11949
  // like "NLog >= 4.5.0"
@@ -11960,6 +12194,14 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
11960
12194
  };
11961
12195
  }
11962
12196
 
12197
+ /**
12198
+ * Parse a .NET packages.lock.json file and return the package list, dependency tree,
12199
+ * and list of direct/root dependencies.
12200
+ *
12201
+ * @param {string} csLockData Raw JSON string of the packages.lock.json file
12202
+ * @param {string} pkgLockFile Path to the packages.lock.json file, used for evidence properties
12203
+ * @returns {{ pkgList: Object[], dependenciesList: Object[], rootList: Object[] }}
12204
+ */
11963
12205
  export function parseCsPkgLockData(csLockData, pkgLockFile) {
11964
12206
  const pkgList = [];
11965
12207
  const dependenciesList = [];
@@ -12078,6 +12320,14 @@ export function parseCsPkgLockData(csLockData, pkgLockFile) {
12078
12320
  };
12079
12321
  }
12080
12322
 
12323
+ /**
12324
+ * Parse a Paket dependency manager lock file (paket.lock) and return the package list
12325
+ * and dependency tree.
12326
+ *
12327
+ * @param {string} paketLockData Raw text contents of the paket.lock file
12328
+ * @param {string} pkgLockFile Path to the paket.lock file, used for evidence properties
12329
+ * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
12330
+ */
12081
12331
  export function parsePaketLockData(paketLockData, pkgLockFile) {
12082
12332
  const pkgList = [];
12083
12333
  const dependenciesList = [];
@@ -12252,6 +12502,16 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12252
12502
  const rootRequiresMap = {};
12253
12503
  if (rootRequires) {
12254
12504
  for (const rr of Object.keys(rootRequires)) {
12505
+ // Skip platform requirements (php, hhvm, ext-*, lib-*) — they are never
12506
+ // Composer package names and must not be used to identify root packages.
12507
+ if (
12508
+ rr === "php" ||
12509
+ rr === "hhvm" ||
12510
+ rr.startsWith("ext-") ||
12511
+ rr.startsWith("lib-")
12512
+ ) {
12513
+ continue;
12514
+ }
12255
12515
  rootRequiresMap[rr] = true;
12256
12516
  }
12257
12517
  }
@@ -12403,6 +12663,15 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12403
12663
  };
12404
12664
  }
12405
12665
 
12666
+ /**
12667
+ * Parse an sbt dependency tree output file and return the package list and dependency tree.
12668
+ *
12669
+ * Reads a file produced by the sbt `dependencyTree` command and extracts Maven artifact
12670
+ * coordinates, building a hierarchical dependency graph. Evicted packages and ranges are ignored.
12671
+ *
12672
+ * @param {string} sbtTreeFile Path to the sbt dependency tree output file
12673
+ * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
12674
+ */
12406
12675
  export function parseSbtTree(sbtTreeFile) {
12407
12676
  const pkgList = [];
12408
12677
  const dependenciesList = [];
@@ -12719,6 +12988,10 @@ export function convertOSQueryResults(
12719
12988
  if (publisher === "null") {
12720
12989
  publisher = "";
12721
12990
  }
12991
+ // For vscode-extension purl type, the publisher is used as the namespace
12992
+ if (queryObj.purlType === "vscode-extension" && publisher) {
12993
+ group = publisher.toLowerCase();
12994
+ }
12722
12995
  let scope;
12723
12996
  const compScope = res.priority;
12724
12997
  if (["required", "optional", "excluded"].includes(compScope)) {
@@ -12819,6 +13092,17 @@ export function convertOSQueryResults(
12819
13092
  return pkgList;
12820
13093
  }
12821
13094
 
13095
+ /**
13096
+ * Create a PackageURL object from a repository URL string, package type, and version.
13097
+ *
13098
+ * Supports HTTPS URLs, SSH `git@` URLs, Bitbucket SSH URLs, and local paths.
13099
+ * Extracts the namespace (host + path prefix) and repository name from the URL.
13100
+ *
13101
+ * @param {string} type PackageURL type (e.g. `"swift"`, `"generic"`)
13102
+ * @param {string} repoUrl Repository URL string
13103
+ * @param {string} version Package version
13104
+ * @returns {PackageURL|undefined} PackageURL object, or undefined for unsupported URL formats
13105
+ */
12822
13106
  export function purlFromUrlString(type, repoUrl, version) {
12823
13107
  let namespace = "";
12824
13108
  let name;
@@ -13111,6 +13395,20 @@ export async function collectMvnDependencies(
13111
13395
  return jarNSMapping;
13112
13396
  }
13113
13397
 
13398
+ /**
13399
+ * Collect Gradle project dependencies by scanning the Gradle cache directory for JAR files
13400
+ * and their associated POM files.
13401
+ *
13402
+ * Uses the `GRADLE_CACHE_DIR` or `GRADLE_USER_HOME` environment variables to locate the
13403
+ * Gradle files-2.1 cache, then delegates to {@link collectJarNS} to extract namespace
13404
+ * and purl information from those JARs.
13405
+ *
13406
+ * @param {string} _gradleCmd Gradle command (unused; reserved for future use)
13407
+ * @param {string} _basePath Base project path (unused; reserved for future use)
13408
+ * @param {boolean} _cleanup Whether to clean up temporary files (unused; reserved for future use)
13409
+ * @param {boolean} _includeCacheDir Whether to include cache directory (unused; reserved for future use)
13410
+ * @returns {Promise<Object>} JAR namespace mapping object returned by collectJarNS
13411
+ */
13114
13412
  export async function collectGradleDependencies(
13115
13413
  _gradleCmd,
13116
13414
  _basePath,
@@ -13324,6 +13622,16 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
13324
13622
  return jarNSMapping;
13325
13623
  }
13326
13624
 
13625
+ /**
13626
+ * Convert a JAR namespace mapping (produced by {@link collectJarNS}) into an array
13627
+ * of CycloneDX package component objects.
13628
+ *
13629
+ * Each entry in the mapping is resolved to a component with name, group, version,
13630
+ * purl, hashes, namespace properties, and source file evidence.
13631
+ *
13632
+ * @param {Object} jarNSMapping Map of purl string to `{ jarFile, pom, namespaces, hashes }`
13633
+ * @returns {Promise<Object[]>} Array of component objects derived from the JAR mapping
13634
+ */
13327
13635
  export async function convertJarNSToPackages(jarNSMapping) {
13328
13636
  const pkgList = [];
13329
13637
  for (const purl of Object.keys(jarNSMapping)) {
@@ -13427,6 +13735,12 @@ export function parsePomXml(pomXmlData) {
13427
13735
  return undefined;
13428
13736
  }
13429
13737
 
13738
+ /**
13739
+ * Parse a JAR MANIFEST.MF file and return its key-value pairs as an object.
13740
+ *
13741
+ * @param {string} jarMetadata Raw text contents of a MANIFEST.MF file
13742
+ * @returns {Object} Key-value pairs extracted from the manifest
13743
+ */
13430
13744
  export function parseJarManifest(jarMetadata) {
13431
13745
  const metadata = {};
13432
13746
  if (!jarMetadata) {
@@ -13444,6 +13758,12 @@ export function parseJarManifest(jarMetadata) {
13444
13758
  return metadata;
13445
13759
  }
13446
13760
 
13761
+ /**
13762
+ * Parse a Maven pom.properties file and return its key-value pairs as an object.
13763
+ *
13764
+ * @param {string} pomProperties Raw text contents of a pom.properties file
13765
+ * @returns {Object} Key-value pairs extracted from the properties file
13766
+ */
13447
13767
  export function parsePomProperties(pomProperties) {
13448
13768
  const properties = {};
13449
13769
  if (!pomProperties) {
@@ -13461,6 +13781,13 @@ export function parsePomProperties(pomProperties) {
13461
13781
  return properties;
13462
13782
  }
13463
13783
 
13784
+ /**
13785
+ * Encode a string for safe inclusion in a PackageURL, percent-encoding special characters
13786
+ * while preserving already-encoded `%40` sequences and keeping `:` and `/` unencoded.
13787
+ *
13788
+ * @param {string} s String to encode
13789
+ * @returns {string} Encoded string suitable for use in a PackageURL component
13790
+ */
13464
13791
  export function encodeForPurl(s) {
13465
13792
  return s && !s.includes("%40")
13466
13793
  ? encodeURIComponent(s).replace(/%3A/g, ":").replace(/%2F/g, "/")
@@ -14364,10 +14691,10 @@ export async function parsePodfileLock(podfileLock, projectPath) {
14364
14691
  },
14365
14692
  ];
14366
14693
  let podspec = join(projectLocation, `${podName}.podspec`);
14367
- if (!existsSync(podspec)) {
14694
+ if (!safeExistsSync(podspec)) {
14368
14695
  podspec = `${podspec}.json`;
14369
14696
  }
14370
- if (existsSync(podspec)) {
14697
+ if (safeExistsSync(podspec)) {
14371
14698
  dependency.metadata.properties.push({
14372
14699
  name: "cdx:pods:podspecLocation",
14373
14700
  value: podspec,
@@ -15112,6 +15439,19 @@ export function getAtomCommand() {
15112
15439
  return "atom";
15113
15440
  }
15114
15441
 
15442
+ /**
15443
+ * Execute the atom tool against a source directory or file with the given arguments.
15444
+ *
15445
+ * Resolves the atom binary via `getAtomCommand`, sets up the required environment
15446
+ * (including `JAVA_HOME` from `ATOM_JAVA_HOME` if set), and spawns the process.
15447
+ * Logs diagnostic messages for common failure modes such as unsupported Java versions,
15448
+ * missing `astgen`, and JVM crashes.
15449
+ *
15450
+ * @param {string} src Path to the source directory or file to analyse
15451
+ * @param {string[]} args Arguments to pass to the atom command
15452
+ * @param {Object} extra_env Additional environment variables to merge into the process environment
15453
+ * @returns {boolean} `true` if atom executed successfully and the language is supported; `false` otherwise
15454
+ */
15115
15455
  export function executeAtom(src, args, extra_env = {}) {
15116
15456
  const cwd =
15117
15457
  safeExistsSync(src) && lstatSync(src).isDirectory() ? src : dirname(src);
@@ -16195,6 +16535,13 @@ export function getPipTreeForPackages(
16195
16535
  }
16196
16536
 
16197
16537
  // taken from a very old package https://github.com/keithamus/parse-packagejson-name/blob/master/index.js
16538
+ /**
16539
+ * Parse a package.json `name` field (or a plain string) and extract its scope,
16540
+ * full name, project name, and module name components.
16541
+ *
16542
+ * @param {string|Object} name The package name string or an object with a `name` property
16543
+ * @returns {{ scope: string|null, fullName: string, projectName: string|null, moduleName: string|null }}
16544
+ */
16198
16545
  export function parsePackageJsonName(name) {
16199
16546
  const nameRegExp = /^(?:@([^/]+)\/)?(([^.]+)(?:\.(.*))?)$/;
16200
16547
  const returnObject = {
@@ -16246,6 +16593,7 @@ export async function addEvidenceForImports(
16246
16593
  : [name];
16247
16594
  let isImported = false;
16248
16595
  for (const alias of aliases) {
16596
+ const isWasmAlias = /\.wasm([?#].*)?$/i.test(alias);
16249
16597
  const all_includes = impPkgs.filter(
16250
16598
  (find_pkg) =>
16251
16599
  find_pkg.startsWith(alias) &&
@@ -16255,12 +16603,20 @@ export async function addEvidenceForImports(
16255
16603
  find_pkg.startsWith(alias),
16256
16604
  );
16257
16605
  if (all_exports?.length) {
16258
- let exportedModules = new Set(all_exports);
16606
+ let exportedModules = new Set(isWasmAlias ? [] : all_exports);
16259
16607
  pkg.properties = pkg.properties || [];
16260
16608
  for (const subevidence of all_exports) {
16261
16609
  const evidences = allExports[subevidence];
16262
16610
  for (const evidence of evidences) {
16263
16611
  if (evidence && Object.keys(evidence).length) {
16612
+ if (isWasmAlias) {
16613
+ for (const wasmImportedModule of evidence.importedModules ||
16614
+ []) {
16615
+ if (wasmImportedModule?.length) {
16616
+ exportedModules.add(wasmImportedModule);
16617
+ }
16618
+ }
16619
+ }
16264
16620
  if (evidence.exportedModules.length > 1) {
16265
16621
  for (const aexpsubm of evidence.exportedModules) {
16266
16622
  // Be selective on the submodule names
@@ -16295,6 +16651,8 @@ export async function addEvidenceForImports(
16295
16651
  if (impPkgs.includes(alias) || all_includes.length) {
16296
16652
  isImported = true;
16297
16653
  let importedModules = new Set();
16654
+ let wasmExportedModules = new Set();
16655
+ const seenOccurrenceLocations = new Set();
16298
16656
  pkg.scope = "required";
16299
16657
  for (const subevidence of all_includes) {
16300
16658
  const evidences = allImports[subevidence];
@@ -16302,16 +16660,23 @@ export async function addEvidenceForImports(
16302
16660
  if (evidence && Object.keys(evidence).length && evidence.fileName) {
16303
16661
  pkg.evidence = pkg.evidence || {};
16304
16662
  pkg.evidence.occurrences = pkg.evidence.occurrences || [];
16305
- pkg.evidence.occurrences.push({
16306
- location: `${evidence.fileName}${
16307
- evidence.lineNumber ? `#${evidence.lineNumber}` : ""
16308
- }`,
16309
- });
16663
+ const occurrenceLocation = `${evidence.fileName}${
16664
+ evidence.lineNumber ? `#${evidence.lineNumber}` : ""
16665
+ }`;
16666
+ if (!seenOccurrenceLocations.has(occurrenceLocation)) {
16667
+ pkg.evidence.occurrences.push({
16668
+ location: occurrenceLocation,
16669
+ });
16670
+ seenOccurrenceLocations.add(occurrenceLocation);
16671
+ }
16310
16672
  importedModules.add(evidence.importedAs);
16311
16673
  for (const importedSm of evidence.importedModules || []) {
16312
16674
  if (!importedSm) {
16313
16675
  continue;
16314
16676
  }
16677
+ if (isWasmAlias) {
16678
+ wasmExportedModules.add(importedSm);
16679
+ }
16315
16680
  // Store both the short and long form of the imported sub modules
16316
16681
  if (importedSm.length > 3) {
16317
16682
  importedModules.add(importedSm);
@@ -16322,6 +16687,7 @@ export async function addEvidenceForImports(
16322
16687
  }
16323
16688
  }
16324
16689
  importedModules = Array.from(importedModules);
16690
+ wasmExportedModules = Array.from(wasmExportedModules);
16325
16691
  if (importedModules.length) {
16326
16692
  pkg.properties = pkg.properties || [];
16327
16693
  pkg.properties.push({
@@ -16329,6 +16695,15 @@ export async function addEvidenceForImports(
16329
16695
  value: importedModules.join(","),
16330
16696
  });
16331
16697
  }
16698
+ if (isWasmAlias && wasmExportedModules.length) {
16699
+ pkg.properties = pkg.properties || [];
16700
+ if (!pkg.properties.some((p) => p.name === "ExportedModules")) {
16701
+ pkg.properties.push({
16702
+ name: "ExportedModules",
16703
+ value: wasmExportedModules.join(","),
16704
+ });
16705
+ }
16706
+ }
16332
16707
  break;
16333
16708
  }
16334
16709
  if (
@@ -16373,6 +16748,16 @@ export async function addEvidenceForImports(
16373
16748
  return pkgList;
16374
16749
  }
16375
16750
 
16751
+ /**
16752
+ * Comparator function for sorting CycloneDX component objects.
16753
+ *
16754
+ * Compares components by `bom-ref`, then `purl`, then `name`, using locale-aware
16755
+ * string comparison on the first available key.
16756
+ *
16757
+ * @param {Object|string} a First component to compare
16758
+ * @param {Object|string} b Second component to compare
16759
+ * @returns {number} Negative, zero, or positive integer as required by Array.sort
16760
+ */
16376
16761
  export function componentSorter(a, b) {
16377
16762
  if (a && b) {
16378
16763
  for (const k of ["bom-ref", "purl", "name"]) {
@@ -16384,6 +16769,19 @@ export function componentSorter(a, b) {
16384
16769
  return a.localeCompare(b);
16385
16770
  }
16386
16771
 
16772
+ /**
16773
+ * Parse a CMake-generated dot/graphviz file and extract components and their dependency
16774
+ * relationships.
16775
+ *
16776
+ * The first `digraph` entry becomes the parent component. Subsequent `node` entries
16777
+ * with a `label` attribute are treated as direct dependencies, while commented
16778
+ * `node -> node` relationships are used to construct the dependency graph.
16779
+ *
16780
+ * @param {string} dotFile Path to the CMake-generated dot file
16781
+ * @param {string} pkgType PackageURL type to assign to extracted packages (e.g. `"generic"`)
16782
+ * @param {Object} options CLI options; may contain `projectGroup`, `projectName`, and `projectVersion`
16783
+ * @returns {{ parentComponent: Object, pkgList: Object[], dependenciesList: Object[] }}
16784
+ */
16387
16785
  export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
16388
16786
  const dotGraphData = readFileSync(dotFile, { encoding: "utf-8" });
16389
16787
  const pkgList = [];
@@ -16493,6 +16891,19 @@ export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
16493
16891
  };
16494
16892
  }
16495
16893
 
16894
+ /**
16895
+ * Parse a CMake-like build file (CMakeLists.txt, meson.build, etc.) and extract the
16896
+ * parent component and list of dependency packages.
16897
+ *
16898
+ * Handles `set`, `project`, `find_package`, `find_library`, `find_dependency`,
16899
+ * `find_file`, `FetchContent_MakeAvailable`, and `dependency()` directives.
16900
+ * Uses the MesonWrapDB to improve name resolution confidence.
16901
+ *
16902
+ * @param {string} cmakeListFile Path to the CMake-like build file
16903
+ * @param {string} pkgType PackageURL type to assign to extracted packages (e.g. `"generic"`)
16904
+ * @param {Object} options CLI options; may contain `projectGroup`, `projectName`, and `projectVersion`
16905
+ * @returns {{ parentComponent: Object, pkgList: Object[] }}
16906
+ */
16496
16907
  export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
16497
16908
  let cmakeListData = readFileSync(cmakeListFile, { encoding: "utf-8" });
16498
16909
  const pkgList = [];
@@ -16746,6 +17157,14 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
16746
17157
  };
16747
17158
  }
16748
17159
 
17160
+ /**
17161
+ * Find the OS package component that provides a given file, by searching the
17162
+ * `PkgProvides` property of each package in the OS package list.
17163
+ *
17164
+ * @param {string} afile Filename or path to look up (matched case-insensitively)
17165
+ * @param {Object[]} osPkgsList Array of OS package component objects to search
17166
+ * @returns {Object|undefined} The matching OS package component, or undefined if not found
17167
+ */
16749
17168
  export function getOSPackageForFile(afile, osPkgsList) {
16750
17169
  for (const ospkg of osPkgsList) {
16751
17170
  for (const props of ospkg.properties || []) {
@@ -17359,6 +17778,18 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
17359
17778
  };
17360
17779
  }
17361
17780
 
17781
+ /**
17782
+ * Enrich .NET package components with occurrence evidence and imported module/method
17783
+ * information from a dosai dependency slices file.
17784
+ *
17785
+ * Builds a mapping of DLL filenames to purls using the `PackageFiles` property of each
17786
+ * package, then reads the slices file to add occurrence locations, imported modules,
17787
+ * called methods, and assembly version information where available.
17788
+ *
17789
+ * @param {Object[]} pkgList Array of .NET package component objects to enrich
17790
+ * @param {string} slicesFile Path to the dosai dependency slices JSON file
17791
+ * @returns {Object[]} The enriched package list (same array, mutated in place)
17792
+ */
17362
17793
  export function addEvidenceForDotnet(pkgList, slicesFile) {
17363
17794
  // We need two datastructures.
17364
17795
  // dll to purl mapping from the pkgList
@@ -17793,7 +18224,7 @@ export function collectSharedLibs(
17793
18224
  }
17794
18225
 
17795
18226
  function collectAllLdConfs(basePath, ldConf, allLdConfDirs, libPaths) {
17796
- if (ldConf && existsSync(join(basePath, ldConf))) {
18227
+ if (ldConf && safeExistsSync(join(basePath, ldConf))) {
17797
18228
  const ldConfData = readFileSync(join(basePath, ldConf), "utf-8");
17798
18229
  for (let line of ldConfData.split("\n")) {
17799
18230
  line = line.replace("\r", "").trim();
@@ -17979,6 +18410,14 @@ export function retrieveCdxgenVersion() {
17979
18410
  return `\x1b[1mCycloneDX Generator ${packageJson.version}\x1b[0m\nRuntime: ${runtimeInfo.runtime}, Version: ${runtimeInfo.version}`;
17980
18411
  }
17981
18412
 
18413
+ /**
18414
+ * Retrieve the version of the cdxgen plugins binary package from package.json.
18415
+ *
18416
+ * Reads the local package.json and searches the `optionalDependencies` for a package
18417
+ * whose name starts with `@cdxgen/cdxgen-plugins-bin`, returning its declared version.
18418
+ *
18419
+ * @returns {string|undefined} Version string of the plugins binary package, or undefined if not found
18420
+ */
17982
18421
  export function retrieveCdxgenPluginVersion() {
17983
18422
  const packageJsonAsString = readFileSync(
17984
18423
  join(dirNameStr, "package.json"),
@@ -18043,3 +18482,13 @@ export function splitCommandArgs(commandString) {
18043
18482
  }
18044
18483
  return args;
18045
18484
  }
18485
+
18486
+ /**
18487
+ * Convert hyphenated strings to camel case.
18488
+ *
18489
+ * @param {String} str String to convert
18490
+ * @returns {String} camelCased string
18491
+ */
18492
+ export function toCamel(str) {
18493
+ return str.replace(/-([a-z])/g, (_, g) => g.toUpperCase());
18494
+ }