@cyclonedx/cdxgen 8.1.4 → 8.1.6

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/README.md CHANGED
@@ -52,7 +52,7 @@ NOTE:
52
52
  Footnotes:
53
53
 
54
54
  - [1] - For multi-module application, the BoM file could include components that may not be included in the packaged war or ear file.
55
- - [2] - Use pip freeze to improve the accuracy for requirements.txt based parsing. `python -m pip freeze > requirements.txt`
55
+ - [2] - Pip freeze is automatically performed to improve precision.
56
56
  - [3] - Perform dotnet or nuget restore to generate project.assets.json. Without this file cdxgen would not include indirect dependencies.
57
57
  - [4] - See section on plugins
58
58
  - [5] - Powered by osquery. See section on plugins
@@ -86,36 +86,38 @@ docker run --rm -v /tmp:/tmp -v $(pwd):/app:rw -t ghcr.io/cyclonedx/cdxgen -r /a
86
86
  ```text
87
87
  $ cdxgen -h
88
88
  Options:
89
- -o, --output Output file for bom.xml or bom.json. Default console
90
- -t, --type Project type
91
- -r, --recurse Recurse mode suitable for mono-repos [boolean]
92
- -p, --print Print the SBoM as a table [boolean]
93
- -c, --resolve-class Resolve class names for packages. jars only for now.
94
- [boolean]
95
- --deep Perform deep searches for components. Useful while
96
- scanning live OS and oci images. [boolean]
97
- --server-url Dependency track url. Eg:
98
- https://deptrack.cyclonedx.io
99
- --api-key Dependency track api key
100
- --project-group Dependency track project group
101
- --project-name Dependency track project name. Default use
102
- the directory name
103
- --project-version Dependency track project version
104
- --project-id Dependency track project id. Either
105
- provide the id or the project name and version together
106
- --required-only Include only the packages with required scope on the
107
- SBoM. [boolean]
108
- --fail-on-error Fail if any dependency extractor fails. [boolean]
109
- --no-babel Do not use babel to perform usage analysis for
110
- JavaScript/TypeScript projects. [boolean]
89
+ -o, --output Output file for bom.xml or bom.json. Default
90
+ bom.json
91
+ -t, --type Project type
92
+ -r, --recurse Recurse mode suitable for mono-repos [boolean]
93
+ -p, --print Print the SBoM as a table. Defaults to true if
94
+ output file is not specified with -o [boolean]
95
+ -c, --resolve-class Resolve class names for packages. jars only for
96
+ now. [boolean]
97
+ --deep Perform deep searches for components. Useful
98
+ while scanning live OS and oci images. [boolean]
99
+ --server-url Dependency track url. Eg:
100
+ https://deptrack.cyclonedx.io
101
+ --api-key Dependency track api key
102
+ --project-group Dependency track project group
103
+ --project-name Dependency track project name. Default use the
104
+ directory name
105
+ --project-version Dependency track project version [default: ""]
106
+ --project-id Dependency track project id. Either provide the
107
+ id or the project name and version together
108
+ --required-only Include only the packages with required scope on
109
+ the SBoM. [boolean]
110
+ --fail-on-error Fail if any dependency extractor fails. [boolean]
111
+ --no-babel Do not use babel to perform usage analysis for
112
+ JavaScript/TypeScript projects. [boolean]
111
113
  --generate-key-and-sign Generate an RSA public/private key pair and then
112
114
  sign the generated SBoM using JSON Web
113
115
  Signatures. [boolean]
114
116
  --server Run cdxgen as a server [boolean]
115
117
  --server-host Listen address [default: "127.0.0.1"]
116
118
  --server-port Listen port [default: "9090"]
117
- --version Show version number [boolean]
118
- -h Show help [boolean]
119
+ --version Show version number [boolean]
120
+ -h Show help [boolean]
119
121
  ```
120
122
 
121
123
  ## Example
package/analyzer.js CHANGED
@@ -13,10 +13,19 @@ const IGNORE_DIRS = [
13
13
  "e2e",
14
14
  "examples",
15
15
  "cypress",
16
- "site-packages"
16
+ "site-packages",
17
+ "typings",
18
+ "api_docs",
19
+ "dev_docs",
20
+ "types",
21
+ "mock",
22
+ "mocks"
17
23
  ];
18
24
 
19
- const IGNORE_FILE_PATTERN = new RegExp("(conf|test|spec|mock)\\.(js|ts)$", "i");
25
+ const IGNORE_FILE_PATTERN = new RegExp(
26
+ "(conf|test|spec|mock|\\.d)\\.(js|ts|tsx)$",
27
+ "i"
28
+ );
20
29
 
