@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
package/lib/cli/index.js CHANGED
@@ -2,9 +2,9 @@ import { Buffer } from "node:buffer";
2
2
  import {
3
3
  accessSync,
4
4
  constants,
5
- existsSync,
6
5
  lstatSync,
7
6
  mkdtempSync,
7
+ readdirSync,
8
8
  readFileSync,
9
9
  rmSync,
10
10
  statSync,
@@ -19,22 +19,19 @@ import got from "got";
19
19
  import { PackageURL } from "packageurl-js";
20
20
  import { gte, lte } from "semver";
21
21
  import { parse } from "ssri";
22
- import { table } from "table";
23
22
  import { v4 as uuidv4 } from "uuid";
24
23
  import { parse as loadYaml } from "yaml";
25
24
 
26
25
  import { findJSImportsExports } from "../helpers/analyzer.js";
27
26
  import { parseCaxaMetadata } from "../helpers/caxa.js";
28
- import { collectOSCryptoLibs } from "../helpers/cbomutils.js";
29
- import {
30
- collectEnvInfo,
31
- GIT_COMMAND,
32
- getBranch,
33
- getOriginUrl,
34
- gitTreeHashes,
35
- listFiles,
36
- } from "../helpers/envcontext.js";
27
+ import { mergeDependencies, trimComponents } from "../helpers/depsUtils.js";
28
+ import { GIT_COMMAND } from "../helpers/envcontext.js";
37
29
  import { thoughtLog } from "../helpers/logger.js";
30
+ import {
31
+ buildDependencyTrackBomPayload,
32
+ getDependencyTrackBomUrl,
33
+ } from "../helpers/remote/dependency-track.js";
34
+ import { table } from "../helpers/table.js";
38
35
  import {
39
36
  addEvidenceForDotnet,
40
37
  addEvidenceForImports,
@@ -176,6 +173,14 @@ import {
176
173
  shouldFetchLicense,
177
174
  splitOutputByGradleProjects,
178
175
  } from "../helpers/utils.js";
176
+ import {
177
+ cleanupTempDir,
178
+ collectInstalledExtensions,
179
+ discoverIdeExtensionDirs,
180
+ extractVsixToTempDir,
181
+ parseVsixFile,
182
+ VSCODE_EXTENSION_PURL_TYPE,
183
+ } from "../helpers/vsixutils.js";
179
184
  import {
180
185
  executeOsQuery,
181
186
  getBinaryBom,
@@ -438,160 +443,6 @@ const addLifecyclesSection = (options) => {
438
443
  return lifecycles;
439
444
  };
440
445
 
441
- /**
442
- * Method to generate the formulation section based on git metadata
443
- *
444
- * @param {Object} options
445
- * @param {Object} context Context
446
- * @returns {Array} formulation array
447
- */
448
- const addFormulationSection = (options, context) => {
449
- const formulation = [];
450
- const provides = [];
451
- const gitBranch = getBranch();
452
- const originUrl = getOriginUrl();
453
- const gitFiles = listFiles();
454
- const treeHashes = gitTreeHashes();
455
- let parentOmniborId;
456
- let treeOmniborId;
457
- let components = [];
458
- const aformulation = {};
459
- // Reuse any existing formulation components
460
- // See: PR #1172
461
- if (context?.formulationList?.length) {
462
- components = components.concat(trimComponents(context.formulationList));
463
- }
464
- if (options.specVersion >= 1.6 && Object.keys(treeHashes).length === 2) {
465
- parentOmniborId = `gitoid:blob:sha1:${treeHashes.parent}`;
466
- treeOmniborId = `gitoid:blob:sha1:${treeHashes.tree}`;
467
- components.push({
468
- type: "file",
469
- name: "git-parent",
470
- description: "Git Parent Node.",
471
- "bom-ref": parentOmniborId,
472
- omniborId: [parentOmniborId],
473
- swhid: [`swh:1:rev:${treeHashes.parent}`],
474
- });
475
- components.push({
476
- type: "file",
477
- name: "git-tree",
478
- description: "Git Tree Node.",
479
- "bom-ref": treeOmniborId,
480
- omniborId: [treeOmniborId],
481
- swhid: [`swh:1:rev:${treeHashes.tree}`],
482
- });
483
- provides.push({
484
- ref: parentOmniborId,
485
- provides: [treeOmniborId],
486
- });
487
- }
488
- // Collect git related components
489
- if (gitBranch && gitFiles) {
490
- const gitFileComponents = gitFiles.map((f) =>
491
- options.specVersion >= 1.6
492
- ? {
493
- type: "file",
494
- name: f.name,
495
- version: f.hash,
496
- omniborId: [f.omniborId],
497
- swhid: [f.swhid],
498
- }
499
- : {
500
- type: "file",
501
- name: f.name,
502
- version: f.hash,
503
- },
504
- );
505
- components = components.concat(gitFileComponents);
506
- // Complete the Artifact Dependency Graph
507
- if (options.specVersion >= 1.6 && treeOmniborId) {
508
- provides.push({
509
- ref: treeOmniborId,
510
- provides: gitFiles.map((f) => f.ref),
511
- });
512
- }
513
- }
514
- // Collect build environment details
515
- const infoComponents = collectEnvInfo(options.path);
516
- if (infoComponents?.length) {
517
- components = components.concat(infoComponents);
518
- }
519
- // Should we include the OS crypto libraries
520
- if (options.includeCrypto) {
521
- const cryptoLibs = collectOSCryptoLibs(options);
522
- if (cryptoLibs?.length) {
523
- components = components.concat(cryptoLibs);
524
- }
525
- }
526
- aformulation["bom-ref"] = uuidv4();
527
- aformulation.components = trimComponents(components);
528
- let environmentVars = gitBranch?.length
529
- ? [{ name: "GIT_BRANCH", value: gitBranch }]
530
- : [];
531
- const envPrefixes = [
532
- "GIT",
533
- "ANDROID",
534
- "DENO",
535
- "DOTNET",
536
- "JAVA_",
537
- "SDKMAN",
538
- "CARGO",
539
- "CONDA",
540
- "RUST",
541
- "GEM_",
542
- "SCALA_",
543
- "MAVEN_",
544
- "GRADLE_",
545
- "NODE_",
546
- ];
547
- const envBlocklist = [
548
- "key",
549
- "token",
550
- "pass",
551
- "secret",
552
- "user",
553
- "email",
554
- "auth",
555
- "session",
556
- "proxy",
557
- "cred",
558
- ];
559
-
560
- for (const aevar of Object.keys(process.env)) {
561
- const lower = aevar.toLowerCase();
562
- const value = process.env[aevar] ?? "";
563
- if (
564
- envPrefixes.some((p) => aevar.startsWith(p)) &&
565
- !envBlocklist.some((b) => lower.includes(b)) &&
566
- !envBlocklist.some((b) => value.includes(b)) &&
567
- value.length
568
- ) {
569
- environmentVars.push({
570
- name: aevar,
571
- value,
572
- });
573
- }
574
- }
575
- if (!environmentVars.length) {
576
- environmentVars = undefined;
577
- }
578
- let sourceInput;
579
- if (environmentVars) {
580
- sourceInput = { environmentVars };
581
- }
582
- const sourceWorkflow = {
583
- "bom-ref": uuidv4(),
584
- uid: uuidv4(),
585
- taskTypes: originUrl ? ["build", "clone"] : ["build"],
586
- };
587
- if (sourceInput) {
588
- sourceWorkflow.inputs = [sourceInput];
589
- }
590
- aformulation.workflows = [sourceWorkflow];
591
- formulation.push(aformulation);
592
- return { formulation, provides };
593
- };
594
-
595
446
  /**
596
447
  * Function to create metadata block
597
448
  *
@@ -1000,6 +851,7 @@ function addExternalReferences(opkg) {
1000
851
  * @param {Object} allImports All imports
1001
852
  * @param {Object} pkg Package object
1002
853
  * @param {string} ptype Package type
854
+ * @returns {Object[]} Array of component objects
1003
855
  */
1004
856
  export function listComponents(options, allImports, pkg, ptype = "npm") {
1005
857
  const compMap = {};
@@ -1067,6 +919,15 @@ function addComponent(
1067
919
  purl = undefined;
1068
920
  purlString = undefined;
1069
921
  }
922
+ // Some applications like github workflow steps and commands do not have purl
923
+ if (
924
+ pkg.purl === undefined &&
925
+ !pkg?.["bom-ref"]?.startsWith("pkg:") &&
926
+ pkg?.type === "application"
927
+ ) {
928
+ purl = undefined;
929
+ purlString = undefined;
930
+ }
1070
931
  const description = pkg.description || undefined;
1071
932
  let compScope = pkg.scope;
1072
933
  if (allImports) {
@@ -1106,7 +967,12 @@ function addComponent(
1106
967
  component.data = pkg.data || undefined;
1107
968
  }
1108
969
  component["type"] = determinePackageType(pkg);
1109
- component["bom-ref"] = decodeURIComponent(purlString);
970
+ if (purlString) {
971
+ component["bom-ref"] = decodeURIComponent(purlString);
972
+ } else if (pkg["bom-ref"]) {
973
+ component["bom-ref"] = pkg["bom-ref"];
974
+ }
975
+
1110
976
  if (
1111
977
  component.externalReferences === undefined ||
1112
978
  component.externalReferences.length === 0
@@ -1136,6 +1002,18 @@ function addComponent(
1136
1002
  }
1137
1003
  delete component.authors;
1138
1004
  }
1005
+ // Downgrade from 1.7
1006
+ if (options.specVersion < 1.7) {
1007
+ if (component.isExternal) {
1008
+ delete component.isExternal;
1009
+ }
1010
+ if (component.versionRange) {
1011
+ console.warn(
1012
+ `Version Range is not supported in ${options.specVersion} specifications. Please run cdxgen with --spec-version 1.7`,
1013
+ );
1014
+ delete component.versionRange;
1015
+ }
1016
+ }
1139
1017
  // Retain any tags
1140
1018
  if (
1141
1019
  options.specVersion >= 1.6 &&
@@ -1155,12 +1033,10 @@ function addComponent(
1155
1033
  if (pkg.components) {
1156
1034
  component.components = pkg.components;
1157
1035
  }
1036
+ const compMapKey = component.purl || component["bom-ref"];
1158
1037
  // Issue: 1353. We need to keep merging the properties
1159
- if (compMap[component.purl]) {
1160
- const mergedComponents = trimComponents([
1161
- compMap[component.purl],
1162
- component,
1163
- ]);
1038
+ if (compMap[compMapKey]) {
1039
+ const mergedComponents = trimComponents([compMap[compMapKey], component]);
1164
1040
  if (mergedComponents?.length === 1) {
1165
1041
  component = mergedComponents[0];
1166
1042
  }
@@ -1196,7 +1072,7 @@ function addComponent(
1196
1072
  component.evidence.identity = pkg.evidence.identity[0];
1197
1073
  }
1198
1074
  }
1199
- compMap[component.purl] = component;
1075
+ compMap[compMapKey] = component;
1200
1076
  }
1201
1077
  if (pkg.dependencies) {
1202
1078
  Object.keys(pkg.dependencies)
@@ -1380,24 +1256,22 @@ const buildBomNSData = (options, pkgInfo, ptype, context) => {
1380
1256
  // CycloneDX Json Template
1381
1257
  const jsonTpl = {
1382
1258
  bomFormat: "CycloneDX",
1383
- specVersion: `${options.specVersion || "1.5"}`,
1259
+ specVersion: `${options.specVersion || "1.7"}`,
1384
1260
  serialNumber: serialNum,
1385
1261
  version: 1,
1386
1262
  metadata: metadata,
1387
1263
  components,
1388
1264
  dependencies,
1389
1265
  };
1390
- const formulationData =
1391
- options.includeFormulation && options.specVersion >= 1.5
1392
- ? addFormulationSection(options, context)
1393
- : undefined;
1394
- if (formulationData) {
1395
- jsonTpl.formulation = formulationData.formulation;
1396
- }
1397
1266
  bomNSData.bomJson = jsonTpl;
1398
1267
  bomNSData.nsMapping = nsMapping;
1399
1268
  bomNSData.dependencies = dependencies;
1400
1269
  bomNSData.parentComponent = parentComponent;
1270
+ // Carry language-specific formulation data (e.g. Pixi) so that
1271
+ // postProcess can merge it when building the final formulation section.
1272
+ if (context?.formulationList?.length) {
1273
+ bomNSData.formulationList = context.formulationList;
1274
+ }
1401
1275
  }
1402
1276
  return bomNSData;
1403
1277
  };
@@ -1489,6 +1363,7 @@ export async function createJarBom(path, options) {
1489
1363
  *
1490
1364
  * @param {string} path to the project
1491
1365
  * @param {Object} options Parse options from the cli
1366
+ * @returns {Object|undefined} BOM object
1492
1367
  */
1493
1368
  export function createAndroidBom(path, options) {
1494
1369
  return createBinaryBom(path, options);
@@ -1499,6 +1374,7 @@ export function createAndroidBom(path, options) {
1499
1374
  *
1500
1375
  * @param {string} path to the project
1501
1376
  * @param {Object} options Parse options from the cli
1377
+ * @returns {Object|undefined} BOM object
1502
1378
  */
1503
1379
  export function createBinaryBom(path, options) {
1504
1380
  const tempDir = mkdtempSync(join(getTmpDir(), "blint-tmp-"));
@@ -1522,6 +1398,7 @@ export function createBinaryBom(path, options) {
1522
1398
  *
1523
1399
  * @param {string} path to the project
1524
1400
  * @param {Object} options Parse options from the cli
1401
+ * @returns {Promise<Object>} Promise resolving to BOM object
1525
1402
  */
1526
1403
  export async function createJavaBom(path, options) {
1527
1404
  let jarNSMapping = {};
@@ -1617,6 +1494,8 @@ export async function createJavaBom(path, options) {
1617
1494
  }
1618
1495
  let result;
1619
1496
  let mvnArgs;
1497
+ // FIXME: How do we motivate everyone to upgrade to 1.7?
1498
+ const toolsSpecVersion = 1.6;
1620
1499
  if (isQuarkus) {
1621
1500
  thoughtLog(
1622
1501
  "This appears to be a Quarkus project. Let's use the right Maven plugin.",
@@ -1627,12 +1506,14 @@ export async function createJavaBom(path, options) {
1627
1506
  "quarkus:dependency-sbom",
1628
1507
  "-Dquarkus.analytics.disabled=true",
1629
1508
  ];
1630
- if (options.specVersion) {
1509
+ if (options.specVersion >= 1.6) {
1631
1510
  mvnArgs = mvnArgs.concat(
1632
- `-Dquarkus.dependency.sbom.schema-version=${options.specVersion}`,
1511
+ `-Dquarkus.dependency.sbom.schema-version=${toolsSpecVersion}`,
1633
1512
  );
1634
1513
  }
1635
1514
  } else {
1515
+ // FIXME: The last maven plugin release was on November 28th, 2024.
1516
+ // Should we fork this repo and maintain it ourselves?
1636
1517
  const cdxMavenPlugin =
1637
1518
  process.env.CDX_MAVEN_PLUGIN ||
1638
1519
  "org.cyclonedx:cyclonedx-maven-plugin:2.9.1";
@@ -1654,9 +1535,11 @@ export async function createJavaBom(path, options) {
1654
1535
  const addArgs = process.env.MVN_ARGS.split(" ");
1655
1536
  mvnArgs = mvnArgs.concat(addArgs);
1656
1537
  }
1657
- // specVersion 1.4 doesn't support externalReferences.type=disribution-intake
1538
+ // specVersion 1.4 doesn't support externalReferences.type=distribution-intake
1658
1539
  // so we need to run the plugin with the correct version
1659
- if (options.specVersion) {
1540
+ if (options.specVersion >= 1.6) {
1541
+ mvnArgs = mvnArgs.concat(`-DschemaVersion=${toolsSpecVersion}`);
1542
+ } else if (options.specVersion > 1.4) {
1660
1543
  mvnArgs = mvnArgs.concat(`-DschemaVersion=${options.specVersion}`);
1661
1544
  }
1662
1545
  }
@@ -2769,6 +2652,7 @@ export async function createJavaBom(path, options) {
2769
2652
  *
2770
2653
  * @param {string} path to the project
2771
2654
  * @param {Object} options Parse options from the cli
2655
+ * @returns {Promise<Object>} Promise resolving to BOM object
2772
2656
  */
2773
2657
  export async function createNodejsBom(path, options) {
2774
2658
  let pkgList = [];
@@ -2849,6 +2733,21 @@ export async function createNodejsBom(path, options) {
2849
2733
  `${options.multiProject ? "**/" : ""}bower.json`,
2850
2734
  options,
2851
2735
  );
2736
+ if (DEBUG_MODE) {
2737
+ const wasmFiles = getAllFiles(
2738
+ path,
2739
+ `${options.multiProject ? "**/" : ""}*.wasm`,
2740
+ {
2741
+ ...options,
2742
+ includeNodeModulesDir: true,
2743
+ },
2744
+ );
2745
+ if (wasmFiles?.length) {
2746
+ console.log(
2747
+ `Found ${wasmFiles.length} wasm files in this project. cdxgen will make a best attempt to identify the exports and imports from these files.`,
2748
+ );
2749
+ }
2750
+ }
2852
2751
  // Parse min js files
2853
2752
  if (minJsFiles?.length) {
2854
2753
  manifestFiles = manifestFiles.concat(minJsFiles);
@@ -3203,8 +3102,9 @@ export async function createNodejsBom(path, options) {
3203
3102
  const basePath = dirname(f);
3204
3103
  // Determine the parent component
3205
3104
  const packageJsonF = join(basePath, "package.json");
3206
- const pnpmHooks = join(basePath, ".pnpmfile.cjs");
3207
- if (safeExistsSync(pnpmHooks)) {
3105
+ const pnpmCjsHooks = join(basePath, ".pnpmfile.cjs");
3106
+ const pnpmMjsHooks = join(basePath, ".pnpmfile.mjs");
3107
+ if (safeExistsSync(pnpmMjsHooks) || safeExistsSync(pnpmCjsHooks)) {
3208
3108
  thoughtLog("Wait, this pnpm project uses install hooks.");
3209
3109
  }
3210
3110
  if (!Object.keys(parentComponent).length) {
@@ -3367,6 +3267,7 @@ export async function createNodejsBom(path, options) {
3367
3267
  const pnpmLock = join(path, "common", "config", "rush", "pnpm-lock.yaml");
3368
3268
  if (safeExistsSync(swFile)) {
3369
3269
  let pkgList = await parseNodeShrinkwrap(swFile);
3270
+ pkgList = addWasmComponentsFromImports(pkgList, allImports);
3370
3271
  if (allImports && Object.keys(allImports).length) {
3371
3272
  pkgList = await addEvidenceForImports(
3372
3273
  pkgList,
@@ -3383,9 +3284,13 @@ export async function createNodejsBom(path, options) {
3383
3284
  }
3384
3285
  if (safeExistsSync(pnpmLock)) {
3385
3286
  const pnpmLockObj = await parsePnpmLock(pnpmLock);
3287
+ let pkgList = addWasmComponentsFromImports(
3288
+ pnpmLockObj.pkgList,
3289
+ allImports,
3290
+ );
3386
3291
  if (allImports && Object.keys(allImports).length) {
3387
3292
  pkgList = await addEvidenceForImports(
3388
- pnpmLockObj.pkgList,
3293
+ pkgList,
3389
3294
  allImports,
3390
3295
  allExports,
3391
3296
  options.deep,
@@ -3662,6 +3567,7 @@ export async function createNodejsBom(path, options) {
3662
3567
  options.parentComponent = parentComponent;
3663
3568
  }
3664
3569
  if (allImports && Object.keys(allImports).length) {
3570
+ pkgList = addWasmComponentsFromImports(pkgList, allImports);
3665
3571
  pkgList = await addEvidenceForImports(
3666
3572
  pkgList,
3667
3573
  allImports,
@@ -3677,6 +3583,84 @@ export async function createNodejsBom(path, options) {
3677
3583
  });
3678
3584
  }
3679
3585
 
3586
+ const WASM_IMPORT_PATTERN = /\.wasm([?#].*)?$/i;
3587
+
3588
+ /**
3589
+ * Adds generic wasm components from discovered source imports.
3590
+ *
3591
+ * @param {Array<Object>} pkgList Node.js package list
3592
+ * @param {Object} allImports analyzer imports map
3593
+ * @returns {Array<Object>} pkgList enriched with wasm components
3594
+ */
3595
+ const addWasmComponentsFromImports = (pkgList, allImports) => {
3596
+ if (!allImports || !Object.keys(allImports).length) {
3597
+ return pkgList;
3598
+ }
3599
+ const existingPurls = new Set();
3600
+ for (const pkg of pkgList) {
3601
+ if (pkg?.purl) {
3602
+ existingPurls.add(pkg.purl);
3603
+ }
3604
+ }
3605
+ for (const [importPath, occurrences] of Object.entries(allImports)) {
3606
+ if (!WASM_IMPORT_PATTERN.test(importPath)) {
3607
+ continue;
3608
+ }
3609
+ const cleanImportPath = importPath.replace(/[?#].*$/, "");
3610
+ const normalizedImportPath = cleanImportPath
3611
+ .replace(/\\/g, "/")
3612
+ .replace(/^\.\//, "");
3613
+ const wasmComponentName = normalizedImportPath || cleanImportPath;
3614
+ const wasmFileName = basename(cleanImportPath);
3615
+ if (!allImports[wasmComponentName]) {
3616
+ allImports[wasmComponentName] = new Set();
3617
+ }
3618
+ for (const occurrence of occurrences) {
3619
+ allImports[wasmComponentName].add(occurrence);
3620
+ }
3621
+ const wasmPurl = new PackageURL(
3622
+ "generic",
3623
+ "",
3624
+ wasmFileName,
3625
+ "",
3626
+ normalizedImportPath ? { path: normalizedImportPath } : undefined,
3627
+ undefined,
3628
+ ).toString();
3629
+ if (existingPurls.has(wasmPurl)) {
3630
+ continue;
3631
+ }
3632
+ const firstOccurrence = Array.from(occurrences)[0];
3633
+ const srcFile = firstOccurrence?.importedAs || importPath;
3634
+ pkgList.push({
3635
+ name: wasmComponentName,
3636
+ type: "library",
3637
+ purl: wasmPurl,
3638
+ "bom-ref": wasmPurl,
3639
+ properties: [
3640
+ {
3641
+ name: "SrcFile",
3642
+ value: srcFile,
3643
+ },
3644
+ ],
3645
+ evidence: {
3646
+ identity: {
3647
+ field: "purl",
3648
+ confidence: 0.3,
3649
+ methods: [
3650
+ {
3651
+ technique: "filename",
3652
+ confidence: 0.3,
3653
+ value: srcFile,
3654
+ },
3655
+ ],
3656
+ },
3657
+ },
3658
+ });
3659
+ existingPurls.add(wasmPurl);
3660
+ }
3661
+ return pkgList;
3662
+ };
3663
+
3680
3664
  /**
3681
3665
  * Function to create bom string for Projects that use Pixi package manager.
3682
3666
  * createPixiBom is based on createPythonBom.
@@ -3687,6 +3671,7 @@ export async function createNodejsBom(path, options) {
3687
3671
  *
3688
3672
  * @param {String} path
3689
3673
  * @param {Object} options
3674
+ * @returns {Object | null} BOM object, or `null` when `pixi.lock` is absent and `options.installDeps` is false
3690
3675
  */
3691
3676
  export function createPixiBom(path, options) {
3692
3677
  const allImports = {};
@@ -3765,6 +3750,7 @@ export function createPixiBom(path, options) {
3765
3750
  *
3766
3751
  * @param {string} path to the project
3767
3752
  * @param {Object} options Parse options from the cli
3753
+ * @returns {Promise<Object>} Promise resolving to BOM object
3768
3754
  */
3769
3755
  export async function createPythonBom(path, options) {
3770
3756
  let allImports = {};
@@ -4306,6 +4292,7 @@ export async function createPythonBom(path, options) {
4306
4292
  *
4307
4293
  * @param {string} path to the project
4308
4294
  * @param {Object} options Parse options from the cli
4295
+ * @returns {Promise<Object | undefined>} Promise resolving to a BOM object or `undefined`
4309
4296
  */
4310
4297
  export async function createGoBom(path, options) {
4311
4298
  let pkgList = [];
@@ -4732,6 +4719,7 @@ export async function createGoBom(path, options) {
4732
4719
  *
4733
4720
  * @param {string} path to the project
4734
4721
  * @param {Object} options Parse options from the cli
4722
+ * @returns {Promise<Object|undefined>} Promise resolving to a BOM object or undefined
4735
4723
  */
4736
4724
  export async function createRustBom(path, options) {
4737
4725
  let pkgList = [];
@@ -4871,6 +4859,7 @@ export async function createRustBom(path, options) {
4871
4859
  *
4872
4860
  * @param {string} path to the project
4873
4861
  * @param {Object} options Parse options from the cli
4862
+ * @returns {Promise<Object>} Promise resolving to BOM object
4874
4863
  */
4875
4864
  export async function createDartBom(path, options) {
4876
4865
  const pubFiles = getAllFiles(
@@ -4942,6 +4931,7 @@ export async function createDartBom(path, options) {
4942
4931
  *
4943
4932
  * @param {string} path to the project
4944
4933
  * @param {Object} options Parse options from the cli
4934
+ * @returns {Object} BOM object
4945
4935
  */
4946
4936
  export function createCppBom(path, options) {
4947
4937
  let parentComponent;
@@ -5153,6 +5143,7 @@ export function createCppBom(path, options) {
5153
5143
  *
5154
5144
  * @param {string} path to the project
5155
5145
  * @param {Object} options Parse options from the cli
5146
+ * @returns {Object} BOM object
5156
5147
  */
5157
5148
  export function createClojureBom(path, options) {
5158
5149
  const ednFiles = getAllFiles(
@@ -5270,6 +5261,7 @@ export function createClojureBom(path, options) {
5270
5261
  *
5271
5262
  * @param {string} path to the project
5272
5263
  * @param {Object} options Parse options from the cli
5264
+ * @returns {Object} BOM object
5273
5265
  */
5274
5266
  export function createHaskellBom(path, options) {
5275
5267
  const cabalFiles = getAllFiles(
@@ -5302,6 +5294,7 @@ export function createHaskellBom(path, options) {
5302
5294
  *
5303
5295
  * @param {string} path to the project
5304
5296
  * @param {Object} options Parse options from the cli
5297
+ * @returns {Object} BOM object
5305
5298
  */
5306
5299
  export function createElixirBom(path, options) {
5307
5300
  const mixFiles = getAllFiles(
@@ -5334,6 +5327,7 @@ export function createElixirBom(path, options) {
5334
5327
  *
5335
5328
  * @param {string} path to the project
5336
5329
  * @param {Object} options Parse options from the cli
5330
+ * @returns {Object} BOM object
5337
5331
  */
5338
5332
  export function createGitHubBom(path, options) {
5339
5333
  const ghactionFiles = getAllFiles(
@@ -5365,6 +5359,7 @@ export function createGitHubBom(path, options) {
5365
5359
  *
5366
5360
  * @param {string} path to the project
5367
5361
  * @param {Object} options Parse options from the cli
5362
+ * @returns {Object} BOM object
5368
5363
  */
5369
5364
  export function createCloudBuildBom(path, options) {
5370
5365
  const cbFiles = getAllFiles(path, "cloudbuild.yml", options);
@@ -5393,6 +5388,7 @@ export function createCloudBuildBom(path, options) {
5393
5388
  *
5394
5389
  * @param {string} _path to the project
5395
5390
  * @param {Object} options Parse options from the cli
5391
+ * @returns {Promise<Object>} Promise resolving to BOM object
5396
5392
  */
5397
5393
  export function createOSBom(_path, options) {
5398
5394
  console.warn(
@@ -5451,6 +5447,7 @@ export function createOSBom(_path, options) {
5451
5447
  *
5452
5448
  * @param {string} path to the project
5453
5449
  * @param {Object} options Parse options from the cli
5450
+ * @returns {Promise<Object>} Promise resolving to BOM object
5454
5451
  */
5455
5452
  export async function createJenkinsBom(path, options) {
5456
5453
  let pkgList = [];
@@ -5500,6 +5497,7 @@ export async function createJenkinsBom(path, options) {
5500
5497
  *
5501
5498
  * @param {string} path to the project
5502
5499
  * @param {Object} options Parse options from the cli
5500
+ * @returns {Object} BOM object
5503
5501
  */
5504
5502
  export function createHelmBom(path, options) {
5505
5503
  let pkgList = [];
@@ -5532,6 +5530,7 @@ export function createHelmBom(path, options) {
5532
5530
  *
5533
5531
  * @param {string} path to the project
5534
5532
  * @param {Object} options Parse options from the cli
5533
+ * @returns {Promise<Object>} Promise resolving to BOM object
5535
5534
  */
5536
5535
  export async function createSwiftBom(path, options) {
5537
5536
  const swiftFiles = getAllFiles(
@@ -5676,6 +5675,7 @@ export async function createSwiftBom(path, options) {
5676
5675
  *
5677
5676
  * @param {string} path to the project
5678
5677
  * @param {Object} options Parse options from the cli
5678
+ * @returns {Promise<Object | undefined>} Promise resolving to a BOM object, or `undefined` when no Podfiles are found
5679
5679
  */
5680
5680
  export async function createCocoaBom(path, options) {
5681
5681
  const cocoaFiles = getAllFiles(
@@ -5692,7 +5692,7 @@ export async function createCocoaBom(path, options) {
5692
5692
  for (const podFile of cocoaFiles) {
5693
5693
  const projectPath = dirname(podFile);
5694
5694
  const lockFile = `${podFile}.lock`;
5695
- if (!existsSync(lockFile) || options.deep) {
5695
+ if (!safeExistsSync(lockFile) || options.deep) {
5696
5696
  if (options.installDeps) {
5697
5697
  executePodCommand(["install"], projectPath, options);
5698
5698
  } else {
@@ -5830,6 +5830,7 @@ export async function createCocoaBom(path, options) {
5830
5830
  *
5831
5831
  * @param {string} path to the project
5832
5832
  * @param {Object} options Parse options from the cli
5833
+ * @returns {Promise<Object>} Promise resolving to BOM object
5833
5834
  */
5834
5835
  export async function createNixBom(path, options) {
5835
5836
  let pkgList = [];
@@ -5952,6 +5953,7 @@ export async function createNixBom(path, options) {
5952
5953
  *
5953
5954
  * @param {string} path to the project
5954
5955
  * @param {Object} options Parse options from the cli
5956
+ * @returns {Promise<Object>} Promise resolving to BOM object
5955
5957
  */
5956
5958
  export async function createCaxaBom(path, options) {
5957
5959
  let pkgList = [];
@@ -6003,6 +6005,7 @@ export async function createCaxaBom(path, options) {
6003
6005
  *
6004
6006
  * @param {string} path to the project
6005
6007
  * @param {Object} options Parse options from the cli
6008
+ * @returns {Promise<Object>} Promise resolving to BOM object
6006
6009
  */
6007
6010
  export async function createContainerSpecLikeBom(path, options) {
6008
6011
  let services = [];
@@ -6331,6 +6334,7 @@ export async function createContainerSpecLikeBom(path, options) {
6331
6334
  *
6332
6335
  * @param {string} path to the project
6333
6336
  * @param {Object} options Parse options from the cli
6337
+ * @returns {Object} BOM object
6334
6338
  */
6335
6339
  export function createPHPBom(path, options) {
6336
6340
  let dependencies = [];
@@ -6438,7 +6442,7 @@ export function createPHPBom(path, options) {
6438
6442
  // Track all the modules in a mono-repo
6439
6443
  if (!Object.keys(parentComponent).length) {
6440
6444
  parentComponent = { ...moduleParent };
6441
- } else {
6445
+ } else if (moduleParent?.["bom-ref"]) {
6442
6446
  parentComponent.components = parentComponent.components || [];
6443
6447
  parentComponent.components.push(moduleParent);
6444
6448
  }
@@ -6454,7 +6458,9 @@ export function createPHPBom(path, options) {
6454
6458
  dependencies.splice(0, 0, {
6455
6459
  ref: moduleParent["bom-ref"],
6456
6460
  dependsOn: [
6457
- ...new Set(retMap.rootList.map((p) => p["bom-ref"])),
6461
+ ...new Set(
6462
+ retMap.rootList.map((p) => p["bom-ref"]).filter(Boolean),
6463
+ ),
6458
6464
  ].sort(),
6459
6465
  });
6460
6466
  }
@@ -6467,9 +6473,9 @@ export function createPHPBom(path, options) {
6467
6473
  }
6468
6474
  // Complete the root dependency tree
6469
6475
  if (parentComponent?.components?.length) {
6470
- const parentDependsOn = parentComponent.components.map(
6471
- (d) => d["bom-ref"],
6472
- );
6476
+ const parentDependsOn = parentComponent.components
6477
+ .map((d) => d["bom-ref"])
6478
+ .filter(Boolean);
6473
6479
  dependencies = mergeDependencies(
6474
6480
  [{ ref: parentComponent["bom-ref"], dependsOn: parentDependsOn }],
6475
6481
  dependencies,
@@ -6491,6 +6497,7 @@ export function createPHPBom(path, options) {
6491
6497
  *
6492
6498
  * @param {string} path to the project
6493
6499
  * @param {Object} options Parse options from the cli
6500
+ * @returns {Promise<Object>} Promise resolving to BOM object
6494
6501
  */
6495
6502
  export async function createRubyBom(path, options) {
6496
6503
  // We can look for gem files within node_modules directory
@@ -6744,6 +6751,7 @@ export async function createRubyBom(path, options) {
6744
6751
  *
6745
6752
  * @param {string} path to the project
6746
6753
  * @param {Object} options Parse options from the cli
6754
+ * @returns {Promise<Object|undefined>} Promise resolving to BOM object
6747
6755
  */
6748
6756
  export async function createCsharpBom(path, options) {
6749
6757
  let manifestFiles = [];
@@ -7179,11 +7187,202 @@ export async function createCsharpBom(path, options) {
7179
7187
  });
7180
7188
  }
7181
7189
 
7190
+ /**
7191
+ * Function to create BOM for VS Code / IDE extensions.
7192
+ * Supports two modes:
7193
+ * 1. Directory scan: Discovers `.vsix` files and installed extension directories
7194
+ * 2. IDE discovery: Automatically finds extensions installed by known IDEs
7195
+ *
7196
+ * @param {string} path to the project or directory to scan
7197
+ * @param {Object} options Parse options from the cli
7198
+ * @returns {Promise<Object>} Promise resolving to BOM object
7199
+ */
7200
+ export async function createVscodeExtensionBom(path, options) {
7201
+ let pkgList = [];
7202
+ let dependencies = [];
7203
+ const tempDirs = [];
7204
+
7205
+ // Mode 1: Scan for .vsix files in the given directory, or treat the input
7206
+ // path as a single .vsix file.
7207
+ let vsixFiles = [];
7208
+ if (path.endsWith(".vsix")) {
7209
+ vsixFiles = [resolve(path)];
7210
+ } else {
7211
+ vsixFiles = getAllFiles(
7212
+ path,
7213
+ `${options.multiProject ? "**/" : ""}*.vsix`,
7214
+ options,
7215
+ );
7216
+ }
7217
+ if (vsixFiles.length) {
7218
+ if (DEBUG_MODE) {
7219
+ console.log(`Found ${vsixFiles.length} .vsix file(s) to parse`);
7220
+ }
7221
+ for (const f of vsixFiles) {
7222
+ if (DEBUG_MODE) {
7223
+ console.log(`Parsing ${f}`);
7224
+ }
7225
+ // Get the extension component metadata
7226
+ const component = await parseVsixFile(f);
7227
+ if (component) {
7228
+ pkgList.push(component);
7229
+ }
7230
+ // Extract the vsix to a temp dir and run deep analysis
7231
+ const extractedDir = await extractVsixToTempDir(f);
7232
+ if (extractedDir) {
7233
+ tempDirs.push(extractedDir);
7234
+ const deepResult = await analyzeExtensionDir(extractedDir, options);
7235
+ if (deepResult.pkgList.length) {
7236
+ pkgList = pkgList.concat(deepResult.pkgList);
7237
+ }
7238
+ if (deepResult.dependencies.length) {
7239
+ dependencies = mergeDependencies(
7240
+ dependencies,
7241
+ deepResult.dependencies,
7242
+ );
7243
+ }
7244
+ }
7245
+ }
7246
+ }
7247
+
7248
+ // Mode 2: Auto-discover extensions from known IDE locations
7249
+ if (options.deep || options.projectType?.includes("ide-extensions")) {
7250
+ const ideDirs = discoverIdeExtensionDirs();
7251
+ if (ideDirs.length) {
7252
+ if (DEBUG_MODE) {
7253
+ console.log(
7254
+ `Discovered IDE extension directories: ${ideDirs.map((d) => `${d.name}: ${d.dir}`).join(", ")}`,
7255
+ );
7256
+ }
7257
+ const ideExtensions = collectInstalledExtensions(ideDirs);
7258
+ if (ideExtensions.length) {
7259
+ if (DEBUG_MODE) {
7260
+ console.log(
7261
+ `Found ${ideExtensions.length} IDE extension(s) from ${ideDirs.length} IDE location(s)`,
7262
+ );
7263
+ }
7264
+ pkgList = pkgList.concat(ideExtensions);
7265
+ // Deep analysis for IDE extension directories
7266
+ for (const ideDir of ideDirs) {
7267
+ await analyzeInstalledExtensionDirs(
7268
+ ideDir.dir,
7269
+ options,
7270
+ pkgList,
7271
+ dependencies,
7272
+ );
7273
+ }
7274
+ }
7275
+ }
7276
+ }
7277
+
7278
+ // Clean up temp directories from vsix extraction
7279
+ for (const td of tempDirs) {
7280
+ cleanupTempDir(td);
7281
+ }
7282
+ pkgList = trimComponents(pkgList);
7283
+ return buildBomNSData(options, pkgList, VSCODE_EXTENSION_PURL_TYPE, {
7284
+ src: path,
7285
+ filename: vsixFiles.join(", "),
7286
+ nsMapping: {},
7287
+ dependencies,
7288
+ });
7289
+ }
7290
+
7291
+ /**
7292
+ * Analyze an extracted extension directory for bundled dependencies.
7293
+ * Looks for npm lock files, node_modules, package.json files, minified JS,
7294
+ * and runs the babel-based analyzer on the source.
7295
+ *
7296
+ * @param {string} extDir Path to the extracted extension directory
7297
+ * @param {Object} options CLI options
7298
+ * @returns {Promise<{pkgList: Object[], dependencies: Object[]}>}
7299
+ */
7300
+ async function analyzeExtensionDir(extDir, options) {
7301
+ const pkgList = [];
7302
+ let dependencies = [];
7303
+ // Check if the extension directory contains node.js project artifacts
7304
+ const hasPackageJson = safeExistsSync(join(extDir, "package.json"));
7305
+ const hasNodeModules = safeExistsSync(join(extDir, "node_modules"));
7306
+ const hasLockFile =
7307
+ safeExistsSync(join(extDir, "package-lock.json")) ||
7308
+ safeExistsSync(join(extDir, "yarn.lock")) ||
7309
+ safeExistsSync(join(extDir, "pnpm-lock.yaml"));
7310
+
7311
+ // If there are lock files or node_modules, run the full Node.js BOM generator
7312
+ if (hasPackageJson && (hasLockFile || hasNodeModules)) {
7313
+ if (DEBUG_MODE) {
7314
+ console.log(
7315
+ `Running Node.js BOM analysis on extension directory: ${extDir}`,
7316
+ );
7317
+ }
7318
+ const nodeBomOptions = {
7319
+ ...options,
7320
+ path: extDir,
7321
+ multiProject: true,
7322
+ installDeps: false,
7323
+ noBabel: false,
7324
+ projectType: ["js"],
7325
+ };
7326
+ const bomData = await createNodejsBom(extDir, nodeBomOptions);
7327
+ if (bomData?.bomJson?.components?.length) {
7328
+ for (const comp of bomData.bomJson.components) {
7329
+ pkgList.push(comp);
7330
+ }
7331
+ }
7332
+ if (bomData?.bomJson?.dependencies?.length) {
7333
+ dependencies = mergeDependencies(
7334
+ dependencies,
7335
+ bomData.bomJson.dependencies,
7336
+ );
7337
+ }
7338
+ return { pkgList, dependencies };
7339
+ }
7340
+ return { pkgList, dependencies };
7341
+ }
7342
+
7343
+ /**
7344
+ * Run deep analysis on installed extension subdirectories within a parent
7345
+ * extensions directory. Each subdirectory represents an installed extension.
7346
+ *
7347
+ * @param {string} extensionsDir Parent directory containing extension subdirs
7348
+ * @param {Object} options CLI options
7349
+ * @param {Object[]} pkgList Mutable array to push discovered components into
7350
+ * @param {Object[]} dependencies Mutable array to merge dependencies into
7351
+ */
7352
+ async function analyzeInstalledExtensionDirs(
7353
+ extensionsDir,
7354
+ options,
7355
+ pkgList,
7356
+ dependencies,
7357
+ ) {
7358
+ let entries;
7359
+ try {
7360
+ entries = readdirSync(extensionsDir, { withFileTypes: true });
7361
+ } catch (_e) {
7362
+ return;
7363
+ }
7364
+ for (const entry of entries) {
7365
+ if (!entry.isDirectory() || entry.name.startsWith(".")) {
7366
+ continue;
7367
+ }
7368
+ const extDir = join(extensionsDir, entry.name);
7369
+ const deepResult = await analyzeExtensionDir(extDir, options);
7370
+ if (deepResult.pkgList.length) {
7371
+ pkgList.push(...deepResult.pkgList);
7372
+ }
7373
+ if (deepResult.dependencies.length) {
7374
+ const merged = mergeDependencies(dependencies, deepResult.dependencies);
7375
+ dependencies.splice(0, dependencies.length, ...merged);
7376
+ }
7377
+ }
7378
+ }
7379
+
7182
7380
  /**
7183
7381
  * Function to create bom object for cryptographic certificate files
7184
7382
  *
7185
7383
  * @param {string} path to the project
7186
7384
  * @param {Object} options Parse options from the cli
7385
+ * @returns {Promise<Object>} Promise resolving to BOM object
7187
7386
  */
7188
7387
  export async function createCryptoCertsBom(path, options) {
7189
7388
  const pkgList = [];
@@ -7220,191 +7419,6 @@ export async function createCryptoCertsBom(path, options) {
7220
7419
  };
7221
7420
  }
7222
7421
 
7223
- export function mergeDependencies(
7224
- dependencies,
7225
- newDependencies,
7226
- parentComponent = {},
7227
- ) {
7228
- if (!parentComponent && DEBUG_MODE) {
7229
- console.log(
7230
- "Unable to determine parent component. Dependencies will be flattened.",
7231
- );
7232
- }
7233
- let providesFound = false;
7234
- const deps_map = {};
7235
- const provides_map = {};
7236
- const parentRef = parentComponent?.["bom-ref"]
7237
- ? parentComponent["bom-ref"]
7238
- : undefined;
7239
- const combinedDeps = dependencies.concat(newDependencies || []);
7240
- for (const adep of combinedDeps) {
7241
- if (!deps_map[adep.ref]) {
7242
- deps_map[adep.ref] = new Set();
7243
- }
7244
- if (!provides_map[adep.ref]) {
7245
- provides_map[adep.ref] = new Set();
7246
- }
7247
- if (adep["dependsOn"]) {
7248
- for (const eachDepends of adep["dependsOn"]) {
7249
- if (parentRef && eachDepends) {
7250
- if (eachDepends.toLowerCase() !== parentRef.toLowerCase()) {
7251
- deps_map[adep.ref].add(eachDepends);
7252
- }
7253
- } else {
7254
- deps_map[adep.ref].add(eachDepends);
7255
- }
7256
- }
7257
- }
7258
- if (adep["provides"]) {
7259
- providesFound = true;
7260
- for (const eachProvides of adep["provides"]) {
7261
- if (
7262
- parentRef &&
7263
- eachProvides.toLowerCase() !== parentRef.toLowerCase()
7264
- ) {
7265
- provides_map[adep.ref].add(eachProvides);
7266
- }
7267
- }
7268
- }
7269
- }
7270
- const retlist = [];
7271
- for (const akey of Object.keys(deps_map)) {
7272
- if (providesFound) {
7273
- retlist.push({
7274
- ref: akey,
7275
- dependsOn: Array.from(deps_map[akey]).sort(),
7276
- provides: Array.from(provides_map[akey]).sort(),
7277
- });
7278
- } else {
7279
- retlist.push({
7280
- ref: akey,
7281
- dependsOn: Array.from(deps_map[akey]).sort(),
7282
- });
7283
- }
7284
- }
7285
- return retlist;
7286
- }
7287
-
7288
- /**
7289
- * Trim duplicate components by retaining all the properties
7290
- *
7291
- * @param {Array} components Components
7292
- *
7293
- * @returns {Array} Filtered components
7294
- */
7295
- export function trimComponents(components) {
7296
- const keyCache = {};
7297
- const filteredComponents = [];
7298
- for (const comp of components) {
7299
- const key = (
7300
- comp.purl ||
7301
- comp["bom-ref"] ||
7302
- comp.name + comp.version
7303
- ).toLowerCase();
7304
- if (!keyCache[key]) {
7305
- keyCache[key] = comp;
7306
- } else {
7307
- const existingComponent = keyCache[key];
7308
- // We need to retain any properties that differ
7309
- if (comp.properties) {
7310
- if (existingComponent.properties) {
7311
- for (const newprop of comp.properties) {
7312
- if (
7313
- !existingComponent.properties.find(
7314
- (prop) =>
7315
- prop.name === newprop.name && prop.value === newprop.value,
7316
- )
7317
- ) {
7318
- existingComponent.properties.push(newprop);
7319
- }
7320
- }
7321
- } else {
7322
- existingComponent.properties = comp.properties;
7323
- }
7324
- }
7325
- // Retain all component.evidence.identity
7326
- if (comp?.evidence?.identity) {
7327
- if (!existingComponent.evidence) {
7328
- existingComponent.evidence = { identity: [] };
7329
- } else if (!existingComponent?.evidence?.identity) {
7330
- existingComponent.evidence.identity = [];
7331
- } else if (
7332
- existingComponent?.evidence?.identity &&
7333
- !Array.isArray(existingComponent.evidence.identity)
7334
- ) {
7335
- existingComponent.evidence.identity = [
7336
- existingComponent.evidence.identity,
7337
- ];
7338
- }
7339
- // comp.evidence.identity can be an array or object
7340
- // Merge the evidence.identity based on methods or objects
7341
- const isIdentityArray = Array.isArray(comp.evidence.identity);
7342
- const identities = isIdentityArray
7343
- ? comp.evidence.identity
7344
- : [comp.evidence.identity];
7345
- for (const aident of identities) {
7346
- let methodBasedMerge = false;
7347
- if (aident?.methods?.length) {
7348
- for (const amethod of aident.methods) {
7349
- for (const existIdent of existingComponent.evidence.identity) {
7350
- if (existIdent.field === aident.field) {
7351
- if (!existIdent.methods) {
7352
- existIdent.methods = [];
7353
- }
7354
- let isDup = false;
7355
- for (const emethod of existIdent.methods) {
7356
- if (emethod?.value === amethod?.value) {
7357
- isDup = true;
7358
- break;
7359
- }
7360
- }
7361
- if (!isDup) {
7362
- existIdent.methods.push(amethod);
7363
- }
7364
- methodBasedMerge = true;
7365
- }
7366
- }
7367
- }
7368
- }
7369
- if (!methodBasedMerge && aident.field && aident.confidence) {
7370
- existingComponent.evidence.identity.push(aident);
7371
- }
7372
- }
7373
- if (!isIdentityArray) {
7374
- const firstIdentity = existingComponent.evidence.identity[0];
7375
- let identConfidence = firstIdentity?.confidence;
7376
- // We need to set the confidence to the max of all confidences
7377
- if (firstIdentity?.methods?.length > 1) {
7378
- for (const aidentMethod of firstIdentity.methods) {
7379
- if (
7380
- aidentMethod?.confidence &&
7381
- aidentMethod.confidence > identConfidence
7382
- ) {
7383
- identConfidence = aidentMethod.confidence;
7384
- }
7385
- }
7386
- }
7387
- firstIdentity.confidence = identConfidence;
7388
- existingComponent.evidence = {
7389
- identity: firstIdentity,
7390
- };
7391
- }
7392
- }
7393
- // If the component is required in any of the child projects, then make it required
7394
- if (
7395
- existingComponent?.scope !== "required" &&
7396
- comp?.scope === "required"
7397
- ) {
7398
- existingComponent.scope = "required";
7399
- }
7400
- }
7401
- }
7402
- for (const akey of Object.keys(keyCache)) {
7403
- filteredComponents.push(keyCache[akey]);
7404
- }
7405
- return filteredComponents;
7406
- }
7407
-
7408
7422
  /**
7409
7423
  * Dedupe components
7410
7424
  *
@@ -7445,7 +7459,7 @@ export function dedupeBom(options, components, parentComponent, dependencies) {
7445
7459
  components,
7446
7460
  bomJson: {
7447
7461
  bomFormat: "CycloneDX",
7448
- specVersion: `${options.specVersion || 1.5}`,
7462
+ specVersion: `${options.specVersion || 1.7}`,
7449
7463
  serialNumber: serialNum,
7450
7464
  version: 1,
7451
7465
  metadata: addMetadata(parentComponent, options, {}),
@@ -7461,11 +7475,13 @@ export function dedupeBom(options, components, parentComponent, dependencies) {
7461
7475
  *
7462
7476
  * @param {string[]} pathList list of to the project
7463
7477
  * @param {Object} options Parse options from the cli
7478
+ * @returns {Promise<Object>} Promise resolving to BOM object
7464
7479
  */
7465
7480
  export async function createMultiXBom(pathList, options) {
7466
7481
  let components = [];
7467
7482
  let dependencies = [];
7468
7483
  let bomData;
7484
+ let formulationList = [];
7469
7485
  let parentComponent = determineParentComponent(options) || {};
7470
7486
  let parentSubComponents = [];
7471
7487
  options.createMultiXBom = true;
@@ -7676,6 +7692,9 @@ export async function createMultiXBom(pathList, options) {
7676
7692
  parentSubComponents.push(bomData.parentComponent);
7677
7693
  }
7678
7694
  }
7695
+ if (bomData?.formulationList?.length) {
7696
+ formulationList = formulationList.concat(bomData.formulationList);
7697
+ }
7679
7698
  }
7680
7699
  if (hasAnyProjectType(["oci", "go"], options)) {
7681
7700
  if (!hasAnyProjectType(["oci"], options, false)) {
@@ -8168,6 +8187,27 @@ export async function createMultiXBom(pathList, options) {
8168
8187
  }
8169
8188
  }
8170
8189
  }
8190
+ if (hasAnyProjectType(["vscode-extension"], options)) {
8191
+ bomData = await createVscodeExtensionBom(path, options);
8192
+ if (bomData?.bomJson?.components?.length) {
8193
+ if (DEBUG_MODE) {
8194
+ console.log(
8195
+ `Found ${bomData.bomJson.components.length} VS Code extension(s) at ${path}`,
8196
+ );
8197
+ }
8198
+ components = components.concat(bomData.bomJson.components);
8199
+ dependencies = mergeDependencies(
8200
+ dependencies,
8201
+ bomData.bomJson.dependencies,
8202
+ );
8203
+ if (
8204
+ bomData.parentComponent &&
8205
+ Object.keys(bomData.parentComponent).length
8206
+ ) {
8207
+ parentSubComponents.push(bomData.parentComponent);
8208
+ }
8209
+ }
8210
+ }
8171
8211
  // Collect any crypto keys
8172
8212
  if (options.specVersion >= 1.6 && options.includeCrypto) {
8173
8213
  if (!hasAnyProjectType(["oci"], options, false)) {
@@ -8266,7 +8306,16 @@ export async function createMultiXBom(pathList, options) {
8266
8306
  }
8267
8307
  }
8268
8308
  }
8269
- return dedupeBom(options, components, parentComponent, dependencies);
8309
+ const multiResult = dedupeBom(
8310
+ options,
8311
+ components,
8312
+ parentComponent,
8313
+ dependencies,
8314
+ );
8315
+ if (formulationList.length) {
8316
+ multiResult.formulationList = formulationList;
8317
+ }
8318
+ return multiResult;
8270
8319
  }
8271
8320
 
8272
8321
  /**
@@ -8274,6 +8323,7 @@ export async function createMultiXBom(pathList, options) {
8274
8323
  *
8275
8324
  * @param {string} path to the project
8276
8325
  * @param {Object} options Parse options from the cli
8326
+ * @returns {Promise<Object|undefined>} Promise resolving to BOM object, or undefined if path is not readable
8277
8327
  */
8278
8328
  export async function createXBom(path, options) {
8279
8329
  try {
@@ -8512,6 +8562,16 @@ export async function createXBom(path, options) {
8512
8562
  return await createJenkinsBom(path, options);
8513
8563
  }
8514
8564
 
8565
+ // VS Code extensions (.vsix files)
8566
+ const vsixFiles = getAllFiles(
8567
+ path,
8568
+ `${options.multiProject ? "**/" : ""}*.vsix`,
8569
+ options,
8570
+ );
8571
+ if (vsixFiles.length) {
8572
+ return await createVscodeExtensionBom(path, options);
8573
+ }
8574
+
8515
8575
  // Helm charts
8516
8576
  const chartFiles = getAllFiles(
8517
8577
  path,
@@ -8619,6 +8679,7 @@ export async function createXBom(path, options) {
8619
8679
  *
8620
8680
  * @param {string} path to the project
8621
8681
  * @param {Object} options Parse options from the cli
8682
+ * @returns {Promise<Object>} Promise resolving to BOM object
8622
8683
  */
8623
8684
  export async function createBom(path, options) {
8624
8685
  let { projectType } = options;
@@ -8863,6 +8924,9 @@ export async function createBom(path, options) {
8863
8924
  if (PROJECT_TYPE_ALIASES["caxa"].includes(projectType[0])) {
8864
8925
  return await createCaxaBom(path, options);
8865
8926
  }
8927
+ if (PROJECT_TYPE_ALIASES["vscode-extension"].includes(projectType[0])) {
8928
+ return await createVscodeExtensionBom(path, options);
8929
+ }
8866
8930
  switch (projectType[0]) {
8867
8931
  case "jar":
8868
8932
  return createJarBom(path, options);
@@ -8901,66 +8965,22 @@ export async function createBom(path, options) {
8901
8965
  * @throws {Error} if the request fails
8902
8966
  */
8903
8967
  export async function submitBom(args, bomContents) {
8904
- const serverUrl = `${args.serverUrl.replace(/\/$/, "")}/api/v1/bom`;
8905
- let encodedBomContents = Buffer.from(JSON.stringify(bomContents)).toString(
8906
- "base64",
8907
- );
8908
- if (encodedBomContents.startsWith("77u/")) {
8909
- encodedBomContents = encodedBomContents.substring(4);
8910
- }
8911
- const bomPayload = {
8912
- autoCreate: "true",
8913
- bom: encodedBomContents,
8914
- };
8915
- const projectVersion = args.projectVersion || "main";
8916
- if (
8917
- typeof args.projectId !== "undefined" ||
8918
- (typeof args.projectName !== "undefined" &&
8919
- typeof projectVersion !== "undefined")
8920
- ) {
8921
- if (typeof args.projectId !== "undefined") {
8922
- bomPayload.project = args.projectId;
8923
- }
8924
- if (typeof args.projectName !== "undefined") {
8925
- bomPayload.projectName = args.projectName;
8926
- }
8927
- if (typeof projectVersion !== "undefined") {
8928
- bomPayload.projectVersion = projectVersion;
8929
- }
8930
- } else {
8968
+ const serverUrl = getDependencyTrackBomUrl(args.serverUrl);
8969
+ const bomPayload = buildDependencyTrackBomPayload(args, bomContents);
8970
+ if (!bomPayload) {
8931
8971
  console.log(
8932
- "projectId, projectName and projectVersion, or all three must be provided.",
8972
+ "Invalid Dependency-Track submission arguments. Provide projectId or projectName (projectVersion defaults to main) and specify parent project either by UUID or by parent project name + version.",
8933
8973
  );
8934
8974
  args.failOnError && process.exit(1);
8935
8975
  return;
8936
8976
  }
8937
- if (
8938
- typeof args.parentProjectId !== "undefined" ||
8939
- typeof args.parentUUID !== "undefined"
8940
- ) {
8941
- bomPayload.parentUUID = args.parentProjectId || args.parentUUID;
8942
- }
8943
- // Add project tags if provided
8944
- // see https://docs.dependencytrack.org/2024/10/01/v4.12.0/
8945
- // corresponding API usage documentation can be found on the
8946
- // API docs site of your instance, see
8947
- // https://docs.dependencytrack.org/integrations/rest-api/
8948
- // or public instance see https://yoursky.blue/documentation/rest-api
8949
- if (typeof args.projectTag !== "undefined") {
8950
- // If args.projectTag is not an array, convert it to an array
8951
- // Attention, array items should be of form { name: "tagName " }
8952
- // see https://yoursky.blue/documentation/rest-api#tag/bom/operation/UploadBomBase64Encoded
8953
- bomPayload.projectTags = (
8954
- Array.isArray(args.projectTag) ? args.projectTag : [args.projectTag]
8955
- ).map((tag) => ({ name: tag }));
8956
- }
8957
8977
  if (DEBUG_MODE) {
8958
8978
  console.log(
8959
8979
  "Submitting BOM to",
8960
8980
  serverUrl,
8961
8981
  "params",
8962
8982
  args.projectName,
8963
- projectVersion,
8983
+ bomPayload.projectVersion,
8964
8984
  );
8965
8985
  }
8966
8986
  try {