@cyclonedx/cdxgen 12.1.5 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +47 -39
  2. package/bin/cdxgen.js +175 -96
  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 +327 -372
  14. package/lib/evinser/db.js +137 -0
  15. package/lib/{helpers → evinser}/db.poku.js +2 -6
  16. package/lib/evinser/evinser.js +2 -14
  17. package/lib/helpers/bomSigner.js +312 -0
  18. package/lib/helpers/bomSigner.poku.js +156 -0
  19. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  20. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  21. package/lib/helpers/ciParsers/circleCi.js +286 -0
  22. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  23. package/lib/helpers/ciParsers/common.js +24 -0
  24. package/lib/helpers/ciParsers/githubActions.js +636 -0
  25. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  26. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  27. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  28. package/lib/helpers/ciParsers/jenkins.js +181 -0
  29. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  30. package/lib/helpers/depsUtils.js +203 -0
  31. package/lib/helpers/depsUtils.poku.js +150 -0
  32. package/lib/helpers/display.js +423 -4
  33. package/lib/helpers/envcontext.js +18 -3
  34. package/lib/helpers/formulationParsers.js +351 -0
  35. package/lib/helpers/logger.js +14 -0
  36. package/lib/helpers/protobom.js +9 -9
  37. package/lib/helpers/pythonutils.js +9 -0
  38. package/lib/helpers/utils.js +681 -406
  39. package/lib/helpers/utils.poku.js +55 -255
  40. package/lib/helpers/versutils.js +202 -0
  41. package/lib/helpers/versutils.poku.js +315 -0
  42. package/lib/helpers/vsixutils.js +1061 -0
  43. package/lib/helpers/vsixutils.poku.js +2247 -0
  44. package/lib/managers/binary.js +19 -19
  45. package/lib/managers/docker.js +108 -1
  46. package/lib/managers/oci.js +10 -0
  47. package/lib/managers/piptree.js +3 -9
  48. package/lib/parsers/npmrc.js +17 -13
  49. package/lib/parsers/npmrc.poku.js +41 -5
  50. package/lib/server/openapi.yaml +1 -1
  51. package/lib/server/server.js +40 -11
  52. package/lib/server/server.poku.js +123 -144
  53. package/lib/stages/postgen/annotator.js +1 -1
  54. package/lib/stages/postgen/auditBom.js +197 -0
  55. package/lib/stages/postgen/auditBom.poku.js +378 -0
  56. package/lib/stages/postgen/postgen.js +54 -1
  57. package/lib/stages/postgen/postgen.poku.js +90 -1
  58. package/lib/stages/postgen/ruleEngine.js +369 -0
  59. package/lib/stages/pregen/envAudit.js +299 -0
  60. package/lib/stages/pregen/envAudit.poku.js +572 -0
  61. package/lib/stages/pregen/pregen.js +12 -8
  62. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  63. package/lib/validator/complianceEngine.js +241 -0
  64. package/lib/validator/complianceEngine.poku.js +168 -0
  65. package/lib/validator/complianceRules.js +1610 -0
  66. package/lib/validator/complianceRules.poku.js +328 -0
  67. package/lib/validator/index.js +222 -0
  68. package/lib/validator/index.poku.js +144 -0
  69. package/lib/validator/reporters/annotations.js +121 -0
  70. package/lib/validator/reporters/console.js +149 -0
  71. package/lib/validator/reporters/index.js +41 -0
  72. package/lib/validator/reporters/json.js +37 -0
  73. package/lib/validator/reporters/sarif.js +184 -0
  74. package/lib/validator/reporters.poku.js +150 -0
  75. package/package.json +8 -8
  76. package/types/bin/sign.d.ts +3 -0
  77. package/types/bin/sign.d.ts.map +1 -0
  78. package/types/bin/validate.d.ts +3 -0
  79. package/types/bin/validate.d.ts.map +1 -0
  80. package/types/helpers/utils.d.ts +0 -1
  81. package/types/lib/cli/index.d.ts +49 -52
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/evinser/db.d.ts +34 -0
  84. package/types/lib/evinser/db.d.ts.map +1 -0
  85. package/types/lib/evinser/evinser.d.ts +63 -16
  86. package/types/lib/evinser/evinser.d.ts.map +1 -1
  87. package/types/lib/helpers/bomSigner.d.ts +27 -0
  88. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  89. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  90. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  91. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  92. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  93. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  94. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  95. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  96. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  97. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  98. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  99. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  100. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  101. package/types/lib/helpers/depsUtils.d.ts +21 -0
  102. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  103. package/types/lib/helpers/display.d.ts +111 -11
  104. package/types/lib/helpers/display.d.ts.map +1 -1
  105. package/types/lib/helpers/envcontext.d.ts +19 -7
  106. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  107. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  108. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  109. package/types/lib/helpers/logger.d.ts +15 -1
  110. package/types/lib/helpers/logger.d.ts.map +1 -1
  111. package/types/lib/helpers/protobom.d.ts +2 -2
  112. package/types/lib/helpers/pythonutils.d.ts +10 -1
  113. package/types/lib/helpers/pythonutils.d.ts.map +1 -1
  114. package/types/lib/helpers/utils.d.ts +532 -128
  115. package/types/lib/helpers/utils.d.ts.map +1 -1
  116. package/types/lib/helpers/versutils.d.ts +8 -0
  117. package/types/lib/helpers/versutils.d.ts.map +1 -0
  118. package/types/lib/helpers/vsixutils.d.ts +130 -0
  119. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  120. package/types/lib/managers/docker.d.ts +12 -31
  121. package/types/lib/managers/docker.d.ts.map +1 -1
  122. package/types/lib/managers/oci.d.ts +11 -1
  123. package/types/lib/managers/oci.d.ts.map +1 -1
  124. package/types/lib/managers/piptree.d.ts.map +1 -1
  125. package/types/lib/parsers/npmrc.d.ts +4 -1
  126. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  127. package/types/lib/server/server.d.ts +21 -2
  128. package/types/lib/server/server.d.ts.map +1 -1
  129. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  130. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  131. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  132. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  133. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  134. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  135. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  136. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  137. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  138. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  139. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  140. package/types/lib/validator/complianceEngine.d.ts +66 -0
  141. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  142. package/types/lib/validator/complianceRules.d.ts +70 -0
  143. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  144. package/types/lib/validator/index.d.ts +70 -0
  145. package/types/lib/validator/index.d.ts.map +1 -0
  146. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  147. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  148. package/types/lib/validator/reporters/console.d.ts +30 -0
  149. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  150. package/types/lib/validator/reporters/index.d.ts +21 -0
  151. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  152. package/types/lib/validator/reporters/json.d.ts +11 -0
  153. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  154. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  155. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  156. package/lib/helpers/db.js +0 -162
  157. package/lib/stages/pregen/env-audit.js +0 -34
  158. package/lib/stages/pregen/env-audit.poku.js +0 -290
  159. package/types/helpers/db.d.ts +0 -35
  160. package/types/helpers/db.d.ts.map +0 -1
  161. package/types/lib/helpers/db.d.ts +0 -35
  162. package/types/lib/helpers/db.d.ts.map +0 -1
  163. package/types/lib/helpers/validator.d.ts.map +0 -1
  164. package/types/lib/stages/pregen/env-audit.d.ts +0 -2
  165. package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
  166. package/types/managers/binary.d.ts +0 -37
  167. package/types/managers/binary.d.ts.map +0 -1
  168. package/types/managers/docker.d.ts +0 -56
  169. package/types/managers/docker.d.ts.map +0 -1
  170. package/types/managers/oci.d.ts +0 -2
  171. package/types/managers/oci.d.ts.map +0 -1
  172. package/types/managers/piptree.d.ts +0 -2
  173. package/types/managers/piptree.d.ts.map +0 -1
  174. package/types/server/server.d.ts +0 -34
  175. package/types/server/server.d.ts.map +0 -1
  176. package/types/stages/postgen/annotator.d.ts +0 -27
  177. package/types/stages/postgen/annotator.d.ts.map +0 -1
  178. package/types/stages/postgen/postgen.d.ts +0 -51
  179. package/types/stages/postgen/postgen.d.ts.map +0 -1
  180. package/types/stages/pregen/pregen.d.ts +0 -59
  181. 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
  }