21
30
  const getAllFiles = (dir, extn, files, result, regex) => {
22
31
  files = files || fs.readdirSync(dir);
package/bin/cdxgen CHANGED
@@ -10,7 +10,7 @@ const bomServer = require("../server.js");
10
10
  const args = require("yargs")
11
11
  .option("output", {
12
12
  alias: "o",
13
- description: "Output file for bom.xml or bom.json. Default console"
13
+ description: "Output file for bom.xml or bom.json. Default bom.json"
14
14
  })
15
15
  .option("type", {
16
16
  alias: "t",
@@ -24,7 +24,8 @@ const args = require("yargs")
24
24
  .option("print", {
25
25
  alias: "p",
26
26
  type: "boolean",
27
- description: "Print the SBoM as a table"
27
+ description:
28
+ "Print the SBoM as a table. Defaults to true if output file is not specified with -o"
28
29
  })
29
30
  .option("resolve-class", {
30
31
  alias: "c",
@@ -154,7 +155,10 @@ let options = {
154
155
  return await bomServer.start(options);
155
156
  }
156
157
  const bomNSData = (await bom.createBom(filePath, options)) || {};
157
-
158
+ if (!args.output) {
159
+ args.output = "bom.json";
160
+ args.print = true;
161
+ }
158
162
  if (
159
163
  args.output &&
160
164
  (typeof args.output === "string" || args.output instanceof String)
@@ -286,8 +290,9 @@ let options = {
286
290
  }
287
291
 
288
292
  // Automatically submit the bom data
289
- if (args.serverUrl && args.apiKey) {
293
+ if (args.serverUrl && args.serverUrl != true && args.apiKey) {
290
294
  try {
295
+ // TODO: Need to use json format for v9
291
296
  const dbody = await bom.submitBom(args, bomNSData.bomXml);
292
297
  console.log("Response from server", dbody);
293
298
  } catch (err) {
package/docker.js CHANGED
@@ -381,7 +381,7 @@ const getImage = async (fullImageName) => {
381
381
  `Unable to pull ${fullImageName}. Check if the name is valid. Perform any authentication prior to invoking cdxgen.`
382
382
  );
383
383
  console.log(
384
- `Trying manually pulling this image using docker pull ${fullImageName}`
384
+ `Trying to manually pulling this image using docker pull ${fullImageName}`
385
385
  );
386
386
  }
387
387
  return localData;
package/index.js CHANGED
@@ -39,6 +39,11 @@ if (process.env.LEIN_CMD) {
39
39
  LEIN_CMD = process.env.LEIN_CMD;
40
40
  }
41
41
 
42
+ let PIP_CMD = "pip";
43
+ if (process.env.PIP_CMD) {
44
+ PIP_CMD = process.env.PIP_CMD;
45
+ }
46
+
42
47
  // Construct sbt cache directory
43
48
  let SBT_CACHE_DIR =
44
49
  process.env.SBT_CACHE_DIR || pathLib.join(os.homedir(), ".ivy2", "cache");
@@ -1887,7 +1892,7 @@ const createPythonBom = async (path, options) => {
1887
1892
  );
1888
1893
  const reqFiles = utils.getAllFiles(
1889
1894
  path,
1890
- (options.multiProject ? "**/" : "") + "requirements.txt"
1895
+ (options.multiProject ? "**/" : "") + "*requirements.txt"
1891
1896
  );
1892
1897
  const reqDirFiles = utils.getAllFiles(
1893
1898
  path,
@@ -1977,7 +1982,28 @@ const createPythonBom = async (path, options) => {
1977
1982
  metadataFilename = "requirements.txt";
1978
1983
  if (reqFiles && reqFiles.length) {
1979
1984
  for (let f of reqFiles) {
1980
- const reqData = fs.readFileSync(f, { encoding: "utf-8" });
1985
+ const basePath = pathLib.dirname(f);
1986
+ let reqData = undefined;
1987
+ // Attempt to pip freeze to improve precision
1988
+ if (options.installDeps) {
1989
+ const result = spawnSync(PIP_CMD, ["freeze", "-r", f, "-l"], {
1990
+ cwd: basePath,
1991
+ encoding: "utf-8",
1992
+ timeout: TIMEOUT_MS
1993
+ });
1994
+ if (result.status === 0 && result.stdout) {
1995
+ reqData = Buffer.from(result.stdout).toString();
1996
+ }
1997
+ }
1998
+ // Fallback to parsing requirements file
1999
+ if (!reqData) {
2000
+ if (DEBUG_MODE) {
2001
+ console.log(
2002
+ `Falling back to manually parsing ${f}. The result would be incomplete!`
2003
+ );
2004
+ }
2005
+ reqData = fs.readFileSync(f, { encoding: "utf-8" });
2006
+ }
1981
2007
  const dlist = await utils.parseReqFile(reqData);
1982
2008
  if (dlist && dlist.length) {
1983
2009
  pkgList = pkgList.concat(dlist);
@@ -2104,6 +2130,7 @@ const createGoBom = async (path, options) => {
2104
2130
  (options.multiProject ? "**/" : "") + "go.mod"
2105
2131
  );
2106
2132
  if (gomodFiles.length) {
2133
+ let shouldManuallyParse = false;
2107
2134
  // Use the go list -deps and go mod why commands to generate a good quality BoM for non-docker invocations
2108
2135
  if (!["docker", "oci", "os"].includes(options.projectType)) {
2109
2136
  for (let f of gomodFiles) {
@@ -2127,6 +2154,7 @@ const createGoBom = async (path, options) => {
2127
2154
  { cwd: basePath, encoding: "utf-8", timeout: TIMEOUT_MS }
2128
2155
  );
2129
2156
  if (result.status !== 0 || result.error) {
2157
+ shouldManuallyParse = true;
2130
2158
  console.error(result.stdout, result.stderr);
2131
2159
  options.failOnError && process.exit(1);
2132
2160
  }
@@ -2138,6 +2166,7 @@ const createGoBom = async (path, options) => {
2138
2166
  pkgList = pkgList.concat(dlist);
2139
2167
  }
2140
2168
  } else {
2169
+ shouldManuallyParse = true;
2141
2170
  console.error("go unexpectedly didn't return any output");
2142
2171
  options.failOnError && process.exit(1);
2143
2172
  }
@@ -2182,11 +2211,13 @@ const createGoBom = async (path, options) => {
2182
2211
  if (DEBUG_MODE) {
2183
2212
  console.log(`Required packages: ${Object.keys(allImports).length}`);
2184
2213
  }
2185
- return buildBomNSData(options, pkgList, "golang", {
2186
- allImports,
2187
- src: path,
2188
- filename: gomodFiles.join(", ")
2189
- });
2214
+ if (pkgList.length && !shouldManuallyParse) {
2215
+ return buildBomNSData(options, pkgList, "golang", {
2216
+ allImports,
2217
+ src: path,
2218
+ filename: gomodFiles.join(", ")
2219
+ });
2220
+ }
2190
2221
  }
2191
2222
  // Parse the gomod files manually. The resultant BoM would be incomplete
2192
2223
  if (!["docker", "oci", "os"].includes(options.projectType)) {
@@ -3455,7 +3486,7 @@ const createMultiXBom = async (pathList, options) => {
3455
3486
  if (bomData && bomData.bomJson && bomData.bomJson.components) {
3456
3487
  if (DEBUG_MODE) {
3457
3488
  console.log(
3458
- `Found ${bomData.bomJson.components.length} node.js packages at ${path}`
3489
+ `Found ${bomData.bomJson.components.length} npm packages at ${path}`
3459
3490
  );
3460
3491
  }
3461
3492
  components = components.concat(bomData.bomJson.components);
@@ -3812,7 +3843,7 @@ const createXBom = async (path, options) => {
3812
3843
  const poetryMode = fs.existsSync(pathLib.join(path, "poetry.lock"));
3813
3844
  const reqFiles = utils.getAllFiles(
3814
3845
  path,
3815
- (options.multiProject ? "**/" : "") + "requirements.txt"
3846
+ (options.multiProject ? "**/" : "") + "*requirements.txt"
3816
3847
  );
3817
3848
  const reqDirFiles = utils.getAllFiles(
3818
3849
  path,
@@ -4275,15 +4306,25 @@ exports.submitBom = async (args, bomContents) => {
4275
4306
  if (encodedBomContents.startsWith("77u/")) {
4276
4307
  encodedBomContents = encodedBomContents.substring(4);
4277
4308
  }
4309
+ const projectVersion =
4310
+ args.projectVersion && args.projectVersion.length
4311
+ ? args.projectVersion
4312
+ : "master";
4278
4313
  const bomPayload = {
4279
4314
  project: args.projectId,
4280
4315
  projectName: args.projectName,
4281
- projectVersion: args.projectVersion,
4316
+ projectVersion: projectVersion,
4282
4317
  autoCreate: "true",
4283
4318
  bom: encodedBomContents
4284
4319
  };
4285
4320
  if (DEBUG_MODE) {
4286
- console.log("Submitting BOM to", serverUrl);
4321
+ console.log(
4322
+ "Submitting BOM to",
4323
+ serverUrl,
4324
+ "params",
4325
+ args.projectName,
4326
+ projectVersion
4327
+ );
4287
4328
  }
4288
4329
  return await got(serverUrl, {
4289
4330
  method: "PUT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyclonedx/cdxgen",
3
- "version": "8.1.4",
3
+ "version": "8.1.6",
4
4
  "description": "Creates CycloneDX Software Bill-of-Materials (SBOM) from source or container image",
5
5
  "homepage": "http://github.com/cyclonedx/cdxgen",
6
6
  "author": "Prabhu Subramanian <prabhu@appthreat.com>",
package/utils.js CHANGED
@@ -1756,7 +1756,13 @@ exports.parsePoetrylockData = parsePoetrylockData;
1756
1756
  const parseReqFile = async function (reqData) {
1757
1757
  const pkgList = [];
1758
1758
  let fetchIndirectDeps = false;
1759
+ let compScope = undefined;
1759
1760
  reqData.split("\n").forEach((l) => {
1761
+ if (l.includes("# Basic requirements")) {
1762
+ compScope = "required";
1763
+ } else if (l.includes("added by pip freeze")) {
1764
+ compScope = undefined;
1765
+ }
1760
1766
  if (!l.startsWith("#")) {
1761
1767
  if (l.indexOf("=") > -1) {
1762
1768
  let tmpA = l.split(/(==|<=|~=|>=)/);
@@ -1773,7 +1779,8 @@ const parseReqFile = async function (reqData) {
1773
1779
  if (!tmpA[0].includes("=") && !tmpA[0].trim().includes(" ")) {
1774
1780
  pkgList.push({
1775
1781
  name: tmpA[0].trim(),
1776
- version: versionStr
1782
+ version: versionStr,
1783
+ scope: compScope
1777
1784
  });
1778
1785
  }
1779
1786
  } else if (/[>|[|@]/.test(l)) {
@@ -1784,7 +1791,8 @@ const parseReqFile = async function (reqData) {
1784
1791
  if (!tmpA[0].trim().includes(" ")) {
1785
1792
  pkgList.push({
1786
1793
  name: tmpA[0].trim(),
1787
- version: null
1794
+ version: null,
1795
+ scope: compScope
1788
1796
  });
1789
1797
  }
1790
1798
  } else if (l) {
@@ -1795,7 +1803,8 @@ const parseReqFile = async function (reqData) {
1795
1803
  if (!l.includes(" ")) {
1796
1804
  pkgList.push({
1797
1805
  name: l,
1798
- version: null
1806
+ version: null,
1807
+ scope: compScope
1799
1808
  });
1800
1809
  }
1801
1810
  }
@@ -2799,10 +2808,11 @@ const recurseImageNameLookup = (keyValueObj, pkgList, imgList) => {
2799
2808
  typeof imageLike === "string" &&
2800
2809
  !imgList.includes(imageLike)
2801
2810
  ) {
2802
- if (imageLike.includes(":${VERSION:")) {
2811
+ if (imageLike.includes("VERSION")) {
2803
2812
  imageLike = imageLike
2804
2813
  .replace(":${VERSION:-", ":")
2805
2814
  .replace(":${VERSION:", ":")
2815
+ .replace(":%VERSION%", ":latest")
2806
2816
  .replace("}", "");
2807
2817
  }
2808
2818
  pkgList.push({ image: imageLike });
package/utils.test.js CHANGED
@@ -1348,7 +1348,7 @@ test("parseGemspecData", async () => {
1348
1348
  });
1349
1349
  });
1350
1350
 
1351
- test("parse requirements.txt with comments", async () => {
1351
+ test("parse requirements.txt", async () => {
1352
1352
  jest.setTimeout(120000);
1353
1353
  let deps = await utils.parseReqFile(
1354
1354
  fs.readFileSync(
@@ -1357,6 +1357,15 @@ test("parse requirements.txt with comments", async () => {
1357
1357
  )
1358
1358
  );
1359
1359
  expect(deps.length).toEqual(31);
1360
+ deps = await utils.parseReqFile(
1361
+ fs.readFileSync("./test/data/requirements.freeze.txt", (encoding = "utf-8"))
1362
+ );
1363
+ expect(deps.length).toEqual(113);
1364
+ expect(deps[0]).toEqual({
1365
+ name: "elasticsearch",
1366
+ version: "8.6.2",
1367
+ scope: "required"
1368
+ });
1360
1369
  });
1361
1370
 
1362
1371
  test("parse poetry.lock", async () => {