@cyclonedx/cdxgen 8.0.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/analyzer.js ADDED
@@ -0,0 +1,189 @@
1
+ const babelParser = require("@babel/parser");
2
+ const babelTraverse = require("@babel/traverse").default;
3
+ const { join } = require("path");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const IGNORE_DIRS = [
8
+ "node_modules",
9
+ "venv",
10
+ "docs",
11
+ "test",
12
+ "tests",
13
+ "e2e",
14
+ "examples",
15
+ "cypress",
16
+ "site-packages"
17
+ ];
18
+
19
+ const IGNORE_FILE_PATTERN = new RegExp("(conf|test|spec|mock)\\.(js|ts)$", "i");
20
+
21
+ const getAllFiles = (dir, extn, files, result, regex) => {
22
+ files = files || fs.readdirSync(dir);
23
+ result = result || [];
24
+ regex = regex || new RegExp(`\\${extn}$`);
25
+
26
+ for (let i = 0; i < files.length; i++) {
27
+ if (IGNORE_FILE_PATTERN.test(files[i])) {
28
+ continue;
29
+ }
30
+ let file = join(dir, files[i]);
31
+ if (fs.statSync(file).isDirectory()) {
32
+ // Ignore directories
33
+ const dirName = path.basename(file);
34
+ if (
35
+ dirName.startsWith(".") ||
36
+ IGNORE_DIRS.includes(dirName.toLowerCase())
37
+ ) {
38
+ continue;
39
+ }
40
+ try {
41
+ result = getAllFiles(file, extn, fs.readdirSync(file), result, regex);
42
+ } catch (error) {
43
+ continue;
44
+ }
45
+ } else {
46
+ if (regex.test(file)) {
47
+ result.push(file);
48
+ }
49
+ }
50
+ }
51
+ return result;
52
+ };
53
+
54
+ const babelParserOptions = {
55
+ sourceType: "unambiguous",
56
+ allowImportExportEverywhere: true,
57
+ allowAwaitOutsideFunction: true,
58
+ allowReturnOutsideFunction: true,
59
+ allowSuperOutsideMethod: true,
60
+ errorRecovery: true,
61
+ allowUndeclaredExports: true,
62
+ attachComment: false,
63
+ plugins: [
64
+ "optionalChaining",
65
+ "classProperties",
66
+ "decorators-legacy",
67
+ "exportDefaultFrom",
68
+ "doExpressions",
69
+ "numericSeparator",
70
+ "dynamicImport",
71
+ "jsx",
72
+ "typescript"
73
+ ]
74
+ };
75
+
76
+ /**
77
+ * Filter only references to (t|jsx?) or (less|scss) files for now.
78
+ * Opt to use our relative paths.
79
+ */
80
+ const setFileRef = (allImports, file, pathway) => {
81
+ // remove unexpected extension imports
82
+ if (/\.(svg|png|jpg|d\.ts)/.test(pathway)) {
83
+ return;
84
+ }
85
+
86
+ // replace relative imports with full path
87
+ let module = pathway;
88
+ if (/\.\//g.test(pathway) || /\.\.\//g.test(pathway)) {
89
+ module = path.resolve(file, "..", pathway);
90
+ }
91
+
92
+ // initialise or increase reference count for file
93
+ if (allImports.hasOwnProperty(module)) {
94
+ allImports[module] = allImports[module] + 1;
95
+ } else {
96
+ allImports[module] = 1;
97
+ }
98
+
99
+ // Handle module package name
100
+ // Eg: zone.js/dist/zone will be referred to as zone.js in package.json
101
+ if (!path.isAbsolute(module) && module.includes("/")) {
102
+ const modPkg = module.split("/")[0];
103
+ if (allImports.hasOwnProperty(modPkg)) {
104
+ allImports[modPkg] = allImports[modPkg] + 1;
105
+ } else {
106
+ allImports[modPkg] = 1;
107
+ }
108
+ }
109
+ };
110
+
111
+ /**
112
+ * Check AST tree for any (j|tsx?) files and set a file
113
+ * references for any import, require or dynamic import files.
114
+ */
115
+ const parseFileASTTree = (file, allImports) => {
116
+ const ast = babelParser.parse(
117
+ fs.readFileSync(file, "utf-8"),
118
+ babelParserOptions
119
+ );
120
+ babelTraverse(ast, {
121
+ // Used for all ES6 import statements
122
+ ImportDeclaration: (path) => {
123
+ if (path && path.node) {
124
+ setFileRef(allImports, file, path.node.source.value);
125
+ }
126
+ },
127
+ // For require('') statements
128
+ Identifier: (path) => {
129
+ if (
130
+ path &&
131
+ path.node &&
132
+ path.node.name === "require" &&
133
+ path.parent.type === "CallExpression"
134
+ ) {
135
+ setFileRef(allImports, file, path.parent.arguments[0].value);
136
+ }
137
+ },
138
+ // Use for dynamic imports like routes.jsx
139
+ CallExpression: (path) => {
140
+ if (path && path.node && path.node.callee.type === "Import") {
141
+ setFileRef(allImports, file, path.node.arguments[0].value);
142
+ }
143
+ },
144
+ // Use for export barrells
145
+ ExportAllDeclaration: (path) => {
146
+ setFileRef(allImports, file, path.node.source.value);
147
+ },
148
+ ExportNamedDeclaration: (path) => {
149
+ // ensure there is a path export
150
+ if (path && path.node && path.node.source) {
151
+ setFileRef(allImports, file, path.node.source.value);
152
+ }
153
+ }
154
+ });
155
+ };
156
+
157
+ /**
158
+ * Return paths to all (j|tsx?) files.
159
+ */
160
+ const getAllSrcJSAndTSFiles = (src) =>
161
+ Promise.all([
162
+ getAllFiles(src, ".js"),
163
+ getAllFiles(src, ".jsx"),
164
+ getAllFiles(src, ".ts"),
165
+ getAllFiles(src, ".tsx")
166
+ ]);
167
+
168
+ /**
169
+ * Where Node CLI runs from.
170
+ */
171
+ const findJSImports = async (src) => {
172
+ const allImports = {};
173
+ const errFiles = [];
174
+ try {
175
+ const promiseMap = await getAllSrcJSAndTSFiles(src);
176
+ const srcFiles = promiseMap.flatMap((d) => d);
177
+ for (const file of srcFiles) {
178
+ try {
179
+ parseFileASTTree(file, allImports);
180
+ } catch (err) {
181
+ errFiles.push(file);
182
+ }
183
+ }
184
+ return allImports;
185
+ } catch (err) {
186
+ return allImports;
187
+ }
188
+ };
189
+ exports.findJSImports = findJSImports;
package/bin/cdxgen ADDED
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+
3
+ const bom = require("../index.js");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const jws = require("jws");
7
+ const crypto = require("crypto");
8
+ const bomServer = require("../server.js");
9
+
10
+ const args = require("yargs")
11
+ .option("output", {
12
+ alias: "o",
13
+ description: "Output file for bom.xml or bom.json. Default console"
14
+ })
15
+ .option("type", {
16
+ alias: "t",
17
+ description: "Project type"
18
+ })
19
+ .option("recurse", {
20
+ alias: "r",
21
+ type: "boolean",
22
+ description: "Recurse mode suitable for mono-repos"
23
+ })
24
+ .option("print", {
25
+ alias: "p",
26
+ type: "boolean",
27
+ description: "Print the SBoM as a table"
28
+ })
29
+ .option("resolve-class", {
30
+ alias: "c",
31
+ type: "boolean",
32
+ description: "Resolve class names for packages. jars only for now."
33
+ })
34
+ .option("deep", {
35
+ type: "boolean",
36
+ description:
37
+ "Perform deep searches for components. Useful while scanning live OS and oci images."
38
+ })
39
+ .option("server-url", {
40
+ description: "Dependency track url. Eg: https://deptrack.cyclonedx.io"
41
+ })
42
+ .option("api-key", {
43
+ description: "Dependency track api key"
44
+ })
45
+ .option("project-group", {
46
+ description: "Dependency track project group"
47
+ })
48
+ .option("project-name", {
49
+ description: "Dependency track project name. Default use the directory name"
50
+ })
51
+ .option("project-version", {
52
+ description: "Dependency track project version",
53
+ default: ""
54
+ })
55
+ .option("project-id", {
56
+ description:
57
+ "Dependency track project id. Either provide the id or the project name and version together"
58
+ })
59
+ .option("required-only", {
60
+ type: "boolean",
61
+ description: "Include only the packages with required scope on the SBoM."
62
+ })
63
+ .option("fail-on-error", {
64
+ type: "boolean",
65
+ description: "Fail if any dependency extractor fails."
66
+ })
67
+ .option("no-babel", {
68
+ type: "boolean",
69
+ description:
70
+ "Do not use babel to perform usage analysis for JavaScript/TypeScript projects."
71
+ })
72
+ .option("generate-key-and-sign", {
73
+ type: "boolean",
74
+ description:
75
+ "Generate an RSA public/private key pair and then sign the generated SBoM using JSON Web Signatures."
76
+ })
77
+ .option("server", {
78
+ type: "boolean",
79
+ description: "Run cdxgen as a server"
80
+ })
81
+ .option("server-host", {
82
+ description: "Listen address",
83
+ default: "127.0.0.1"
84
+ })
85
+ .option("server-port", {
86
+ description: "Listen port",
87
+ default: "9090"
88
+ })
89
+ .scriptName("cdxgen")
90
+ .version()
91
+ .help("h").argv;
92
+
93
+ if (args.version) {
94
+ const packageJsonAsString = fs.readFileSync(
95
+ path.join(__dirname, "../", "package.json"),
96
+ "utf-8"
97
+ );
98
+ const packageJson = JSON.parse(packageJsonAsString);
99
+
100
+ console.log(packageJson.version);
101
+ process.exit(0);
102
+ }
103
+
104
+ if (process.env.GLOBAL_AGENT_HTTP_PROXY || process.env.HTTP_PROXY) {
105
+ // Support standard HTTP_PROXY variable if the user doesn't override the namespace
106
+ if (!process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE) {
107
+ process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = "";
108
+ }
109
+ const globalAgent = require("global-agent");
110
+ globalAgent.bootstrap();
111
+ }
112
+
113
+ let filePath = args._[0] || ".";
114
+ if (!args.projectName) {
115
+ if (filePath !== ".") {
116
+ args.projectName = path.basename(filePath);
117
+ } else {
118
+ args.projectName = path.basename(path.resolve(filePath));
119
+ }
120
+ }
121
+
122
+ /**
123
+ * projectType: python, nodejs, java, golang
124
+ * multiProject: Boolean to indicate monorepo or multi-module projects
125
+ */
126
+ let options = {
127
+ dev: true,
128
+ projectType: args.type,
129
+ multiProject: args.recurse,
130
+ depth: 3,
131
+ output: args.output,
132
+ resolveClass: args.resolveClass,
133
+ installDeps: true,
134
+ requiredOnly: args.requiredOnly,
135
+ failOnError: args.failOnError,
136
+ noBabel: args.noBabel || args.babel === false,
137
+ deep: args.deep,
138
+ generateKeyAndSign: args.generateKeyAndSign,
139
+ project: args.projectId,
140
+ projectName: args.projectName,
141
+ projectGroup: args.projectGroup,
142
+ projectVersion: args.projectVersion,
143
+ server: args.server,
144
+ serverHost: args.serverHost,
145
+ serverPort: args.serverPort
146
+ };
147
+
148
+ /**
149
+ * Method to start the bom creation process
150
+ */
151
+ (async () => {
152
+ // Start SBoM server
153
+ if (args.server) {
154
+ return await bomServer.start(options);
155
+ }
156
+ const bomNSData = (await bom.createBom(filePath, options)) || {};
157
+
158
+ if (args.output) {
159
+ if (bomNSData.bomXmlFiles) {
160
+ console.log("BOM files produced:", bomNSData.bomXmlFiles);
161
+ } else {
162
+ const jsonFile = args.output.replace(".xml", ".json");
163
+ // Create bom json file
164
+ if (bomNSData.bomJson) {
165
+ let jsonPayload = undefined;
166
+ if (
167
+ typeof bomNSData.bomJson === "string" ||
168
+ bomNSData.bomJson instanceof String
169
+ ) {
170
+ fs.writeFileSync(jsonFile, bomNSData.bomJson);
171
+ jsonPayload = bomNSData.bomJson;
172
+ } else {
173
+ jsonPayload = JSON.stringify(bomNSData.bomJson, null, 2);
174
+ fs.writeFileSync(jsonFile, jsonPayload);
175
+ }
176
+ if (
177
+ jsonPayload &&
178
+ (args.generateKeyAndSign ||
179
+ (process.env.SBOM_SIGN_ALGORITHM &&
180
+ process.env.SBOM_SIGN_ALGORITHM !== "none" &&
181
+ process.env.SBOM_SIGN_PRIVATE_KEY &&
182
+ fs.existsSync(process.env.SBOM_SIGN_PRIVATE_KEY)))
183
+ ) {
184
+ let alg = process.env.SBOM_SIGN_ALGORITHM || "RS512";
185
+ if (alg.includes("none")) {
186
+ alg = "RS512";
187
+ }
188
+ let privateKeyToUse = undefined;
189
+ let jwkPublicKey = undefined;
190
+ if (args.generateKeyAndSign) {
191
+ const dirName = path.dirname(jsonFile);
192
+ const publicKeyFile = path.join(dirName, "public.key");
193
+ const privateKeyFile = path.join(dirName, "private.key");
194
+ const { privateKey, publicKey } = crypto.generateKeyPairSync(
195
+ "rsa",
196
+ {
197
+ modulusLength: 4096,
198
+ publicKeyEncoding: {
199
+ type: "spki",
200
+ format: "pem"
201
+ },
202
+ privateKeyEncoding: {
203
+ type: "pkcs8",
204
+ format: "pem"
205
+ }
206
+ }
207
+ );
208
+ fs.writeFileSync(publicKeyFile, publicKey);
209
+ fs.writeFileSync(privateKeyFile, privateKey);
210
+ console.log(
211
+ "Created public/private key pairs for testing purposes",
212
+ publicKeyFile,
213
+ privateKeyFile
214
+ );
215
+ privateKeyToUse = privateKey;
216
+ jwkPublicKey = crypto
217
+ .createPublicKey(publicKey)
218
+ .export({ format: "jwk" });
219
+ } else {
220
+ privateKeyToUse = fs.readFileSync(
221
+ process.env.SBOM_SIGN_PRIVATE_KEY,
222
+ "utf8"
223
+ );
224
+ if (
225
+ process.env.SBOM_SIGN_PUBLIC_KEY &&
226
+ fs.existsSync(process.env.SBOM_SIGN_PUBLIC_KEY)
227
+ ) {
228
+ jwkPublicKey = crypto
229
+ .createPublicKey(
230
+ fs.readFileSync(process.env.SBOM_SIGN_PUBLIC_KEY, "utf8")
231
+ )
232
+ .export({ format: "jwk" });
233
+ }
234
+ }
235
+ try {
236
+ const signature = jws.sign({
237
+ header: { alg },
238
+ payload: jsonPayload,
239
+ privateKey: privateKeyToUse
240
+ });
241
+ if (signature) {
242
+ const bomJsonUnsignedObj = JSON.parse(jsonPayload);
243
+ const signatureBlock = {
244
+ algorithm: alg,
245
+ value: signature
246
+ };
247
+ if (jwkPublicKey) {
248
+ signatureBlock.publicKey = jwkPublicKey;
249
+ }
250
+ bomJsonUnsignedObj.signature = signatureBlock;
251
+ fs.writeFileSync(
252
+ jsonFile,
253
+ JSON.stringify(bomJsonUnsignedObj, null, 2)
254
+ );
255
+ }
256
+ } catch (ex) {
257
+ console.log("SBoM signing was unsuccessful", ex);
258
+ console.log("Check if the private key was exported in PEM format");
259
+ }
260
+ }
261
+ }
262
+ // Create bom xml file
263
+ if (bomNSData.bomXml) {
264
+ const xmlFile = args.output.replace(".json", ".xml");
265
+ fs.writeFileSync(xmlFile, bomNSData.bomXml);
266
+ }
267
+ //
268
+ if (bomNSData.nsMapping && Object.keys(bomNSData.nsMapping).length) {
269
+ const nsFile = jsonFile + ".map";
270
+ fs.writeFileSync(nsFile, JSON.stringify(bomNSData.nsMapping));
271
+ console.log("Namespace mapping file written to", nsFile);
272
+ }
273
+ }
274
+ } else if (!args.print) {
275
+ if (bomNSData.bomJson) {
276
+ console.log(JSON.stringify(bomNSData.bomJson, null, 2));
277
+ } else if (bomNSData.bomXml) {
278
+ console.log(Buffer.from(bomNSData.bomXml).toString());
279
+ } else {
280
+ console.log("Unable to produce BOM for", filePath);
281
+ console.log("Try running the command with -t <type> or -r argument");
282
+ }
283
+ }
284
+
285
+ // Automatically submit the bom data
286
+ if (args.serverUrl && args.apiKey) {
287
+ try {
288
+ const dbody = await bom.submitBom(args, bomNSData.bomXml);
289
+ console.log("Response from server", dbody);
290
+ } catch (err) {
291
+ console.log(err);
292
+ }
293
+ }
294
+
295
+ if (args.print && bomNSData.bomJson && bomNSData.bomJson.components) {
296
+ const { table } = require("table");
297
+ const data = [["Group", "Name", "Version", "Scope"]];
298
+ for (let comp of bomNSData.bomJson.components) {
299
+ data.push([comp.group || "", comp.name, comp.version, comp.scope || ""]);
300
+ }
301
+ const config = {
302
+ header: {
303
+ alignment: "center",
304
+ content: "Software Bill-of-Materials\nGenerated by @cyclonedx/cdxgen"
305
+ }
306
+ };
307
+ console.log(table(data, config));
308
+ console.log(
309
+ "BOM includes",
310
+ bomNSData.bomJson.components.length,
311
+ "components and",
312
+ bomNSData.bomJson.dependencies.length,
313
+ "dependencies"
314
+ );
315
+ }
316
+ })();