@cyclonedx/cdxgen 9.8.8 → 9.8.10
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 +14 -1
- package/bin/cdxgen.js +57 -25
- package/bin/evinse.js +24 -0
- package/index.js +3 -6
- package/package.json +7 -6
- package/postgen.js +92 -0
- package/postgen.test.js +70 -0
- package/server.js +18 -6
- package/utils.js +34 -19
- package/utils.test.js +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,7 @@ import { createBom, submitBom } from "npm:@cyclonedx/cdxgen@^9.0.1";
|
|
|
126
126
|
|
|
127
127
|
```text
|
|
128
128
|
$ cdxgen -h
|
|
129
|
+
Options:
|
|
129
130
|
-o, --output Output file for bom.xml or bom.json. Default bom.
|
|
130
131
|
json
|
|
131
132
|
-t, --type Project type
|
|
@@ -149,7 +150,9 @@ $ cdxgen -h
|
|
|
149
150
|
d or the project name and version together
|
|
150
151
|
--parent-project-id Dependency track parent project id
|
|
151
152
|
--required-only Include only the packages with required scope on
|
|
152
|
-
the SBOM.
|
|
153
|
+
the SBOM. Would set compositions.aggregate to inc
|
|
154
|
+
omplete unless --no-auto-compositions is passed.
|
|
155
|
+
[boolean]
|
|
153
156
|
--fail-on-error Fail if any dependency extractor fails. [boolean]
|
|
154
157
|
--no-babel Do not use babel to perform usage analysis for Ja
|
|
155
158
|
vaScript/TypeScript projects. [boolean]
|
|
@@ -166,11 +169,21 @@ $ cdxgen -h
|
|
|
166
169
|
--validate Validate the generated SBOM using json schema. De
|
|
167
170
|
faults to true. Pass --no-validate to disable.
|
|
168
171
|
[boolean] [default: true]
|
|
172
|
+
--evidence Generate SBOM with evidence for supported languag
|
|
173
|
+
es. WIP [boolean] [default: false]
|
|
169
174
|
--usages-slices-file Path for the usages slice file created by atom.
|
|
170
175
|
--data-flow-slices-file Path for the data-flow slice file created by atom
|
|
171
176
|
.
|
|
172
177
|
--spec-version CycloneDX Specification version to use. Defaults
|
|
173
178
|
to 1.5 [default: 1.5]
|
|
179
|
+
--filter Filter components containining this word in purl.
|
|
180
|
+
Multiple values allowed. [array]
|
|
181
|
+
--only Include components only containining this word in
|
|
182
|
+
purl. Useful to generate BOM with first party co
|
|
183
|
+
mponents alone. Multiple values allowed. [array]
|
|
184
|
+
--auto-compositions Automatically set compositions when the BOM was f
|
|
185
|
+
iltered. Defaults to true
|
|
186
|
+
[boolean] [default: true]
|
|
174
187
|
-h, --help Show help [boolean]
|
|
175
188
|
-v, --version Show version number [boolean]
|
|
176
189
|
```
|
package/bin/cdxgen.js
CHANGED
|
@@ -11,6 +11,29 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import globalAgent from "global-agent";
|
|
12
12
|
import process from "node:process";
|
|
13
13
|
import { printTable, printDependencyTree } from "../display.js";
|
|
14
|
+
import { findUpSync } from "find-up";
|
|
15
|
+
import { load as _load } from "js-yaml";
|
|
16
|
+
import { postProcess } from "../postgen.js";
|
|
17
|
+
|
|
18
|
+
// Support for config files
|
|
19
|
+
const configPath = findUpSync([
|
|
20
|
+
".cdxgenrc",
|
|
21
|
+
".cdxgen.json",
|
|
22
|
+
".cdxgen.yml",
|
|
23
|
+
".cdxgen.yaml"
|
|
24
|
+
]);
|
|
25
|
+
let config = {};
|
|
26
|
+
if (configPath) {
|
|
27
|
+
try {
|
|
28
|
+
if (configPath.endsWith(".yml") || configPath.endsWith(".yaml")) {
|
|
29
|
+
config = _load(fs.readFileSync(configPath, "utf-8"));
|
|
30
|
+
} else {
|
|
31
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.log("Invalid config file", configPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
14
37
|
|
|
15
38
|
let url = import.meta.url;
|
|
16
39
|
if (!url.startsWith("file://")) {
|
|
@@ -22,6 +45,7 @@ import yargs from "yargs";
|
|
|
22
45
|
import { hideBin } from "yargs/helpers";
|
|
23
46
|
|
|
24
47
|
const args = yargs(hideBin(process.argv))
|
|
48
|
+
.env("CDXGEN")
|
|
25
49
|
.option("output", {
|
|
26
50
|
alias: "o",
|
|
27
51
|
description: "Output file for bom.xml or bom.json. Default bom.json"
|
|
@@ -77,7 +101,8 @@ const args = yargs(hideBin(process.argv))
|
|
|
77
101
|
})
|
|
78
102
|
.option("required-only", {
|
|
79
103
|
type: "boolean",
|
|
80
|
-
description:
|
|
104
|
+
description:
|
|
105
|
+
"Include only the packages with required scope on the SBOM. Would set compositions.aggregate to incomplete unless --no-auto-compositions is passed."
|
|
81
106
|
})
|
|
82
107
|
.option("fail-on-error", {
|
|
83
108
|
type: "boolean",
|
|
@@ -132,6 +157,28 @@ const args = yargs(hideBin(process.argv))
|
|
|
132
157
|
description: "CycloneDX Specification version to use. Defaults to 1.5",
|
|
133
158
|
default: 1.5
|
|
134
159
|
})
|
|
160
|
+
.option("filter", {
|
|
161
|
+
description:
|
|
162
|
+
"Filter components containining this word in purl. Multiple values allowed."
|
|
163
|
+
})
|
|
164
|
+
.option("only", {
|
|
165
|
+
description:
|
|
166
|
+
"Include components only containining this word in purl. Useful to generate BOM with first party components alone. Multiple values allowed."
|
|
167
|
+
})
|
|
168
|
+
.array("filter")
|
|
169
|
+
.array("only")
|
|
170
|
+
.option("auto-compositions", {
|
|
171
|
+
type: "boolean",
|
|
172
|
+
default: true,
|
|
173
|
+
description:
|
|
174
|
+
"Automatically set compositions when the BOM was filtered. Defaults to true"
|
|
175
|
+
})
|
|
176
|
+
.example([
|
|
177
|
+
["$0 -t java .", "Generate a Java SBOM for the current directory"],
|
|
178
|
+
["$0 --server", "Run cdxgen as a server"]
|
|
179
|
+
])
|
|
180
|
+
.epilogue("for documentation, visit https://cyclonedx.github.io/cdxgen")
|
|
181
|
+
.config(config)
|
|
135
182
|
.scriptName("cdxgen")
|
|
136
183
|
.version()
|
|
137
184
|
.alias("v", "version")
|
|
@@ -177,32 +224,14 @@ if (process.argv[1].includes("obom") && !args.type) {
|
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
/**
|
|
180
|
-
*
|
|
181
|
-
* multiProject: Boolean to indicate monorepo or multi-module projects
|
|
227
|
+
* Command line options
|
|
182
228
|
*/
|
|
183
|
-
const options = {
|
|
229
|
+
const options = Object.assign({}, args, {
|
|
184
230
|
projectType: args.type,
|
|
185
231
|
multiProject: args.recurse,
|
|
186
|
-
output: args.output,
|
|
187
|
-
resolveClass: args.resolveClass,
|
|
188
|
-
installDeps: args.installDeps,
|
|
189
|
-
requiredOnly: args.requiredOnly,
|
|
190
|
-
failOnError: args.failOnError,
|
|
191
232
|
noBabel: args.noBabel || args.babel === false,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
project: args.projectId,
|
|
195
|
-
projectName: args.projectName,
|
|
196
|
-
projectGroup: args.projectGroup,
|
|
197
|
-
projectVersion: args.projectVersion,
|
|
198
|
-
server: args.server,
|
|
199
|
-
serverHost: args.serverHost,
|
|
200
|
-
serverPort: args.serverPort,
|
|
201
|
-
specVersion: args.specVersion,
|
|
202
|
-
evidence: args.evidence,
|
|
203
|
-
usagesSlicesFile: args.usagesSlicesFile,
|
|
204
|
-
dataFlowSlicesFile: args.dataFlowSlicesFile
|
|
205
|
-
};
|
|
233
|
+
project: args.projectId
|
|
234
|
+
});
|
|
206
235
|
|
|
207
236
|
/**
|
|
208
237
|
* Check for node >= 20 permissions
|
|
@@ -243,7 +272,7 @@ const checkPermissions = (filePath) => {
|
|
|
243
272
|
// Start SBOM server
|
|
244
273
|
if (args.server) {
|
|
245
274
|
const serverModule = await import("../server.js");
|
|
246
|
-
return
|
|
275
|
+
return serverModule.start(options);
|
|
247
276
|
}
|
|
248
277
|
// Check if cdxgen has the required permissions
|
|
249
278
|
if (!checkPermissions(filePath)) {
|
|
@@ -253,7 +282,10 @@ const checkPermissions = (filePath) => {
|
|
|
253
282
|
if (!options.usagesSlicesFile) {
|
|
254
283
|
options.usagesSlicesFile = `${options.projectName}-usages.json`;
|
|
255
284
|
}
|
|
256
|
-
|
|
285
|
+
let bomNSData = (await createBom(filePath, options)) || {};
|
|
286
|
+
if (options.requiredOnly || options["filter"] || options["only"]) {
|
|
287
|
+
bomNSData = postProcess(bomNSData, options);
|
|
288
|
+
}
|
|
257
289
|
if (!args.output) {
|
|
258
290
|
args.output = "bom.json";
|
|
259
291
|
}
|
package/bin/evinse.js
CHANGED
|
@@ -10,6 +10,28 @@ import process from "node:process";
|
|
|
10
10
|
import { analyzeProject, createEvinseFile, prepareDB } from "../evinser.js";
|
|
11
11
|
import { validateBom } from "../validator.js";
|
|
12
12
|
import { printCallStack, printOccurrences, printServices } from "../display.js";
|
|
13
|
+
import { findUpSync } from "find-up";
|
|
14
|
+
import { load as _load } from "js-yaml";
|
|
15
|
+
|
|
16
|
+
// Support for config files
|
|
17
|
+
const configPath = findUpSync([
|
|
18
|
+
".cdxgenrc",
|
|
19
|
+
".cdxgen.json",
|
|
20
|
+
".cdxgen.yml",
|
|
21
|
+
".cdxgen.yaml"
|
|
22
|
+
]);
|
|
23
|
+
let config = {};
|
|
24
|
+
if (configPath) {
|
|
25
|
+
try {
|
|
26
|
+
if (configPath.endsWith(".yml") || configPath.endsWith(".yaml")) {
|
|
27
|
+
config = _load(fs.readFileSync(configPath, "utf-8"));
|
|
28
|
+
} else {
|
|
29
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.log("Invalid config file", configPath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
13
35
|
|
|
14
36
|
const isWin = _platform() === "win32";
|
|
15
37
|
const isMac = _platform() === "darwin";
|
|
@@ -28,6 +50,7 @@ if (!process.env.ATOM_DB && !fs.existsSync(ATOM_DB)) {
|
|
|
28
50
|
}
|
|
29
51
|
}
|
|
30
52
|
const args = yargs(hideBin(process.argv))
|
|
53
|
+
.env("EVINSE")
|
|
31
54
|
.option("input", {
|
|
32
55
|
alias: "i",
|
|
33
56
|
description: "Input SBOM file. Default bom.json",
|
|
@@ -88,6 +111,7 @@ const args = yargs(hideBin(process.argv))
|
|
|
88
111
|
type: "boolean",
|
|
89
112
|
description: "Print the evidences as table"
|
|
90
113
|
})
|
|
114
|
+
.config(config)
|
|
91
115
|
.scriptName("evinse")
|
|
92
116
|
.version()
|
|
93
117
|
.help("h").argv;
|
package/index.js
CHANGED
|
@@ -707,9 +707,6 @@ function addComponent(
|
|
|
707
707
|
compScope = "optional";
|
|
708
708
|
}
|
|
709
709
|
}
|
|
710
|
-
if (options.requiredOnly && ["optional", "excluded"].includes(compScope)) {
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
710
|
const component = {
|
|
714
711
|
author,
|
|
715
712
|
publisher,
|
|
@@ -1016,7 +1013,7 @@ const buildBomNSData = (options, pkgInfo, ptype, context) => {
|
|
|
1016
1013
|
allImports = context.allImports;
|
|
1017
1014
|
}
|
|
1018
1015
|
const nsMapping = context.nsMapping || {};
|
|
1019
|
-
const dependencies =
|
|
1016
|
+
const dependencies = context.dependencies || [];
|
|
1020
1017
|
const parentComponent =
|
|
1021
1018
|
determineParentComponent(options) || context.parentComponent;
|
|
1022
1019
|
const metadata = addMetadata(parentComponent, "json", options);
|
|
@@ -1330,7 +1327,7 @@ export const createJavaBom = async (path, options) => {
|
|
|
1330
1327
|
if (bomJsonObj.components) {
|
|
1331
1328
|
pkgList = pkgList.concat(bomJsonObj.components);
|
|
1332
1329
|
}
|
|
1333
|
-
if (bomJsonObj.dependencies
|
|
1330
|
+
if (bomJsonObj.dependencies) {
|
|
1334
1331
|
dependencies = mergeDependencies(
|
|
1335
1332
|
dependencies,
|
|
1336
1333
|
bomJsonObj.dependencies,
|
|
@@ -2697,7 +2694,7 @@ export const createGoBom = async (path, options) => {
|
|
|
2697
2694
|
const dlist = await parseGosumData(gosumData);
|
|
2698
2695
|
if (dlist && dlist.length) {
|
|
2699
2696
|
dlist.forEach((pkg) => {
|
|
2700
|
-
gosumMap[`${pkg.
|
|
2697
|
+
gosumMap[`${pkg.name}@${pkg.version}`] = pkg._integrity;
|
|
2701
2698
|
});
|
|
2702
2699
|
}
|
|
2703
2700
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyclonedx/cdxgen",
|
|
3
|
-
"version": "9.8.
|
|
3
|
+
"version": "9.8.10",
|
|
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>",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"docs": "docsify serve docs",
|
|
42
|
-
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --inject-globals false docker.test.js utils.test.js display.test.js",
|
|
42
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --inject-globals false docker.test.js utils.test.js display.test.js postgen.test.js",
|
|
43
43
|
"watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --inject-globals false",
|
|
44
44
|
"lint": "eslint *.js *.test.js bin/*.js",
|
|
45
|
-
"pretty": "prettier --write *.js data/*.json bin/*.js"
|
|
45
|
+
"pretty": "prettier --write *.js data/*.json bin/*.js *.md docs/*.md"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=16"
|
|
@@ -57,11 +57,12 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@babel/parser": "^7.23.0",
|
|
59
59
|
"@babel/traverse": "^7.23.0",
|
|
60
|
-
"@npmcli/arborist": "^7.
|
|
60
|
+
"@npmcli/arborist": "^7.2.0",
|
|
61
61
|
"ajv": "^8.12.0",
|
|
62
62
|
"ajv-formats": "^2.1.1",
|
|
63
63
|
"cheerio": "^1.0.0-rc.12",
|
|
64
64
|
"edn-data": "^1.0.0",
|
|
65
|
+
"find-up": "^6.3.0",
|
|
65
66
|
"glob": "^10.3.10",
|
|
66
67
|
"global-agent": "^3.0.0",
|
|
67
68
|
"got": "^13.0.0",
|
|
@@ -101,9 +102,9 @@
|
|
|
101
102
|
"devDependencies": {
|
|
102
103
|
"caxa": "^3.0.1",
|
|
103
104
|
"docsify-cli": "^4.4.4",
|
|
104
|
-
"eslint": "^8.
|
|
105
|
+
"eslint": "^8.51.0",
|
|
105
106
|
"eslint-config-prettier": "^9.0.0",
|
|
106
|
-
"eslint-plugin-prettier": "^5.0.
|
|
107
|
+
"eslint-plugin-prettier": "^5.0.1",
|
|
107
108
|
"jest": "^29.7.0",
|
|
108
109
|
"prettier": "3.0.3"
|
|
109
110
|
}
|
package/postgen.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const postProcess = (bomNSData, options) => {
|
|
2
|
+
let jsonPayload = bomNSData.bomJson;
|
|
3
|
+
if (
|
|
4
|
+
typeof bomNSData.bomJson === "string" ||
|
|
5
|
+
bomNSData.bomJson instanceof String
|
|
6
|
+
) {
|
|
7
|
+
jsonPayload = JSON.parse(bomNSData.bomJson);
|
|
8
|
+
}
|
|
9
|
+
bomNSData.bomJson = filterBom(jsonPayload, options);
|
|
10
|
+
return bomNSData;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const filterBom = (bomJson, options) => {
|
|
14
|
+
const newPkgMap = {};
|
|
15
|
+
let filtered = false;
|
|
16
|
+
for (const comp of bomJson.components) {
|
|
17
|
+
if (
|
|
18
|
+
options.requiredOnly &&
|
|
19
|
+
comp.scope &&
|
|
20
|
+
["optional", "excluded"].includes(comp.scope)
|
|
21
|
+
) {
|
|
22
|
+
filtered = true;
|
|
23
|
+
continue;
|
|
24
|
+
} else if (options.only && options.only.length) {
|
|
25
|
+
if (!Array.isArray(options.only)) {
|
|
26
|
+
options.only = [options.only];
|
|
27
|
+
}
|
|
28
|
+
let purlfiltered = false;
|
|
29
|
+
for (const filterstr of options.only) {
|
|
30
|
+
if (filterstr.length && !comp.purl.toLowerCase().includes(filterstr)) {
|
|
31
|
+
filtered = true;
|
|
32
|
+
purlfiltered = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!purlfiltered) {
|
|
37
|
+
newPkgMap[comp["bom-ref"]] = comp;
|
|
38
|
+
}
|
|
39
|
+
} else if (options.filter && options.filter.length) {
|
|
40
|
+
if (!Array.isArray(options.filter)) {
|
|
41
|
+
options.filter = [options.filter];
|
|
42
|
+
}
|
|
43
|
+
let purlfiltered = false;
|
|
44
|
+
for (const filterstr of options.filter) {
|
|
45
|
+
if (filterstr.length && comp.purl.toLowerCase().includes(filterstr)) {
|
|
46
|
+
filtered = true;
|
|
47
|
+
purlfiltered = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!purlfiltered) {
|
|
52
|
+
newPkgMap[comp["bom-ref"]] = comp;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
newPkgMap[comp["bom-ref"]] = comp;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (filtered) {
|
|
59
|
+
const newcomponents = [];
|
|
60
|
+
const newdependencies = [];
|
|
61
|
+
for (const aref of Object.keys(newPkgMap).sort()) {
|
|
62
|
+
newcomponents.push(newPkgMap[aref]);
|
|
63
|
+
}
|
|
64
|
+
for (const adep of bomJson.dependencies) {
|
|
65
|
+
if (newPkgMap[adep.ref]) {
|
|
66
|
+
const newdepson = (adep.dependsOn || []).filter((d) => newPkgMap[d]);
|
|
67
|
+
newdependencies.push({
|
|
68
|
+
ref: adep.ref,
|
|
69
|
+
dependsOn: newdepson
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
bomJson.components = newcomponents;
|
|
74
|
+
bomJson.dependencies = newdependencies;
|
|
75
|
+
// We set the compositions.aggregate to incomplete by default
|
|
76
|
+
if (
|
|
77
|
+
options.specVersion >= 1.5 &&
|
|
78
|
+
options.autoCompositions &&
|
|
79
|
+
bomJson.metadata &&
|
|
80
|
+
bomJson.metadata.component
|
|
81
|
+
) {
|
|
82
|
+
if (!bomJson.compositions) {
|
|
83
|
+
bomJson.compositions = [];
|
|
84
|
+
}
|
|
85
|
+
bomJson.compositions.push({
|
|
86
|
+
"bom-ref": bomJson.metadata.component["bom-ref"],
|
|
87
|
+
aggregate: options.only ? "incomplete_first_party_only" : "incomplete"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return bomJson;
|
|
92
|
+
};
|
package/postgen.test.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { filterBom } from "./postgen.js";
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { expect, test } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
test("filter bom tests", () => {
|
|
7
|
+
const bomJson = JSON.parse(
|
|
8
|
+
readFileSync("./test/data/bom-postgen-test.json", "utf-8")
|
|
9
|
+
);
|
|
10
|
+
let newBom = filterBom(bomJson, {});
|
|
11
|
+
expect(bomJson).toEqual(newBom);
|
|
12
|
+
expect(newBom.components.length).toEqual(1060);
|
|
13
|
+
newBom = filterBom(bomJson, { requiredOnly: true });
|
|
14
|
+
for (const comp of newBom.components) {
|
|
15
|
+
if (comp.scope && comp.scope !== "required") {
|
|
16
|
+
throw new Error(`${comp.scope} is unexpected`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
expect(newBom.components.length).toEqual(345);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("filter bom tests2", () => {
|
|
23
|
+
const bomJson = JSON.parse(
|
|
24
|
+
readFileSync("./test/data/bom-postgen-test2.json", "utf-8")
|
|
25
|
+
);
|
|
26
|
+
let newBom = filterBom(bomJson, {});
|
|
27
|
+
expect(bomJson).toEqual(newBom);
|
|
28
|
+
expect(newBom.components.length).toEqual(199);
|
|
29
|
+
newBom = filterBom(bomJson, { requiredOnly: true });
|
|
30
|
+
for (const comp of newBom.components) {
|
|
31
|
+
if (comp.scope && comp.scope !== "required") {
|
|
32
|
+
throw new Error(`${comp.scope} is unexpected`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
expect(newBom.components.length).toEqual(199);
|
|
36
|
+
newBom = filterBom(bomJson, { filter: [""] });
|
|
37
|
+
expect(newBom.components.length).toEqual(199);
|
|
38
|
+
newBom = filterBom(bomJson, { filter: ["apache"] });
|
|
39
|
+
for (const comp of newBom.components) {
|
|
40
|
+
if (comp.purl.includes("apache")) {
|
|
41
|
+
throw new Error(`${comp.purl} is unexpected`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
expect(newBom.components.length).toEqual(177);
|
|
45
|
+
newBom = filterBom(bomJson, { filter: ["apache", "json"] });
|
|
46
|
+
for (const comp of newBom.components) {
|
|
47
|
+
if (comp.purl.includes("apache") || comp.purl.includes("json")) {
|
|
48
|
+
throw new Error(`${comp.purl} is unexpected`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
expect(newBom.components.length).toEqual(172);
|
|
52
|
+
expect(newBom.compositions).toBeUndefined();
|
|
53
|
+
newBom = filterBom(bomJson, {
|
|
54
|
+
only: ["org.springframework"],
|
|
55
|
+
specVersion: 1.5,
|
|
56
|
+
autoCompositions: true
|
|
57
|
+
});
|
|
58
|
+
for (const comp of newBom.components) {
|
|
59
|
+
if (!comp.purl.includes("org.springframework")) {
|
|
60
|
+
throw new Error(`${comp.purl} is unexpected`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
expect(newBom.components.length).toEqual(37);
|
|
64
|
+
expect(newBom.compositions).toEqual([
|
|
65
|
+
{
|
|
66
|
+
aggregate: "incomplete_first_party_only",
|
|
67
|
+
"bom-ref": "pkg:maven/sec/java-sec-code@1.0.0?type=jar"
|
|
68
|
+
}
|
|
69
|
+
]);
|
|
70
|
+
});
|
package/server.js
CHANGED
|
@@ -7,6 +7,8 @@ import os from "node:os";
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { createBom, submitBom } from "./index.js";
|
|
10
|
+
import { postProcess } from "./postgen.js";
|
|
11
|
+
|
|
10
12
|
import compression from "compression";
|
|
11
13
|
|
|
12
14
|
// Timeout milliseconds. Default 10 mins
|
|
@@ -60,7 +62,10 @@ const parseQueryString = (q, body, options = {}) => {
|
|
|
60
62
|
"parentUUID",
|
|
61
63
|
"serverUrl",
|
|
62
64
|
"apiKey",
|
|
63
|
-
"specVersion"
|
|
65
|
+
"specVersion",
|
|
66
|
+
"filter",
|
|
67
|
+
"only",
|
|
68
|
+
"autoCompositions"
|
|
64
69
|
];
|
|
65
70
|
|
|
66
71
|
for (const param of queryParams) {
|
|
@@ -94,7 +99,7 @@ const start = (options) => {
|
|
|
94
99
|
.listen(options.serverPort, options.serverHost);
|
|
95
100
|
configureServer(cdxgenServer);
|
|
96
101
|
|
|
97
|
-
app.use("/health", async function (
|
|
102
|
+
app.use("/health", async function (_req, res) {
|
|
98
103
|
res.setHeader("Content-Type", "application/json");
|
|
99
104
|
res.end(JSON.stringify({ status: "OK" }, null, 2));
|
|
100
105
|
});
|
|
@@ -102,7 +107,11 @@ const start = (options) => {
|
|
|
102
107
|
app.use("/sbom", async function (req, res) {
|
|
103
108
|
const q = url.parse(req.url, true).query;
|
|
104
109
|
let cleanup = false;
|
|
105
|
-
|
|
110
|
+
const reqOptions = parseQueryString(
|
|
111
|
+
q,
|
|
112
|
+
req.body,
|
|
113
|
+
Object.assign({}, options)
|
|
114
|
+
);
|
|
106
115
|
const filePath = q.path || q.url || req.body.path || req.body.url;
|
|
107
116
|
if (!filePath) {
|
|
108
117
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -117,7 +126,10 @@ const start = (options) => {
|
|
|
117
126
|
cleanup = true;
|
|
118
127
|
}
|
|
119
128
|
console.log("Generating SBOM for", srcDir);
|
|
120
|
-
|
|
129
|
+
let bomNSData = (await createBom(srcDir, reqOptions)) || {};
|
|
130
|
+
if (reqOptions.requiredOnly || reqOptions["filter"] || reqOptions["only"]) {
|
|
131
|
+
bomNSData = postProcess(bomNSData, reqOptions);
|
|
132
|
+
}
|
|
121
133
|
if (bomNSData.bomJson) {
|
|
122
134
|
if (
|
|
123
135
|
typeof bomNSData.bomJson === "string" ||
|
|
@@ -128,9 +140,9 @@ const start = (options) => {
|
|
|
128
140
|
res.write(JSON.stringify(bomNSData.bomJson, null, 2));
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
|
-
if (
|
|
143
|
+
if (reqOptions.serverUrl && reqOptions.apiKey) {
|
|
132
144
|
console.log("Publishing SBOM to Dependency Track");
|
|
133
|
-
submitBom(
|
|
145
|
+
submitBom(reqOptions, bomNSData.bomJson);
|
|
134
146
|
}
|
|
135
147
|
res.end("\n");
|
|
136
148
|
if (cleanup && srcDir && srcDir.startsWith(os.tmpdir()) && fs.rmSync) {
|
package/utils.js
CHANGED
|
@@ -4634,7 +4634,7 @@ export const parseCsPkgData = async function (pkgData) {
|
|
|
4634
4634
|
attributesKey: "$",
|
|
4635
4635
|
commentKey: "value"
|
|
4636
4636
|
}).packages;
|
|
4637
|
-
if (packages.length == 0) {
|
|
4637
|
+
if (!packages || packages.length == 0) {
|
|
4638
4638
|
return pkgList;
|
|
4639
4639
|
}
|
|
4640
4640
|
packages = packages[0].package;
|
|
@@ -4661,7 +4661,7 @@ export const parseCsProjData = async function (csProjData) {
|
|
|
4661
4661
|
attributesKey: "$",
|
|
4662
4662
|
commentKey: "value"
|
|
4663
4663
|
}).Project;
|
|
4664
|
-
if (projects.length == 0) {
|
|
4664
|
+
if (!projects || projects.length == 0) {
|
|
4665
4665
|
return pkgList;
|
|
4666
4666
|
}
|
|
4667
4667
|
const project = projects[0];
|
|
@@ -4719,6 +4719,9 @@ export const parseCsProjAssetsData = async function (csProjData) {
|
|
|
4719
4719
|
const pkgList = [];
|
|
4720
4720
|
let dependenciesList = [];
|
|
4721
4721
|
let rootPkg = {};
|
|
4722
|
+
// This tracks the resolved version
|
|
4723
|
+
const pkgNameVersionMap = {};
|
|
4724
|
+
const pkgAddedMap = {};
|
|
4722
4725
|
|
|
4723
4726
|
if (!csProjData) {
|
|
4724
4727
|
return { pkgList, dependenciesList };
|
|
@@ -4784,12 +4787,12 @@ export const parseCsProjAssetsData = async function (csProjData) {
|
|
|
4784
4787
|
|
|
4785
4788
|
if (csProjData.libraries && csProjData.targets) {
|
|
4786
4789
|
const lib = csProjData.libraries;
|
|
4790
|
+
// Pass 1: Construct pkgList alone and track name and resolved version
|
|
4787
4791
|
for (const framework in csProjData.targets) {
|
|
4788
4792
|
for (const rootDep of Object.keys(csProjData.targets[framework])) {
|
|
4789
4793
|
// if (rootDep.startsWith("runtime")){
|
|
4790
4794
|
// continue;
|
|
4791
4795
|
// }
|
|
4792
|
-
const depList = new Set();
|
|
4793
4796
|
const [name, version] = rootDep.split("/");
|
|
4794
4797
|
const dpurl = decodeURIComponent(
|
|
4795
4798
|
new PackageURL("nuget", "", name, version, null, null).toString()
|
|
@@ -4810,29 +4813,41 @@ export const parseCsProjAssetsData = async function (csProjData) {
|
|
|
4810
4813
|
}
|
|
4811
4814
|
}
|
|
4812
4815
|
pkgList.push(pkg);
|
|
4813
|
-
|
|
4816
|
+
pkgNameVersionMap[name] = version;
|
|
4817
|
+
pkgAddedMap[name] = true;
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
// Pass 2: Fix the dependency tree
|
|
4821
|
+
for (const framework in csProjData.targets) {
|
|
4822
|
+
for (const rootDep of Object.keys(csProjData.targets[framework])) {
|
|
4823
|
+
const depList = new Set();
|
|
4824
|
+
const [name, version] = rootDep.split("/");
|
|
4825
|
+
const dpurl = decodeURIComponent(
|
|
4826
|
+
new PackageURL("nuget", "", name, version, null, null).toString()
|
|
4827
|
+
);
|
|
4814
4828
|
const dependencies =
|
|
4815
4829
|
csProjData.targets[framework][rootDep].dependencies;
|
|
4816
4830
|
if (dependencies) {
|
|
4817
4831
|
for (const p of Object.keys(dependencies)) {
|
|
4832
|
+
// This condition is not required for assets json that are well-formed.
|
|
4833
|
+
if (!pkgNameVersionMap[p]) {
|
|
4834
|
+
continue;
|
|
4835
|
+
}
|
|
4836
|
+
let dversion = pkgNameVersionMap[p];
|
|
4818
4837
|
const ipurl = decodeURIComponent(
|
|
4819
|
-
new PackageURL(
|
|
4820
|
-
"nuget",
|
|
4821
|
-
"",
|
|
4822
|
-
p,
|
|
4823
|
-
dependencies[p],
|
|
4824
|
-
null,
|
|
4825
|
-
null
|
|
4826
|
-
).toString()
|
|
4838
|
+
new PackageURL("nuget", "", p, dversion, null, null).toString()
|
|
4827
4839
|
);
|
|
4828
4840
|
depList.add(ipurl);
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4841
|
+
if (!pkgAddedMap[p]) {
|
|
4842
|
+
pkgList.push({
|
|
4843
|
+
group: "",
|
|
4844
|
+
name: p,
|
|
4845
|
+
version: dversion,
|
|
4846
|
+
description: "",
|
|
4847
|
+
"bom-ref": ipurl
|
|
4848
|
+
});
|
|
4849
|
+
pkgAddedMap[p] = true;
|
|
4850
|
+
}
|
|
4836
4851
|
}
|
|
4837
4852
|
}
|
|
4838
4853
|
dependenciesList.push({
|
package/utils.test.js
CHANGED
|
@@ -1253,7 +1253,7 @@ test("parse project.assets.json", async () => {
|
|
|
1253
1253
|
const dep_list = await parseCsProjAssetsData(
|
|
1254
1254
|
readFileSync("./test/data/project.assets.json", { encoding: "utf-8" })
|
|
1255
1255
|
);
|
|
1256
|
-
expect(dep_list["pkgList"].length).toEqual(
|
|
1256
|
+
expect(dep_list["pkgList"].length).toEqual(302);
|
|
1257
1257
|
expect(dep_list["pkgList"][0]).toEqual({
|
|
1258
1258
|
"bom-ref": "pkg:nuget/Castle.Core.Tests@0.0.0",
|
|
1259
1259
|
group: "",
|