@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 +27 -25
- package/analyzer.js +11 -2
- package/bin/cdxgen +9 -4
- package/docker.js +1 -1
- package/index.js +52 -11
- package/package.json +1 -1
- package/utils.js +14 -4
- package/utils.test.js +10 -1
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] -
|
|
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
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
--
|
|
98
|
-
|
|
99
|
-
--
|
|
100
|
-
|
|
101
|
-
--
|
|
102
|
-
|
|
103
|
-
--project-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
--
|
|
107
|
-
|
|
108
|
-
--
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
118
|
-
-h
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
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}
|
|
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:
|
|
4316
|
+
projectVersion: projectVersion,
|
|
4282
4317
|
autoCreate: "true",
|
|
4283
4318
|
bom: encodedBomContents
|
|
4284
4319
|
};
|
|
4285
4320
|
if (DEBUG_MODE) {
|
|
4286
|
-
console.log(
|
|
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.
|
|
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("
|
|
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
|
|
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 () => {
|