@@ -303,6 +370,12 @@ export const PREFER_MAVEN_DEPS_TREE = !["false", "0"].includes(
303
370
  process.env?.PREFER_MAVEN_DEPS_TREE,
304
371
  );
305
372
 
373
+ /**
374
+ * Determines whether license information should be fetched from remote sources,
375
+ * based on the FETCH_LICENSE environment variable.
376
+ *
377
+ * @returns {boolean} True if the FETCH_LICENSE env var is set to "true" or "1"
378
+ */
306
379
  export function shouldFetchLicense() {
307
380
  return (
308
381
  process.env.FETCH_LICENSE &&
@@ -310,6 +383,12 @@ export function shouldFetchLicense() {
310
383
  );
311
384
  }
312
385
 
386
+ /**
387
+ * Determines whether VCS (version control system) information should be fetched
388
+ * for Go packages, based on the GO_FETCH_VCS environment variable.
389
+ *
390
+ * @returns {boolean} True if the GO_FETCH_VCS env var is set to "true" or "1"
391
+ */
313
392
  export function shouldFetchVCS() {
314
393
  return (
315
394
  process.env.GO_FETCH_VCS && ["true", "1"].includes(process.env.GO_FETCH_VCS)
@@ -336,6 +415,12 @@ const MAX_LICENSE_ID_LENGTH = 100;
336
415
 
337
416
  export const JAVA_CMD = getJavaCommand();
338
417
 
418
+ /**
419
+ * Returns the Java executable command to use, resolved in priority order:
420
+ * JAVA_CMD env var > JAVA_HOME/bin/java > "java".
421
+ *
422
+ * @returns {string} Path or name of the Java executable
423
+ */
339
424
  export function getJavaCommand() {
340
425
  let javaCmd = "java";
341
426
  if (process.env.JAVA_CMD) {
@@ -352,6 +437,12 @@ export function getJavaCommand() {
352
437
 
353
438
  export const PYTHON_CMD = getPythonCommand();
354
439
 
440
+ /**
441
+ * Returns the Python executable command to use, resolved in priority order:
442
+ * PYTHON_CMD env var > CONDA_PYTHON_EXE env var > "python".
443
+ *
444
+ * @returns {string} Path or name of the Python executable
445
+ */
355
446
  export function getPythonCommand() {
356
447
  let pythonCmd = "python";
357
448
  if (process.env.PYTHON_CMD) {
@@ -486,7 +577,6 @@ export const PROJECT_TYPE_ALIASES = {
486
577
  "typescript",
487
578
  "ts",
488
579
  "tsx",
489
- "vsix",
490
580
  "yarn",
491
581
  "rush",
492
582
  ],
@@ -581,6 +671,14 @@ export const PROJECT_TYPE_ALIASES = {
581
671
  scala: ["scala", "scala3", "sbt", "mill"],
582
672
  nix: ["nix", "nixos", "flake"],
583
673
  caxa: ["caxa"],
674
+ "vscode-extension": [
675
+ "vscode-extension",
676
+ "vsix",
677
+ "vscode",
678
+ "openvsx",
679
+ "vscode-extensions",
680
+ "ide-extensions",
681
+ ],
584
682
  };
585
683
 
586
684
  // Package manager aliases
@@ -1158,6 +1256,13 @@ export function readLicenseText(licenseFilepath, licenseContentType) {
1158
1256
  return null;
1159
1257
  }
1160
1258
 
1259
+ /**
1260
+ * Fetches license information for a list of Swift packages by querying the
1261
+ * GitHub repository license API for packages hosted on github.com.
1262
+ *
1263
+ * @param {Object[]} pkgList List of Swift package objects with optional repository.url fields
1264
+ * @returns {Promise<Object[]>} Resolved list of package objects, each augmented with a license field where available
1265
+ */
1161
1266
  export async function getSwiftPackageMetadata(pkgList) {
1162
1267
  const cdepList = [];
1163
1268
  for (const p of pkgList) {
@@ -1242,8 +1347,13 @@ export async function getNpmMetadata(pkgList) {
1242
1347
  *
1243
1348
  * @param {string} pkgJsonFile package.json file
1244
1349
  * @param {boolean} simple Return a simpler representation of the component by skipping extended attributes and license fetch.
1350
+ * @param {boolean} securityProps Collect security-related properties
1245
1351
  */
1246
- export async function parsePkgJson(pkgJsonFile, simple = false) {
1352
+ export async function parsePkgJson(
1353
+ pkgJsonFile,
1354
+ simple = false,
1355
+ securityProps = false,
1356
+ ) {
1247
1357
  const pkgList = [];
1248
1358
  if (safeExistsSync(pkgJsonFile)) {
1249
1359
  try {
@@ -1306,6 +1416,107 @@ export async function parsePkgJson(pkgJsonFile, simple = false) {
1306
1416
  },
1307
1417
  };
1308
1418
  }
1419
+ if (securityProps) {
1420
+ if (!apkg.properties) {
1421
+ apkg.properties = [];
1422
+ }
1423
+ // Track executable binaries (potential code execution vectors)
1424
+ if (pkgData.bin) {
1425
+ const binValue =
1426
+ typeof pkgData.bin === "object"
1427
+ ? Object.keys(pkgData.bin).join(", ")
1428
+ : pkgData.bin;
1429
+ apkg.properties.push({
1430
+ name: "cdx:npm:bin",
1431
+ value: binValue,
1432
+ });
1433
+ apkg.properties.push({
1434
+ name: "cdx:npm:has_binary",
1435
+ value: "true",
1436
+ });
1437
+ }
1438
+ // Track lifecycle scripts (preinstall, postinstall, etc. - code execution risk)
1439
+ if (pkgData.scripts && Object.keys(pkgData.scripts).length) {
1440
+ const scriptNames = Object.keys(pkgData.scripts).join(", ");
1441
+ apkg.properties.push({
1442
+ name: "cdx:npm:scripts",
1443
+ value: scriptNames,
1444
+ });
1445
+ // Flag high-risk scripts specifically
1446
+ const riskyScripts = [
1447
+ "preinstall",
1448
+ "install",
1449
+ "postinstall",
1450
+ "prepublish",
1451
+ "prepare",
1452
+ ].filter((script) => pkgData.scripts[script]);
1453
+ if (riskyScripts.length) {
1454
+ apkg.properties.push({
1455
+ name: "cdx:npm:risky_scripts",
1456
+ value: riskyScripts.join(", "),
1457
+ });
1458
+ }
1459
+ }
1460
+ // Track platform/architecture constraints
1461
+ if (pkgData.cpu && Array.isArray(pkgData.cpu) && pkgData.cpu.length) {
1462
+ apkg.properties.push({
1463
+ name: "cdx:npm:cpu",
1464
+ value: pkgData.cpu.join(", "),
1465
+ });
1466
+ }
1467
+ if (pkgData.os && Array.isArray(pkgData.os) && pkgData.os.length) {
1468
+ apkg.properties.push({
1469
+ name: "cdx:npm:os",
1470
+ value: pkgData.os.join(", "),
1471
+ });
1472
+ }
1473
+ if (
1474
+ pkgData.libc &&
1475
+ Array.isArray(pkgData.libc) &&
1476
+ pkgData.libc.length
1477
+ ) {
1478
+ apkg.properties.push({
1479
+ name: "cdx:npm:libc",
1480
+ value: pkgData.libc.join(", "),
1481
+ });
1482
+ }
1483
+ // Track deprecation notices
1484
+ if (pkgData.deprecated) {
1485
+ apkg.properties.push({
1486
+ name: "cdx:npm:deprecation_notice",
1487
+ value: pkgData.deprecated,
1488
+ });
1489
+ }
1490
+ // Track if package uses node-gyp (native C/C++ addons = higher risk)
1491
+ if (
1492
+ pkgData.gypfile === true ||
1493
+ pkgData.files?.some((f) => f.endsWith(".gyp") || f.endsWith(".gypi"))
1494
+ ) {
1495
+ apkg.properties.push({
1496
+ name: "cdx:npm:gypfile",
1497
+ value: "true",
1498
+ });
1499
+ apkg.properties.push({
1500
+ name: "cdx:npm:native_addon",
1501
+ value: "true",
1502
+ });
1503
+ const nativeDeps = [
1504
+ "nan",
1505
+ "node-addon-api",
1506
+ "bindings",
1507
+ "node-gyp-build",
1508
+ ];
1509
+ const foundNativeDeps = Object.keys(
1510
+ pkgData.dependencies || {},
1511
+ ).filter((dep) => nativeDeps.includes(dep));
1512
+ if (foundNativeDeps.length) {
1513
+ apkg.properties.push({
1514
+ name: "cdx:npm:native_deps",
1515
+ value: foundNativeDeps.join(", "),
1516
+ });
1517
+ }
1518
+ }
1519
+ }
1309
1520
  pkgList.push(apkg);
1310
1521
  } catch (_err) {
1311
1522
  // continue regardless of error
@@ -1450,7 +1661,10 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1450
1661
  name: "ResolvedUrl",
1451
1662
  value: node.resolved,
1452
1663
  });
1453
- pkg.distribution = { url: node.resolved };
1664
+ pkg.externalReferences.push({
1665
+ type: "distribution",
1666
+ url: node.resolved,
1667
+ });
1454
1668
  }
1455
1669
  }
1456
1670
  if (node.location) {
@@ -1731,7 +1945,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1731
1945
  if (!targetVersion || !targetName) {
1732
1946
  if (pkgSpecVersionCache[`${edge.name}-${edge.spec}`]) {
1733
1947
  targetVersion = pkgSpecVersionCache[`${edge.name}-${edge.spec}`];
1734
- targetName = edge.name;
1948
+ targetName = edge.name.replace(/-cjs$/, "");
1735
1949
  }
1736
1950
  }
1737
1951
  }
@@ -2774,6 +2988,13 @@ function findMatchingWorkspace(workspacePackages, packageName) {
2774
2988
  );
2775
2989
  }
2776
2990
 
2991
+ /**
2992
+ * Parses the workspaces field from a package.json file and returns the list of
2993
+ * workspace glob patterns. Handles both array and object (with packages key) formats.
2994
+ *
2995
+ * @param {string} packageJsonFile Path to the package.json file to parse
2996
+ * @returns {Object} Object with a packages array of workspace glob patterns, or an empty object on error
2997
+ */
2777
2998
  export function parseYarnWorkspace(packageJsonFile) {
2778
2999
  try {
2779
3000
  const packageData = JSON.parse(readFileSync(packageJsonFile, "utf-8"));
@@ -2872,42 +3093,28 @@ export async function pnpmMetadata(pkgList, lockFilePath) {
2872
3093
  if (!pkgList?.length || !lockFilePath) {
2873
3094
  return pkgList;
2874
3095
  }
2875
-
2876
3096
  const baseDir = dirname(lockFilePath);
2877
3097
  const nodeModulesDir = join(baseDir, "node_modules");
2878
-
2879
- // Only proceed if node_modules exists
2880
3098
  if (!safeExistsSync(nodeModulesDir)) {
2881
3099
  return pkgList;
2882
3100
  }
2883
-
2884
3101
  if (DEBUG_MODE) {
2885
3102
  console.log(
2886
3103
  `Metadata for ${pkgList.length} pnpm packages using local node_modules at ${nodeModulesDir}`,
2887
3104
  );
2888
3105
  }
2889
-
2890
3106
  let enhancedCount = 0;
2891
3107
  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
3108
  const packagePath = findPnpmPackagePath(baseDir, pkg.name, pkg.version);
2899
3109
  if (!packagePath) {
2900
3110
  continue;
2901
3111
  }
2902
-
2903
3112
  const packageJsonPath = join(packagePath, "package.json");
2904
3113
  if (!safeExistsSync(packageJsonPath)) {
2905
3114
  continue;
2906
3115
  }
2907
-
2908
3116
  try {
2909
- // Parse the local package.json to get metadata
2910
- const localPkgList = await parsePkgJson(packageJsonPath, true);
3117
+ const localPkgList = await parsePkgJson(packageJsonPath, true, true);
2911
3118
  if (localPkgList && localPkgList.length === 1) {
2912
3119
  const localMetadata = localPkgList[0];
2913
3120
  if (localMetadata && Object.keys(localMetadata).length) {
@@ -2926,16 +3133,27 @@ export async function pnpmMetadata(pkgList, lockFilePath) {
2926
3133
  if (!pkg.repository && localMetadata.repository) {
2927
3134
  pkg.repository = localMetadata.repository;
2928
3135
  }
2929
-
2930
- // Add a property to track that we enhanced from local node_modules
2931
3136
  if (!pkg.properties) {
2932
3137
  pkg.properties = [];
2933
3138
  }
3139
+ if (localMetadata?.properties?.length) {
3140
+ const seenProperties = new Set(
3141
+ pkg.properties.map(
3142
+ (prop) => `${String(prop?.name)}\u0000${String(prop?.value)}`,
3143
+ ),
3144
+ );
3145
+ for (const prop of localMetadata.properties) {
3146
+ const propertyKey = `${String(prop?.name)}\u0000${String(prop?.value)}`;
3147
+ if (!seenProperties.has(propertyKey)) {
3148
+ pkg.properties.push(prop);
3149
+ seenProperties.add(propertyKey);
3150
+ }
3151
+ }
3152
+ }
2934
3153
  pkg.properties.push({
2935
3154
  name: "LocalNodeModulesPath",
2936
3155
  value: packagePath,
2937
3156
  });
2938
-
2939
3157
  enhancedCount++;
2940
3158
  }
2941
3159
  }
@@ -2949,13 +3167,11 @@ export async function pnpmMetadata(pkgList, lockFilePath) {
2949
3167
  }
2950
3168
  }
2951
3169
  }
2952
-
2953
3170
  if (DEBUG_MODE && enhancedCount > 0) {
2954
3171
  console.log(
2955
3172
  `Enhanced metadata for ${enhancedCount} packages from local node_modules`,
2956
3173
  );
2957
3174
  }
2958
-
2959
3175
  return pkgList;
2960
3176
  }
2961
3177
 
@@ -3018,10 +3234,18 @@ export async function parsePnpmLock(
3018
3234
  }
3019
3235
  if (safeExistsSync(pnpmLock)) {
3020
3236
  const lockData = readFileSync(pnpmLock, "utf8");
3021
- const yamlObj = _load(lockData);
3237
+ let yamlObj = parseAllDocuments(lockData);
3022
3238
  if (!yamlObj) {
3023
3239
  return {};
3024
3240
  }
3241
+ if (Array.isArray(yamlObj)) {
3242
+ try {
3243
+ yamlObj = yamlObj[yamlObj.length - 1].toJS();
3244
+ } catch (_e) {
3245
+ console.log(`Unable to parse the pnpm lock file ${pnpmLock}.`);
3246
+ return {};
3247
+ }
3248
+ }
3025
3249
  lockfileVersion = yamlObj.lockfileVersion;
3026
3250
  try {
3027
3251
  lockfileVersion = Number.parseFloat(lockfileVersion, 10);
@@ -3320,6 +3544,7 @@ export async function parsePnpmLock(
3320
3544
  packages[fullName]?.resolution ||
3321
3545
  snapshots[fullName]?.resolution;
3322
3546
  const integrity = resolution?.integrity;
3547
+ const tarball = resolution?.tarball;
3323
3548
  const cpu =
3324
3549
  packages[pkgKeys[k]]?.cpu ||
3325
3550
  snapshots[pkgKeys[k]]?.cpu ||
@@ -3538,10 +3763,10 @@ export async function parsePnpmLock(
3538
3763
  value: pnpmLock,
3539
3764
  },
3540
3765
  ];
3541
- if (hasBin || os || cpu || libc) {
3766
+ if (hasBin) {
3542
3767
  properties.push({
3543
3768
  name: "cdx:npm:has_binary",
3544
- value: `${hasBin}`,
3769
+ value: "true",
3545
3770
  });
3546
3771
  }
3547
3772
  if (deprecatedMessage) {
@@ -3554,7 +3779,7 @@ export async function parsePnpmLock(
3554
3779
  Object.entries(binary_metadata).forEach(([key, value]) => {
3555
3780
  if (!value) return;
3556
3781
  properties.push({
3557
- name: `cdx:pnpm:${key}`,
3782
+ name: `cdx:npm:${key}`,
3558
3783
  value: Array.isArray(value) ? value.join(", ") : value,
3559
3784
  });
3560
3785
  });
@@ -3652,6 +3877,14 @@ export async function parsePnpmLock(
3652
3877
  },
3653
3878
  },
3654
3879
  };
3880
+ if (tarball) {
3881
+ thePkg.externalReferences = [
3882
+ {
3883
+ type: "distribution",
3884
+ url: tarball,
3885
+ },
3886
+ ];
3887
+ }
3655
3888
  // Don't add internal workspace packages to the components list
3656
3889
  if (thePkg.type !== "application") {
3657
3890
  pkgList.push(thePkg);
@@ -4685,6 +4918,15 @@ export function parseLeinDep(rawOutput) {
4685
4918
  return [];
4686
4919
  }
4687
4920
 
4921
+ /**
4922
+ * Recursively walks a parsed EDN map node produced by the Leiningen dependency
4923
+ * tree and collects unique dependency entries into the deps array.
4924
+ *
4925
+ * @param {Object} node Parsed EDN node (expected to have a "map" property)
4926
+ * @param {Object} keys_cache Cache object used to deduplicate entries by group-name-version key
4927
+ * @param {Object[]} deps Accumulator array of dependency objects with group, name, and version fields
4928
+ * @returns {Object[]} The populated deps array
4929
+ */
4688
4930
  export function parseLeinMap(node, keys_cache, deps) {
4689
4931
  if (node["map"]) {
4690
4932
  for (const n of node["map"]) {
@@ -7302,6 +7544,16 @@ async function getGoPkgVCSUrl(group, name) {
7302
7544
  return undefined;
7303
7545
  }
7304
7546
 
7547
+ /**
7548
+ * Builds a Go package component object containing purl, bom-ref, integrity hash,
7549
+ * and optionally license and VCS external reference information.
7550
+ *
7551
+ * @param {string} group Package group (module path prefix, may be empty)
7552
+ * @param {string} name Package name (full module path when group is empty)
7553
+ * @param {string} version Package version string
7554
+ * @param {string} hash Integrity hash (e.g. "sha256-…"), used as _integrity
7555
+ * @returns {Promise<Object>} Component object ready for inclusion in a BOM package list
7556
+ */
7305
7557
  export async function getGoPkgComponent(group, name, version, hash) {
7306
7558
  let license;
7307
7559
  if (shouldFetchLicense()) {
@@ -7485,6 +7737,15 @@ export async function parseGoModData(goModData, gosumMap) {
7485
7737
  };
7486
7738
  }
7487
7739
 
7740
+ /**
7741
+ * Parses a Go modules text file (e.g. vendor/modules.txt) and returns a list of
7742
+ * Go package components. Cross-references the go.sum map for integrity hashes and
7743
+ * sets scope and confidence based on hash availability.
7744
+ *
7745
+ * @param {string} txtFile Path to the modules.txt file
7746
+ * @param {Object} gosumMap Map of "module@version" keys to sha256 hash values from go.sum
7747
+ * @returns {Promise<Object[]>} List of Go package component objects with evidence
7748
+ */
7488
7749
  export async function parseGoModulesTxt(txtFile, gosumMap) {
7489
7750
  const pkgList = [];
7490
7751
  const txtData = readFileSync(txtFile, { encoding: "utf-8" });
@@ -7888,6 +8149,14 @@ export async function parseGosumData(gosumData) {
7888
8149
  return pkgList;
7889
8150
  }
7890
8151
 
8152
+ /**
8153
+ * Parses the contents of a Gopkg.lock or Gopkg.toml file (dep tool format) and
8154
+ * returns a list of Go package components. Optionally fetches license information
8155
+ * for each package when FETCH_LICENSE is enabled.
8156
+ *
8157
+ * @param {string} gopkgData Raw string contents of the Gopkg lock/toml file
8158
+ * @returns {Promise<Object[]>} List of Go package component objects
8159
+ */
7891
8160
  export async function parseGopkgData(gopkgData) {
7892
8161
  const pkgList = [];
7893
8162
  if (!gopkgData) {
@@ -7937,6 +8206,13 @@ export async function parseGopkgData(gopkgData) {
7937
8206
  return pkgList;
7938
8207
  }
7939
8208
 
8209
+ /**
8210
+ * Parses the output of `go version -m` (build info) and returns a list of Go
8211
+ * package components for each "dep" line, including name, version, and integrity hash.
8212
+ *
8213
+ * @param {string} buildInfoData Raw string output from `go version -m`
8214
+ * @returns {Promise<Object[]>} List of Go package component objects
8215
+ */
7940
8216
  export async function parseGoVersionData(buildInfoData) {
7941
8217
  const pkgList = [];
7942
8218
  if (!buildInfoData) {
@@ -9263,6 +9539,13 @@ export async function parseCargoData(
9263
9539
  return pkgList;
9264
9540
  }
9265
9541
 
9542
+ /**
9543
+ * Parses a Cargo.lock file's TOML data and returns a flat dependency graph as an
9544
+ * array of objects mapping each package purl to the purls it directly depends on.
9545
+ *
9546
+ * @param {string} cargoLockData Raw TOML string contents of a Cargo.lock file
9547
+ * @returns {Object[]} Array of dependency relationship objects with ref and dependsOn fields
9548
+ */
9266
9549
  export function parseCargoDependencyData(cargoLockData) {
9267
9550
  const purlFromPackageInfo = (pkg) =>
9268
9551
  decodeURIComponent(
@@ -9322,6 +9605,14 @@ export function parseCargoDependencyData(cargoLockData) {
9322
9605
  return result;
9323
9606
  }
9324
9607
 
9608
+ /**
9609
+ * Parses tab-separated cargo-auditable binary metadata output and returns a list
9610
+ * of Rust package components. Optionally fetches crates.io metadata when
9611
+ * FETCH_LICENSE is enabled.
9612
+ *
9613
+ * @param {string} cargoData Tab-separated string output from cargo-auditable or similar tool
9614
+ * @returns {Promise<Object[]>} List of Rust package component objects with group, name, and version
9615
+ */
9325
9616
  export async function parseCargoAuditableData(cargoData) {
9326
9617
  const pkgList = [];
9327
9618
  if (!cargoData) {
@@ -9424,6 +9715,13 @@ export async function parsePubLockData(pubLockData, lockFile) {
9424
9715
  return { rootList, pkgList };
9425
9716
  }
9426
9717
 
9718
+ /**
9719
+ * Parses a Dart pub package's pubspec.yaml content and returns a list containing
9720
+ * a single component object with name, description, version, homepage, and purl.
9721
+ *
9722
+ * @param {string} pubYamlData Raw YAML string contents of a pubspec.yaml file
9723
+ * @returns {Object[]} List containing a single Dart package component object
9724
+ */
9427
9725
  export function parsePubYamlData(pubYamlData) {
9428
9726
  const pkgList = [];
9429
9727
  let yamlObj;
@@ -9450,6 +9748,14 @@ export function parsePubYamlData(pubYamlData) {
9450
9748
  return pkgList;
9451
9749
  }
9452
9750
 
9751
+ /**
9752
+ * Parses Helm chart YAML data (Chart.yaml or repository index.yaml) and returns
9753
+ * a list of Helm chart component objects including the chart itself and any
9754
+ * declared dependencies or index entries.
9755
+ *
9756
+ * @param {string} helmData Raw YAML string contents of a Helm Chart.yaml or index.yaml file
9757
+ * @returns {Object[]} List of Helm chart component objects with name, version, and optional homepage/repository
9758
+ */
9453
9759
  export function parseHelmYamlData(helmData) {
9454
9760
  const pkgList = [];
9455
9761
  let yamlObj;
@@ -9515,6 +9821,17 @@ export function parseHelmYamlData(helmData) {
9515
9821
  return pkgList;
9516
9822
  }
9517
9823
 
9824
+ /**
9825
+ * Recursively walks a parsed YAML/JSON object structure to find container image
9826
+ * references stored under common keys (image, repository, dockerImage, etc.) and
9827
+ * appends discovered image and service entries to pkgList while tracking seen
9828
+ * images in imgList to avoid duplicates.
9829
+ *
9830
+ * @param {Object|Array|string} keyValueObj The object, array, or string node to inspect
9831
+ * @param {Object[]} pkgList Accumulator array that receives {image} and {service} entries
9832
+ * @param {string[]} imgList Accumulator array of image name strings already seen
9833
+ * @returns {string[]} The updated imgList
9834
+ */
9518
9835
  export function recurseImageNameLookup(keyValueObj, pkgList, imgList) {
9519
9836
  if (typeof keyValueObj === "string" || keyValueObj instanceof String) {
9520
9837
  return imgList;
@@ -9600,6 +9917,14 @@ function substituteBuildArgs(statement, buildArgs) {
9600
9917
  return statement;
9601
9918
  }
9602
9919
 
9920
+ /**
9921
+ * Parses the contents of a Dockerfile or Containerfile and returns a list of
9922
+ * base image objects referenced by FROM instructions, substituting ARG default
9923
+ * values where possible and skipping multi-stage build alias references.
9924
+ *
9925
+ * @param {string} fileContents Raw string contents of the Dockerfile/Containerfile
9926
+ * @returns {Object[]} Array of objects with an image property for each unique base image
9927
+ */
9603
9928
  export function parseContainerFile(fileContents) {
9604
9929
  const buildArgs = new Map();
9605
9930
  const imagesSet = new Set();
@@ -9667,6 +9992,13 @@ export function parseContainerFile(fileContents) {
9667
9992
  });
9668
9993
  }
9669
9994
 
9995
+ /**
9996
+ * Parses a Bitbucket Pipelines YAML file and extracts all Docker image references
9997
+ * used as build environments and pipe references (docker:// pipes are normalized).
9998
+ *
9999
+ * @param {string} fileContents Raw string contents of the bitbucket-pipelines.yml file
10000
+ * @returns {Object[]} Array of objects with an image property for each referenced image or pipe
10001
+ */
9670
10002
  export function parseBitbucketPipelinesFile(fileContents) {
9671
10003
  const imgList = [];
9672
10004
 
@@ -9728,6 +10060,14 @@ export function parseBitbucketPipelinesFile(fileContents) {
9728
10060
  return imgList;
9729
10061
  }
9730
10062
 
10063
+ /**
10064
+ * Parses container specification data such as Docker Compose files, Kubernetes
10065
+ * manifests, Tekton tasks, Skaffold configs, or Kustomize overlays (YAML, possibly
10066
+ * multi-document) and returns a list of image, service, and OCI spec entries.
10067
+ *
10068
+ * @param {string} dcData Raw YAML string contents of the container spec file
10069
+ * @returns {Object[]} Array of objects with image, service, or ociSpec properties
10070
+ */
9731
10071
  export function parseContainerSpecData(dcData) {
9732
10072
  const pkgList = [];
9733
10073
  const imgList = [];
@@ -9795,6 +10135,14 @@ export function parseContainerSpecData(dcData) {
9795
10135
  return pkgList;
9796
10136
  }
9797
10137
 
10138
+ /**
10139
+ * Identifies the data flow direction of a Privado processing object based on its
10140
+ * sinkId value: "write" sinks map to "inbound", "read" sinks to "outbound", and
10141
+ * HTTP/gRPC sinks to "bi-directional".
10142
+ *
10143
+ * @param {Object} processingObj Privado processing object, expected to have a sinkId property
10144
+ * @returns {string} Flow direction string: "inbound", "outbound", "bi-directional", or "unknown"
10145
+ */
9798
10146
  export function identifyFlow(processingObj) {
9799
10147
  let flow = "unknown";
9800
10148
  if (processingObj.sinkId) {
@@ -9821,6 +10169,14 @@ function convertProcessing(processing_list) {
9821
10169
  return data_list;
9822
10170
  }
9823
10171
 
10172
+ /**
10173
+ * Parses a Privado data flow JSON file and returns a list of service objects
10174
+ * enriched with data classifications, endpoints, trust-boundary flag, violations,
10175
+ * and git metadata properties extracted from the scan result.
10176
+ *
10177
+ * @param {string} f Path to the Privado scan result JSON file
10178
+ * @returns {Object[]} List of service component objects suitable for a SaaSBOM
10179
+ */
9824
10180
  export function parsePrivadoFile(f) {
9825
10181
  const pData = readFileSync(f, { encoding: "utf-8" });
9826
10182
  const servlist = [];
@@ -9902,6 +10258,15 @@ export function parsePrivadoFile(f) {
9902
10258
  return servlist;
9903
10259
  }
9904
10260
 
10261
+ /**
10262
+ * Parses an OpenAPI specification (JSON or YAML string) and returns a list
10263
+ * containing a single service object with name, version, endpoints, and
10264
+ * authentication flag derived from the spec's info, servers, paths, and
10265
+ * securitySchemes sections.
10266
+ *
10267
+ * @param {string} oaData Raw JSON or YAML string contents of an OpenAPI specification
10268
+ * @returns {Object[]} List containing a single service component object
10269
+ */
9905
10270
  export function parseOpenapiSpecData(oaData) {
9906
10271
  const servlist = [];
9907
10272
  if (!oaData) {
@@ -9954,6 +10319,13 @@ export function parseOpenapiSpecData(oaData) {
9954
10319
  return servlist;
9955
10320
  }
9956
10321
 
10322
+ /**
10323
+ * Parses Haskell Cabal freeze file content and extracts package name and version
10324
+ * pairs from constraint lines (lines containing " ==").
10325
+ *
10326
+ * @param {string} cabalData Raw string contents of a Cabal freeze file
10327
+ * @returns {Object[]} List of package objects with name and version fields
10328
+ */
9957
10329
  export function parseCabalData(cabalData) {
9958
10330
  const pkgList = [];
9959
10331
  if (!cabalData) {
@@ -9982,6 +10354,13 @@ export function parseCabalData(cabalData) {
9982
10354
  return pkgList;
9983
10355
  }
9984
10356
 
10357
+ /**
10358
+ * Parses an Elixir mix.lock file and extracts Hex package name and version pairs
10359
+ * from lines containing ":hex".
10360
+ *
10361
+ * @param {string} mixData Raw string contents of a mix.lock file
10362
+ * @returns {Object[]} List of package objects with name and version fields
10363
+ */
9985
10364
  export function parseMixLockData(mixData) {
9986
10365
  const pkgList = [];
9987
10366
  if (!mixData) {
@@ -10009,376 +10388,27 @@ export function parseMixLockData(mixData) {
10009
10388
  return pkgList;
10010
10389
  }
10011
10390
 
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
10391
  /**
10286
- * Analyze permissions for write access.
10392
+ * Parses a GitHub Actions workflow YAML file and returns a list of action
10393
+ * components for each step that uses an external action (steps with a "uses"
10394
+ * field). Each component captures the action name, group, version/commit SHA,
10395
+ * version pinning type, job context (runner, permissions, environment), and
10396
+ * workflow-level metadata (triggers, concurrency, write permissions).
10287
10397
  *
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
10398
+ * @param {string} f Path to the GitHub Actions workflow YAML file
10399
+ * @returns {Object[]} List of action component objects with purl, properties, and evidence
10323
10400
  */
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";
10401
+ export function parseGitHubWorkflowData(f) {
10402
+ const { components } = parseWorkflowFile(f);
10403
+ return components.filter((c) => c.scope === "required");
10342
10404
  }
10343
10405
 
10344
10406
  /**
10345
- * Extract package manager commands from run steps
10407
+ * Parse Google Cloud Build YAML data and extract container image steps as packages.
10408
+ *
10409
+ * @param {string} cbwData Raw YAML string of a Cloud Build configuration file
10410
+ * @returns {Object[]} Array of package objects parsed from the build steps
10346
10411
  */
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
10412
  export function parseCloudBuildData(cbwData) {
10383
10413
  const pkgList = [];
10384
10414
  const keys_cache = {};
@@ -10455,6 +10485,16 @@ function untilFirst(separator, inputStr) {
10455
10485
  ];
10456
10486
  }
10457
10487
 
10488
+ /**
10489
+ * Map a Conan package reference string to a PackageURL string, name, and version.
10490
+ *
10491
+ * Parses a full Conan package reference of the form
10492
+ * `name/version@user/channel#recipe_revision:package_id#package_revision`
10493
+ * and returns the equivalent purl string together with the extracted name and version.
10494
+ *
10495
+ * @param {string} conanPkgRef Conan package reference string
10496
+ * @returns {Array} Tuple of [purlString, name, version], or [null, null, null] on parse failure
10497
+ */
10458
10498
  export function mapConanPkgRefToPurlStringAndNameAndVersion(conanPkgRef) {
10459
10499
  // A full Conan package reference may be composed of the following segments:
10460
10500
  // conanPkgRef = "name/version@user/channel#recipe_revision:package_id#package_revision"
@@ -10580,6 +10620,16 @@ export function mapConanPkgRefToPurlStringAndNameAndVersion(conanPkgRef) {
10580
10620
  return [purl, info.name, info.version];
10581
10621
  }
10582
10622
 
10623
+ /**
10624
+ * Parse Conan lock file data (conan.lock) and return the package list, dependency map,
10625
+ * and parent component dependencies.
10626
+ *
10627
+ * Supports both the legacy `graph_lock.nodes` format (Conan 1.x) and the newer
10628
+ * `requires` format (Conan 2.x).
10629
+ *
10630
+ * @param {string} conanLockData Raw JSON string of the Conan lock file
10631
+ * @returns {{ pkgList: Object[], dependencies: Object, parentComponentDependencies: string[] }}
10632
+ */
10583
10633
  export function parseConanLockData(conanLockData) {
10584
10634
  const pkgList = [];
10585
10635
  const dependencies = {};
@@ -10663,6 +10713,12 @@ export function parseConanLockData(conanLockData) {
10663
10713
  return { pkgList, dependencies, parentComponentDependencies };
10664
10714
  }
10665
10715
 
10716
+ /**
10717
+ * Parse a Conan conanfile.txt and extract required and optional packages.
10718
+ *
10719
+ * @param {string} conanData Raw text contents of a conanfile.txt
10720
+ * @returns {Object[]} Array of package objects with purl, name, version, and scope
10721
+ */
10666
10722
  export function parseConanData(conanData) {
10667
10723
  const pkgList = [];
10668
10724
  if (!conanData) {
@@ -10698,6 +10754,12 @@ export function parseConanData(conanData) {
10698
10754
  return pkgList;
10699
10755
  }
10700
10756
 
10757
+ /**
10758
+ * Parse Leiningen project.clj data and extract dependency packages.
10759
+ *
10760
+ * @param {string} leinData Raw text contents of a Leiningen project.clj file
10761
+ * @returns {Object[]} Array of package objects with group, name, and version
10762
+ */
10701
10763
  export function parseLeiningenData(leinData) {
10702
10764
  const pkgList = [];
10703
10765
  if (!leinData) {
@@ -10732,6 +10794,14 @@ export function parseLeiningenData(leinData) {
10732
10794
  return pkgList;
10733
10795
  }
10734
10796
 
10797
+ /**
10798
+ * Parse EDN (Extensible Data Notation) deps.edn data and extract dependency packages.
10799
+ *
10800
+ * Handles Clojure deps.edn files, extracting packages listed under the `:deps` key.
10801
+ *
10802
+ * @param {string} rawEdnData Raw EDN text contents of a deps.edn file
10803
+ * @returns {Object[]} Array of package objects with group, name, and version
10804
+ */
10735
10805
  export function parseEdnData(rawEdnData) {
10736
10806
  const pkgList = [];
10737
10807
  if (!rawEdnData) {
@@ -10811,7 +10881,7 @@ export function parseFlakeNix(flakeNixFile) {
10811
10881
  const pkgList = [];
10812
10882
  const dependencies = [];
10813
10883
 
10814
- if (!existsSync(flakeNixFile)) {
10884
+ if (!safeExistsSync(flakeNixFile)) {
10815
10885
  return { pkgList, dependencies };
10816
10886
  }
10817
10887
 
@@ -10896,7 +10966,7 @@ export function parseFlakeLock(flakeLockFile) {
10896
10966
  const pkgList = [];
10897
10967
  const dependencies = [];
10898
10968
 
10899
- if (!existsSync(flakeLockFile)) {
10969
+ if (!safeExistsSync(flakeLockFile)) {
10900
10970
  return { pkgList, dependencies };
10901
10971
  }
10902
10972
 
@@ -11178,6 +11248,13 @@ export function parseNuspecData(nupkgFile, nuspecData) {
11178
11248
  };
11179
11249
  }
11180
11250
 
11251
+ /**
11252
+ * Parse a C# packages.config XML file and return a list of NuGet package components.
11253
+ *
11254
+ * @param {string} pkgData Raw XML string of a packages.config file
11255
+ * @param {string} pkgFile Path to the packages.config file, used for evidence properties
11256
+ * @returns {Object[]} Array of NuGet package objects with purl, name, and version
11257
+ */
11181
11258
  export function parseCsPkgData(pkgData, pkgFile) {
11182
11259
  const pkgList = [];
11183
11260
  if (!pkgData) {
@@ -11710,6 +11787,17 @@ export function parseCsProjData(
11710
11787
  };
11711
11788
  }
11712
11789
 
11790
+ /**
11791
+ * Parse a .NET project.assets.json file and return the package list and dependency tree.
11792
+ *
11793
+ * Extracts NuGet packages and their transitive dependency relationships from the
11794
+ * `libraries` and `targets` sections of a project.assets.json file produced by
11795
+ * the .NET restore process.
11796
+ *
11797
+ * @param {string} csProjData Raw JSON string of the project.assets.json file
11798
+ * @param {string} assetsJsonFile Path to the project.assets.json file, used for evidence properties
11799
+ * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
11800
+ */
11713
11801
  export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
11714
11802
  // extract name, operator, version from .NET package representation
11715
11803
  // like "NLog >= 4.5.0"
@@ -11960,6 +12048,14 @@ export function parseCsProjAssetsData(csProjData, assetsJsonFile) {
11960
12048
  };
11961
12049
  }
11962
12050
 
12051
+ /**
12052
+ * Parse a .NET packages.lock.json file and return the package list, dependency tree,
12053
+ * and list of direct/root dependencies.
12054
+ *
12055
+ * @param {string} csLockData Raw JSON string of the packages.lock.json file
12056
+ * @param {string} pkgLockFile Path to the packages.lock.json file, used for evidence properties
12057
+ * @returns {{ pkgList: Object[], dependenciesList: Object[], rootList: Object[] }}
12058
+ */
11963
12059
  export function parseCsPkgLockData(csLockData, pkgLockFile) {
11964
12060
  const pkgList = [];
11965
12061
  const dependenciesList = [];
@@ -12078,6 +12174,14 @@ export function parseCsPkgLockData(csLockData, pkgLockFile) {
12078
12174
  };
12079
12175
  }
12080
12176
 
12177
+ /**
12178
+ * Parse a Paket dependency manager lock file (paket.lock) and return the package list
12179
+ * and dependency tree.
12180
+ *
12181
+ * @param {string} paketLockData Raw text contents of the paket.lock file
12182
+ * @param {string} pkgLockFile Path to the paket.lock file, used for evidence properties
12183
+ * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
12184
+ */
12081
12185
  export function parsePaketLockData(paketLockData, pkgLockFile) {
12082
12186
  const pkgList = [];
12083
12187
  const dependenciesList = [];
@@ -12252,6 +12356,16 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12252
12356
  const rootRequiresMap = {};
12253
12357
  if (rootRequires) {
12254
12358
  for (const rr of Object.keys(rootRequires)) {
12359
+ // Skip platform requirements (php, hhvm, ext-*, lib-*) — they are never
12360
+ // Composer package names and must not be used to identify root packages.
12361
+ if (
12362
+ rr === "php" ||
12363
+ rr === "hhvm" ||
12364
+ rr.startsWith("ext-") ||
12365
+ rr.startsWith("lib-")
12366
+ ) {
12367
+ continue;
12368
+ }
12255
12369
  rootRequiresMap[rr] = true;
12256
12370
  }
12257
12371
  }
@@ -12403,6 +12517,15 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
12403
12517
  };
12404
12518
  }
12405
12519
 
12520
+ /**
12521
+ * Parse an sbt dependency tree output file and return the package list and dependency tree.
12522
+ *
12523
+ * Reads a file produced by the sbt `dependencyTree` command and extracts Maven artifact
12524
+ * coordinates, building a hierarchical dependency graph. Evicted packages and ranges are ignored.
12525
+ *
12526
+ * @param {string} sbtTreeFile Path to the sbt dependency tree output file
12527
+ * @returns {{ pkgList: Object[], dependenciesList: Object[] }}
12528
+ */
12406
12529
  export function parseSbtTree(sbtTreeFile) {
12407
12530
  const pkgList = [];
12408
12531
  const dependenciesList = [];
@@ -12719,6 +12842,10 @@ export function convertOSQueryResults(
12719
12842
  if (publisher === "null") {
12720
12843
  publisher = "";
12721
12844
  }
12845
+ // For vscode-extension purl type, the publisher is used as the namespace
12846
+ if (queryObj.purlType === "vscode-extension" && publisher) {
12847
+ group = publisher.toLowerCase();
12848
+ }
12722
12849
  let scope;
12723
12850
  const compScope = res.priority;
12724
12851
  if (["required", "optional", "excluded"].includes(compScope)) {
@@ -12819,6 +12946,17 @@ export function convertOSQueryResults(
12819
12946
  return pkgList;
12820
12947
  }
12821
12948
 
12949
+ /**
12950
+ * Create a PackageURL object from a repository URL string, package type, and version.
12951
+ *
12952
+ * Supports HTTPS URLs, SSH `git@` URLs, Bitbucket SSH URLs, and local paths.
12953
+ * Extracts the namespace (host + path prefix) and repository name from the URL.
12954
+ *
12955
+ * @param {string} type PackageURL type (e.g. `"swift"`, `"generic"`)
12956
+ * @param {string} repoUrl Repository URL string
12957
+ * @param {string} version Package version
12958
+ * @returns {PackageURL|undefined} PackageURL object, or undefined for unsupported URL formats
12959
+ */
12822
12960
  export function purlFromUrlString(type, repoUrl, version) {
12823
12961
  let namespace = "";
12824
12962
  let name;
@@ -13111,6 +13249,20 @@ export async function collectMvnDependencies(
13111
13249
  return jarNSMapping;
13112
13250
  }
13113
13251
 
13252
+ /**
13253
+ * Collect Gradle project dependencies by scanning the Gradle cache directory for JAR files
13254
+ * and their associated POM files.
13255
+ *
13256
+ * Uses the `GRADLE_CACHE_DIR` or `GRADLE_USER_HOME` environment variables to locate the
13257
+ * Gradle files-2.1 cache, then delegates to {@link collectJarNS} to extract namespace
13258
+ * and purl information from those JARs.
13259
+ *
13260
+ * @param {string} _gradleCmd Gradle command (unused; reserved for future use)
13261
+ * @param {string} _basePath Base project path (unused; reserved for future use)
13262
+ * @param {boolean} _cleanup Whether to clean up temporary files (unused; reserved for future use)
13263
+ * @param {boolean} _includeCacheDir Whether to include cache directory (unused; reserved for future use)
13264
+ * @returns {Promise<Object>} JAR namespace mapping object returned by collectJarNS
13265
+ */
13114
13266
  export async function collectGradleDependencies(
13115
13267
  _gradleCmd,
13116
13268
  _basePath,
@@ -13324,6 +13476,16 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
13324
13476
  return jarNSMapping;
13325
13477
  }
13326
13478
 
13479
+ /**
13480
+ * Convert a JAR namespace mapping (produced by {@link collectJarNS}) into an array
13481
+ * of CycloneDX package component objects.
13482
+ *
13483
+ * Each entry in the mapping is resolved to a component with name, group, version,
13484
+ * purl, hashes, namespace properties, and source file evidence.
13485
+ *
13486
+ * @param {Object} jarNSMapping Map of purl string to `{ jarFile, pom, namespaces, hashes }`
13487
+ * @returns {Promise<Object[]>} Array of component objects derived from the JAR mapping
13488
+ */
13327
13489
  export async function convertJarNSToPackages(jarNSMapping) {
13328
13490
  const pkgList = [];
13329
13491
  for (const purl of Object.keys(jarNSMapping)) {
@@ -13427,6 +13589,12 @@ export function parsePomXml(pomXmlData) {
13427
13589
  return undefined;
13428
13590
  }
13429
13591
 
13592
+ /**
13593
+ * Parse a JAR MANIFEST.MF file and return its key-value pairs as an object.
13594
+ *
13595
+ * @param {string} jarMetadata Raw text contents of a MANIFEST.MF file
13596
+ * @returns {Object} Key-value pairs extracted from the manifest
13597
+ */
13430
13598
  export function parseJarManifest(jarMetadata) {
13431
13599
  const metadata = {};
13432
13600
  if (!jarMetadata) {
@@ -13444,6 +13612,12 @@ export function parseJarManifest(jarMetadata) {
13444
13612
  return metadata;
13445
13613
  }
13446
13614
 
13615
+ /**
13616
+ * Parse a Maven pom.properties file and return its key-value pairs as an object.
13617
+ *
13618
+ * @param {string} pomProperties Raw text contents of a pom.properties file
13619
+ * @returns {Object} Key-value pairs extracted from the properties file
13620
+ */
13447
13621
  export function parsePomProperties(pomProperties) {
13448
13622
  const properties = {};
13449
13623
  if (!pomProperties) {
@@ -13461,6 +13635,13 @@ export function parsePomProperties(pomProperties) {
13461
13635
  return properties;
13462
13636
  }
13463
13637
 
13638
+ /**
13639
+ * Encode a string for safe inclusion in a PackageURL, percent-encoding special characters
13640
+ * while preserving already-encoded `%40` sequences and keeping `:` and `/` unencoded.
13641
+ *
13642
+ * @param {string} s String to encode
13643
+ * @returns {string} Encoded string suitable for use in a PackageURL component
13644
+ */
13464
13645
  export function encodeForPurl(s) {
13465
13646
  return s && !s.includes("%40")
13466
13647
  ? encodeURIComponent(s).replace(/%3A/g, ":").replace(/%2F/g, "/")
@@ -14364,10 +14545,10 @@ export async function parsePodfileLock(podfileLock, projectPath) {
14364
14545
  },
14365
14546
  ];
14366
14547
  let podspec = join(projectLocation, `${podName}.podspec`);
14367
- if (!existsSync(podspec)) {
14548
+ if (!safeExistsSync(podspec)) {
14368
14549
  podspec = `${podspec}.json`;
14369
14550
  }
14370
- if (existsSync(podspec)) {
14551
+ if (safeExistsSync(podspec)) {
14371
14552
  dependency.metadata.properties.push({
14372
14553
  name: "cdx:pods:podspecLocation",
14373
14554
  value: podspec,
@@ -15112,6 +15293,19 @@ export function getAtomCommand() {
15112
15293
  return "atom";
15113
15294
  }
15114
15295
 
15296
+ /**
15297
+ * Execute the atom tool against a source directory or file with the given arguments.
15298
+ *
15299
+ * Resolves the atom binary via `getAtomCommand`, sets up the required environment
15300
+ * (including `JAVA_HOME` from `ATOM_JAVA_HOME` if set), and spawns the process.
15301
+ * Logs diagnostic messages for common failure modes such as unsupported Java versions,
15302
+ * missing `astgen`, and JVM crashes.
15303
+ *
15304
+ * @param {string} src Path to the source directory or file to analyse
15305
+ * @param {string[]} args Arguments to pass to the atom command
15306
+ * @param {Object} extra_env Additional environment variables to merge into the process environment
15307
+ * @returns {boolean} `true` if atom executed successfully and the language is supported; `false` otherwise
15308
+ */
15115
15309
  export function executeAtom(src, args, extra_env = {}) {
15116
15310
  const cwd =
15117
15311
  safeExistsSync(src) && lstatSync(src).isDirectory() ? src : dirname(src);
@@ -16195,6 +16389,13 @@ export function getPipTreeForPackages(
16195
16389
  }
16196
16390
 
16197
16391
  // taken from a very old package https://github.com/keithamus/parse-packagejson-name/blob/master/index.js
16392
+ /**
16393
+ * Parse a package.json `name` field (or a plain string) and extract its scope,
16394
+ * full name, project name, and module name components.
16395
+ *
16396
+ * @param {string|Object} name The package name string or an object with a `name` property
16397
+ * @returns {{ scope: string|null, fullName: string, projectName: string|null, moduleName: string|null }}
16398
+ */
16198
16399
  export function parsePackageJsonName(name) {
16199
16400
  const nameRegExp = /^(?:@([^/]+)\/)?(([^.]+)(?:\.(.*))?)$/;
16200
16401
  const returnObject = {
@@ -16373,6 +16574,16 @@ export async function addEvidenceForImports(
16373
16574
  return pkgList;
16374
16575
  }
16375
16576
 
16577
+ /**
16578
+ * Comparator function for sorting CycloneDX component objects.
16579
+ *
16580
+ * Compares components by `bom-ref`, then `purl`, then `name`, using locale-aware
16581
+ * string comparison on the first available key.
16582
+ *
16583
+ * @param {Object|string} a First component to compare
16584
+ * @param {Object|string} b Second component to compare
16585
+ * @returns {number} Negative, zero, or positive integer as required by Array.sort
16586
+ */
16376
16587
  export function componentSorter(a, b) {
16377
16588
  if (a && b) {
16378
16589
  for (const k of ["bom-ref", "purl", "name"]) {
@@ -16384,6 +16595,19 @@ export function componentSorter(a, b) {
16384
16595
  return a.localeCompare(b);
16385
16596
  }
16386
16597
 
16598
+ /**
16599
+ * Parse a CMake-generated dot/graphviz file and extract components and their dependency
16600
+ * relationships.
16601
+ *
16602
+ * The first `digraph` entry becomes the parent component. Subsequent `node` entries
16603
+ * with a `label` attribute are treated as direct dependencies, while commented
16604
+ * `node -> node` relationships are used to construct the dependency graph.
16605
+ *
16606
+ * @param {string} dotFile Path to the CMake-generated dot file
16607
+ * @param {string} pkgType PackageURL type to assign to extracted packages (e.g. `"generic"`)
16608
+ * @param {Object} options CLI options; may contain `projectGroup`, `projectName`, and `projectVersion`
16609
+ * @returns {{ parentComponent: Object, pkgList: Object[], dependenciesList: Object[] }}
16610
+ */
16387
16611
  export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
16388
16612
  const dotGraphData = readFileSync(dotFile, { encoding: "utf-8" });
16389
16613
  const pkgList = [];
@@ -16493,6 +16717,19 @@ export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
16493
16717
  };
16494
16718
  }
16495
16719
 
16720
+ /**
16721
+ * Parse a CMake-like build file (CMakeLists.txt, meson.build, etc.) and extract the
16722
+ * parent component and list of dependency packages.
16723
+ *
16724
+ * Handles `set`, `project`, `find_package`, `find_library`, `find_dependency`,
16725
+ * `find_file`, `FetchContent_MakeAvailable`, and `dependency()` directives.
16726
+ * Uses the MesonWrapDB to improve name resolution confidence.
16727
+ *
16728
+ * @param {string} cmakeListFile Path to the CMake-like build file
16729
+ * @param {string} pkgType PackageURL type to assign to extracted packages (e.g. `"generic"`)
16730
+ * @param {Object} options CLI options; may contain `projectGroup`, `projectName`, and `projectVersion`
16731
+ * @returns {{ parentComponent: Object, pkgList: Object[] }}
16732
+ */
16496
16733
  export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
16497
16734
  let cmakeListData = readFileSync(cmakeListFile, { encoding: "utf-8" });
16498
16735
  const pkgList = [];
@@ -16746,6 +16983,14 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
16746
16983
  };
16747
16984
  }
16748
16985
 
16986
+ /**
16987
+ * Find the OS package component that provides a given file, by searching the
16988
+ * `PkgProvides` property of each package in the OS package list.
16989
+ *
16990
+ * @param {string} afile Filename or path to look up (matched case-insensitively)
16991
+ * @param {Object[]} osPkgsList Array of OS package component objects to search
16992
+ * @returns {Object|undefined} The matching OS package component, or undefined if not found
16993
+ */
16749
16994
  export function getOSPackageForFile(afile, osPkgsList) {
16750
16995
  for (const ospkg of osPkgsList) {
16751
16996
  for (const props of ospkg.properties || []) {
@@ -17359,6 +17604,18 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
17359
17604
  };
17360
17605
  }
17361
17606
 
17607
+ /**
17608
+ * Enrich .NET package components with occurrence evidence and imported module/method
17609
+ * information from a dosai dependency slices file.
17610
+ *
17611
+ * Builds a mapping of DLL filenames to purls using the `PackageFiles` property of each
17612
+ * package, then reads the slices file to add occurrence locations, imported modules,
17613
+ * called methods, and assembly version information where available.
17614
+ *
17615
+ * @param {Object[]} pkgList Array of .NET package component objects to enrich
17616
+ * @param {string} slicesFile Path to the dosai dependency slices JSON file
17617
+ * @returns {Object[]} The enriched package list (same array, mutated in place)
17618
+ */
17362
17619
  export function addEvidenceForDotnet(pkgList, slicesFile) {
17363
17620
  // We need two datastructures.
17364
17621
  // dll to purl mapping from the pkgList
@@ -17793,7 +18050,7 @@ export function collectSharedLibs(
17793
18050
  }
17794
18051
 
17795
18052
  function collectAllLdConfs(basePath, ldConf, allLdConfDirs, libPaths) {
17796
- if (ldConf && existsSync(join(basePath, ldConf))) {
18053
+ if (ldConf && safeExistsSync(join(basePath, ldConf))) {
17797
18054
  const ldConfData = readFileSync(join(basePath, ldConf), "utf-8");
17798
18055
  for (let line of ldConfData.split("\n")) {
17799
18056
  line = line.replace("\r", "").trim();
@@ -17979,6 +18236,14 @@ export function retrieveCdxgenVersion() {
17979
18236
  return `\x1b[1mCycloneDX Generator ${packageJson.version}\x1b[0m\nRuntime: ${runtimeInfo.runtime}, Version: ${runtimeInfo.version}`;
17980
18237
  }
17981
18238
 
18239
+ /**
18240
+ * Retrieve the version of the cdxgen plugins binary package from package.json.
18241
+ *
18242
+ * Reads the local package.json and searches the `optionalDependencies` for a package
18243
+ * whose name starts with `@cdxgen/cdxgen-plugins-bin`, returning its declared version.
18244
+ *
18245
+ * @returns {string|undefined} Version string of the plugins binary package, or undefined if not found
18246
+ */
17982
18247
  export function retrieveCdxgenPluginVersion() {
17983
18248
  const packageJsonAsString = readFileSync(
17984
18249
  join(dirNameStr, "package.json"),
@@ -18043,3 +18308,13 @@ export function splitCommandArgs(commandString) {
18043
18308
  }
18044
18309
  return args;
18045
18310
  }
18311
+
18312
+ /**
18313
+ * Convert hyphenated strings to camel case.
18314
+ *
18315
+ * @param {String} str String to convert
18316
+ * @returns {String} camelCased string
18317
+ */
18318
+ export function toCamel(str) {
18319
+ return str.replace(/-([a-z])/g, (_, g) => g.toUpperCase());
18320
+ }