@cyclonedx/cdxgen 9.1.0 → 9.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.
package/index.js CHANGED
@@ -1,7 +1,5 @@
1
- import Ajv from "ajv";
2
- import addFormats from "ajv-formats";
3
1
  import { platform as _platform, homedir, tmpdir } from "node:os";
4
- import { basename, join, dirname, sep } from "node:path";
2
+ import { basename, join, dirname, sep, resolve } from "node:path";
5
3
  import { parse } from "ssri";
6
4
  import {
7
5
  lstatSync,
@@ -91,7 +89,8 @@ import {
91
89
  parseCsProjAssetsData,
92
90
  parseCsPkgLockData,
93
91
  parseCsPkgData,
94
- parseCsProjData
92
+ parseCsProjData,
93
+ DEBUG_MODE
95
94
  } from "./utils.js";
96
95
  import { spawnSync } from "node:child_process";
97
96
  import { fileURLToPath } from "node:url";
@@ -154,13 +153,6 @@ if (process.env.SWIFT_CMD) {
154
153
  const SBT_CACHE_DIR =
155
154
  process.env.SBT_CACHE_DIR || join(homedir(), ".ivy2", "cache");
156
155
 
157
- // Debug mode flag
158
- const DEBUG_MODE =
159
- process.env.CDXGEN_DEBUG_MODE === "debug" ||
160
- process.env.SCAN_DEBUG_MODE === "debug" ||
161
- process.env.SHIFTLEFT_LOGGING_LEVEL === "debug" ||
162
- process.env.NODE_ENV === "development";
163
-
164
156
  // CycloneDX Hash pattern
165
157
  const HASH_PATTERN =
166
158
  "^([a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128})$";
@@ -168,7 +160,16 @@ const HASH_PATTERN =
168
160
  // Timeout milliseconds. Default 10 mins
169
161
  const TIMEOUT_MS = parseInt(process.env.CDXGEN_TIMEOUT_MS) || 10 * 60 * 1000;
170
162
 
171
- const createDefaultParentComponent = (path) => {
163
+ /**
164
+ * Creates a default parent component based on the directory name.
165
+ *
166
+ * @param {string} path Directory or file name
167
+ * @param {string} type Package type
168
+ * @returns component object
169
+ */
170
+ const createDefaultParentComponent = (path, type = "application") => {
171
+ // Expands any relative path such as dot
172
+ path = resolve(path);
172
173
  // Create a parent component based on the directory name
173
174
  let dirName =
174
175
  existsSync(path) && lstatSync(path).isDirectory()
@@ -182,7 +183,7 @@ const createDefaultParentComponent = (path) => {
182
183
  type: "application"
183
184
  };
184
185
  const ppurl = new PackageURL(
185
- "application",
186
+ type,
186
187
  parentComponent.group,
187
188
  parentComponent.name,
188
189
  parentComponent.version,
@@ -257,6 +258,41 @@ function addDependencies(dependencies) {
257
258
  return deps_list;
258
259
  }
259
260
 
261
+ const addToolsSection = (options, format) => {
262
+ if (options.specVersion === 1.4) {
263
+ if (format === "json") {
264
+ return [
265
+ {
266
+ vendor: "cyclonedx",
267
+ name: "cdxgen",
268
+ version: _version
269
+ }
270
+ ];
271
+ } else {
272
+ return [
273
+ {
274
+ tool: {
275
+ vendor: "cyclonedx",
276
+ name: "cdxgen",
277
+ version: _version
278
+ }
279
+ }
280
+ ];
281
+ }
282
+ }
283
+ return {
284
+ components: [
285
+ {
286
+ group: "@cyclonedx",
287
+ name: "cdxgen",
288
+ version: _version,
289
+ purl: `pkg:npm/%40cyclonedx/cdxgen@${_version}`,
290
+ type: "application",
291
+ "bom-ref": `pkg:npm/@cyclonedx/cdxgen@${_version}`
292
+ }
293
+ ]
294
+ };
295
+ };
260
296
  /**
261
297
  * Function to create metadata block
262
298
  *
@@ -264,20 +300,10 @@ function addDependencies(dependencies) {
264
300
  function addMetadata(parentComponent = {}, format = "xml", options = {}) {
265
301
  // DO NOT fork this project to just change the vendor or author's name
266
302
  // Try to contribute to this project by sending PR or filing issues
303
+ const tools = addToolsSection(options, format);
267
304
  const metadata = {
268
305
  timestamp: new Date().toISOString(),
269
- tools: {
270
- components: [
271
- {
272
- group: "@cyclonedx",
273
- name: "cdxgen",
274
- version: _version,
275
- purl: `pkg:npm/%40cyclonedx/cdxgen@${_version}`,
276
- type: "application",
277
- "bom-ref": `pkg:npm/%40cyclonedx/cdxgen@${_version}`
278
- }
279
- ]
280
- },
306
+ tools,
281
307
  authors: [
282
308
  {
283
309
  author: { name: "Prabhu Subramanian", email: "prabhu@appthreat.com" }
@@ -286,58 +312,35 @@ function addMetadata(parentComponent = {}, format = "xml", options = {}) {
286
312
  supplier: undefined
287
313
  };
288
314
  if (format === "json") {
289
- metadata.tools = {
290
- components: [
291
- {
292
- group: "@cyclonedx",
293
- name: "cdxgen",
294
- version: _version,
295
- purl: `pkg:npm/%40cyclonedx/cdxgen@${_version}`,
296
- type: "application",
297
- "bom-ref": `pkg:npm/%40cyclonedx/cdxgen@${_version}`
298
- }
299
- ]
300
- };
315
+ metadata.tools = tools;
301
316
  metadata.authors = [
302
317
  { name: "Prabhu Subramanian", email: "prabhu@appthreat.com" }
303
318
  ];
304
319
  }
305
- if (
306
- parentComponent &&
307
- Object.keys(parentComponent) &&
308
- Object.keys(parentComponent).length
309
- ) {
310
- const allPComponents = listComponents(
311
- {},
312
- {},
313
- parentComponent,
314
- parentComponent.type,
315
- format
316
- );
317
- if (allPComponents.length) {
318
- const firstPComp = allPComponents[0];
319
- if (format == "xml" && firstPComp.component) {
320
- metadata.component = firstPComp.component;
321
- } else {
322
- // Retain the components of parent component
323
- // Bug #317 fix
324
- if (parentComponent && parentComponent.components) {
325
- firstPComp.components = parentComponent.components;
326
- }
327
- if (firstPComp.evidence) {
328
- delete firstPComp.evidence;
329
- }
330
- metadata.component = firstPComp;
331
- }
332
- } else {
333
- // As a fallback, retain the parent component
334
- if (format === "json") {
335
- if (parentComponent.evidence) {
336
- delete parentComponent.evidence;
320
+ if (parentComponent && Object.keys(parentComponent).length) {
321
+ if (parentComponent) {
322
+ delete parentComponent.evidence;
323
+ delete parentComponent._integrity;
324
+ }
325
+ if (parentComponent && parentComponent.components) {
326
+ for (const comp of parentComponent.components) {
327
+ delete comp.evidence;
328
+ delete comp._integrity;
329
+ if (!comp["bom-ref"] && comp.name && comp.type) {
330
+ let fullName =
331
+ comp.group && comp.group.length
332
+ ? `${comp.group}/${comp.name}`
333
+ : comp.name;
334
+ if (comp.version && comp.version.length) {
335
+ fullName = `${fullName}@${comp.version}`;
336
+ }
337
+ comp["bom-ref"] = `pkg:${comp.type}/${fullName}`;
337
338
  }
338
- metadata.component = parentComponent;
339
339
  }
340
340
  }
341
+ if (format === "json") {
342
+ metadata.component = parentComponent;
343
+ }
341
344
  }
342
345
  if (options) {
343
346
  const mproperties = [];
@@ -599,16 +602,6 @@ function addComponent(
599
602
  if (!ptype && pkg.qualifiers && pkg.qualifiers.type === "jar") {
600
603
  ptype = "maven";
601
604
  }
602
- // Skip @types package for npm
603
- if (
604
- ptype == "npm" &&
605
- (group === "types" ||
606
- group === "@types" ||
607
- !name ||
608
- name.startsWith("@types"))
609
- ) {
610
- return;
611
- }
612
605
  const version = pkg.version;
613
606
  if (!version || ["dummy", "ignore"].includes(version)) {
614
607
  return;
@@ -664,10 +657,10 @@ function addComponent(
664
657
  };
665
658
  if (format === "xml") {
666
659
  component["@type"] = determinePackageType(pkg);
667
- component["@bom-ref"] = purlString;
660
+ component["@bom-ref"] = decodeURIComponent(purlString);
668
661
  } else {
669
662
  component["type"] = determinePackageType(pkg);
670
- component["bom-ref"] = purlString;
663
+ component["bom-ref"] = decodeURIComponent(purlString);
671
664
  }
672
665
  if (
673
666
  component.externalReferences === undefined ||
@@ -680,7 +673,11 @@ function addComponent(
680
673
  // Retain any component properties
681
674
  if (format === "json") {
682
675
  // Retain evidence
683
- if (pkg.evidence && Object.keys(pkg.evidence).length) {
676
+ if (
677
+ options.specVersion >= 1.5 &&
678
+ pkg.evidence &&
679
+ Object.keys(pkg.evidence).length
680
+ ) {
684
681
  component.evidence = pkg.evidence;
685
682
  }
686
683
  if (pkg.properties && pkg.properties.length) {
@@ -887,7 +884,10 @@ const buildBomXml = (
887
884
  const bom = create("bom", {
888
885
  encoding: "utf-8",
889
886
  separateArrayItems: true
890
- }).att("xmlns", "http://cyclonedx.org/schema/bom/1.5");
887
+ }).att(
888
+ "xmlns",
889
+ `http://cyclonedx.org/schema/bom/${"" + (options.specVersion || 1.5)}`
890
+ );
891
891
  bom.att("serialNumber", serialNum);
892
892
  bom.att("version", 1);
893
893
  const metadata = addMetadata(parentComponent, "xml", options);
@@ -934,7 +934,7 @@ const buildBomNSData = (options, pkgInfo, ptype, context) => {
934
934
  allImports = context.allImports;
935
935
  }
936
936
  const nsMapping = context.nsMapping || {};
937
- const dependencies = context.dependencies || [];
937
+ const dependencies = !options.requiredOnly ? context.dependencies || [] : [];
938
938
  const parentComponent =
939
939
  determineParentComponent(options) || context.parentComponent;
940
940
  const metadata = addMetadata(parentComponent, "json", options);
@@ -950,7 +950,7 @@ const buildBomNSData = (options, pkgInfo, ptype, context) => {
950
950
  // CycloneDX 1.5 Json Template
951
951
  const jsonTpl = {
952
952
  bomFormat: "CycloneDX",
953
- specVersion: "1.5",
953
+ specVersion: "" + (options.specVersion || "1.5"),
954
954
  serialNumber: serialNum,
955
955
  version: 1,
956
956
  metadata: metadata,
@@ -1131,11 +1131,17 @@ export const createJavaBom = async (path, options) => {
1131
1131
  "Resolve the above maven error. This could be due to the following:\n"
1132
1132
  );
1133
1133
  if (
1134
- result.stderr &&
1135
- result.stderr.includes(
1136
- "Could not resolve dependencies" ||
1137
- result.stderr.includes("no dependency information available")
1138
- )
1134
+ result.stdout &&
1135
+ (result.stdout.includes("Non-resolvable parent POM") ||
1136
+ result.stdout.includes("points at wrong local POM"))
1137
+ ) {
1138
+ console.log(
1139
+ "1. Check if the pom.xml contains valid settings such `parent.relativePath` to make mvn command work from within the sub-directory."
1140
+ );
1141
+ } else if (
1142
+ result.stdout &&
1143
+ (result.stdout.includes("Could not resolve dependencies") ||
1144
+ result.stdout.includes("no dependency information available"))
1139
1145
  ) {
1140
1146
  console.log(
1141
1147
  "1. Try building the project with 'mvn package -Dmaven.test.skip=true' using the correct version of Java and maven before invoking cdxgen."
@@ -1316,17 +1322,21 @@ export const createJavaBom = async (path, options) => {
1316
1322
  }
1317
1323
  if (gradleFiles && gradleFiles.length && options.installDeps) {
1318
1324
  const gradleCmd = getGradleCommand(path, null);
1325
+ const defaultDepTaskArgs = ["-q", "--console", "plain", "--build-cache"];
1319
1326
  allProjects.push(parentComponent);
1327
+ let depTaskWithArgs = ["dependencies"];
1328
+ if (process.env.GRADLE_DEPENDENCY_TASK) {
1329
+ depTaskWithArgs = process.env.GRADLE_DEPENDENCY_TASK.split(" ");
1330
+ }
1320
1331
  for (const sp of allProjects) {
1321
1332
  let gradleDepArgs = [
1322
1333
  sp.purl === parentComponent.purl
1323
- ? "dependencies"
1324
- : `:${sp.name}:dependencies`,
1325
- "-q",
1326
- "--console",
1327
- "plain",
1328
- "--build-cache"
1334
+ ? depTaskWithArgs[0]
1335
+ : `:${sp.name}:${depTaskWithArgs[0]}`
1329
1336
  ];
1337
+ gradleDepArgs = gradleDepArgs
1338
+ .concat(depTaskWithArgs.slice(1))
1339
+ .concat(defaultDepTaskArgs);
1330
1340
  // Support custom GRADLE_ARGS such as --configuration runtimeClassPath
1331
1341
  if (process.env.GRADLE_ARGS) {
1332
1342
  const addArgs = process.env.GRADLE_ARGS.split(" ");
@@ -1673,6 +1683,7 @@ export const createNodejsBom = async (path, options) => {
1673
1683
  let manifestFiles = [];
1674
1684
  let dependencies = [];
1675
1685
  let parentComponent = {};
1686
+ const parentSubComponents = [];
1676
1687
  let ppurl = "";
1677
1688
  // Docker mode requires special handling
1678
1689
  if (["docker", "oci", "os"].includes(options.projectType)) {
@@ -1807,8 +1818,14 @@ export const createNodejsBom = async (path, options) => {
1807
1818
  // Parse package-lock.json if available
1808
1819
  const parsedList = await parsePkgLock(f);
1809
1820
  const dlist = parsedList.pkgList;
1810
- parentComponent = dlist.splice(0, 1)[0] || {};
1811
- parentComponent.type = "application";
1821
+ const tmpParentComponent = dlist.splice(0, 1)[0] || {};
1822
+ tmpParentComponent.type = "application";
1823
+ // Create a default parent component based on directory name
1824
+ if (!Object.keys(parentComponent).length) {
1825
+ parentComponent = tmpParentComponent;
1826
+ } else {
1827
+ parentSubComponents.push(tmpParentComponent);
1828
+ }
1812
1829
  if (dlist && dlist.length) {
1813
1830
  pkgList = pkgList.concat(dlist);
1814
1831
  }
@@ -1886,28 +1903,38 @@ export const createNodejsBom = async (path, options) => {
1886
1903
  if (existsSync(packageJsonF)) {
1887
1904
  const pcs = await parsePkgJson(packageJsonF);
1888
1905
  if (pcs.length) {
1889
- parentComponent = pcs[0];
1890
- parentComponent.type = "application";
1906
+ const tmpParentComponent = pcs[0];
1907
+ tmpParentComponent.type = "application";
1908
+ if (!Object.keys(parentComponent).length) {
1909
+ parentComponent = tmpParentComponent;
1910
+ } else {
1911
+ parentSubComponents.push(tmpParentComponent);
1912
+ }
1891
1913
  }
1892
1914
  } else {
1893
1915
  let dirName = dirname(f);
1894
1916
  const tmpA = dirName.split(sep);
1895
1917
  dirName = tmpA[tmpA.length - 1];
1896
- parentComponent = {
1918
+ const tmpParentComponent = {
1897
1919
  group: "",
1898
1920
  name: dirName,
1899
1921
  type: "application"
1900
1922
  };
1901
1923
  ppurl = new PackageURL(
1902
1924
  "npm",
1903
- parentComponent.group,
1904
- parentComponent.name,
1905
- parentComponent.version,
1925
+ tmpParentComponent.group,
1926
+ tmpParentComponent.name,
1927
+ tmpParentComponent.version,
1906
1928
  null,
1907
1929
  null
1908
1930
  ).toString();
1909
- parentComponent["bom-ref"] = ppurl;
1910
- parentComponent["purl"] = ppurl;
1931
+ tmpParentComponent["bom-ref"] = ppurl;
1932
+ tmpParentComponent["purl"] = ppurl;
1933
+ if (!Object.keys(parentComponent).length) {
1934
+ parentComponent = tmpParentComponent;
1935
+ } else {
1936
+ parentSubComponents.push(tmpParentComponent);
1937
+ }
1911
1938
  }
1912
1939
  // Parse yarn.lock if available. This check is after rush.json since
1913
1940
  // rush.js could include yarn.lock :(
@@ -1966,17 +1993,17 @@ export const createNodejsBom = async (path, options) => {
1966
1993
  parentComponent
1967
1994
  });
1968
1995
  }
1969
- // Projects containing just min files or bower
1970
- if (pkgList) {
1971
- return buildBomNSData(options, pkgList, "npm", {
1972
- allImports,
1973
- src: path,
1974
- filename: manifestFiles.join(", "),
1975
- dependencies,
1976
- parentComponent
1977
- });
1996
+ // Retain the components of parent component
1997
+ if (parentSubComponents.length) {
1998
+ parentComponent.components = parentSubComponents;
1978
1999
  }
1979
- return {};
2000
+ return buildBomNSData(options, pkgList, "npm", {
2001
+ allImports,
2002
+ src: path,
2003
+ filename: manifestFiles.join(", "),
2004
+ dependencies,
2005
+ parentComponent
2006
+ });
1980
2007
  };
1981
2008
 
1982
2009
  /**
@@ -1991,7 +2018,7 @@ export const createPythonBom = async (path, options) => {
1991
2018
  let dependencies = [];
1992
2019
  let pkgList = [];
1993
2020
  const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-venv-"));
1994
- const parentComponent = createDefaultParentComponent(path);
2021
+ const parentComponent = createDefaultParentComponent(path, "pypi");
1995
2022
  const pipenvMode = existsSync(join(path, "Pipfile"));
1996
2023
  const poetryFiles = getAllFiles(
1997
2024
  path,
@@ -2099,6 +2126,12 @@ export const createPythonBom = async (path, options) => {
2099
2126
  pkgList = pkgList.concat(pkgMap.pkgList);
2100
2127
  frozen = true;
2101
2128
  }
2129
+ if (pkgMap.dependenciesList) {
2130
+ dependencies = mergeDependencies(
2131
+ dependencies,
2132
+ pkgMap.dependenciesList
2133
+ );
2134
+ }
2102
2135
  }
2103
2136
  // Fallback to parsing manually
2104
2137
  if (!pkgList.length || !frozen) {
@@ -2148,7 +2181,9 @@ export const createPythonBom = async (path, options) => {
2148
2181
  if (retMap.pkgList && retMap.pkgList.length) {
2149
2182
  pkgList = pkgList.concat(retMap.pkgList);
2150
2183
  for (const p of retMap.pkgList) {
2151
- parentDependsOn.push(`pkg:pypi/${p.name}@${p.version}`);
2184
+ if (p.version) {
2185
+ parentDependsOn.push(`pkg:pypi/${p.name}@${p.version}`);
2186
+ }
2152
2187
  }
2153
2188
  }
2154
2189
  if (retMap.dependenciesList) {
@@ -2563,7 +2598,7 @@ export const createDartBom = async (path, options) => {
2563
2598
  * @param path to the project
2564
2599
  * @param options Parse options from the cli
2565
2600
  */
2566
- export const createCppBom = async (path, options) => {
2601
+ export const createCppBom = (path, options) => {
2567
2602
  const conanLockFiles = getAllFiles(
2568
2603
  path,
2569
2604
  (options.multiProject ? "**/" : "") + "conan.lock"
@@ -2732,7 +2767,7 @@ export const createClojureBom = (path, options) => {
2732
2767
  * @param path to the project
2733
2768
  * @param options Parse options from the cli
2734
2769
  */
2735
- export const createHaskellBom = async (path, options) => {
2770
+ export const createHaskellBom = (path, options) => {
2736
2771
  const cabalFiles = getAllFiles(
2737
2772
  path,
2738
2773
  (options.multiProject ? "**/" : "") + "cabal.project.freeze"
@@ -2763,7 +2798,7 @@ export const createHaskellBom = async (path, options) => {
2763
2798
  * @param path to the project
2764
2799
  * @param options Parse options from the cli
2765
2800
  */
2766
- export const createElixirBom = async (path, options) => {
2801
+ export const createElixirBom = (path, options) => {
2767
2802
  const mixFiles = getAllFiles(
2768
2803
  path,
2769
2804
  (options.multiProject ? "**/" : "") + "mix.lock"
@@ -2794,7 +2829,7 @@ export const createElixirBom = async (path, options) => {
2794
2829
  * @param path to the project
2795
2830
  * @param options Parse options from the cli
2796
2831
  */
2797
- export const createGitHubBom = async (path, options) => {
2832
+ export const createGitHubBom = (path, options) => {
2798
2833
  const ghactionFiles = getAllFiles(path, ".github/workflows/" + "*.yml");
2799
2834
  let pkgList = [];
2800
2835
  if (ghactionFiles.length) {
@@ -2822,7 +2857,7 @@ export const createGitHubBom = async (path, options) => {
2822
2857
  * @param path to the project
2823
2858
  * @param options Parse options from the cli
2824
2859
  */
2825
- export const createCloudBuildBom = async (path, options) => {
2860
+ export const createCloudBuildBom = (path, options) => {
2826
2861
  const cbFiles = getAllFiles(path, "cloudbuild.yml");
2827
2862
  let pkgList = [];
2828
2863
  if (cbFiles.length) {
@@ -2943,7 +2978,7 @@ export const createJenkinsBom = async (path, options) => {
2943
2978
  * @param path to the project
2944
2979
  * @param options Parse options from the cli
2945
2980
  */
2946
- export const createHelmBom = async (path, options) => {
2981
+ export const createHelmBom = (path, options) => {
2947
2982
  let pkgList = [];
2948
2983
  const yamlFiles = getAllFiles(
2949
2984
  path,
@@ -2990,7 +3025,7 @@ export const createSwiftBom = (path, options) => {
2990
3025
  if (pkgResolvedFiles.length) {
2991
3026
  for (const f of pkgResolvedFiles) {
2992
3027
  if (!parentComponent || !Object.keys(parentComponent).length) {
2993
- parentComponent = createDefaultParentComponent(f);
3028
+ parentComponent = createDefaultParentComponent(f, "swift");
2994
3029
  }
2995
3030
  if (DEBUG_MODE) {
2996
3031
  console.log("Parsing", f);
@@ -3618,13 +3653,15 @@ export const trimComponents = (components, format) => {
3618
3653
  const filteredComponents = [];
3619
3654
  for (const comp of components) {
3620
3655
  if (format === "xml" && comp.component) {
3621
- if (!keyCache[comp.component.purl]) {
3622
- keyCache[comp.component.purl] = true;
3656
+ const key = comp.component.purl || comp.component["bom-ref"];
3657
+ if (!keyCache[key]) {
3658
+ keyCache[key] = true;
3623
3659
  filteredComponents.push(comp);
3624
3660
  }
3625
3661
  } else {
3626
- if (!keyCache[comp.purl]) {
3627
- keyCache[comp.purl] = true;
3662
+ const key = comp.purl || comp["bom-ref"];
3663
+ if (!keyCache[key]) {
3664
+ keyCache[key] = true;
3628
3665
  filteredComponents.push(comp);
3629
3666
  }
3630
3667
  }
@@ -3670,7 +3707,7 @@ export const dedupeBom = (
3670
3707
  ),
3671
3708
  bomJson: {
3672
3709
  bomFormat: "CycloneDX",
3673
- specVersion: "1.5",
3710
+ specVersion: "" + (options.specVersion || 1.5),
3674
3711
  serialNumber: serialNum,
3675
3712
  version: 1,
3676
3713
  metadata: addMetadata(parentComponent, "json", options),
@@ -3692,7 +3729,8 @@ export const createMultiXBom = async (pathList, options) => {
3692
3729
  let dependencies = [];
3693
3730
  let componentsXmls = [];
3694
3731
  let bomData = undefined;
3695
- let parentComponent = determineParentComponent(options);
3732
+ let parentComponent = determineParentComponent(options) || {};
3733
+ let parentSubComponents = [];
3696
3734
  if (
3697
3735
  ["docker", "oci", "container"].includes(options.projectType) &&
3698
3736
  options.allLayersExplodedDir
@@ -3743,15 +3781,31 @@ export const createMultiXBom = async (pathList, options) => {
3743
3781
  }
3744
3782
  components = components.concat(bomData.bomJson.components);
3745
3783
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3746
- if (!parentComponent || !Object.keys(parentComponent).length) {
3747
- parentComponent = bomData.parentComponent;
3784
+ if (
3785
+ bomData.parentComponent &&
3786
+ Object.keys(bomData.parentComponent).length
3787
+ ) {
3788
+ parentSubComponents.push(bomData.parentComponent);
3789
+ }
3790
+ if (
3791
+ bomData.parentComponent.components &&
3792
+ bomData.parentComponent.components.length
3793
+ ) {
3794
+ parentSubComponents = parentSubComponents.concat(
3795
+ bomData.parentComponent.components
3796
+ );
3748
3797
  }
3749
3798
  componentsXmls = componentsXmls.concat(
3750
3799
  listComponents(options, {}, bomData.bomJson.components, "npm", "xml")
3751
3800
  );
3752
3801
  }
3753
3802
  bomData = await createJavaBom(path, options);
3754
- if (bomData && bomData.bomJson && bomData.bomJson.components) {
3803
+ if (
3804
+ bomData &&
3805
+ bomData.bomJson &&
3806
+ bomData.bomJson.components &&
3807
+ bomData.bomJson.components.length
3808
+ ) {
3755
3809
  if (DEBUG_MODE) {
3756
3810
  console.log(
3757
3811
  `Found ${bomData.bomJson.components.length} java packages at ${path}`
@@ -3759,15 +3813,23 @@ export const createMultiXBom = async (pathList, options) => {
3759
3813
  }
3760
3814
  components = components.concat(bomData.bomJson.components);
3761
3815
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3762
- if (!parentComponent || !Object.keys(parentComponent).length) {
3763
- parentComponent = bomData.parentComponent;
3816
+ if (
3817
+ bomData.parentComponent &&
3818
+ Object.keys(bomData.parentComponent).length
3819
+ ) {
3820
+ parentSubComponents.push(bomData.parentComponent);
3764
3821
  }
3765
3822
  componentsXmls = componentsXmls.concat(
3766
3823
  listComponents(options, {}, bomData.bomJson.components, "maven", "xml")
3767
3824
  );
3768
3825
  }
3769
3826
  bomData = await createPythonBom(path, options);
3770
- if (bomData && bomData.bomJson && bomData.bomJson.components) {
3827
+ if (
3828
+ bomData &&
3829
+ bomData.bomJson &&
3830
+ bomData.bomJson.components &&
3831
+ bomData.bomJson.components.length
3832
+ ) {
3771
3833
  if (DEBUG_MODE) {
3772
3834
  console.log(
3773
3835
  `Found ${bomData.bomJson.components.length} python packages at ${path}`
@@ -3775,15 +3837,23 @@ export const createMultiXBom = async (pathList, options) => {
3775
3837
  }
3776
3838
  components = components.concat(bomData.bomJson.components);
3777
3839
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3778
- if (!parentComponent || !Object.keys(parentComponent).length) {
3779
- parentComponent = bomData.parentComponent;
3840
+ if (
3841
+ bomData.parentComponent &&
3842
+ Object.keys(bomData.parentComponent).length
3843
+ ) {
3844
+ parentSubComponents.push(bomData.parentComponent);
3780
3845
  }
3781
3846
  componentsXmls = componentsXmls.concat(
3782
3847
  listComponents(options, {}, bomData.bomJson.components, "pypi", "xml")
3783
3848
  );
3784
3849
  }
3785
3850
  bomData = await createGoBom(path, options);
3786
- if (bomData && bomData.bomJson && bomData.bomJson.components) {
3851
+ if (
3852
+ bomData &&
3853
+ bomData.bomJson &&
3854
+ bomData.bomJson.components &&
3855
+ bomData.bomJson.components.length
3856
+ ) {
3787
3857
  if (DEBUG_MODE) {
3788
3858
  console.log(
3789
3859
  `Found ${bomData.bomJson.components.length} go packages at ${path}`
@@ -3791,8 +3861,11 @@ export const createMultiXBom = async (pathList, options) => {
3791
3861
  }
3792
3862
  components = components.concat(bomData.bomJson.components);
3793
3863
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3794
- if (!parentComponent || !Object.keys(parentComponent).length) {
3795
- parentComponent = bomData.parentComponent;
3864
+ if (
3865
+ bomData.parentComponent &&
3866
+ Object.keys(bomData.parentComponent).length
3867
+ ) {
3868
+ parentSubComponents.push(bomData.parentComponent);
3796
3869
  }
3797
3870
  componentsXmls = componentsXmls.concat(
3798
3871
  listComponents(options, {}, bomData.bomJson.components, "golang", "xml")
@@ -3807,8 +3880,11 @@ export const createMultiXBom = async (pathList, options) => {
3807
3880
  }
3808
3881
  components = components.concat(bomData.bomJson.components);
3809
3882
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3810
- if (!parentComponent || !Object.keys(parentComponent).length) {
3811
- parentComponent = bomData.parentComponent;
3883
+ if (
3884
+ bomData.parentComponent &&
3885
+ Object.keys(bomData.parentComponent).length
3886
+ ) {
3887
+ parentSubComponents.push(bomData.parentComponent);
3812
3888
  }
3813
3889
  componentsXmls = componentsXmls.concat(
3814
3890
  listComponents(options, {}, bomData.bomJson.components, "cargo", "xml")
@@ -3823,8 +3899,11 @@ export const createMultiXBom = async (pathList, options) => {
3823
3899
  }
3824
3900
  components = components.concat(bomData.bomJson.components);
3825
3901
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3826
- if (!parentComponent || !Object.keys(parentComponent).length) {
3827
- parentComponent = bomData.parentComponent;
3902
+ if (
3903
+ bomData.parentComponent &&
3904
+ Object.keys(bomData.parentComponent).length
3905
+ ) {
3906
+ parentSubComponents.push(bomData.parentComponent);
3828
3907
  }
3829
3908
  componentsXmls = componentsXmls.concat(
3830
3909
  listComponents(
@@ -3845,8 +3924,11 @@ export const createMultiXBom = async (pathList, options) => {
3845
3924
  }
3846
3925
  components = components.concat(bomData.bomJson.components);
3847
3926
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3848
- if (!parentComponent || !Object.keys(parentComponent).length) {
3849
- parentComponent = bomData.parentComponent;
3927
+ if (
3928
+ bomData.parentComponent &&
3929
+ Object.keys(bomData.parentComponent).length
3930
+ ) {
3931
+ parentSubComponents.push(bomData.parentComponent);
3850
3932
  }
3851
3933
  componentsXmls = componentsXmls.concat(
3852
3934
  listComponents(options, {}, bomData.bomJson.components, "gem", "xml")
@@ -3861,8 +3943,11 @@ export const createMultiXBom = async (pathList, options) => {
3861
3943
  }
3862
3944
  components = components.concat(bomData.bomJson.components);
3863
3945
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3864
- if (!parentComponent || !Object.keys(parentComponent).length) {
3865
- parentComponent = bomData.parentComponent;
3946
+ if (
3947
+ bomData.parentComponent &&
3948
+ Object.keys(bomData.parentComponent).length
3949
+ ) {
3950
+ parentSubComponents.push(bomData.parentComponent);
3866
3951
  }
3867
3952
  componentsXmls = componentsXmls.concat(
3868
3953
  listComponents(options, {}, bomData.bomJson.components, "nuget", "xml")
@@ -3877,14 +3962,17 @@ export const createMultiXBom = async (pathList, options) => {
3877
3962
  }
3878
3963
  components = components.concat(bomData.bomJson.components);
3879
3964
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3880
- if (!parentComponent || !Object.keys(parentComponent).length) {
3881
- parentComponent = bomData.parentComponent;
3965
+ if (
3966
+ bomData.parentComponent &&
3967
+ Object.keys(bomData.parentComponent).length
3968
+ ) {
3969
+ parentSubComponents.push(bomData.parentComponent);
3882
3970
  }
3883
3971
  componentsXmls = componentsXmls.concat(
3884
3972
  listComponents(options, {}, bomData.bomJson.components, "pub", "xml")
3885
3973
  );
3886
3974
  }
3887
- bomData = await createHaskellBom(path, options);
3975
+ bomData = createHaskellBom(path, options);
3888
3976
  if (bomData && bomData.bomJson && bomData.bomJson.components) {
3889
3977
  if (DEBUG_MODE) {
3890
3978
  console.log(
@@ -3893,8 +3981,11 @@ export const createMultiXBom = async (pathList, options) => {
3893
3981
  }
3894
3982
  components = components.concat(bomData.bomJson.components);
3895
3983
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3896
- if (!parentComponent || !Object.keys(parentComponent).length) {
3897
- parentComponent = bomData.parentComponent;
3984
+ if (
3985
+ bomData.parentComponent &&
3986
+ Object.keys(bomData.parentComponent).length
3987
+ ) {
3988
+ parentSubComponents.push(bomData.parentComponent);
3898
3989
  }
3899
3990
  componentsXmls = componentsXmls.concat(
3900
3991
  listComponents(
@@ -3906,7 +3997,7 @@ export const createMultiXBom = async (pathList, options) => {
3906
3997
  )
3907
3998
  );
3908
3999
  }
3909
- bomData = await createElixirBom(path, options);
4000
+ bomData = createElixirBom(path, options);
3910
4001
  if (bomData && bomData.bomJson && bomData.bomJson.components) {
3911
4002
  if (DEBUG_MODE) {
3912
4003
  console.log(
@@ -3915,14 +4006,17 @@ export const createMultiXBom = async (pathList, options) => {
3915
4006
  }
3916
4007
  components = components.concat(bomData.bomJson.components);
3917
4008
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3918
- if (!parentComponent || !Object.keys(parentComponent).length) {
3919
- parentComponent = bomData.parentComponent;
4009
+ if (
4010
+ bomData.parentComponent &&
4011
+ Object.keys(bomData.parentComponent).length
4012
+ ) {
4013
+ parentSubComponents.push(bomData.parentComponent);
3920
4014
  }
3921
4015
  componentsXmls = componentsXmls.concat(
3922
4016
  listComponents(options, {}, bomData.bomJson.components, "hex", "xml")
3923
4017
  );
3924
4018
  }
3925
- bomData = await createCppBom(path, options);
4019
+ bomData = createCppBom(path, options);
3926
4020
  if (bomData && bomData.bomJson && bomData.bomJson.components) {
3927
4021
  if (DEBUG_MODE) {
3928
4022
  console.log(
@@ -3931,8 +4025,11 @@ export const createMultiXBom = async (pathList, options) => {
3931
4025
  }
3932
4026
  components = components.concat(bomData.bomJson.components);
3933
4027
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3934
- if (!parentComponent || !Object.keys(parentComponent).length) {
3935
- parentComponent = bomData.parentComponent;
4028
+ if (
4029
+ bomData.parentComponent &&
4030
+ Object.keys(bomData.parentComponent).length
4031
+ ) {
4032
+ parentSubComponents.push(bomData.parentComponent);
3936
4033
  }
3937
4034
  componentsXmls = componentsXmls.concat(
3938
4035
  listComponents(options, {}, bomData.bomJson.components, "conan", "xml")
@@ -3947,8 +4044,11 @@ export const createMultiXBom = async (pathList, options) => {
3947
4044
  }
3948
4045
  components = components.concat(bomData.bomJson.components);
3949
4046
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3950
- if (!parentComponent || !Object.keys(parentComponent).length) {
3951
- parentComponent = bomData.parentComponent;
4047
+ if (
4048
+ bomData.parentComponent &&
4049
+ Object.keys(bomData.parentComponent).length
4050
+ ) {
4051
+ parentSubComponents.push(bomData.parentComponent);
3952
4052
  }
3953
4053
  componentsXmls = componentsXmls.concat(
3954
4054
  listComponents(
@@ -3960,7 +4060,7 @@ export const createMultiXBom = async (pathList, options) => {
3960
4060
  )
3961
4061
  );
3962
4062
  }
3963
- bomData = await createGitHubBom(path, options);
4063
+ bomData = createGitHubBom(path, options);
3964
4064
  if (bomData && bomData.bomJson && bomData.bomJson.components) {
3965
4065
  if (DEBUG_MODE) {
3966
4066
  console.log(
@@ -3969,14 +4069,17 @@ export const createMultiXBom = async (pathList, options) => {
3969
4069
  }
3970
4070
  components = components.concat(bomData.bomJson.components);
3971
4071
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3972
- if (!parentComponent || !Object.keys(parentComponent).length) {
3973
- parentComponent = bomData.parentComponent;
4072
+ if (
4073
+ bomData.parentComponent &&
4074
+ Object.keys(bomData.parentComponent).length
4075
+ ) {
4076
+ parentSubComponents.push(bomData.parentComponent);
3974
4077
  }
3975
4078
  componentsXmls = componentsXmls.concat(
3976
4079
  listComponents(options, {}, bomData.bomJson.components, "github", "xml")
3977
4080
  );
3978
4081
  }
3979
- bomData = await createCloudBuildBom(path, options);
4082
+ bomData = createCloudBuildBom(path, options);
3980
4083
  if (bomData && bomData.bomJson && bomData.bomJson.components) {
3981
4084
  if (DEBUG_MODE) {
3982
4085
  console.log(
@@ -3985,8 +4088,11 @@ export const createMultiXBom = async (pathList, options) => {
3985
4088
  }
3986
4089
  components = components.concat(bomData.bomJson.components);
3987
4090
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
3988
- if (!parentComponent || !Object.keys(parentComponent).length) {
3989
- parentComponent = bomData.parentComponent;
4091
+ if (
4092
+ bomData.parentComponent &&
4093
+ Object.keys(bomData.parentComponent).length
4094
+ ) {
4095
+ parentSubComponents.push(bomData.parentComponent);
3990
4096
  }
3991
4097
  componentsXmls = componentsXmls.concat(
3992
4098
  listComponents(
@@ -4012,8 +4118,11 @@ export const createMultiXBom = async (pathList, options) => {
4012
4118
  }
4013
4119
  components = components.concat(bomData.bomJson.components);
4014
4120
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
4015
- if (!parentComponent || !Object.keys(parentComponent).length) {
4016
- parentComponent = bomData.parentComponent;
4121
+ if (
4122
+ bomData.parentComponent &&
4123
+ Object.keys(bomData.parentComponent).length
4124
+ ) {
4125
+ parentSubComponents.push(bomData.parentComponent);
4017
4126
  }
4018
4127
  componentsXmls = componentsXmls.concat(
4019
4128
  listComponents(options, {}, bomData.bomJson.components, "swift", "xml")
@@ -4030,8 +4139,11 @@ export const createMultiXBom = async (pathList, options) => {
4030
4139
  }
4031
4140
  components = components.concat(bomData.bomJson.components);
4032
4141
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
4033
- if (!parentComponent || !Object.keys(parentComponent).length) {
4034
- parentComponent = bomData.parentComponent;
4142
+ if (
4143
+ bomData.parentComponent &&
4144
+ Object.keys(bomData.parentComponent).length
4145
+ ) {
4146
+ parentSubComponents.push(bomData.parentComponent);
4035
4147
  }
4036
4148
  componentsXmls = componentsXmls.concat(
4037
4149
  listComponents(options, {}, bomData.bomJson.components, "maven", "xml")
@@ -4048,14 +4160,36 @@ export const createMultiXBom = async (pathList, options) => {
4048
4160
  }
4049
4161
  components = components.concat(bomData.bomJson.components);
4050
4162
  dependencies = dependencies.concat(bomData.bomJson.dependencies);
4051
- if (!parentComponent || !Object.keys(parentComponent).length) {
4052
- parentComponent = bomData.parentComponent;
4163
+ if (
4164
+ bomData.parentComponent &&
4165
+ Object.keys(bomData.parentComponent).length
4166
+ ) {
4167
+ parentSubComponents.push(bomData.parentComponent);
4053
4168
  }
4054
4169
  componentsXmls = componentsXmls.concat(
4055
4170
  listComponents(options, {}, bomData.bomJson.components, "maven", "xml")
4056
4171
  );
4057
4172
  }
4058
4173
  }
4174
+ // Retain the components of parent component
4175
+ if (parentSubComponents.length) {
4176
+ if (!parentComponent || !Object.keys(parentComponent).length) {
4177
+ parentComponent = parentSubComponents[0];
4178
+ }
4179
+ // Our naive approach to appending to sub-components could result in same parent being included as a child
4180
+ // This is filtered out here
4181
+ parentSubComponents = parentSubComponents.filter(
4182
+ (c) => c["bom-ref"] !== parentComponent["bom-ref"]
4183
+ );
4184
+ parentComponent.components = trimComponents(parentSubComponents, "json");
4185
+ if (
4186
+ parentComponent.components.length == 1 &&
4187
+ parentComponent.components[0].name == parentComponent.name
4188
+ ) {
4189
+ parentComponent = parentComponent.components[0];
4190
+ delete parentComponent.components;
4191
+ }
4192
+ }
4059
4193
  return dedupeBom(
4060
4194
  options,
4061
4195
  components,
@@ -4213,7 +4347,7 @@ export const createXBom = async (path, options) => {
4213
4347
  (options.multiProject ? "**/" : "") + "cabal.project.freeze"
4214
4348
  );
4215
4349
  if (hackageFiles.length) {
4216
- return await createHaskellBom(path, options);
4350
+ return createHaskellBom(path, options);
4217
4351
  }
4218
4352
 
4219
4353
  // Elixir
@@ -4222,7 +4356,7 @@ export const createXBom = async (path, options) => {
4222
4356
  (options.multiProject ? "**/" : "") + "mix.lock"
4223
4357
  );
4224
4358
  if (mixFiles.length) {
4225
- return await createElixirBom(path, options);
4359
+ return createElixirBom(path, options);
4226
4360
  }
4227
4361
 
4228
4362
  // cpp
@@ -4235,7 +4369,7 @@ export const createXBom = async (path, options) => {
4235
4369
  (options.multiProject ? "**/" : "") + "conanfile.txt"
4236
4370
  );
4237
4371
  if (conanLockFiles.length || conanFiles.length) {
4238
- return await createCppBom(path, options);
4372
+ return createCppBom(path, options);
4239
4373
  }
4240
4374
 
4241
4375
  // clojure
@@ -4254,7 +4388,7 @@ export const createXBom = async (path, options) => {
4254
4388
  // GitHub actions
4255
4389
  const ghactionFiles = getAllFiles(path, ".github/workflows/" + "*.yml");
4256
4390
  if (ghactionFiles.length) {
4257
- return await createGitHubBom(path, options);
4391
+ return createGitHubBom(path, options);
4258
4392
  }
4259
4393
 
4260
4394
  // Jenkins plugins
@@ -4276,7 +4410,7 @@ export const createXBom = async (path, options) => {
4276
4410
  (options.multiProject ? "**/" : "") + "values.yaml"
4277
4411
  );
4278
4412
  if (chartFiles.length || yamlFiles.length) {
4279
- return await createHelmBom(path, options);
4413
+ return createHelmBom(path, options);
4280
4414
  }
4281
4415
 
4282
4416
  // Docker compose, kubernetes and skaffold
@@ -4302,7 +4436,7 @@ export const createXBom = async (path, options) => {
4302
4436
  (options.multiProject ? "**/" : "") + "cloudbuild.yaml"
4303
4437
  );
4304
4438
  if (cbFiles.length) {
4305
- return await createCloudBuildBom(path, options);
4439
+ return createCloudBuildBom(path, options);
4306
4440
  }
4307
4441
 
4308
4442
  // Swift
@@ -4505,18 +4639,18 @@ export const createBom = async (path, options) => {
4505
4639
  case "hackage":
4506
4640
  case "cabal":
4507
4641
  options.multiProject = true;
4508
- return await createHaskellBom(path, options);
4642
+ return createHaskellBom(path, options);
4509
4643
  case "elixir":
4510
4644
  case "hex":
4511
4645
  case "mix":
4512
4646
  options.multiProject = true;
4513
- return await createElixirBom(path, options);
4647
+ return createElixirBom(path, options);
4514
4648
  case "c":
4515
4649
  case "cpp":
4516
4650
  case "c++":
4517
4651
  case "conan":
4518
4652
  options.multiProject = true;
4519
- return await createCppBom(path, options);
4653
+ return createCppBom(path, options);
4520
4654
  case "clojure":
4521
4655
  case "edn":
4522
4656
  case "clj":
@@ -4526,7 +4660,7 @@ export const createBom = async (path, options) => {
4526
4660
  case "github":
4527
4661
  case "actions":
4528
4662
  options.multiProject = true;
4529
- return await createGitHubBom(path, options);
4663
+ return createGitHubBom(path, options);
4530
4664
  case "os":
4531
4665
  case "osquery":
4532
4666
  case "windows":
@@ -4539,11 +4673,11 @@ export const createBom = async (path, options) => {
4539
4673
  case "helm":
4540
4674
  case "charts":
4541
4675
  options.multiProject = true;
4542
- return await createHelmBom(path, options);
4676
+ return createHelmBom(path, options);
4543
4677
  case "helm-index":
4544
4678
  case "helm-repo":
4545
4679
  options.multiProject = true;
4546
- return await createHelmBom(
4680
+ return createHelmBom(
4547
4681
  join(homedir(), ".cache", "helm", "repository"),
4548
4682
  options
4549
4683
  );
@@ -4561,7 +4695,7 @@ export const createBom = async (path, options) => {
4561
4695
  return await createContainerSpecLikeBom(path, options);
4562
4696
  case "cloudbuild":
4563
4697
  options.multiProject = true;
4564
- return await createCloudBuildBom(path, options);
4698
+ return createCloudBuildBom(path, options);
4565
4699
  case "swift":
4566
4700
  options.multiProject = true;
4567
4701
  return createSwiftBom(path, options);
@@ -4658,38 +4792,3 @@ export async function submitBom(args, bomContents) {
4658
4792
  }
4659
4793
  }
4660
4794
  }
4661
-
4662
- /**
4663
- * Validate the generated bom using jsonschema
4664
- *
4665
- * @param {object} bomJson content
4666
- */
4667
- export const validateBom = (bomJson) => {
4668
- if (!bomJson) {
4669
- return true;
4670
- }
4671
- const schema = JSON.parse(
4672
- readFileSync(join(dirName, "data", "bom-1.5.schema.json"))
4673
- );
4674
- const defsSchema = JSON.parse(
4675
- readFileSync(join(dirName, "data", "jsf-0.82.schema.json"))
4676
- );
4677
- const spdxSchema = JSON.parse(
4678
- readFileSync(join(dirName, "data", "spdx.schema.json"))
4679
- );
4680
- const ajv = new Ajv({
4681
- schemas: [schema, defsSchema, spdxSchema],
4682
- strict: false,
4683
- logger: false
4684
- });
4685
- addFormats(ajv);
4686
- const validate = ajv.getSchema(
4687
- "http://cyclonedx.org/schema/bom-1.5.schema.json"
4688
- );
4689
- const isValid = validate(bomJson);
4690
- if (!isValid) {
4691
- console.log(validate.errors);
4692
- return false;
4693
- }
4694
- return true;
4695
- };