@cyclonedx/cdxgen 9.3.2 → 9.4.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/README.md +79 -2
- package/bin/cdxgen.js +22 -1
- package/bin/repl.js +311 -0
- package/display.js +3 -0
- package/docker.js +1 -1
- package/docker.test.js +8 -5
- package/index.js +66 -34
- package/package.json +13 -10
- package/piptree.js +2 -2
- package/utils.js +116 -46
- package/utils.test.js +24 -5
package/README.md
CHANGED
|
@@ -10,6 +10,12 @@ NOTE:
|
|
|
10
10
|
|
|
11
11
|
CycloneDX 1.5 specification is brand new and unsupported by many downstream tools. Use version 8.6.0 for 1.4 compatibility or pass the argument `--spec-version 1.4`.
|
|
12
12
|
|
|
13
|
+
## Why cdxgen?
|
|
14
|
+
|
|
15
|
+
A typical application might comprise of several repos, components, and libraries linked together. Traditional techniques to generate a single SBoM per language or package manifest do not work in enterprise environments. So we built cdxgen - the universal polyglot SBoM generator!
|
|
16
|
+
|
|
17
|
+
<img src="./docs/why-cdxgen.jpg" alt="why cdxgen" width="256">
|
|
18
|
+
|
|
13
19
|
## Supported languages and package format
|
|
14
20
|
|
|
15
21
|
| Language/Platform | Package format | Transitive dependencies |
|
|
@@ -61,7 +67,7 @@ Footnotes:
|
|
|
61
67
|
- [4] - See section on plugins
|
|
62
68
|
- [5] - Powered by osquery. See section on plugins
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
<img src="./docs/cdxgen-tree.jpg" alt="cdxgen tree" width="256">
|
|
65
71
|
|
|
66
72
|
### Automatic usage detection
|
|
67
73
|
|
|
@@ -375,10 +381,33 @@ cdxgen can sign the generated SBoM json file to increase authenticity and non-re
|
|
|
375
381
|
- SBOM_SIGN_PRIVATE_KEY: Location to the RSA private key
|
|
376
382
|
- SBOM_SIGN_PUBLIC_KEY: Optional. Location to the RSA public key
|
|
377
383
|
|
|
378
|
-
To generate test public/private key pairs, you can run cdxgen by passing the argument `--generate-key-and-sign`. The generated json file would have an attribute called `signature` which could be used for validation. [jwt.io](jwt.io) is a known site that could be used for such signature validation.
|
|
384
|
+
To generate test public/private key pairs, you can run cdxgen by passing the argument `--generate-key-and-sign`. The generated json file would have an attribute called `signature` which could be used for validation. [jwt.io](https://jwt.io) is a known site that could be used for such signature validation.
|
|
379
385
|
|
|
380
386
|

|
|
381
387
|
|
|
388
|
+
### Verifying the signature (Node.js example)
|
|
389
|
+
|
|
390
|
+
There are many [libraries](https://jwt.io/#libraries-io) available to validate JSON Web Tokens. Below is a javascript example.
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
# npm install jws
|
|
394
|
+
const jws = require("jws");
|
|
395
|
+
const fs = require("fs");
|
|
396
|
+
// Location of the SBoM json file
|
|
397
|
+
const bomJsonFile = "bom.json";
|
|
398
|
+
// Location of the public key
|
|
399
|
+
const publicKeyFile = "public.key";
|
|
400
|
+
const bomJson = JSON.parse(fs.readFileSync(bomJsonFile, "utf8"));
|
|
401
|
+
// Retrieve the signature
|
|
402
|
+
const bomSignature = bomJson.signature.value;
|
|
403
|
+
const validationResult = jws.verify(bomSignature, bomJson.signature.algorithm, fs.readFileSync(publicKeyFile, "utf8"));
|
|
404
|
+
if (validationResult) {
|
|
405
|
+
console.log("Signature is valid!");
|
|
406
|
+
} else {
|
|
407
|
+
console.log("SBoM signature is invalid :(");
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
382
411
|
## Automatic services detection
|
|
383
412
|
|
|
384
413
|
cdxgen could automatically detect names of services from YAML manifests such as docker-compose or Kubernetes or Skaffold manifests. These would be populated under the `services` attribute in the generated SBoM. Please help improve this feature by filing issues for any inaccurate detection.
|
|
@@ -414,6 +443,50 @@ const bomNSData = await createBom(filePath, options);
|
|
|
414
443
|
const dbody = await submitBom(args, bomNSData.bomJson);
|
|
415
444
|
```
|
|
416
445
|
|
|
446
|
+
## Interactive mode
|
|
447
|
+
|
|
448
|
+
`cdxi` is a new interactive REPL server to interactively create, import and search an SBoM. All the exported functions from cdxgen and node.js could be used in this mode. In addition, several custom commands are defined.
|
|
449
|
+
|
|
450
|
+
### Custom commands
|
|
451
|
+
|
|
452
|
+
| Command | Description |
|
|
453
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
454
|
+
| .create | Create an SBoM from a path |
|
|
455
|
+
| .import | Import an existing SBoM from a path. Any SBoM in CycloneDX format is supported. |
|
|
456
|
+
| .search | Search the given string in the components name, group, purl and description |
|
|
457
|
+
| .sort | Sort the components based on the given attribute. Eg: .sort name to sort by name. Accepts full jsonata [order by](http://docs.jsonata.org/path-operators#order-by-) clause too. Eg: `.sort components^(>name)` |
|
|
458
|
+
| .query | Pass a raw query in [jsonata](http://docs.jsonata.org/) format |
|
|
459
|
+
| .print | Print the SBoM as a table |
|
|
460
|
+
| .tree | Print the dependency tree if available |
|
|
461
|
+
| .validate | Validate the SBoM |
|
|
462
|
+
| .exit | To exit the shell |
|
|
463
|
+
| .save | To save the modified SBoM to a new file |
|
|
464
|
+
| .update | Update components based on query expression. Use syntax `\| query \| new object \|`. See example. |
|
|
465
|
+
|
|
466
|
+
### Sample REPL usage
|
|
467
|
+
|
|
468
|
+
Start the REPL server.
|
|
469
|
+
|
|
470
|
+
```shell
|
|
471
|
+
cdxi
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Below are some example commands to create an SBoM for a spring application and perform searches and queries.
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
.create /mnt/work/vuln-spring
|
|
478
|
+
.print
|
|
479
|
+
.search spring
|
|
480
|
+
.query components[name ~> /spring/ and scope = "required"]
|
|
481
|
+
.sort name
|
|
482
|
+
.sort components^(>name)
|
|
483
|
+
.update | components[name ~> /spring/] | {'publisher': "foo"} |
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### REPL History
|
|
487
|
+
|
|
488
|
+
Repl history will get persisted under `$HOME/.config/.cdxgen` directory. To override this location, use the environment variable `CDXGEN_REPL_HISTORY`.
|
|
489
|
+
|
|
417
490
|
## Node.js >= 20 permission model
|
|
418
491
|
|
|
419
492
|
Refer to the [permissions document](./docs/PERMISSIONS.md)
|
|
@@ -427,3 +500,7 @@ npm run lint
|
|
|
427
500
|
npm run pretty
|
|
428
501
|
npm test
|
|
429
502
|
```
|
|
503
|
+
|
|
504
|
+
## Enterprise support
|
|
505
|
+
|
|
506
|
+
Enterprise support including custom development and integration services are available via AppThreat Ltd. Free community support is also available via [discord](https://discord.gg/tmmtjCEHNV).
|
package/bin/cdxgen.js
CHANGED
|
@@ -266,9 +266,10 @@ const checkPermissions = (filePath) => {
|
|
|
266
266
|
}
|
|
267
267
|
let privateKeyToUse = undefined;
|
|
268
268
|
let jwkPublicKey = undefined;
|
|
269
|
+
let publicKeyFile = undefined;
|
|
269
270
|
if (args.generateKeyAndSign) {
|
|
270
271
|
const jdirName = dirname(jsonFile);
|
|
271
|
-
|
|
272
|
+
publicKeyFile = join(jdirName, "public.key");
|
|
272
273
|
const privateKeyFile = join(jdirName, "private.key");
|
|
273
274
|
const { privateKey, publicKey } = crypto.generateKeyPairSync(
|
|
274
275
|
"rsa",
|
|
@@ -331,6 +332,26 @@ const checkPermissions = (filePath) => {
|
|
|
331
332
|
jsonFile,
|
|
332
333
|
JSON.stringify(bomJsonUnsignedObj, null, 2)
|
|
333
334
|
);
|
|
335
|
+
if (publicKeyFile) {
|
|
336
|
+
// Verifying this signature
|
|
337
|
+
const signatureVerification = jws.verify(
|
|
338
|
+
signature,
|
|
339
|
+
alg,
|
|
340
|
+
fs.readFileSync(publicKeyFile, "utf8")
|
|
341
|
+
);
|
|
342
|
+
if (signatureVerification) {
|
|
343
|
+
console.log(
|
|
344
|
+
"SBoM signature is verifiable with the public key and the algorithm",
|
|
345
|
+
publicKeyFile,
|
|
346
|
+
alg
|
|
347
|
+
);
|
|
348
|
+
} else {
|
|
349
|
+
console.log("SBoM signature verification was unsuccessful");
|
|
350
|
+
console.log(
|
|
351
|
+
"Check if the public key was exported in PEM format"
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
334
355
|
}
|
|
335
356
|
} catch (ex) {
|
|
336
357
|
console.log("SBoM signing was unsuccessful", ex);
|
package/bin/repl.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import repl from "node:repl";
|
|
2
|
+
import jsonata from "jsonata";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
|
|
8
|
+
import { createBom } from "../index.js";
|
|
9
|
+
import { validateBom } from "../validator.js";
|
|
10
|
+
import { printTable, printDependencyTree } from "../display.js";
|
|
11
|
+
|
|
12
|
+
const options = {
|
|
13
|
+
useColors: true,
|
|
14
|
+
breakEvalOnSigint: true,
|
|
15
|
+
preview: true,
|
|
16
|
+
prompt: "↝ ",
|
|
17
|
+
ignoreUndefined: true,
|
|
18
|
+
useGlobal: true
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const cdxArt = ` ██████╗██████╗ ██╗ ██╗
|
|
22
|
+
██╔════╝██╔══██╗╚██╗██╔╝
|
|
23
|
+
██║ ██║ ██║ ╚███╔╝
|
|
24
|
+
██║ ██║ ██║ ██╔██╗
|
|
25
|
+
╚██████╗██████╔╝██╔╝ ██╗
|
|
26
|
+
╚═════╝╚═════╝ ╚═╝ ╚═╝
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
console.log("\n" + cdxArt);
|
|
30
|
+
|
|
31
|
+
// The current sbom is stored here
|
|
32
|
+
let sbom = undefined;
|
|
33
|
+
|
|
34
|
+
let historyFile = undefined;
|
|
35
|
+
const historyConfigDir = join(homedir(), ".config", ".cdxgen");
|
|
36
|
+
if (!process.env.CDXGEN_REPL_HISTORY && !fs.existsSync(historyConfigDir)) {
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(historyConfigDir, { recursive: true });
|
|
39
|
+
historyFile = join(historyConfigDir, ".repl_history");
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
historyFile = join(historyConfigDir, ".repl_history");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const importSbom = (sbomOrPath) => {
|
|
48
|
+
if (sbomOrPath && sbomOrPath.endsWith(".json") && fs.existsSync(sbomOrPath)) {
|
|
49
|
+
try {
|
|
50
|
+
sbom = JSON.parse(fs.readFileSync(sbomOrPath, "utf-8"));
|
|
51
|
+
console.log(`✅ SBoM imported successfully from ${sbomOrPath}`);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.log(
|
|
54
|
+
`⚠ Unable to import the SBoM from ${sbomOrPath} due to ${e}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
console.log(`⚠ ${sbomOrPath} is invalid.`);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
// Load any sbom passed from the command line
|
|
62
|
+
if (process.argv.length > 2) {
|
|
63
|
+
importSbom(process.argv[process.argv.length - 1]);
|
|
64
|
+
console.log("💭 Type .print to view the SBoM as a table");
|
|
65
|
+
} else if (fs.existsSync("bom.json")) {
|
|
66
|
+
// If the current directory has a bom.json load it
|
|
67
|
+
importSbom("bom.json");
|
|
68
|
+
} else {
|
|
69
|
+
console.log("💭 Use .create <path> to create an SBoM for the given path.");
|
|
70
|
+
console.log("💭 Use .import <json> to import an existing SBoM.");
|
|
71
|
+
console.log("💭 Type .exit or press ctrl+d to close.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cdxgenRepl = repl.start(options);
|
|
75
|
+
if (historyFile) {
|
|
76
|
+
cdxgenRepl.setupHistory(
|
|
77
|
+
process.env.CDXGEN_REPL_HISTORY || historyFile,
|
|
78
|
+
(err) => {
|
|
79
|
+
if (err) {
|
|
80
|
+
console.log(
|
|
81
|
+
"⚠ REPL history would not be persisted for this session. Set the environment variable CDXGEN_REPL_HISTORY to specify a custom history file"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
cdxgenRepl.defineCommand("create", {
|
|
88
|
+
help: "create an SBoM for the given path",
|
|
89
|
+
async action(sbomOrPath) {
|
|
90
|
+
this.clearBufferedCommand();
|
|
91
|
+
const tempDir = fs.mkdtempSync(join(tmpdir(), "cdxgen-repl-"));
|
|
92
|
+
const bomFile = join(tempDir, "bom.json");
|
|
93
|
+
const bomNSData = await createBom(sbomOrPath, {
|
|
94
|
+
multiProject: true,
|
|
95
|
+
installDeps: true,
|
|
96
|
+
output: bomFile
|
|
97
|
+
});
|
|
98
|
+
if (bomNSData) {
|
|
99
|
+
sbom = bomNSData.bomJson;
|
|
100
|
+
console.log("✅ SBoM imported successfully.");
|
|
101
|
+
console.log("💭 Type .print to view the SBoM as a table");
|
|
102
|
+
} else {
|
|
103
|
+
console.log("SBoM was not generated successfully");
|
|
104
|
+
}
|
|
105
|
+
this.displayPrompt();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
cdxgenRepl.defineCommand("import", {
|
|
109
|
+
help: "import an existing SBoM",
|
|
110
|
+
action(sbomOrPath) {
|
|
111
|
+
this.clearBufferedCommand();
|
|
112
|
+
importSbom(sbomOrPath);
|
|
113
|
+
this.displayPrompt();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
cdxgenRepl.defineCommand("exit", {
|
|
117
|
+
help: "exit",
|
|
118
|
+
action() {
|
|
119
|
+
this.close();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
cdxgenRepl.defineCommand("sbom", {
|
|
123
|
+
help: "show the current sbom",
|
|
124
|
+
action() {
|
|
125
|
+
if (sbom) {
|
|
126
|
+
console.log(sbom);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(
|
|
129
|
+
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
this.displayPrompt();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
cdxgenRepl.defineCommand("search", {
|
|
136
|
+
help: "search the current sbom",
|
|
137
|
+
async action(searchStr) {
|
|
138
|
+
if (sbom) {
|
|
139
|
+
if (searchStr) {
|
|
140
|
+
try {
|
|
141
|
+
if (!searchStr.includes("~>")) {
|
|
142
|
+
searchStr = `components[group ~> /${searchStr}/i or name ~> /${searchStr}/i or description ~> /${searchStr}/i or publisher ~> /${searchStr}/i or purl ~> /${searchStr}/i]`;
|
|
143
|
+
}
|
|
144
|
+
const expression = jsonata(searchStr);
|
|
145
|
+
let components = await expression.evaluate(sbom);
|
|
146
|
+
if (!components) {
|
|
147
|
+
console.log("No results found!");
|
|
148
|
+
} else {
|
|
149
|
+
printTable({ components, dependencies: [] });
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.log(e);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
console.log(
|
|
156
|
+
"⚠ Specify the search string. Eg: .search <search string>"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
console.log(
|
|
161
|
+
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
this.displayPrompt();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
cdxgenRepl.defineCommand("sort", {
|
|
168
|
+
help: "sort the current sbom based on the attribute",
|
|
169
|
+
async action(sortStr) {
|
|
170
|
+
if (sbom) {
|
|
171
|
+
if (sortStr) {
|
|
172
|
+
try {
|
|
173
|
+
if (!sortStr.includes("^")) {
|
|
174
|
+
sortStr = `components^(${sortStr})`;
|
|
175
|
+
}
|
|
176
|
+
const expression = jsonata(sortStr);
|
|
177
|
+
let components = await expression.evaluate(sbom);
|
|
178
|
+
if (!components) {
|
|
179
|
+
console.log("No results found!");
|
|
180
|
+
} else {
|
|
181
|
+
printTable({ components, dependencies: [] });
|
|
182
|
+
// Store the sorted list in memory
|
|
183
|
+
if (components.length === sbom.components.length) {
|
|
184
|
+
sbom.components = components;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.log(e);
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
console.log("⚠ Specify the attribute to sort by. Eg: .sort name");
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
console.log(
|
|
195
|
+
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
this.displayPrompt();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
cdxgenRepl.defineCommand("query", {
|
|
202
|
+
help: "query the current sbom",
|
|
203
|
+
async action(querySpec) {
|
|
204
|
+
if (sbom) {
|
|
205
|
+
if (querySpec) {
|
|
206
|
+
try {
|
|
207
|
+
const expression = jsonata(querySpec);
|
|
208
|
+
console.log(await expression.evaluate(sbom));
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.log(e);
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
console.log(
|
|
214
|
+
"⚠ Specify the search specification in jsonata format. Eg: .query metadata.component"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
console.log(
|
|
219
|
+
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
this.displayPrompt();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
cdxgenRepl.defineCommand("print", {
|
|
226
|
+
help: "print the current sbom as a table",
|
|
227
|
+
action() {
|
|
228
|
+
if (sbom) {
|
|
229
|
+
printTable(sbom);
|
|
230
|
+
} else {
|
|
231
|
+
console.log(
|
|
232
|
+
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
this.displayPrompt();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
cdxgenRepl.defineCommand("tree", {
|
|
239
|
+
help: "display the dependency tree",
|
|
240
|
+
action() {
|
|
241
|
+
if (sbom) {
|
|
242
|
+
printDependencyTree(sbom);
|
|
243
|
+
} else {
|
|
244
|
+
console.log(
|
|
245
|
+
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
this.displayPrompt();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
cdxgenRepl.defineCommand("validate", {
|
|
252
|
+
help: "validate the sbom",
|
|
253
|
+
action() {
|
|
254
|
+
if (sbom) {
|
|
255
|
+
const result = validateBom(sbom);
|
|
256
|
+
if (result) {
|
|
257
|
+
console.log("SBoM is valid!");
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
console.log(
|
|
261
|
+
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
this.displayPrompt();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
cdxgenRepl.defineCommand("save", {
|
|
268
|
+
help: "save the sbom to a new file",
|
|
269
|
+
action(saveToFile) {
|
|
270
|
+
if (sbom) {
|
|
271
|
+
if (!saveToFile) {
|
|
272
|
+
saveToFile = "bom.json";
|
|
273
|
+
}
|
|
274
|
+
fs.writeFileSync(saveToFile, JSON.stringify(sbom, null, 2));
|
|
275
|
+
console.log(`SBoM saved successfully to ${saveToFile}`);
|
|
276
|
+
} else {
|
|
277
|
+
console.log(
|
|
278
|
+
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
this.displayPrompt();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
cdxgenRepl.defineCommand("update", {
|
|
285
|
+
help: "update the sbom components based on the given query",
|
|
286
|
+
async action(updateSpec) {
|
|
287
|
+
if (sbom) {
|
|
288
|
+
if (!updateSpec) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!updateSpec.startsWith("|")) {
|
|
292
|
+
updateSpec = "|" + updateSpec;
|
|
293
|
+
}
|
|
294
|
+
if (!updateSpec.endsWith("|")) {
|
|
295
|
+
updateSpec = updateSpec + "|";
|
|
296
|
+
}
|
|
297
|
+
updateSpec = "$ ~> " + updateSpec;
|
|
298
|
+
const expression = jsonata(updateSpec);
|
|
299
|
+
const newSbom = await expression.evaluate(sbom);
|
|
300
|
+
if (newSbom && newSbom.components.length <= sbom.components.length) {
|
|
301
|
+
sbom = newSbom;
|
|
302
|
+
}
|
|
303
|
+
console.log("SBoM updated successfully.");
|
|
304
|
+
} else {
|
|
305
|
+
console.log(
|
|
306
|
+
"⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
this.displayPrompt();
|
|
310
|
+
}
|
|
311
|
+
});
|
package/display.js
CHANGED
|
@@ -13,6 +13,9 @@ const MAX_TREE_DEPTH = 3;
|
|
|
13
13
|
|
|
14
14
|
export const printTable = (bomJson) => {
|
|
15
15
|
const data = [["Group", "Name", "Version", "Scope"]];
|
|
16
|
+
if (!bomJson || !bomJson.components) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
16
19
|
for (const comp of bomJson.components) {
|
|
17
20
|
data.push([comp.group || "", comp.name, comp.version, comp.scope || ""]);
|
|
18
21
|
}
|
package/docker.js
CHANGED
|
@@ -24,7 +24,7 @@ import { x } from "tar";
|
|
|
24
24
|
import { spawnSync } from "node:child_process";
|
|
25
25
|
import { DEBUG_MODE } from "./utils.js";
|
|
26
26
|
|
|
27
|
-
const isWin = _platform() === "win32";
|
|
27
|
+
export const isWin = _platform() === "win32";
|
|
28
28
|
|
|
29
29
|
let dockerConn = undefined;
|
|
30
30
|
let isPodman = false;
|
package/docker.test.js
CHANGED
|
@@ -3,14 +3,17 @@ import {
|
|
|
3
3
|
parseImageName,
|
|
4
4
|
getImage,
|
|
5
5
|
removeImage,
|
|
6
|
-
exportImage
|
|
6
|
+
exportImage,
|
|
7
|
+
isWin
|
|
7
8
|
} from "./docker.js";
|
|
8
9
|
import { expect, test } from "@jest/globals";
|
|
9
10
|
|
|
10
11
|
test("docker connection", async () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
if (!(isWin && process.env.CI === "true")) {
|
|
13
|
+
const dockerConn = await getConnection();
|
|
14
|
+
expect(dockerConn);
|
|
15
|
+
}
|
|
16
|
+
}, 120000);
|
|
14
17
|
|
|
15
18
|
test("parseImageName tests", () => {
|
|
16
19
|
expect(parseImageName("debian")).toEqual({
|
|
@@ -59,7 +62,7 @@ test("parseImageName tests", () => {
|
|
|
59
62
|
digest: "5d008306a7c5d09ba0161a3408fa3839dc2c9dd991ffb68adecc1040399fe9e1",
|
|
60
63
|
platform: ""
|
|
61
64
|
});
|
|
62
|
-
});
|
|
65
|
+
}, 120000);
|
|
63
66
|
|
|
64
67
|
test("docker getImage", async () => {
|
|
65
68
|
const imageData = await getImage("hello-world:latest");
|
package/index.js
CHANGED
|
@@ -199,18 +199,25 @@ const createDefaultParentComponent = (path, type = "application") => {
|
|
|
199
199
|
|
|
200
200
|
const determineParentComponent = (options) => {
|
|
201
201
|
let parentComponent = undefined;
|
|
202
|
-
if (options.
|
|
202
|
+
if (options.parentComponent && Object.keys(options.parentComponent).length) {
|
|
203
|
+
return options.parentComponent;
|
|
204
|
+
} else if (options.projectName && options.projectVersion) {
|
|
203
205
|
parentComponent = {
|
|
204
206
|
group: options.projectGroup || "",
|
|
205
207
|
name: options.projectName,
|
|
206
208
|
version: "" + options.projectVersion || "",
|
|
207
209
|
type: "application"
|
|
208
210
|
};
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
const ppurl = new PackageURL(
|
|
212
|
+
parentComponent.type,
|
|
213
|
+
parentComponent.group,
|
|
214
|
+
parentComponent.name,
|
|
215
|
+
parentComponent.version,
|
|
216
|
+
null,
|
|
217
|
+
null
|
|
218
|
+
).toString();
|
|
219
|
+
parentComponent["bom-ref"] = ppurl;
|
|
220
|
+
parentComponent["purl"] = decodeURIComponent(ppurl);
|
|
214
221
|
}
|
|
215
222
|
return parentComponent;
|
|
216
223
|
};
|
|
@@ -323,11 +330,18 @@ function addMetadata(parentComponent = {}, format = "xml", options = {}) {
|
|
|
323
330
|
if (parentComponent) {
|
|
324
331
|
delete parentComponent.evidence;
|
|
325
332
|
delete parentComponent._integrity;
|
|
333
|
+
delete parentComponent.license;
|
|
334
|
+
if (!parentComponent["purl"] && parentComponent["bom-ref"]) {
|
|
335
|
+
parentComponent["purl"] = decodeURIComponent(
|
|
336
|
+
parentComponent["bom-ref"]
|
|
337
|
+
);
|
|
338
|
+
}
|
|
326
339
|
}
|
|
327
340
|
if (parentComponent && parentComponent.components) {
|
|
328
341
|
for (const comp of parentComponent.components) {
|
|
329
342
|
delete comp.evidence;
|
|
330
343
|
delete comp._integrity;
|
|
344
|
+
delete comp.license;
|
|
331
345
|
if (!comp["bom-ref"] && comp.name && comp.type) {
|
|
332
346
|
let fullName =
|
|
333
347
|
comp.group && comp.group.length
|
|
@@ -1336,7 +1350,7 @@ export const createJavaBom = async (path, options) => {
|
|
|
1336
1350
|
let gradleDepArgs = [
|
|
1337
1351
|
sp.purl === parentComponent.purl
|
|
1338
1352
|
? depTaskWithArgs[0]
|
|
1339
|
-
: `:${sp.name}:${depTaskWithArgs[0]}`
|
|
1353
|
+
: `:${sp.name.replace(/\//, ":")}:${depTaskWithArgs[0]}`
|
|
1340
1354
|
];
|
|
1341
1355
|
gradleDepArgs = gradleDepArgs
|
|
1342
1356
|
.concat(depTaskWithArgs.slice(1))
|
|
@@ -1786,9 +1800,9 @@ export const createNodejsBom = async (path, options) => {
|
|
|
1786
1800
|
parentComponent.type = "application";
|
|
1787
1801
|
ppurl = new PackageURL(
|
|
1788
1802
|
"npm",
|
|
1789
|
-
parentComponent.group,
|
|
1790
|
-
parentComponent.name,
|
|
1791
|
-
parentComponent.version,
|
|
1803
|
+
options.projectGroup || parentComponent.group,
|
|
1804
|
+
options.projectName || parentComponent.name,
|
|
1805
|
+
options.projectVersion || parentComponent.version,
|
|
1792
1806
|
null,
|
|
1793
1807
|
null
|
|
1794
1808
|
).toString();
|
|
@@ -1806,9 +1820,9 @@ export const createNodejsBom = async (path, options) => {
|
|
|
1806
1820
|
};
|
|
1807
1821
|
ppurl = new PackageURL(
|
|
1808
1822
|
"npm",
|
|
1809
|
-
parentComponent.group,
|
|
1810
|
-
parentComponent.name,
|
|
1811
|
-
parentComponent.version,
|
|
1823
|
+
options.projectGroup || parentComponent.group,
|
|
1824
|
+
options.projectName || parentComponent.name,
|
|
1825
|
+
options.projectVersion || parentComponent.version,
|
|
1812
1826
|
null,
|
|
1813
1827
|
null
|
|
1814
1828
|
).toString();
|
|
@@ -1837,11 +1851,10 @@ export const createNodejsBom = async (path, options) => {
|
|
|
1837
1851
|
console.log(`Parsing ${f}`);
|
|
1838
1852
|
}
|
|
1839
1853
|
// Parse package-lock.json if available
|
|
1840
|
-
const parsedList = await parsePkgLock(f);
|
|
1854
|
+
const parsedList = await parsePkgLock(f, options);
|
|
1841
1855
|
const dlist = parsedList.pkgList;
|
|
1842
1856
|
const tmpParentComponent = dlist.splice(0, 1)[0] || {};
|
|
1843
1857
|
tmpParentComponent.type = "application";
|
|
1844
|
-
// Create a default parent component based on directory name
|
|
1845
1858
|
if (!Object.keys(parentComponent).length) {
|
|
1846
1859
|
parentComponent = tmpParentComponent;
|
|
1847
1860
|
} else {
|
|
@@ -1929,9 +1942,9 @@ export const createNodejsBom = async (path, options) => {
|
|
|
1929
1942
|
tmpParentComponent.type = "application";
|
|
1930
1943
|
ppurl = new PackageURL(
|
|
1931
1944
|
"npm",
|
|
1932
|
-
tmpParentComponent.group,
|
|
1933
|
-
tmpParentComponent.name,
|
|
1934
|
-
tmpParentComponent.version,
|
|
1945
|
+
options.projectGroup || tmpParentComponent.group,
|
|
1946
|
+
options.projectName || tmpParentComponent.name,
|
|
1947
|
+
options.projectVersion || tmpParentComponent.version,
|
|
1935
1948
|
null,
|
|
1936
1949
|
null
|
|
1937
1950
|
).toString();
|
|
@@ -1948,15 +1961,15 @@ export const createNodejsBom = async (path, options) => {
|
|
|
1948
1961
|
const tmpA = dirName.split(sep);
|
|
1949
1962
|
dirName = tmpA[tmpA.length - 1];
|
|
1950
1963
|
const tmpParentComponent = {
|
|
1951
|
-
group: "",
|
|
1952
|
-
name: dirName,
|
|
1964
|
+
group: options.projectGroup || "",
|
|
1965
|
+
name: options.projectName || dirName,
|
|
1953
1966
|
type: "application"
|
|
1954
1967
|
};
|
|
1955
1968
|
ppurl = new PackageURL(
|
|
1956
1969
|
"npm",
|
|
1957
1970
|
tmpParentComponent.group,
|
|
1958
1971
|
tmpParentComponent.name,
|
|
1959
|
-
tmpParentComponent.version,
|
|
1972
|
+
options.projectVersion || tmpParentComponent.version,
|
|
1960
1973
|
null,
|
|
1961
1974
|
null
|
|
1962
1975
|
).toString();
|
|
@@ -2030,6 +2043,8 @@ export const createNodejsBom = async (path, options) => {
|
|
|
2030
2043
|
if (parentSubComponents.length) {
|
|
2031
2044
|
parentComponent.components = parentSubComponents;
|
|
2032
2045
|
}
|
|
2046
|
+
// We need to set this to force our version to be used rather than the directory name based one.
|
|
2047
|
+
options.parentComponent = parentComponent;
|
|
2033
2048
|
return buildBomNSData(options, pkgList, "npm", {
|
|
2034
2049
|
allImports,
|
|
2035
2050
|
src: path,
|
|
@@ -2260,14 +2275,20 @@ export const createPythonBom = async (path, options) => {
|
|
|
2260
2275
|
pkgMap = getPipFrozenTree(path, undefined, tempDir);
|
|
2261
2276
|
}
|
|
2262
2277
|
// Get the imported modules and a dedupe list of packages
|
|
2263
|
-
const parentDependsOn =
|
|
2278
|
+
const parentDependsOn = new Set();
|
|
2264
2279
|
const retMap = await getPyModules(path, pkgList);
|
|
2265
2280
|
if (retMap.pkgList && retMap.pkgList.length) {
|
|
2266
2281
|
pkgList = pkgList.concat(retMap.pkgList);
|
|
2267
2282
|
for (const p of retMap.pkgList) {
|
|
2268
|
-
if (
|
|
2269
|
-
|
|
2283
|
+
if (
|
|
2284
|
+
!p.version ||
|
|
2285
|
+
(parentComponent &&
|
|
2286
|
+
p.name === parentComponent.name &&
|
|
2287
|
+
(p.version === parentComponent.version || p.version === "latest"))
|
|
2288
|
+
) {
|
|
2289
|
+
continue;
|
|
2270
2290
|
}
|
|
2291
|
+
parentDependsOn.add(`pkg:pypi/${p.name}@${p.version}`);
|
|
2271
2292
|
}
|
|
2272
2293
|
}
|
|
2273
2294
|
if (retMap.dependenciesList) {
|
|
@@ -2285,11 +2306,11 @@ export const createPythonBom = async (path, options) => {
|
|
|
2285
2306
|
if (
|
|
2286
2307
|
parentComponent &&
|
|
2287
2308
|
p.name === parentComponent.name &&
|
|
2288
|
-
p.version === parentComponent.version
|
|
2309
|
+
(p.version === parentComponent.version || p.version === "latest")
|
|
2289
2310
|
) {
|
|
2290
2311
|
continue;
|
|
2291
2312
|
}
|
|
2292
|
-
parentDependsOn.
|
|
2313
|
+
parentDependsOn.add(`pkg:pypi/${p.name}@${p.version}`);
|
|
2293
2314
|
}
|
|
2294
2315
|
if (pkgMap.pkgList && pkgMap.pkgList.length) {
|
|
2295
2316
|
pkgList = pkgList.concat(pkgMap.pkgList);
|
|
@@ -2301,13 +2322,22 @@ export const createPythonBom = async (path, options) => {
|
|
|
2301
2322
|
parentComponent
|
|
2302
2323
|
);
|
|
2303
2324
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2325
|
+
let parentPresent = false;
|
|
2326
|
+
for (const d of dependencies) {
|
|
2327
|
+
if (d.ref === parentComponent["bom-ref"]) {
|
|
2328
|
+
parentPresent = true;
|
|
2329
|
+
break;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
if (!parentPresent) {
|
|
2333
|
+
const pdependencies = {
|
|
2334
|
+
ref: parentComponent["bom-ref"],
|
|
2335
|
+
dependsOn: Array.from(parentDependsOn).filter(
|
|
2336
|
+
(r) => parentComponent && r !== parentComponent["bom-ref"]
|
|
2337
|
+
)
|
|
2338
|
+
};
|
|
2339
|
+
dependencies.splice(0, 0, pdependencies);
|
|
2340
|
+
}
|
|
2311
2341
|
}
|
|
2312
2342
|
}
|
|
2313
2343
|
// Final fallback is to manually parse setup.py if we still
|
|
@@ -3736,7 +3766,9 @@ export const mergeDependencies = (
|
|
|
3736
3766
|
parentComponent = {}
|
|
3737
3767
|
) => {
|
|
3738
3768
|
if (!parentComponent && DEBUG_MODE) {
|
|
3739
|
-
console.log(
|
|
3769
|
+
console.log(
|
|
3770
|
+
"Unable to determine parent component. Dependencies will be flattened."
|
|
3771
|
+
);
|
|
3740
3772
|
}
|
|
3741
3773
|
const deps_map = {};
|
|
3742
3774
|
const parentRef =
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyclonedx/cdxgen",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.0",
|
|
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>",
|
|
@@ -31,13 +31,14 @@
|
|
|
31
31
|
"type": "module",
|
|
32
32
|
"exports": "./index.js",
|
|
33
33
|
"bin": {
|
|
34
|
-
"cdxgen": "./bin/cdxgen.js"
|
|
34
|
+
"cdxgen": "./bin/cdxgen.js",
|
|
35
|
+
"cdxi": "./bin/repl.js"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
38
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --inject-globals false",
|
|
38
39
|
"watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --inject-globals false",
|
|
39
|
-
"lint": "eslint *.js *.test.js bin
|
|
40
|
-
"pretty": "prettier --write *.js data/*.json bin
|
|
40
|
+
"lint": "eslint *.js *.test.js bin/*.js",
|
|
41
|
+
"pretty": "prettier --write *.js data/*.json bin/*.js --trailing-comma=none"
|
|
41
42
|
},
|
|
42
43
|
"engines": {
|
|
43
44
|
"node": ">=16"
|
|
@@ -50,8 +51,8 @@
|
|
|
50
51
|
"url": "https://github.com/cyclonedx/cdxgen/issues"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
|
-
"@babel/parser": "^7.22.
|
|
54
|
-
"@babel/traverse": "^7.22.
|
|
54
|
+
"@babel/parser": "^7.22.10",
|
|
55
|
+
"@babel/traverse": "^7.22.10",
|
|
55
56
|
"ajv": "^8.12.0",
|
|
56
57
|
"ajv-formats": "^2.1.1",
|
|
57
58
|
"cheerio": "^1.0.0-rc.12",
|
|
@@ -75,11 +76,12 @@
|
|
|
75
76
|
"yargs": "^17.7.2"
|
|
76
77
|
},
|
|
77
78
|
"optionalDependencies": {
|
|
78
|
-
"@appthreat/atom": "^1.0.
|
|
79
|
+
"@appthreat/atom": "^1.0.1",
|
|
79
80
|
"@cyclonedx/cdxgen-plugins-bin": "^1.2.0",
|
|
80
81
|
"body-parser": "^1.20.2",
|
|
81
82
|
"compression": "^1.7.4",
|
|
82
|
-
"connect": "^3.7.0"
|
|
83
|
+
"connect": "^3.7.0",
|
|
84
|
+
"jsonata": "^2.0.3"
|
|
83
85
|
},
|
|
84
86
|
"files": [
|
|
85
87
|
"*.js",
|
|
@@ -88,7 +90,8 @@
|
|
|
88
90
|
],
|
|
89
91
|
"devDependencies": {
|
|
90
92
|
"caxa": "^3.0.1",
|
|
91
|
-
"eslint": "^8.
|
|
92
|
-
"jest": "^29.5.0"
|
|
93
|
+
"eslint": "^8.47.0",
|
|
94
|
+
"jest": "^29.5.0",
|
|
95
|
+
"prettier": "3.0.1"
|
|
93
96
|
}
|
|
94
97
|
}
|
package/piptree.js
CHANGED
|
@@ -57,12 +57,11 @@ def find_deps(idx, visited, reqs, traverse_count):
|
|
|
57
57
|
if not d:
|
|
58
58
|
continue
|
|
59
59
|
r.project_name = d.project_name if d is not None else r.project_name
|
|
60
|
-
if len(visited) > 100
|
|
60
|
+
if len(visited) > 100 or visited.get(r.project_name, 0) > 5:
|
|
61
61
|
return freqs
|
|
62
62
|
specs = sorted(r.specs, reverse=True)
|
|
63
63
|
specs_str = ",".join(["".join(sp) for sp in specs]) if specs else ""
|
|
64
64
|
dreqs = d.requires()
|
|
65
|
-
visited[r.project_name] = True
|
|
66
65
|
freqs.append(
|
|
67
66
|
{
|
|
68
67
|
"name": r.project_name,
|
|
@@ -71,6 +70,7 @@ def find_deps(idx, visited, reqs, traverse_count):
|
|
|
71
70
|
"dependencies": find_deps(idx, visited, dreqs, traverse_count + 1) if dreqs and traverse_count < 200 else [],
|
|
72
71
|
}
|
|
73
72
|
)
|
|
73
|
+
visited[r.project_name] = visited.get(r.project_name, 0) + 1
|
|
74
74
|
return freqs
|
|
75
75
|
|
|
76
76
|
|
package/utils.js
CHANGED
|
@@ -497,12 +497,16 @@ export const parsePkgJson = async (pkgJsonFile) => {
|
|
|
497
497
|
* Parse nodejs package lock file
|
|
498
498
|
*
|
|
499
499
|
* @param {string} pkgLockFile package-lock.json file
|
|
500
|
+
* @param {object} options Command line options
|
|
500
501
|
*/
|
|
501
|
-
export const parsePkgLock = async (pkgLockFile) => {
|
|
502
|
+
export const parsePkgLock = async (pkgLockFile, options = {}) => {
|
|
502
503
|
let pkgList = [];
|
|
503
504
|
const dependenciesList = [];
|
|
504
505
|
const depKeys = {};
|
|
505
506
|
let rootPkg = {};
|
|
507
|
+
if (!options) {
|
|
508
|
+
options = {};
|
|
509
|
+
}
|
|
506
510
|
if (existsSync(pkgLockFile)) {
|
|
507
511
|
const lockData = JSON.parse(readFileSync(pkgLockFile, "utf8"));
|
|
508
512
|
rootPkg.name = lockData.name || "";
|
|
@@ -510,16 +514,16 @@ export const parsePkgLock = async (pkgLockFile) => {
|
|
|
510
514
|
if (lockData.name && lockData.packages && lockData.packages[""]) {
|
|
511
515
|
// Build the initial dependency tree for the root package
|
|
512
516
|
rootPkg = {
|
|
513
|
-
group: "",
|
|
514
|
-
name: lockData.name,
|
|
515
|
-
version: lockData.version,
|
|
517
|
+
group: options.projectGroup || "",
|
|
518
|
+
name: options.projectName || lockData.name,
|
|
519
|
+
version: options.projectVersion || lockData.version,
|
|
516
520
|
type: "application",
|
|
517
521
|
"bom-ref": decodeURIComponent(
|
|
518
522
|
new PackageURL(
|
|
519
523
|
"npm",
|
|
520
|
-
"",
|
|
521
|
-
lockData.name,
|
|
522
|
-
lockData.version,
|
|
524
|
+
options.projectGroup || "",
|
|
525
|
+
options.projectName || lockData.name,
|
|
526
|
+
options.projectVersion || lockData.version,
|
|
523
527
|
null,
|
|
524
528
|
null
|
|
525
529
|
).toString()
|
|
@@ -531,10 +535,10 @@ export const parsePkgLock = async (pkgLockFile) => {
|
|
|
531
535
|
dirName = tmpA[tmpA.length - 1];
|
|
532
536
|
// v1 lock file
|
|
533
537
|
rootPkg = {
|
|
534
|
-
group: "",
|
|
535
|
-
name: lockData.name || dirName,
|
|
536
|
-
version: lockData.version || "",
|
|
537
|
-
type: "
|
|
538
|
+
group: options.projectGroup || "",
|
|
539
|
+
name: options.projectName || lockData.name || dirName,
|
|
540
|
+
version: options.projectVersion || lockData.version || "",
|
|
541
|
+
type: "npm"
|
|
538
542
|
};
|
|
539
543
|
}
|
|
540
544
|
if (rootPkg && rootPkg.name) {
|
|
@@ -625,6 +629,7 @@ export const yarnLockToIdentMap = function (lockData) {
|
|
|
625
629
|
const identMap = {};
|
|
626
630
|
let currentIdents = [];
|
|
627
631
|
lockData.split("\n").forEach((l) => {
|
|
632
|
+
l = l.replace("\r", "");
|
|
628
633
|
if (l === "\n" || l.startsWith("#")) {
|
|
629
634
|
return;
|
|
630
635
|
}
|
|
@@ -687,6 +692,7 @@ export const parseYarnLock = async function (yarnLockFile) {
|
|
|
687
692
|
const identMap = yarnLockToIdentMap(lockData);
|
|
688
693
|
let prefixAtSymbol = false;
|
|
689
694
|
lockData.split("\n").forEach((l) => {
|
|
695
|
+
l = l.replace("\r", "");
|
|
690
696
|
if (l === "\n" || l.startsWith("#")) {
|
|
691
697
|
return;
|
|
692
698
|
}
|
|
@@ -1664,7 +1670,7 @@ export const parseGradleProjects = function (rawOutput) {
|
|
|
1664
1670
|
if (typeof rawOutput === "string") {
|
|
1665
1671
|
const tmpA = rawOutput.split("\n");
|
|
1666
1672
|
tmpA.forEach((l) => {
|
|
1667
|
-
l = l.replace("\r", "")
|
|
1673
|
+
l = l.replace("\r", "");
|
|
1668
1674
|
if (l.startsWith("Root project ")) {
|
|
1669
1675
|
rootProject = l
|
|
1670
1676
|
.split("Root project ")[1]
|
|
@@ -1676,7 +1682,8 @@ export const parseGradleProjects = function (rawOutput) {
|
|
|
1676
1682
|
const projName = tmpB[1].split(" ")[0].replace(/'/g, "");
|
|
1677
1683
|
// Include all projects including test projects
|
|
1678
1684
|
if (projName.startsWith(":")) {
|
|
1679
|
-
|
|
1685
|
+
// Handle the case where the project name could have a space. Eg: +--- project :app (*)
|
|
1686
|
+
projects.add(projName.split(" ")[0]);
|
|
1680
1687
|
}
|
|
1681
1688
|
}
|
|
1682
1689
|
} else if (l.includes("--- project ")) {
|
|
@@ -1684,7 +1691,7 @@ export const parseGradleProjects = function (rawOutput) {
|
|
|
1684
1691
|
if (tmpB && tmpB.length > 1) {
|
|
1685
1692
|
const projName = tmpB[1];
|
|
1686
1693
|
if (projName.startsWith(":")) {
|
|
1687
|
-
projects.add(projName);
|
|
1694
|
+
projects.add(projName.split(" ")[0]);
|
|
1688
1695
|
}
|
|
1689
1696
|
}
|
|
1690
1697
|
}
|
|
@@ -1726,7 +1733,7 @@ export const parseGradleProperties = function (rawOutput) {
|
|
|
1726
1733
|
const spStrs = tmpB[1].replace(/[[\]']/g, "").split(", ");
|
|
1727
1734
|
const tmpprojects = spStrs
|
|
1728
1735
|
.flatMap((s) => s.replace("project ", ""))
|
|
1729
|
-
.filter((s) => !["
|
|
1736
|
+
.filter((s) => ![""].includes(s.trim()));
|
|
1730
1737
|
tmpprojects.forEach(projects.add, projects);
|
|
1731
1738
|
}
|
|
1732
1739
|
}
|
|
@@ -1893,6 +1900,7 @@ export const parseKVDep = function (rawOutput) {
|
|
|
1893
1900
|
if (typeof rawOutput === "string") {
|
|
1894
1901
|
const deps = [];
|
|
1895
1902
|
rawOutput.split("\n").forEach((l) => {
|
|
1903
|
+
l = l.replace("\r", "");
|
|
1896
1904
|
const tmpA = l.split(":");
|
|
1897
1905
|
if (tmpA.length === 3) {
|
|
1898
1906
|
deps.push({
|
|
@@ -2331,6 +2339,9 @@ export const parsePyProjectToml = (tomlFile) => {
|
|
|
2331
2339
|
pkg.name = value;
|
|
2332
2340
|
break;
|
|
2333
2341
|
case "version":
|
|
2342
|
+
if (value.includes("{")) {
|
|
2343
|
+
value = "latest";
|
|
2344
|
+
}
|
|
2334
2345
|
pkg.version = value;
|
|
2335
2346
|
break;
|
|
2336
2347
|
case "authors":
|
|
@@ -2365,6 +2376,7 @@ export const parsePoetrylockData = async function (lockData, lockFile) {
|
|
|
2365
2376
|
return pkgList;
|
|
2366
2377
|
}
|
|
2367
2378
|
lockData.split("\n").forEach((l) => {
|
|
2379
|
+
l = l.replace("\r", "");
|
|
2368
2380
|
let key = null;
|
|
2369
2381
|
let value = null;
|
|
2370
2382
|
// Package section starts with this marker
|
|
@@ -2420,6 +2432,7 @@ export async function parseReqFile(reqData, fetchDepsInfo) {
|
|
|
2420
2432
|
const pkgList = [];
|
|
2421
2433
|
let compScope = undefined;
|
|
2422
2434
|
reqData.split("\n").forEach((l) => {
|
|
2435
|
+
l = l.replace("\r", "");
|
|
2423
2436
|
l = l.trim();
|
|
2424
2437
|
if (l.startsWith("Skipping line") || l.startsWith("(add")) {
|
|
2425
2438
|
return;
|
|
@@ -3146,6 +3159,7 @@ export const parseGemfileLockData = async function (gemLockData) {
|
|
|
3146
3159
|
let specsFound = false;
|
|
3147
3160
|
gemLockData.split("\n").forEach((l) => {
|
|
3148
3161
|
l = l.trim();
|
|
3162
|
+
l = l.replace("\r", "");
|
|
3149
3163
|
if (specsFound) {
|
|
3150
3164
|
const tmpA = l.split(" ");
|
|
3151
3165
|
if (tmpA && tmpA.length == 2) {
|
|
@@ -3358,6 +3372,7 @@ export const parseCargoData = async function (cargoData) {
|
|
|
3358
3372
|
cargoData.split("\n").forEach((l) => {
|
|
3359
3373
|
let key = null;
|
|
3360
3374
|
let value = null;
|
|
3375
|
+
l = l.replace("\r", "");
|
|
3361
3376
|
// Ignore version = 3 found at the top of newer lock files
|
|
3362
3377
|
if (!pkg && l.startsWith("version =")) {
|
|
3363
3378
|
return;
|
|
@@ -3402,6 +3417,7 @@ export const parseCargoAuditableData = async function (cargoData) {
|
|
|
3402
3417
|
return pkgList;
|
|
3403
3418
|
}
|
|
3404
3419
|
cargoData.split("\n").forEach((l) => {
|
|
3420
|
+
l = l.replace("\r", "");
|
|
3405
3421
|
const tmpA = l.split("\t");
|
|
3406
3422
|
if (tmpA && tmpA.length > 2) {
|
|
3407
3423
|
let group = dirname(tmpA[0].trim());
|
|
@@ -3433,6 +3449,7 @@ export const parsePubLockData = async function (pubLockData) {
|
|
|
3433
3449
|
pubLockData.split("\n").forEach((l) => {
|
|
3434
3450
|
let key = null;
|
|
3435
3451
|
let value = null;
|
|
3452
|
+
l = l.replace("\r", "");
|
|
3436
3453
|
if (!pkg && (l.startsWith("sdks:") || !l.startsWith(" "))) {
|
|
3437
3454
|
return;
|
|
3438
3455
|
}
|
|
@@ -3848,6 +3865,7 @@ export const parseCabalData = function (cabalData) {
|
|
|
3848
3865
|
if (!l.includes(" ==")) {
|
|
3849
3866
|
return;
|
|
3850
3867
|
}
|
|
3868
|
+
l = l.replace("\r", "");
|
|
3851
3869
|
if (l.includes(" ==")) {
|
|
3852
3870
|
const tmpA = l.split(" ==");
|
|
3853
3871
|
const name = tmpA[0]
|
|
@@ -3875,6 +3893,7 @@ export const parseMixLockData = function (mixData) {
|
|
|
3875
3893
|
if (!l.includes(":hex")) {
|
|
3876
3894
|
return;
|
|
3877
3895
|
}
|
|
3896
|
+
l = l.replace("\r", "");
|
|
3878
3897
|
if (l.includes(":hex")) {
|
|
3879
3898
|
const tmpA = l.split(",");
|
|
3880
3899
|
if (tmpA.length > 3) {
|
|
@@ -4969,6 +4988,7 @@ export const parseJarManifest = function (jarMetadata) {
|
|
|
4969
4988
|
return metadata;
|
|
4970
4989
|
}
|
|
4971
4990
|
jarMetadata.split("\n").forEach((l) => {
|
|
4991
|
+
l = l.replace("\r", "");
|
|
4972
4992
|
if (l.includes(": ")) {
|
|
4973
4993
|
const tmpA = l.split(": ");
|
|
4974
4994
|
if (tmpA && tmpA.length === 2) {
|
|
@@ -4999,10 +5019,19 @@ export const extractJarArchive = function (jarFile, tempDir) {
|
|
|
4999
5019
|
const fname = basename(jarFile);
|
|
5000
5020
|
let pomname = undefined;
|
|
5001
5021
|
// If there is a pom file in the same directory, try to use it
|
|
5022
|
+
let manifestname = join(dirname(jarFile), "META-INF", "MANIFEST.MF");
|
|
5023
|
+
// Issue 439: Current implementation checks for existance of a .pom file, but .pom file is not used.
|
|
5024
|
+
// Instead code expects to find META-INF/MANIFEST.MF in the same folder as a .jar file.
|
|
5025
|
+
// For now check for presence of both .pom and MANIFEST.MF files.
|
|
5002
5026
|
if (jarFile.endsWith(".jar")) {
|
|
5003
5027
|
pomname = jarFile.replace(".jar", ".pom");
|
|
5004
5028
|
}
|
|
5005
|
-
if (
|
|
5029
|
+
if (
|
|
5030
|
+
pomname &&
|
|
5031
|
+
existsSync(pomname) &&
|
|
5032
|
+
manifestname &&
|
|
5033
|
+
existsSync(manifestname)
|
|
5034
|
+
) {
|
|
5006
5035
|
tempDir = dirname(jarFile);
|
|
5007
5036
|
} else if (!existsSync(join(tempDir, fname))) {
|
|
5008
5037
|
// Only copy if the file doesn't exist
|
|
@@ -5548,7 +5577,11 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
|
|
|
5548
5577
|
*
|
|
5549
5578
|
* By checking the environment variable "VIRTUAL_ENV" we decide whether to create an env or not
|
|
5550
5579
|
*/
|
|
5551
|
-
if (
|
|
5580
|
+
if (
|
|
5581
|
+
!process.env.VIRTUAL_ENV &&
|
|
5582
|
+
reqOrSetupFile &&
|
|
5583
|
+
!reqOrSetupFile.endsWith("poetry.lock")
|
|
5584
|
+
) {
|
|
5552
5585
|
result = spawnSync(PYTHON_CMD, ["-m", "venv", tempVenvDir], {
|
|
5553
5586
|
encoding: "utf-8"
|
|
5554
5587
|
});
|
|
@@ -5562,29 +5595,43 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
|
|
|
5562
5595
|
}
|
|
5563
5596
|
}
|
|
5564
5597
|
} else {
|
|
5598
|
+
if (DEBUG_MODE) {
|
|
5599
|
+
console.log(join("Using virtual environment in ", tempVenvDir));
|
|
5600
|
+
}
|
|
5565
5601
|
env.VIRTUAL_ENV = tempVenvDir;
|
|
5566
5602
|
env.PATH = `${join(
|
|
5567
5603
|
tempVenvDir,
|
|
5568
|
-
platform()
|
|
5604
|
+
platform() === "win32" ? "Scripts" : "bin"
|
|
5569
5605
|
)}${_delimiter}${process.env.PATH || ""}`;
|
|
5570
5606
|
}
|
|
5571
5607
|
}
|
|
5572
5608
|
/**
|
|
5573
5609
|
* We now have a virtual environment so we can attempt to install the project and perform
|
|
5574
5610
|
* pip freeze to collect the packages that got installed.
|
|
5611
|
+
* Note that we did not create a virtual environment for poetry because poetry will do this when we run the install.
|
|
5575
5612
|
* This step is accurate but not reproducible since the resulting list could differ based on various factors
|
|
5576
5613
|
* such as the version of python, pip, os, pypi.org availability (and weather?)
|
|
5577
5614
|
*/
|
|
5578
5615
|
// Bug #388. Perform pip install in all virtualenv to make the experience consistent
|
|
5579
5616
|
if (reqOrSetupFile) {
|
|
5580
5617
|
if (reqOrSetupFile.endsWith("poetry.lock")) {
|
|
5581
|
-
let
|
|
5618
|
+
let poetryConfigArgs = [
|
|
5619
|
+
"config",
|
|
5620
|
+
"virtualenvs.options.no-setuptools",
|
|
5621
|
+
"true",
|
|
5622
|
+
"--local"
|
|
5623
|
+
];
|
|
5624
|
+
result = spawnSync("poetry", poetryConfigArgs, {
|
|
5625
|
+
cwd: basePath,
|
|
5626
|
+
encoding: "utf-8",
|
|
5627
|
+
timeout: TIMEOUT_MS
|
|
5628
|
+
});
|
|
5629
|
+
let poetryInstallArgs = ["install", "-n", "--no-root"];
|
|
5582
5630
|
// Attempt to perform poetry install
|
|
5583
|
-
result = spawnSync(
|
|
5631
|
+
result = spawnSync("poetry", poetryInstallArgs, {
|
|
5584
5632
|
cwd: basePath,
|
|
5585
5633
|
encoding: "utf-8",
|
|
5586
|
-
timeout: TIMEOUT_MS
|
|
5587
|
-
env
|
|
5634
|
+
timeout: TIMEOUT_MS
|
|
5588
5635
|
});
|
|
5589
5636
|
if (result.status !== 0 || result.error) {
|
|
5590
5637
|
if (result.stderr && result.stderr.includes("No module named poetry")) {
|
|
@@ -5597,6 +5644,9 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
|
|
|
5597
5644
|
env
|
|
5598
5645
|
});
|
|
5599
5646
|
if (result.status !== 0 || result.error) {
|
|
5647
|
+
if (DEBUG_MODE && result.stderr) {
|
|
5648
|
+
console.log(result.stderr);
|
|
5649
|
+
}
|
|
5600
5650
|
console.log("poetry install has failed.");
|
|
5601
5651
|
console.log(
|
|
5602
5652
|
"1. Install the poetry command using python -m pip install poetry."
|
|
@@ -5610,11 +5660,27 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
|
|
|
5610
5660
|
}
|
|
5611
5661
|
} else {
|
|
5612
5662
|
console.log(
|
|
5613
|
-
"
|
|
5663
|
+
"Poetry install has failed. Setup and activate the poetry virtual environment and re-run cdxgen."
|
|
5614
5664
|
);
|
|
5615
5665
|
}
|
|
5666
|
+
} else {
|
|
5667
|
+
let poetryEnvArgs = ["env info", "--path"];
|
|
5668
|
+
result = spawnSync("poetry", poetryEnvArgs, {
|
|
5669
|
+
cwd: basePath,
|
|
5670
|
+
encoding: "utf-8",
|
|
5671
|
+
timeout: TIMEOUT_MS,
|
|
5672
|
+
env
|
|
5673
|
+
});
|
|
5674
|
+
tempVenvDir = result.stdout.replaceAll(/[\r\n]+/g, "");
|
|
5675
|
+
if (tempVenvDir && tempVenvDir.length) {
|
|
5676
|
+
env.VIRTUAL_ENV = tempVenvDir;
|
|
5677
|
+
env.PATH = `${join(
|
|
5678
|
+
tempVenvDir,
|
|
5679
|
+
platform() === "win32" ? "Scripts" : "bin"
|
|
5680
|
+
)}${_delimiter}${process.env.PATH || ""}`;
|
|
5681
|
+
}
|
|
5616
5682
|
}
|
|
5617
|
-
} else
|
|
5683
|
+
} else {
|
|
5618
5684
|
const pipInstallArgs = [
|
|
5619
5685
|
"-m",
|
|
5620
5686
|
"pip",
|
|
@@ -5659,7 +5725,7 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
|
|
|
5659
5725
|
console.log(
|
|
5660
5726
|
"Possible build errors detected. The resulting list in the SBoM would therefore be incomplete.\nTry installing any missing build tools or development libraries to improve the accuracy."
|
|
5661
5727
|
);
|
|
5662
|
-
if (platform()
|
|
5728
|
+
if (platform() === "win32") {
|
|
5663
5729
|
console.log(
|
|
5664
5730
|
"- Install the appropriate compilers and build tools on Windows by following this documentation - https://wiki.python.org/moin/WindowsCompilers"
|
|
5665
5731
|
);
|
|
@@ -5703,28 +5769,32 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
|
|
|
5703
5769
|
continue;
|
|
5704
5770
|
}
|
|
5705
5771
|
const name = t.name.replace(/_/g, "-").toLowerCase();
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5772
|
+
const version = t.version;
|
|
5773
|
+
let exclude = ["pip", "setuptools", "wheel"];
|
|
5774
|
+
if (!exclude.includes(name)) {
|
|
5775
|
+
pkgList.push({
|
|
5776
|
+
name,
|
|
5777
|
+
version,
|
|
5778
|
+
evidence: {
|
|
5779
|
+
identity: {
|
|
5780
|
+
field: "purl",
|
|
5781
|
+
confidence: 1,
|
|
5782
|
+
methods: [
|
|
5783
|
+
{
|
|
5784
|
+
technique: "instrumentation",
|
|
5785
|
+
confidence: 1,
|
|
5786
|
+
value: env.VIRTUAL_ENV
|
|
5787
|
+
}
|
|
5788
|
+
]
|
|
5789
|
+
}
|
|
5720
5790
|
}
|
|
5721
|
-
}
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5791
|
+
});
|
|
5792
|
+
rootList.push({
|
|
5793
|
+
name,
|
|
5794
|
+
version
|
|
5795
|
+
});
|
|
5796
|
+
flattenDeps(dependenciesMap, pkgList, reqOrSetupFile, t);
|
|
5797
|
+
}
|
|
5728
5798
|
} // end for
|
|
5729
5799
|
for (const k of Object.keys(dependenciesMap)) {
|
|
5730
5800
|
dependenciesList.push({ ref: k, dependsOn: dependenciesMap[k] });
|
package/utils.test.js
CHANGED
|
@@ -295,6 +295,10 @@ test("parse gradle dependencies", () => {
|
|
|
295
295
|
);
|
|
296
296
|
expect(parsedList.pkgList.length).toEqual(152);
|
|
297
297
|
expect(parsedList.dependenciesList.length).toEqual(153);
|
|
298
|
+
parsedList = parseGradleDep(
|
|
299
|
+
readFileSync("./test/data/gradle-android-app.dep", { encoding: "utf-8" })
|
|
300
|
+
);
|
|
301
|
+
expect(parsedList.pkgList.length).toEqual(101);
|
|
298
302
|
});
|
|
299
303
|
|
|
300
304
|
test("parse gradle projects", () => {
|
|
@@ -317,6 +321,11 @@ test("parse gradle projects", () => {
|
|
|
317
321
|
);
|
|
318
322
|
expect(retMap.rootProject).toEqual("fineract");
|
|
319
323
|
expect(retMap.projects.length).toEqual(22);
|
|
324
|
+
retMap = parseGradleProjects(
|
|
325
|
+
readFileSync("./test/data/gradle-android-app.dep", { encoding: "utf-8" })
|
|
326
|
+
);
|
|
327
|
+
expect(retMap.rootProject).toEqual("root");
|
|
328
|
+
expect(retMap.projects).toEqual([":app"]);
|
|
320
329
|
});
|
|
321
330
|
|
|
322
331
|
test("parse gradle properties", () => {
|
|
@@ -366,7 +375,7 @@ test("parse gradle properties", () => {
|
|
|
366
375
|
);
|
|
367
376
|
expect(retMap).toEqual({
|
|
368
377
|
rootProject: "java-test",
|
|
369
|
-
projects: [],
|
|
378
|
+
projects: [":app"],
|
|
370
379
|
metadata: {
|
|
371
380
|
group: "com.ajmalab.demo",
|
|
372
381
|
version: "latest",
|
|
@@ -411,6 +420,13 @@ test("parse gradle properties", () => {
|
|
|
411
420
|
);
|
|
412
421
|
expect(retMap.rootProject).toEqual("elasticsearch");
|
|
413
422
|
expect(retMap.projects.length).toEqual(409);
|
|
423
|
+
retMap = parseGradleProperties(
|
|
424
|
+
readFileSync("./test/data/gradle-properties-android.txt", {
|
|
425
|
+
encoding: "utf-8"
|
|
426
|
+
})
|
|
427
|
+
);
|
|
428
|
+
expect(retMap.rootProject).toEqual("CdxgenAndroidTest");
|
|
429
|
+
expect(retMap.projects.length).toEqual(2);
|
|
414
430
|
});
|
|
415
431
|
|
|
416
432
|
test("parse maven tree", () => {
|
|
@@ -1290,16 +1306,19 @@ test("parsePkgLock", async () => {
|
|
|
1290
1306
|
version: "2.0.0"
|
|
1291
1307
|
});
|
|
1292
1308
|
expect(deps[deps.length - 1].name).toEqual("zone.js");
|
|
1293
|
-
parsedList = await parsePkgLock("./test/data/package-lock-v3.json"
|
|
1309
|
+
parsedList = await parsePkgLock("./test/data/package-lock-v3.json", {
|
|
1310
|
+
projectVersion: "latest",
|
|
1311
|
+
projectName: "cdxgen"
|
|
1312
|
+
});
|
|
1294
1313
|
deps = parsedList.pkgList;
|
|
1295
1314
|
expect(deps.length).toEqual(879);
|
|
1296
1315
|
expect(parsedList.dependenciesList.length).toEqual(879);
|
|
1297
1316
|
expect(deps[0]).toEqual({
|
|
1298
|
-
"bom-ref": "pkg:npm
|
|
1317
|
+
"bom-ref": "pkg:npm/cdxgen@latest",
|
|
1299
1318
|
group: "",
|
|
1300
|
-
name: "
|
|
1319
|
+
name: "cdxgen",
|
|
1301
1320
|
type: "application",
|
|
1302
|
-
version: "
|
|
1321
|
+
version: "latest"
|
|
1303
1322
|
});
|
|
1304
1323
|
expect(deps[deps.length - 1].name).toEqual("yocto-queue");
|
|
1305
1324
|
parsedList = await parsePkgLock("./test/data/package-lock4.json");
|