@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/index.js ADDED
@@ -0,0 +1,4292 @@
1
+ const parsePackageJsonName = require("parse-packagejson-name");
2
+ const os = require("os");
3
+ const pathLib = require("path");
4
+ const ssri = require("ssri");
5
+ const fs = require("fs");
6
+ const got = require("got");
7
+ const { v4: uuidv4 } = require("uuid");
8
+ const { PackageURL } = require("packageurl-js");
9
+ const builder = require("xmlbuilder");
10
+ const utils = require("./utils");
11
+ const { spawnSync } = require("child_process");
12
+ const selfPjson = require("./package.json");
13
+ const { findJSImports } = require("./analyzer");
14
+ const semver = require("semver");
15
+ const dockerLib = require("./docker");
16
+ const binaryLib = require("./binary");
17
+ const osQueries = require("./queries.json");
18
+ const isWin = require("os").platform() === "win32";
19
+
20
+ const { table } = require("table");
21
+
22
+ // Construct gradle cache directory
23
+ let GRADLE_CACHE_DIR =
24
+ process.env.GRADLE_CACHE_DIR ||
25
+ pathLib.join(os.homedir(), ".gradle", "caches", "modules-2", "files-2.1");
26
+ if (process.env.GRADLE_USER_HOME) {
27
+ GRADLE_CACHE_DIR =
28
+ process.env.GRADLE_USER_HOME + "/caches/modules-2/files-2.1";
29
+ }
30
+
31
+ // Clojure CLI
32
+ let CLJ_CMD = "clj";
33
+ if (process.env.CLJ_CMD) {
34
+ CLJ_CMD = process.env.CLJ_CMD;
35
+ }
36
+
37
+ let LEIN_CMD = "lein";
38
+ if (process.env.LEIN_CMD) {
39
+ LEIN_CMD = process.env.LEIN_CMD;
40
+ }
41
+
42
+ // Construct sbt cache directory
43
+ let SBT_CACHE_DIR =
44
+ process.env.SBT_CACHE_DIR || pathLib.join(os.homedir(), ".ivy2", "cache");
45
+
46
+ // Debug mode flag
47
+ const DEBUG_MODE =
48
+ process.env.SCAN_DEBUG_MODE === "debug" ||
49
+ process.env.SHIFTLEFT_LOGGING_LEVEL === "debug" ||
50
+ process.env.NODE_ENV === "development";
51
+
52
+ // CycloneDX Hash pattern
53
+ const HASH_PATTERN =
54
+ "^([a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128})$";
55
+
56
+ // Timeout milliseconds. Default 10 mins
57
+ const TIMEOUT_MS = parseInt(process.env.CDXGEN_TIMEOUT_MS) || 10 * 60 * 1000;
58
+
59
+ const determineParentComponent = (options) => {
60
+ let parentComponent = undefined;
61
+ if (options.projectName) {
62
+ parentComponent = {
63
+ group: options.projectGroup || "",
64
+ name: options.projectName,
65
+ version: "" + options.projectVersion || "",
66
+ type: "application"
67
+ };
68
+ } else if (
69
+ options.parentComponent &&
70
+ Object.keys(options.parentComponent).length
71
+ ) {
72
+ return options.parentComponent;
73
+ }
74
+ return parentComponent;
75
+ };
76
+
77
+ /**
78
+ * Method to create global external references
79
+ *
80
+ * @param pkg
81
+ * @returns {Array}
82
+ */
83
+ function addGlobalReferences(src, filename, format = "xml") {
84
+ let externalReferences = [];
85
+ if (format === "json") {
86
+ externalReferences.push({
87
+ type: "other",
88
+ url: src,
89
+ comment: "Base path"
90
+ });
91
+ } else {
92
+ externalReferences.push({
93
+ reference: { "@type": "other", url: src, comment: "Base path" }
94
+ });
95
+ }
96
+ let packageFileMeta = filename;
97
+ if (!filename.includes(src)) {
98
+ packageFileMeta = pathLib.join(src, filename);
99
+ }
100
+ if (format === "json") {
101
+ externalReferences.push({
102
+ type: "other",
103
+ url: packageFileMeta,
104
+ comment: "Package file"
105
+ });
106
+ } else {
107
+ externalReferences.push({
108
+ reference: {
109
+ "@type": "other",
110
+ url: packageFileMeta,
111
+ comment: "Package file"
112
+ }
113
+ });
114
+ }
115
+ return externalReferences;
116
+ }
117
+
118
+ /**
119
+ * Function to create the services block
120
+ */
121
+ function addServices(services, format = "xml") {
122
+ let serv_list = [];
123
+ for (const aserv of services) {
124
+ if (format === "xml") {
125
+ let service = {
126
+ "@bom-ref": aserv["bom-ref"],
127
+ group: aserv.group || "",
128
+ name: aserv.name,
129
+ version: aserv.version | "latest"
130
+ };
131
+ delete service["bom-ref"];
132
+ const aentry = {
133
+ service
134
+ };
135
+ serv_list.push(aentry);
136
+ } else {
137
+ serv_list.push(aserv);
138
+ }
139
+ }
140
+ return serv_list;
141
+ }
142
+
143
+ /**
144
+ * Function to create the dependency block
145
+ */
146
+ function addDependencies(dependencies) {
147
+ let deps_list = [];
148
+ for (const adep of dependencies) {
149
+ let dependsOnList = adep.dependsOn.map((v) => ({
150
+ "@ref": v
151
+ }));
152
+ const aentry = {
153
+ dependency: { "@ref": adep.ref }
154
+ };
155
+ if (dependsOnList.length) {
156
+ aentry.dependency.dependency = dependsOnList;
157
+ }
158
+ deps_list.push(aentry);
159
+ }
160
+ return deps_list;
161
+ }
162
+
163
+ /**
164
+ * Function to create metadata block
165
+ *
166
+ */
167
+ function addMetadata(parentComponent = {}, format = "xml", options = {}) {
168
+ // DO NOT fork this project to just change the vendor or author's name
169
+ // Try to contribute to this project by sending PR or filing issues
170
+ let metadata = {
171
+ timestamp: new Date().toISOString(),
172
+ tools: [
173
+ {
174
+ tool: {
175
+ vendor: "cyclonedx",
176
+ name: "cdxgen",
177
+ version: selfPjson.version
178
+ }
179
+ }
180
+ ],
181
+ authors: [
182
+ {
183
+ author: { name: "Prabhu Subramanian", email: "prabhu@appthreat.com" }
184
+ }
185
+ ],
186
+ supplier: undefined
187
+ };
188
+ if (format === "json") {
189
+ metadata.tools = [
190
+ {
191
+ vendor: "cyclonedx",
192
+ name: "cdxgen",
193
+ version: selfPjson.version
194
+ }
195
+ ];
196
+ metadata.authors = [
197
+ { name: "Prabhu Subramanian", email: "prabhu@appthreat.com" }
198
+ ];
199
+ }
200
+ if (
201
+ parentComponent &&
202
+ Object.keys(parentComponent) &&
203
+ Object.keys(parentComponent).length
204
+ ) {
205
+ const allPComponents = listComponents(
206
+ {},
207
+ {},
208
+ parentComponent,
209
+ parentComponent.type,
210
+ format
211
+ );
212
+ if (allPComponents.length) {
213
+ const firstPComp = allPComponents[0];
214
+ if (format == "xml" && firstPComp.component) {
215
+ metadata.component = firstPComp.component;
216
+ } else {
217
+ metadata.component = firstPComp;
218
+ }
219
+ } else {
220
+ // As a fallback, retain the parent component
221
+ if (format === "json") {
222
+ metadata.component = parentComponent;
223
+ }
224
+ }
225
+ }
226
+ if (options) {
227
+ const mproperties = [];
228
+ if (options.exportData) {
229
+ const inspectData = options.exportData.inspectData;
230
+ if (inspectData) {
231
+ if (inspectData.Id) {
232
+ mproperties.push({
233
+ name: "oci:image:Id",
234
+ value: inspectData.Id
235
+ });
236
+ }
237
+ if (
238
+ inspectData.RepoTags &&
239
+ Array.isArray(inspectData.RepoTags) &&
240
+ inspectData.RepoTags.length
241
+ ) {
242
+ mproperties.push({
243
+ name: "oci:image:RepoTag",
244
+ value: inspectData.RepoTags[0]
245
+ });
246
+ }
247
+ if (
248
+ inspectData.RepoDigests &&
249
+ Array.isArray(inspectData.RepoDigests) &&
250
+ inspectData.RepoDigests.length
251
+ ) {
252
+ mproperties.push({
253
+ name: "oci:image:RepoDigest",
254
+ value: inspectData.RepoDigests[0]
255
+ });
256
+ }
257
+ if (inspectData.Created) {
258
+ mproperties.push({
259
+ name: "oci:image:Created",
260
+ value: inspectData.Created
261
+ });
262
+ }
263
+ if (inspectData.Architecture) {
264
+ mproperties.push({
265
+ name: "oci:image:Architecture",
266
+ value: inspectData.Architecture
267
+ });
268
+ }
269
+ if (inspectData.Os) {
270
+ mproperties.push({
271
+ name: "oci:image:Os",
272
+ value: inspectData.Os
273
+ });
274
+ }
275
+ }
276
+ const manifestList = options.exportData.manifest;
277
+ if (manifestList && Array.isArray(manifestList) && manifestList.length) {
278
+ const manifest = manifestList[0] || {};
279
+ if (manifest.Config) {
280
+ mproperties.push({
281
+ name: "oci:image:manifest:Config",
282
+ value: manifest.Config
283
+ });
284
+ }
285
+ if (
286
+ manifest.Layers &&
287
+ Array.isArray(manifest.Layers) &&
288
+ manifest.Layers.length
289
+ ) {
290
+ mproperties.push({
291
+ name: "oci:image:manifest:Layers",
292
+ value: manifest.Layers.join("\\n")
293
+ });
294
+ }
295
+ }
296
+ const lastLayerConfig = options.exportData.lastLayerConfig;
297
+ if (lastLayerConfig) {
298
+ if (lastLayerConfig.id) {
299
+ mproperties.push({
300
+ name: "oci:image:lastLayer:Id",
301
+ value: lastLayerConfig.id
302
+ });
303
+ }
304
+ if (lastLayerConfig.parent) {
305
+ mproperties.push({
306
+ name: "oci:image:lastLayer:ParentId",
307
+ value: lastLayerConfig.parent
308
+ });
309
+ }
310
+ if (lastLayerConfig.created) {
311
+ mproperties.push({
312
+ name: "oci:image:lastLayer:Created",
313
+ value: lastLayerConfig.created
314
+ });
315
+ }
316
+ if (lastLayerConfig.config) {
317
+ const env = lastLayerConfig.config.Env;
318
+ if (env && Array.isArray(env) && env.length) {
319
+ mproperties.push({
320
+ name: "oci:image:lastLayer:Env",
321
+ value: env.join("\\n")
322
+ });
323
+ }
324
+ const ccmd = lastLayerConfig.config.Cmd;
325
+ if (ccmd && Array.isArray(ccmd) && ccmd.length) {
326
+ mproperties.push({
327
+ name: "oci:image:lastLayer:Cmd",
328
+ value: ccmd.join(" ")
329
+ });
330
+ }
331
+ }
332
+ }
333
+ }
334
+ if (options.allOSComponentTypes && options.allOSComponentTypes.length) {
335
+ mproperties.push({
336
+ name: "oci:image:componentTypes",
337
+ value: options.allOSComponentTypes.join("\\n")
338
+ });
339
+ }
340
+
341
+ if (mproperties.length) {
342
+ if (format === "json") {
343
+ metadata.properties = mproperties;
344
+ } else {
345
+ metadata.properties = mproperties.map((v) => {
346
+ return {
347
+ property: {
348
+ "@name": v.name,
349
+ "#text": v.value
350
+ }
351
+ };
352
+ });
353
+ }
354
+ }
355
+ }
356
+ return metadata;
357
+ }
358
+
359
+ /**
360
+ * Method to create external references
361
+ *
362
+ * @param pkg
363
+ * @returns {Array}
364
+ */
365
+ function addExternalReferences(opkg, format = "xml") {
366
+ let externalReferences = [];
367
+ let pkgList = [];
368
+ if (Array.isArray(opkg)) {
369
+ pkgList = opkg;
370
+ } else {
371
+ pkgList = [opkg];
372
+ }
373
+ for (const pkg of pkgList) {
374
+ if (pkg.externalReferences) {
375
+ if (format === "xml") {
376
+ for (const ref of pkg.externalReferences) {
377
+ // If the value already comes from json format
378
+ if (ref.type && ref.url) {
379
+ externalReferences.push({
380
+ reference: { "@type": ref.type, url: ref.url }
381
+ });
382
+ }
383
+ }
384
+ } else {
385
+ externalReferences.concat(pkg.externalReferences);
386
+ }
387
+ } else {
388
+ if (format === "xml") {
389
+ if (pkg.homepage && pkg.homepage.url) {
390
+ externalReferences.push({
391
+ reference: { "@type": "website", url: pkg.homepage.url }
392
+ });
393
+ }
394
+ if (pkg.bugs && pkg.bugs.url) {
395
+ externalReferences.push({
396
+ reference: { "@type": "issue-tracker", url: pkg.bugs.url }
397
+ });
398
+ }
399
+ if (pkg.repository && pkg.repository.url) {
400
+ externalReferences.push({
401
+ reference: { "@type": "vcs", url: pkg.repository.url }
402
+ });
403
+ }
404
+ } else {
405
+ if (pkg.homepage && pkg.homepage.url) {
406
+ externalReferences.push({
407
+ type: "website",
408
+ url: pkg.homepage.url
409
+ });
410
+ }
411
+ if (pkg.bugs && pkg.bugs.url) {
412
+ externalReferences.push({
413
+ type: "issue-tracker",
414
+ url: pkg.bugs.url
415
+ });
416
+ }
417
+ if (pkg.repository && pkg.repository.url) {
418
+ externalReferences.push({
419
+ type: "vcs",
420
+ url: pkg.repository.url
421
+ });
422
+ }
423
+ }
424
+ }
425
+ }
426
+ return externalReferences;
427
+ }
428
+
429
+ /**
430
+ * For all modules in the specified package, creates a list of
431
+ * component objects from each one.
432
+ */
433
+ exports.listComponents = listComponents;
434
+ function listComponents(
435
+ options,
436
+ allImports,
437
+ pkg,
438
+ ptype = "npm",
439
+ format = "xml"
440
+ ) {
441
+ let compMap = {};
442
+ let isRootPkg = ptype === "npm";
443
+ if (Array.isArray(pkg)) {
444
+ pkg.forEach((p) => {
445
+ addComponent(options, allImports, p, ptype, compMap, false, format);
446
+ });
447
+ } else {
448
+ addComponent(options, allImports, pkg, ptype, compMap, isRootPkg, format);
449
+ }
450
+ if (format === "xml") {
451
+ return Object.keys(compMap).map((k) => ({ component: compMap[k] }));
452
+ } else {
453
+ return Object.keys(compMap).map((k) => compMap[k]);
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Given the specified package, create a CycloneDX component and add it to the list.
459
+ */
460
+ function addComponent(
461
+ options,
462
+ allImports,
463
+ pkg,
464
+ ptype,
465
+ compMap,
466
+ isRootPkg = false,
467
+ format = "xml"
468
+ ) {
469
+ if (!pkg || pkg.extraneous) {
470
+ return;
471
+ }
472
+ if (!isRootPkg) {
473
+ let pkgIdentifier = parsePackageJsonName(pkg.name);
474
+ let publisher = pkg.publisher || "";
475
+ let group = pkg.group || pkgIdentifier.scope;
476
+ // Create empty group
477
+ group = group || "";
478
+ let name = pkgIdentifier.fullName || pkg.name || "";
479
+ // name is mandatory
480
+ if (!name) {
481
+ return;
482
+ }
483
+ if (!ptype && pkg.qualifiers && pkg.qualifiers.type === "jar") {
484
+ ptype = "maven";
485
+ }
486
+ // Skip @types package for npm
487
+ if (
488
+ ptype == "npm" &&
489
+ (group === "types" || !name || name.startsWith("@types"))
490
+ ) {
491
+ return;
492
+ }
493
+ let version = pkg.version;
494
+ if (!version || ["dummy", "ignore"].includes(version)) {
495
+ return;
496
+ }
497
+ let licenses = pkg.licenses || utils.getLicenses(pkg, format);
498
+
499
+ let purl =
500
+ pkg.purl ||
501
+ new PackageURL(ptype, group, name, version, pkg.qualifiers, pkg.subpath);
502
+ let purlString = purl.toString();
503
+ purlString = decodeURIComponent(purlString);
504
+ let description = { "#cdata": pkg.description };
505
+ if (format === "json") {
506
+ description = pkg.description || "";
507
+ }
508
+ let compScope = pkg.scope;
509
+ if (allImports) {
510
+ const impPkgs = Object.keys(allImports);
511
+ if (
512
+ impPkgs.includes(name) ||
513
+ impPkgs.includes(group + "/" + name) ||
514
+ impPkgs.includes("@" + group + "/" + name) ||
515
+ impPkgs.includes(group) ||
516
+ impPkgs.includes("@" + group)
517
+ ) {
518
+ compScope = "required";
519
+ } else if (impPkgs.length) {
520
+ compScope = "optional";
521
+ }
522
+ }
523
+ if (options.requiredOnly && ["optional", "excluded"].includes(compScope)) {
524
+ return;
525
+ }
526
+ let component = {
527
+ publisher,
528
+ group,
529
+ name,
530
+ version,
531
+ description,
532
+ scope: compScope,
533
+ hashes: [],
534
+ licenses,
535
+ purl: purlString,
536
+ externalReferences: addExternalReferences(pkg, format)
537
+ };
538
+ if (format === "xml") {
539
+ component["@type"] = determinePackageType(pkg);
540
+ component["@bom-ref"] = purlString;
541
+ } else {
542
+ component["type"] = determinePackageType(pkg);
543
+ component["bom-ref"] = purlString;
544
+ }
545
+ if (
546
+ component.externalReferences === undefined ||
547
+ component.externalReferences.length === 0
548
+ ) {
549
+ delete component.externalReferences;
550
+ }
551
+
552
+ processHashes(pkg, component, format);
553
+ // Retain any component properties
554
+ if (format === "json" && pkg.properties && pkg.properties.length) {
555
+ component.properties = pkg.properties;
556
+ }
557
+ if (compMap[component.purl]) return; //remove cycles
558
+ compMap[component.purl] = component;
559
+ }
560
+ if (pkg.dependencies) {
561
+ Object.keys(pkg.dependencies)
562
+ .map((x) => pkg.dependencies[x])
563
+ .filter((x) => typeof x !== "string") //remove cycles
564
+ .map((x) =>
565
+ addComponent(options, allImports, x, ptype, compMap, false, format)
566
+ );
567
+ }
568
+ }
569
+
570
+ /**
571
+ * If the author has described the module as a 'framework', the take their
572
+ * word for it, otherwise, identify the module as a 'library'.
573
+ */
574
+ function determinePackageType(pkg) {
575
+ if (pkg.type === "application") {
576
+ return "application";
577
+ }
578
+ if (pkg.purl) {
579
+ try {
580
+ let purl = PackageURL.fromString(pkg.purl);
581
+ if (purl.type) {
582
+ if (["docker", "oci", "container"].includes(purl.type)) {
583
+ return "container";
584
+ }
585
+ if (["github"].includes(purl.type)) {
586
+ return "application";
587
+ }
588
+ }
589
+ if (purl.namespace) {
590
+ for (const cf of [
591
+ "System.Web",
592
+ "System.ServiceModel",
593
+ "System.Data",
594
+ "spring",
595
+ "flask",
596
+ "django",
597
+ "beego",
598
+ "chi",
599
+ "echo",
600
+ "gin",
601
+ "gorilla",
602
+ "rye",
603
+ "httprouter",
604
+ "akka",
605
+ "dropwizard",
606
+ "vertx",
607
+ "gwt",
608
+ "jax-rs",
609
+ "jax-ws",
610
+ "jsf",
611
+ "play",
612
+ "spark",
613
+ "struts",
614
+ "angular",
615
+ "react",
616
+ "next",
617
+ "ember",
618
+ "express",
619
+ "knex",
620
+ "vue",
621
+ "aiohttp",
622
+ "bottle",
623
+ "cherrypy",
624
+ "drt",
625
+ "falcon",
626
+ "hug",
627
+ "pyramid",
628
+ "sanic",
629
+ "tornado",
630
+ "vibora"
631
+ ]) {
632
+ if (purl.namespace.includes(cf)) {
633
+ return "framework";
634
+ }
635
+ }
636
+ }
637
+ } catch (e) {
638
+ // continue regardless of error
639
+ }
640
+ } else if (pkg.group) {
641
+ if (["actions"].includes(pkg.group)) {
642
+ return "application";
643
+ }
644
+ }
645
+ if (Object.prototype.hasOwnProperty.call(pkg, "keywords")) {
646
+ for (let keyword of pkg.keywords) {
647
+ if (keyword.toLowerCase() === "framework") {
648
+ return "framework";
649
+ }
650
+ }
651
+ }
652
+ return "library";
653
+ }
654
+
655
+ /**
656
+ * Uses the SHA1 shasum (if present) otherwise utilizes Subresource Integrity
657
+ * of the package with support for multiple hashing algorithms.
658
+ */
659
+ function processHashes(pkg, component, format = "xml") {
660
+ if (pkg.hashes) {
661
+ // This attribute would be available when we read a bom json directly
662
+ // Eg: cyclonedx-maven-plugin. See: Bugs: #172, #175
663
+ for (const ahash of pkg.hashes) {
664
+ addComponentHash(ahash.alg, ahash.content, component, format);
665
+ }
666
+ } else if (pkg._shasum) {
667
+ let ahash = { "@alg": "SHA-1", "#text": pkg._shasum };
668
+ if (format === "json") {
669
+ ahash = { alg: "SHA-1", content: pkg._shasum };
670
+ component.hashes.push(ahash);
671
+ } else {
672
+ component.hashes.push({
673
+ hash: ahash
674
+ });
675
+ }
676
+ } else if (pkg._integrity) {
677
+ let integrity = ssri.parse(pkg._integrity) || {};
678
+ // Components may have multiple hashes with various lengths. Check each one
679
+ // that is supported by the CycloneDX specification.
680
+ if (Object.prototype.hasOwnProperty.call(integrity, "sha512")) {
681
+ addComponentHash(
682
+ "SHA-512",
683
+ integrity.sha512[0].digest,
684
+ component,
685
+ format
686
+ );
687
+ }
688
+ if (Object.prototype.hasOwnProperty.call(integrity, "sha384")) {
689
+ addComponentHash(
690
+ "SHA-384",
691
+ integrity.sha384[0].digest,
692
+ component,
693
+ format
694
+ );
695
+ }
696
+ if (Object.prototype.hasOwnProperty.call(integrity, "sha256")) {
697
+ addComponentHash(
698
+ "SHA-256",
699
+ integrity.sha256[0].digest,
700
+ component,
701
+ format
702
+ );
703
+ }
704
+ if (Object.prototype.hasOwnProperty.call(integrity, "sha1")) {
705
+ addComponentHash("SHA-1", integrity.sha1[0].digest, component, format);
706
+ }
707
+ }
708
+ if (component.hashes.length === 0) {
709
+ delete component.hashes; // If no hashes exist, delete the hashes node (it's optional)
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Adds a hash to component.
715
+ */
716
+ function addComponentHash(alg, digest, component, format = "xml") {
717
+ let hash = "";
718
+ // If it is a valid hash simply use it
719
+ if (new RegExp(HASH_PATTERN).test(digest)) {
720
+ hash = digest;
721
+ } else {
722
+ // Check if base64 encoded
723
+ const isBase64Encoded =
724
+ Buffer.from(digest, "base64").toString("base64") === digest;
725
+ hash = isBase64Encoded
726
+ ? Buffer.from(digest, "base64").toString("hex")
727
+ : digest;
728
+ }
729
+ let ahash = { "@alg": alg, "#text": hash };
730
+ if (format === "json") {
731
+ ahash = { alg: alg, content: hash };
732
+ component.hashes.push(ahash);
733
+ } else {
734
+ component.hashes.push({ hash: ahash });
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Return Bom in xml format
740
+ *
741
+ * @param {String} Serial number
742
+ * @param {Object} parentComponent Parent component object
743
+ * @param {Array} components Bom components
744
+ * @param {Object} context Context object
745
+ * @returns bom xml string
746
+ */
747
+ const buildBomXml = (
748
+ serialNum,
749
+ parentComponent,
750
+ components,
751
+ context,
752
+ options = {}
753
+ ) => {
754
+ const bom = builder
755
+ .create("bom", { encoding: "utf-8", separateArrayItems: true })
756
+ .att("xmlns", "http://cyclonedx.org/schema/bom/1.4");
757
+ bom.att("serialNumber", serialNum);
758
+ bom.att("version", 1);
759
+ const metadata = addMetadata(parentComponent, "xml", options);
760
+ bom.ele("metadata").ele(metadata);
761
+ if (components && components.length) {
762
+ bom.ele("components").ele(components);
763
+ if (context && context.src && context.filename) {
764
+ bom
765
+ .ele("externalReferences")
766
+ .ele(addGlobalReferences(context.src, context.filename, "xml"));
767
+ }
768
+ if (context) {
769
+ if (context.services && context.services.length) {
770
+ bom.ele("services").ele(addServices(context.services, "xml"));
771
+ }
772
+ if (context.dependencies && context.dependencies.length) {
773
+ bom.ele("dependencies").ele(addDependencies(context.dependencies));
774
+ }
775
+ }
776
+ const bomString = bom.end({
777
+ pretty: true,
778
+ indent: " ",
779
+ newline: "\n",
780
+ width: 0,
781
+ allowEmpty: false,
782
+ spacebeforeslash: ""
783
+ });
784
+ return bomString;
785
+ }
786
+ return "";
787
+ };
788
+
789
+ /**
790
+ * Return the BOM in xml, json format including any namespace mapping
791
+ */
792
+ const buildBomNSData = (options, pkgInfo, ptype, context) => {
793
+ const bomNSData = {
794
+ bomXml: undefined,
795
+ bomXmlFiles: undefined,
796
+ bomJson: undefined,
797
+ bomJsonFiles: undefined,
798
+ nsMapping: undefined,
799
+ dependencies: undefined,
800
+ parentComponent: undefined
801
+ };
802
+ const serialNum = "urn:uuid:" + uuidv4();
803
+ let allImports = {};
804
+ if (context && context.allImports) {
805
+ allImports = context.allImports;
806
+ }
807
+ const nsMapping = context.nsMapping || {};
808
+ const dependencies = context.dependencies || [];
809
+ const parentComponent =
810
+ determineParentComponent(options) || context.parentComponent;
811
+ const metadata = addMetadata(parentComponent, "json", options);
812
+ const components = listComponents(options, allImports, pkgInfo, ptype, "xml");
813
+ if (components && (components.length || parentComponent)) {
814
+ const bomString = buildBomXml(
815
+ serialNum,
816
+ parentComponent,
817
+ components,
818
+ context,
819
+ options
820
+ );
821
+ // CycloneDX 1.4 Json Template
822
+ const jsonTpl = {
823
+ bomFormat: "CycloneDX",
824
+ specVersion: "1.4",
825
+ serialNumber: serialNum,
826
+ version: 1,
827
+ metadata: metadata,
828
+ components: listComponents(options, allImports, pkgInfo, ptype, "json"),
829
+ dependencies
830
+ };
831
+ if (context && context.src && context.filename) {
832
+ jsonTpl.externalReferences = addGlobalReferences(
833
+ context.src,
834
+ context.filename,
835
+ "json"
836
+ );
837
+ }
838
+ bomNSData.bomXml = bomString;
839
+ bomNSData.bomJson = jsonTpl;
840
+ bomNSData.nsMapping = nsMapping;
841
+ bomNSData.dependencies = dependencies;
842
+ bomNSData.parentComponent = parentComponent;
843
+ }
844
+ return bomNSData;
845
+ };
846
+
847
+ /**
848
+ * Function to create bom string for Java jars
849
+ *
850
+ * @param path to the project
851
+ * @param options Parse options from the cli
852
+ */
853
+ const createJarBom = (path, options) => {
854
+ console.log(
855
+ `About to create SBoM for all jar files under ${path}. This would take a while ...`
856
+ );
857
+ let pkgList = [];
858
+ let jarFiles = utils.getAllFiles(
859
+ path,
860
+ (options.multiProject ? "**/" : "") + "*.[jw]ar"
861
+ );
862
+ // Jenkins plugins
863
+ const hpiFiles = utils.getAllFiles(
864
+ path,
865
+ (options.multiProject ? "**/" : "") + "*.hpi"
866
+ );
867
+ if (hpiFiles.length) {
868
+ jarFiles = jarFiles.concat(hpiFiles);
869
+ }
870
+ let tempDir = fs.mkdtempSync(pathLib.join(os.tmpdir(), "jar-deps-"));
871
+ for (let jar of jarFiles) {
872
+ if (DEBUG_MODE) {
873
+ console.log(`Parsing ${jar}`);
874
+ }
875
+ const dlist = utils.extractJarArchive(jar, tempDir);
876
+ if (dlist && dlist.length) {
877
+ pkgList = pkgList.concat(dlist);
878
+ }
879
+ }
880
+ // Clean up
881
+ if (tempDir && tempDir.startsWith(os.tmpdir()) && fs.rmSync) {
882
+ console.log(`Cleaning up ${tempDir}`);
883
+ fs.rmSync(tempDir, { recursive: true, force: true });
884
+ }
885
+ return buildBomNSData(options, pkgList, "maven", {
886
+ src: path,
887
+ filename: jarFiles.join(", "),
888
+ nsMapping: {}
889
+ });
890
+ };
891
+
892
+ /**
893
+ * Function to create bom string for Java projects
894
+ *
895
+ * @param path to the project
896
+ * @param options Parse options from the cli
897
+ */
898
+ const createJavaBom = async (path, options) => {
899
+ let jarNSMapping = {};
900
+ let pkgList = [];
901
+ let dependencies = [];
902
+ // cyclone-dx-maven plugin creates a component for the app under metadata
903
+ // This is subsequently referred to in the dependencies list
904
+ let parentComponent = {};
905
+ // war/ear mode
906
+ if (path.endsWith(".war")) {
907
+ // Check if the file exists
908
+ if (fs.existsSync(path)) {
909
+ if (DEBUG_MODE) {
910
+ console.log(`Retrieving packages from ${path}`);
911
+ }
912
+ let tempDir = fs.mkdtempSync(pathLib.join(os.tmpdir(), "war-deps-"));
913
+ pkgList = utils.extractJarArchive(path, tempDir);
914
+ if (pkgList.length) {
915
+ pkgList = await utils.getMvnMetadata(pkgList);
916
+ }
917
+ // Should we attempt to resolve class names
918
+ if (options.resolveClass) {
919
+ console.log(
920
+ "Creating class names list based on available jars. This might take a few mins ..."
921
+ );
922
+ jarNSMapping = utils.collectJarNS(tempDir);
923
+ }
924
+ // Clean up
925
+ if (tempDir && tempDir.startsWith(os.tmpdir()) && fs.rmSync) {
926
+ console.log(`Cleaning up ${tempDir}`);
927
+ fs.rmSync(tempDir, { recursive: true, force: true });
928
+ }
929
+ } else {
930
+ console.log(`${path} doesn't exist`);
931
+ }
932
+ return buildBomNSData(options, pkgList, "maven", {
933
+ src: pathLib.dirname(path),
934
+ filename: path,
935
+ nsMapping: jarNSMapping,
936
+ dependencies,
937
+ parentComponent
938
+ });
939
+ } else {
940
+ // maven - pom.xml
941
+ const pomFiles = utils.getAllFiles(
942
+ path,
943
+ (options.multiProject ? "**/" : "") + "pom.xml"
944
+ );
945
+ if (pomFiles && pomFiles.length) {
946
+ let mvnArgs = [
947
+ "org.cyclonedx:cyclonedx-maven-plugin:2.7.2:makeAggregateBom",
948
+ "-DoutputName=bom"
949
+ ];
950
+ // By using quiet mode we can reduce the maxBuffer used and avoid crashes
951
+ if (!DEBUG_MODE) {
952
+ mvnArgs.push("-q");
953
+ }
954
+ // Support for passing additional settings and profile to maven
955
+ if (process.env.MVN_ARGS) {
956
+ const addArgs = process.env.MVN_ARGS.split(" ");
957
+ mvnArgs = mvnArgs.concat(addArgs);
958
+ }
959
+ for (let f of pomFiles) {
960
+ const basePath = pathLib.dirname(f);
961
+ let mavenCmd = utils.getMavenCommand(basePath, path);
962
+ // Should we attempt to resolve class names
963
+ if (options.resolveClass) {
964
+ console.log(
965
+ "Creating class names list based on available jars. This might take a few mins ..."
966
+ );
967
+ jarNSMapping = utils.collectMvnDependencies(mavenCmd, basePath);
968
+ }
969
+ console.log(
970
+ `Executing '${mavenCmd} ${mvnArgs.join(" ")}' in`,
971
+ basePath
972
+ );
973
+ let result = spawnSync(mavenCmd, mvnArgs, {
974
+ cwd: basePath,
975
+ shell: true,
976
+ encoding: "utf-8",
977
+ timeout: TIMEOUT_MS
978
+ });
979
+ // Check if the cyclonedx plugin created the required bom.xml file
980
+ // Sometimes the plugin fails silently for complex maven projects
981
+ const bomJsonFiles = utils.getAllFiles(path, "**/target/*.json");
982
+ const bomGenerated = bomJsonFiles.length;
983
+ if (!bomGenerated || result.status !== 0 || result.error) {
984
+ let tempDir = fs.mkdtempSync(pathLib.join(os.tmpdir(), "cdxmvn-"));
985
+ let tempMvnTree = pathLib.join(tempDir, "mvn-tree.txt");
986
+ let mvnTreeArgs = ["dependency:tree", "-DoutputFile=" + tempMvnTree];
987
+ if (process.env.MVN_ARGS) {
988
+ const addArgs = process.env.MVN_ARGS.split(" ");
989
+ mvnTreeArgs = mvnTreeArgs.concat(addArgs);
990
+ }
991
+ console.log(
992
+ `Fallback to executing ${mavenCmd} ${mvnTreeArgs.join(" ")}`
993
+ );
994
+ result = spawnSync(mavenCmd, mvnTreeArgs, {
995
+ cwd: basePath,
996
+ shell: true,
997
+ encoding: "utf-8",
998
+ timeout: TIMEOUT_MS
999
+ });
1000
+ if (result.status !== 0 || result.error) {
1001
+ console.error(result.stdout, result.stderr);
1002
+ console.log(
1003
+ "Resolve the above maven error. This could be due to the following:\n"
1004
+ );
1005
+ console.log(
1006
+ "1. Java version requirement - Scan or the CI build agent could be using an incompatible version"
1007
+ );
1008
+ console.log(
1009
+ "2. Private maven repository is not serving all the required maven plugins correctly. Refer to your registry documentation to add support for jitpack.io"
1010
+ );
1011
+ console.log(
1012
+ "3. Check if all required environment variables including any maven profile arguments are passed correctly to this tool"
1013
+ );
1014
+ // Do not fall back to methods that can produce incomplete results when failOnError is set
1015
+ options.failOnError && process.exit(1);
1016
+ console.log(
1017
+ "\nFalling back to manual pom.xml parsing. The result would be incomplete!"
1018
+ );
1019
+ const dlist = utils.parsePom(f);
1020
+ if (dlist && dlist.length) {
1021
+ pkgList = pkgList.concat(dlist);
1022
+ }
1023
+ } else {
1024
+ if (fs.existsSync(tempMvnTree)) {
1025
+ const mvnTreeString = fs.readFileSync(tempMvnTree, {
1026
+ encoding: "utf-8"
1027
+ });
1028
+ const parsedList = utils.parseMavenTree(mvnTreeString);
1029
+ const dlist = parsedList.pkgList;
1030
+ parentComponent = dlist.splice(0, 1)[0];
1031
+ parentComponent.type = "application";
1032
+ if (dlist && dlist.length) {
1033
+ pkgList = pkgList.concat(dlist);
1034
+ }
1035
+ if (parsedList.dependenciesList && parsedList.dependenciesList) {
1036
+ dependencies = dependencies.concat(parsedList.dependenciesList);
1037
+ }
1038
+ fs.unlinkSync(tempMvnTree);
1039
+ }
1040
+ }
1041
+ }
1042
+ } // for
1043
+ const bomFiles = utils.getAllFiles(path, "**/target/bom.xml");
1044
+ const bomJsonFiles = utils.getAllFiles(path, "**/target/*.json");
1045
+ for (const abjson of bomJsonFiles) {
1046
+ let bomJsonObj = undefined;
1047
+ try {
1048
+ if (DEBUG_MODE) {
1049
+ console.log(`Extracting data from generated bom file ${abjson}`);
1050
+ }
1051
+ bomJsonObj = JSON.parse(
1052
+ fs.readFileSync(abjson, {
1053
+ encoding: "utf-8"
1054
+ })
1055
+ );
1056
+ if (bomJsonObj) {
1057
+ if (
1058
+ bomJsonObj.metadata &&
1059
+ bomJsonObj.metadata.component &&
1060
+ !Object.keys(parentComponent).length
1061
+ ) {
1062
+ parentComponent = bomJsonObj.metadata.component;
1063
+ pkgList = [];
1064
+ }
1065
+ if (bomJsonObj.components) {
1066
+ pkgList = pkgList.concat(bomJsonObj.components);
1067
+ }
1068
+ if (bomJsonObj.dependencies && !options.requiredOnly) {
1069
+ dependencies = mergeDependencies(
1070
+ dependencies,
1071
+ bomJsonObj.dependencies
1072
+ );
1073
+ }
1074
+ }
1075
+ } catch (err) {
1076
+ if (options.failOnError || DEBUG_MODE) {
1077
+ console.log(err);
1078
+ options.failOnError && process.exit(1);
1079
+ }
1080
+ }
1081
+ }
1082
+ if (pkgList) {
1083
+ pkgList = trimComponents(pkgList, "json");
1084
+ pkgList = await utils.getMvnMetadata(pkgList);
1085
+ return buildBomNSData(options, pkgList, "maven", {
1086
+ src: path,
1087
+ filename: pomFiles.join(", "),
1088
+ nsMapping: jarNSMapping,
1089
+ dependencies,
1090
+ parentComponent
1091
+ });
1092
+ } else if (bomJsonFiles.length) {
1093
+ const bomNSData = {};
1094
+ bomNSData.bomXmlFiles = bomFiles;
1095
+ bomNSData.bomJsonFiles = bomJsonFiles;
1096
+ bomNSData.nsMapping = jarNSMapping;
1097
+ bomNSData.dependencies = dependencies;
1098
+ bomNSData.parentComponent = parentComponent;
1099
+ return bomNSData;
1100
+ }
1101
+ }
1102
+ // gradle
1103
+ let gradleFiles = utils.getAllFiles(
1104
+ path,
1105
+ (options.multiProject ? "**/" : "") + "build.gradle*"
1106
+ );
1107
+ if (gradleFiles && gradleFiles.length && options.installDeps) {
1108
+ let gradleCmd = utils.getGradleCommand(path, null);
1109
+ // Support for multi-project applications
1110
+ if (process.env.GRADLE_MULTI_PROJECT_MODE) {
1111
+ console.log("Executing", gradleCmd, "projects in", path);
1112
+ const result = spawnSync(
1113
+ gradleCmd,
1114
+ ["projects", "-q", "--console", "plain"],
1115
+ { cwd: path, encoding: "utf-8", timeout: TIMEOUT_MS }
1116
+ );
1117
+ if (result.status !== 0 || result.error) {
1118
+ if (result.stderr) {
1119
+ console.error(result.stdout, result.stderr);
1120
+ }
1121
+ console.log(
1122
+ "1. Check if the correct version of java and gradle are installed and available in PATH. For example, some project might require Java 11 with gradle 7."
1123
+ );
1124
+ options.failOnError && process.exit(1);
1125
+ }
1126
+ const stdout = result.stdout;
1127
+ if (stdout) {
1128
+ const cmdOutput = Buffer.from(stdout).toString();
1129
+ const allProjects = utils.parseGradleProjects(cmdOutput);
1130
+ if (!allProjects) {
1131
+ console.log(
1132
+ "No projects found. Is this a gradle multi-project application?"
1133
+ );
1134
+ options.failOnError && process.exit(1);
1135
+ } else {
1136
+ console.log("Found", allProjects.length, "gradle sub-projects");
1137
+ for (let sp of allProjects) {
1138
+ let gradleDepArgs = [
1139
+ sp + ":dependencies",
1140
+ "-q",
1141
+ "--console",
1142
+ "plain"
1143
+ ];
1144
+ // Support custom GRADLE_ARGS such as --configuration runtimeClassPath
1145
+ if (process.env.GRADLE_ARGS) {
1146
+ const addArgs = process.env.GRADLE_ARGS.split(" ");
1147
+ gradleDepArgs = gradleDepArgs.concat(addArgs);
1148
+ }
1149
+ console.log(
1150
+ "Executing",
1151
+ gradleCmd,
1152
+ gradleDepArgs.join(" "),
1153
+ "in",
1154
+ path
1155
+ );
1156
+ const sresult = spawnSync(gradleCmd, gradleDepArgs, {
1157
+ cwd: path,
1158
+ encoding: "utf-8",
1159
+ timeout: TIMEOUT_MS
1160
+ });
1161
+ if (sresult.status !== 0 || sresult.error) {
1162
+ if (options.failOnError || DEBUG_MODE) {
1163
+ console.error(sresult.stdout, sresult.stderr);
1164
+ }
1165
+ options.failOnError && process.exit(1);
1166
+ }
1167
+ const sstdout = sresult.stdout;
1168
+ if (sstdout) {
1169
+ const cmdOutput = Buffer.from(sstdout).toString();
1170
+ const parsedList = utils.parseGradleDep(cmdOutput);
1171
+ const dlist = parsedList.pkgList;
1172
+ parentComponent = dlist.splice(0, 1)[0];
1173
+ if (
1174
+ parsedList.dependenciesList &&
1175
+ parsedList.dependenciesList
1176
+ ) {
1177
+ dependencies = dependencies.concat(
1178
+ parsedList.dependenciesList
1179
+ );
1180
+ }
1181
+ if (dlist && dlist.length) {
1182
+ if (DEBUG_MODE) {
1183
+ console.log(
1184
+ "Found",
1185
+ dlist.length,
1186
+ "packages in gradle project",
1187
+ sp
1188
+ );
1189
+ }
1190
+ pkgList = pkgList.concat(dlist);
1191
+ } else {
1192
+ if (options.failOnError || DEBUG_MODE) {
1193
+ console.log("No packages were found in gradle project", sp);
1194
+ }
1195
+ options.failOnError && process.exit(1);
1196
+ }
1197
+ }
1198
+ }
1199
+ if (pkgList.length) {
1200
+ console.log(
1201
+ "Obtained",
1202
+ pkgList.length,
1203
+ "from this gradle multi-project"
1204
+ );
1205
+ } else {
1206
+ console.log(
1207
+ "No packages found. Unset the environment variable GRADLE_MULTI_PROJECT_MODE and try again."
1208
+ );
1209
+ options.failOnError && process.exit(1);
1210
+ }
1211
+ }
1212
+ } else {
1213
+ console.error("Gradle unexpectedly didn't return any output");
1214
+ options.failOnError && process.exit(1);
1215
+ }
1216
+ } else {
1217
+ let gradleDepArgs = ["dependencies", "-q", "--console", "plain"];
1218
+ // Support for overriding the gradle task name. Issue# 90
1219
+ if (process.env.GRADLE_DEPENDENCY_TASK) {
1220
+ gradleDepArgs = process.env.GRADLE_DEPENDENCY_TASK.split(" ");
1221
+ } else if (process.env.GRADLE_ARGS) {
1222
+ // Support custom GRADLE_ARGS such as --configuration runtimeClassPath
1223
+ const addArgs = process.env.GRADLE_ARGS.split(" ");
1224
+ gradleDepArgs = gradleDepArgs.concat(addArgs);
1225
+ }
1226
+ for (let f of gradleFiles) {
1227
+ const basePath = pathLib.dirname(f);
1228
+ // Fixes #157. Look for wrapper script in the nested directory
1229
+ gradleCmd = utils.getGradleCommand(basePath, path);
1230
+ console.log(
1231
+ "Executing",
1232
+ gradleCmd,
1233
+ gradleDepArgs.join(" "),
1234
+ "in",
1235
+ basePath
1236
+ );
1237
+ const result = spawnSync(gradleCmd, gradleDepArgs, {
1238
+ cwd: basePath,
1239
+ encoding: "utf-8",
1240
+ timeout: TIMEOUT_MS
1241
+ });
1242
+ if (result.status !== 0 || result.error) {
1243
+ if (result.stderr) {
1244
+ console.error(result.stdout, result.stderr);
1245
+ }
1246
+ if (DEBUG_MODE || !result.stderr || options.failOnError) {
1247
+ console.log(
1248
+ "1. Check if the correct version of java and gradle are installed and available in PATH. For example, some project might require Java 11 with gradle 7."
1249
+ );
1250
+ console.log(
1251
+ "2. When using tools such as sdkman, the init script must be invoked to set the PATH variables correctly."
1252
+ );
1253
+ options.failOnError && process.exit(1);
1254
+ }
1255
+ }
1256
+ const stdout = result.stdout;
1257
+ if (stdout) {
1258
+ const cmdOutput = Buffer.from(stdout).toString();
1259
+ const parsedList = utils.parseGradleDep(cmdOutput);
1260
+ const dlist = parsedList.pkgList;
1261
+ parentComponent = dlist.splice(0, 1)[0];
1262
+ if (parsedList.dependenciesList && parsedList.dependenciesList) {
1263
+ dependencies = dependencies.concat(parsedList.dependenciesList);
1264
+ }
1265
+ if (dlist && dlist.length) {
1266
+ pkgList = pkgList.concat(dlist);
1267
+ } else {
1268
+ console.log(
1269
+ "No packages were detected. If this is a multi-project gradle application set the environment variable GRADLE_MULTI_PROJECT_MODE to true and try again."
1270
+ );
1271
+ options.failOnError && process.exit(1);
1272
+ }
1273
+ } else {
1274
+ console.log("Gradle unexpectedly didn't produce any output");
1275
+ options.failOnError && process.exit(1);
1276
+ }
1277
+ }
1278
+ }
1279
+ pkgList = await utils.getMvnMetadata(pkgList);
1280
+ // Should we attempt to resolve class names
1281
+ if (options.resolveClass) {
1282
+ console.log(
1283
+ "Creating class names list based on available jars. This might take a few mins ..."
1284
+ );
1285
+ jarNSMapping = utils.collectJarNS(GRADLE_CACHE_DIR);
1286
+ }
1287
+ return buildBomNSData(options, pkgList, "maven", {
1288
+ src: path,
1289
+ filename: gradleFiles.join(", "),
1290
+ nsMapping: jarNSMapping,
1291
+ dependencies,
1292
+ parentComponent
1293
+ });
1294
+ }
1295
+
1296
+ // Bazel
1297
+ // Look for the BUILD file only in the root directory
1298
+ let bazelFiles = utils.getAllFiles(path, "BUILD");
1299
+ if (bazelFiles && bazelFiles.length) {
1300
+ let BAZEL_CMD = "bazel";
1301
+ if (process.env.BAZEL_HOME) {
1302
+ BAZEL_CMD = pathLib.join(process.env.BAZEL_HOME, "bin", "bazel");
1303
+ }
1304
+ for (let f of bazelFiles) {
1305
+ const basePath = pathLib.dirname(f);
1306
+ // Invoke bazel build first
1307
+ const bazelTarget = process.env.BAZEL_TARGET || ":all";
1308
+ console.log(
1309
+ "Executing",
1310
+ BAZEL_CMD,
1311
+ "build",
1312
+ bazelTarget,
1313
+ "in",
1314
+ basePath
1315
+ );
1316
+ let result = spawnSync(BAZEL_CMD, ["build", bazelTarget], {
1317
+ cwd: basePath,
1318
+ shell: true,
1319
+ encoding: "utf-8",
1320
+ timeout: TIMEOUT_MS
1321
+ });
1322
+ if (result.status !== 0 || result.error) {
1323
+ if (result.stderr) {
1324
+ console.error(result.stdout, result.stderr);
1325
+ }
1326
+ console.log(
1327
+ "1. Check if bazel is installed and available in PATH.\n2. Try building your app with bazel prior to invoking cdxgen"
1328
+ );
1329
+ options.failOnError && process.exit(1);
1330
+ } else {
1331
+ console.log(
1332
+ "Executing",
1333
+ BAZEL_CMD,
1334
+ "aquery --output=textproto --skyframe_state in",
1335
+ basePath
1336
+ );
1337
+ result = spawnSync(
1338
+ BAZEL_CMD,
1339
+ ["aquery", "--output=textproto", "--skyframe_state"],
1340
+ { cwd: basePath, encoding: "utf-8", timeout: TIMEOUT_MS }
1341
+ );
1342
+ if (result.status !== 0 || result.error) {
1343
+ console.error(result.stdout, result.stderr);
1344
+ options.failOnError && process.exit(1);
1345
+ }
1346
+ let stdout = result.stdout;
1347
+ if (stdout) {
1348
+ const cmdOutput = Buffer.from(stdout).toString();
1349
+ const dlist = utils.parseBazelSkyframe(cmdOutput);
1350
+ if (dlist && dlist.length) {
1351
+ pkgList = pkgList.concat(dlist);
1352
+ } else {
1353
+ console.log(
1354
+ "No packages were detected.\n1. Build your project using bazel build command before running cdxgen\n2. Try running the bazel aquery command manually to see if skyframe state can be retrieved."
1355
+ );
1356
+ console.log(
1357
+ "If your project requires a different query, please file a bug at cyclonedx/cdxgen repo!"
1358
+ );
1359
+ options.failOnError && process.exit(1);
1360
+ }
1361
+ } else {
1362
+ console.log("Bazel unexpectedly didn't produce any output");
1363
+ options.failOnError && process.exit(1);
1364
+ }
1365
+ pkgList = await utils.getMvnMetadata(pkgList);
1366
+ return buildBomNSData(options, pkgList, "maven", {
1367
+ src: path,
1368
+ filename: "BUILD",
1369
+ nsMapping: {},
1370
+ dependencies,
1371
+ parentComponent
1372
+ });
1373
+ }
1374
+ }
1375
+ }
1376
+
1377
+ // scala sbt
1378
+ // Identify sbt projects via its `project` directory:
1379
+ // - all SBT project _should_ define build.properties file with sbt version info
1380
+ // - SBT projects _typically_ have some configs/plugins defined in .sbt files
1381
+ // - SBT projects that are still on 0.13.x, can still use the old approach,
1382
+ // where configs are defined via Scala files
1383
+ // Detecting one of those should be enough to determine an SBT project.
1384
+ let sbtProjectFiles = utils.getAllFiles(
1385
+ path,
1386
+ (options.multiProject ? "**/" : "") +
1387
+ "project/{build.properties,*.sbt,*.scala}"
1388
+ );
1389
+
1390
+ let sbtProjects = [];
1391
+ for (let i in sbtProjectFiles) {
1392
+ // parent dir of sbtProjectFile is the `project` directory
1393
+ // parent dir of `project` is the sbt root project directory
1394
+ const baseDir = pathLib.dirname(pathLib.dirname(sbtProjectFiles[i]));
1395
+ sbtProjects = sbtProjects.concat(baseDir);
1396
+ }
1397
+
1398
+ // Fallback in case sbt's project directory is non-existent
1399
+ if (!sbtProjects.length) {
1400
+ sbtProjectFiles = utils.getAllFiles(
1401
+ path,
1402
+ (options.multiProject ? "**/" : "") + "*.sbt"
1403
+ );
1404
+ for (let i in sbtProjectFiles) {
1405
+ const baseDir = pathLib.dirname(sbtProjectFiles[i]);
1406
+ sbtProjects = sbtProjects.concat(baseDir);
1407
+ }
1408
+ }
1409
+
1410
+ sbtProjects = [...new Set(sbtProjects)]; // eliminate duplicates
1411
+
1412
+ let sbtLockFiles = utils.getAllFiles(
1413
+ path,
1414
+ (options.multiProject ? "**/" : "") + "build.sbt.lock"
1415
+ );
1416
+
1417
+ if (sbtProjects && sbtProjects.length) {
1418
+ let pkgList = [];
1419
+ // If the project use sbt lock files
1420
+ if (sbtLockFiles && sbtLockFiles.length) {
1421
+ for (let f of sbtLockFiles) {
1422
+ const dlist = utils.parseSbtLock(f);
1423
+ if (dlist && dlist.length) {
1424
+ pkgList = pkgList.concat(dlist);
1425
+ }
1426
+ }
1427
+ } else {
1428
+ let SBT_CMD = process.env.SBT_CMD || "sbt";
1429
+ let sbtVersion = utils.determineSbtVersion(path);
1430
+ if (DEBUG_MODE) {
1431
+ console.log("Detected sbt version: " + sbtVersion);
1432
+ }
1433
+ // Introduced in 1.2.0 https://www.scala-sbt.org/1.x/docs/sbt-1.2-Release-Notes.html#addPluginSbtFile+command,
1434
+ // however working properly for real only since 1.3.4: https://github.com/sbt/sbt/releases/tag/v1.3.4
1435
+ const standalonePluginFile =
1436
+ sbtVersion != null &&
1437
+ semver.gte(sbtVersion, "1.3.4") &&
1438
+ semver.lte(sbtVersion, "1.4.0");
1439
+ const isDependencyTreeBuiltIn =
1440
+ sbtVersion != null && semver.gte(sbtVersion, "1.4.0");
1441
+ let tempDir = fs.mkdtempSync(pathLib.join(os.tmpdir(), "cdxsbt-"));
1442
+ let tempSbtgDir = fs.mkdtempSync(pathLib.join(os.tmpdir(), "cdxsbtg-"));
1443
+ fs.mkdirSync(tempSbtgDir, { recursive: true });
1444
+ // Create temporary plugins file
1445
+ let tempSbtPlugins = pathLib.join(tempSbtgDir, "dep-plugins.sbt");
1446
+
1447
+ // Requires a custom version of `sbt-dependency-graph` that
1448
+ // supports `--append` for `toFile` subtask.
1449
+ let sbtPluginDefinition = `\naddSbtPlugin("io.shiftleft" % "sbt-dependency-graph" % "0.10.0-append-to-file3")\n`;
1450
+ if (isDependencyTreeBuiltIn) {
1451
+ sbtPluginDefinition = `\naddDependencyTreePlugin\n`;
1452
+ if (DEBUG_MODE) {
1453
+ console.log("Using addDependencyTreePlugin as the custom plugin");
1454
+ }
1455
+ }
1456
+ fs.writeFileSync(tempSbtPlugins, sbtPluginDefinition);
1457
+
1458
+ for (let i in sbtProjects) {
1459
+ const basePath = sbtProjects[i];
1460
+ let dlFile = pathLib.join(tempDir, "dl-" + i + ".tmp");
1461
+ console.log(
1462
+ "Executing",
1463
+ SBT_CMD,
1464
+ "dependencyList in",
1465
+ basePath,
1466
+ "using plugins",
1467
+ tempSbtgDir
1468
+ );
1469
+ var sbtArgs = [];
1470
+ var pluginFile = null;
1471
+ if (standalonePluginFile) {
1472
+ sbtArgs = [
1473
+ `-addPluginSbtFile=${tempSbtPlugins}`,
1474
+ `"dependencyList::toFile ${dlFile} --force"`
1475
+ ];
1476
+ } else {
1477
+ // write to the existing plugins file
1478
+ sbtArgs = [`"dependencyList::toFile ${dlFile} --force"`];
1479
+ pluginFile = utils.addPlugin(basePath, sbtPluginDefinition);
1480
+ }
1481
+ // Note that the command has to be invoked with `shell: true` to properly execut sbt
1482
+ const result = spawnSync(SBT_CMD, sbtArgs, {
1483
+ cwd: basePath,
1484
+ shell: true,
1485
+ encoding: "utf-8",
1486
+ timeout: TIMEOUT_MS
1487
+ });
1488
+ if (result.status !== 0 || result.error) {
1489
+ console.error(result.stdout, result.stderr);
1490
+ console.log(
1491
+ `1. Check if scala and sbt is installed and available in PATH. Only scala 2.10 + sbt 0.13.6+ and 2.12 + sbt 1.0+ is supported for now.`
1492
+ );
1493
+ console.log(
1494
+ `2. Check if the plugin net.virtual-void:sbt-dependency-graph 0.10.0-RC1 can be used in the environment`
1495
+ );
1496
+ console.log(
1497
+ "3. Consider creating a lockfile using sbt-dependency-lock plugin. See https://github.com/stringbean/sbt-dependency-lock"
1498
+ );
1499
+ options.failOnError && process.exit(1);
1500
+ } else if (DEBUG_MODE) {
1501
+ console.log(result.stdout);
1502
+ }
1503
+ if (!standalonePluginFile) {
1504
+ utils.cleanupPlugin(basePath, pluginFile);
1505
+ }
1506
+ if (fs.existsSync(dlFile)) {
1507
+ const cmdOutput = fs.readFileSync(dlFile, { encoding: "utf-8" });
1508
+ if (DEBUG_MODE) {
1509
+ console.log(cmdOutput);
1510
+ }
1511
+ const dlist = utils.parseKVDep(cmdOutput);
1512
+ if (dlist && dlist.length) {
1513
+ pkgList = pkgList.concat(dlist);
1514
+ }
1515
+ } else {
1516
+ if (options.failOnError || DEBUG_MODE) {
1517
+ console.log(`sbt dependencyList did not yield ${dlFile}`);
1518
+ }
1519
+ options.failOnError && process.exit(1);
1520
+ }
1521
+ }
1522
+
1523
+ // Cleanup
1524
+ fs.unlinkSync(tempSbtPlugins);
1525
+ } // else
1526
+
1527
+ if (DEBUG_MODE) {
1528
+ console.log(`Found ${pkgList.length} packages`);
1529
+ }
1530
+ pkgList = await utils.getMvnMetadata(pkgList);
1531
+ // Should we attempt to resolve class names
1532
+ if (options.resolveClass) {
1533
+ console.log(
1534
+ "Creating class names list based on available jars. This might take a few mins ..."
1535
+ );
1536
+ jarNSMapping = utils.collectJarNS(SBT_CACHE_DIR);
1537
+ }
1538
+ return buildBomNSData(options, pkgList, "maven", {
1539
+ src: path,
1540
+ filename: sbtProjects.join(", "),
1541
+ nsMapping: jarNSMapping,
1542
+ dependencies,
1543
+ parentComponent
1544
+ });
1545
+ }
1546
+ }
1547
+ };
1548
+
1549
+ /**
1550
+ * Function to create bom string for Node.js projects
1551
+ *
1552
+ * @param path to the project
1553
+ * @param options Parse options from the cli
1554
+ */
1555
+ const createNodejsBom = async (path, options) => {
1556
+ let pkgList = [];
1557
+ let manifestFiles = [];
1558
+ let dependencies = [];
1559
+ let parentComponent = {};
1560
+ let ppurl = "";
1561
+ // Docker mode requires special handling
1562
+ if (["docker", "oci", "os"].includes(options.projectType)) {
1563
+ const pkgJsonFiles = utils.getAllFiles(path, "**/package.json");
1564
+ // Are there any package.json files in the container?
1565
+ if (pkgJsonFiles.length) {
1566
+ for (let pj of pkgJsonFiles) {
1567
+ const dlist = await utils.parsePkgJson(pj);
1568
+ if (dlist && dlist.length) {
1569
+ pkgList = pkgList.concat(dlist);
1570
+ }
1571
+ }
1572
+ return buildBomNSData(options, pkgList, "npm", {
1573
+ allImports: {},
1574
+ src: path,
1575
+ filename: "package.json",
1576
+ parentComponent
1577
+ });
1578
+ }
1579
+ }
1580
+ let allImports = {};
1581
+ if (
1582
+ !["docker", "oci", "os"].includes(options.projectType) &&
1583
+ !options.noBabel
1584
+ ) {
1585
+ if (DEBUG_MODE) {
1586
+ console.log(
1587
+ `Performing babel-based package usage analysis with source code at ${path}`
1588
+ );
1589
+ }
1590
+ allImports = await findJSImports(path);
1591
+ }
1592
+ const yarnLockFiles = utils.getAllFiles(
1593
+ path,
1594
+ (options.multiProject ? "**/" : "") + "yarn.lock"
1595
+ );
1596
+ const shrinkwrapFiles = utils.getAllFiles(
1597
+ path,
1598
+ (options.multiProject ? "**/" : "") + "npm-shrinkwrap.json"
1599
+ );
1600
+ let pkgLockFiles = utils.getAllFiles(
1601
+ path,
1602
+ (options.multiProject ? "**/" : "") + "package-lock.json"
1603
+ );
1604
+ if (shrinkwrapFiles.length) {
1605
+ pkgLockFiles = pkgLockFiles.concat(shrinkwrapFiles);
1606
+ }
1607
+ const pnpmLockFiles = utils.getAllFiles(
1608
+ path,
1609
+ (options.multiProject ? "**/" : "") + "pnpm-lock.yaml"
1610
+ );
1611
+ const minJsFiles = utils.getAllFiles(
1612
+ path,
1613
+ (options.multiProject ? "**/" : "") + "*min.js"
1614
+ );
1615
+ const bowerFiles = utils.getAllFiles(
1616
+ path,
1617
+ (options.multiProject ? "**/" : "") + "bower.json"
1618
+ );
1619
+ // Parse min js files
1620
+ if (minJsFiles && minJsFiles.length) {
1621
+ manifestFiles = manifestFiles.concat(minJsFiles);
1622
+ for (let f of minJsFiles) {
1623
+ const dlist = await utils.parseMinJs(f);
1624
+ if (dlist && dlist.length) {
1625
+ pkgList = pkgList.concat(dlist);
1626
+ }
1627
+ }
1628
+ }
1629
+ // Parse bower json files
1630
+ if (bowerFiles && bowerFiles.length) {
1631
+ manifestFiles = manifestFiles.concat(bowerFiles);
1632
+ for (let f of bowerFiles) {
1633
+ const dlist = await utils.parseBowerJson(f);
1634
+ if (dlist && dlist.length) {
1635
+ pkgList = pkgList.concat(dlist);
1636
+ }
1637
+ }
1638
+ }
1639
+ if (pnpmLockFiles && pnpmLockFiles.length) {
1640
+ manifestFiles = manifestFiles.concat(pnpmLockFiles);
1641
+ for (let f of pnpmLockFiles) {
1642
+ const basePath = pathLib.dirname(f);
1643
+ // Determine the parent component
1644
+ const packageJsonF = pathLib.join(basePath, "package.json");
1645
+ if (fs.existsSync(packageJsonF)) {
1646
+ const pcs = await utils.parsePkgJson(packageJsonF);
1647
+ if (pcs.length) {
1648
+ parentComponent = pcs[0];
1649
+ parentComponent.type = "application";
1650
+ }
1651
+ } else {
1652
+ let dirName = pathLib.dirname(f);
1653
+ const tmpA = dirName.split(pathLib.sep);
1654
+ dirName = tmpA[tmpA.length - 1];
1655
+ parentComponent = {
1656
+ group: "",
1657
+ name: dirName,
1658
+ type: "application"
1659
+ };
1660
+ ppurl = new PackageURL(
1661
+ "application",
1662
+ parentComponent.group,
1663
+ parentComponent.name,
1664
+ parentComponent.version,
1665
+ null,
1666
+ null
1667
+ ).toString();
1668
+ parentComponent["bom-ref"] = ppurl;
1669
+ parentComponent["purl"] = ppurl;
1670
+ }
1671
+ // Parse the pnpm file
1672
+ const parsedList = await utils.parsePnpmLock(f, parentComponent);
1673
+ const dlist = parsedList.pkgList;
1674
+ if (dlist && dlist.length) {
1675
+ pkgList = pkgList.concat(dlist);
1676
+ }
1677
+ if (parsedList.dependenciesList && parsedList.dependenciesList) {
1678
+ dependencies = mergeDependencies(
1679
+ dependencies,
1680
+ parsedList.dependenciesList
1681
+ );
1682
+ }
1683
+ }
1684
+ return buildBomNSData(options, pkgList, "npm", {
1685
+ allImports,
1686
+ src: path,
1687
+ filename: manifestFiles.join(", "),
1688
+ dependencies,
1689
+ parentComponent
1690
+ });
1691
+ }
1692
+ if (pkgLockFiles && pkgLockFiles.length) {
1693
+ manifestFiles = manifestFiles.concat(pkgLockFiles);
1694
+ for (let f of pkgLockFiles) {
1695
+ if (DEBUG_MODE) {
1696
+ console.log(`Parsing ${f}`);
1697
+ }
1698
+ // Parse package-lock.json if available
1699
+ const parsedList = await utils.parsePkgLock(f);
1700
+ const dlist = parsedList.pkgList;
1701
+ parentComponent = dlist.splice(0, 1)[0];
1702
+ parentComponent.type = "application";
1703
+ if (dlist && dlist.length) {
1704
+ pkgList = pkgList.concat(dlist);
1705
+ }
1706
+ if (parsedList.dependenciesList && parsedList.dependenciesList) {
1707
+ dependencies = mergeDependencies(
1708
+ dependencies,
1709
+ parsedList.dependenciesList
1710
+ );
1711
+ }
1712
+ }
1713
+ }
1714
+ if (fs.existsSync(pathLib.join(path, "rush.json"))) {
1715
+ // Rush.js creates node_modules inside common/temp directory
1716
+ const nmDir = pathLib.join(path, "common", "temp", "node_modules");
1717
+ // Do rush install if we don't have node_modules directory
1718
+ if (!fs.existsSync(nmDir)) {
1719
+ console.log("Executing 'rush install --no-link'", path);
1720
+ const result = spawnSync(
1721
+ "rush",
1722
+ ["install", "--no-link", "--bypass-policy"],
1723
+ {
1724
+ cwd: path,
1725
+ encoding: "utf-8"
1726
+ }
1727
+ );
1728
+ if (result.status == 1 || result.error) {
1729
+ console.error(result.stdout, result.stderr);
1730
+ options.failOnError && process.exit(1);
1731
+ }
1732
+ }
1733
+ // Look for shrinkwrap file
1734
+ const swFile = pathLib.join(
1735
+ path,
1736
+ "tools",
1737
+ "build-tasks",
1738
+ ".rush",
1739
+ "temp",
1740
+ "shrinkwrap-deps.json"
1741
+ );
1742
+ const pnpmLock = pathLib.join(
1743
+ path,
1744
+ "common",
1745
+ "config",
1746
+ "rush",
1747
+ "pnpm-lock.yaml"
1748
+ );
1749
+ if (fs.existsSync(swFile)) {
1750
+ const pkgList = await utils.parseNodeShrinkwrap(swFile);
1751
+ return buildBomNSData(options, pkgList, "npm", {
1752
+ allImports,
1753
+ src: path,
1754
+ filename: "shrinkwrap-deps.json"
1755
+ });
1756
+ } else if (fs.existsSync(pnpmLock)) {
1757
+ const pkgList = await utils.parsePnpmLock(pnpmLock);
1758
+ return buildBomNSData(options, pkgList, "npm", {
1759
+ allImports,
1760
+ src: path,
1761
+ filename: "pnpm-lock.yaml"
1762
+ });
1763
+ } else {
1764
+ console.log(
1765
+ "Neither shrinkwrap file: ",
1766
+ swFile,
1767
+ " nor pnpm lockfile",
1768
+ pnpmLock,
1769
+ "was found!"
1770
+ );
1771
+ options.failOnError && process.exit(1);
1772
+ }
1773
+ }
1774
+ if (yarnLockFiles && yarnLockFiles.length) {
1775
+ manifestFiles = manifestFiles.concat(yarnLockFiles);
1776
+ for (let f of yarnLockFiles) {
1777
+ if (DEBUG_MODE) {
1778
+ console.log(`Parsing ${f}`);
1779
+ }
1780
+ const basePath = pathLib.dirname(f);
1781
+ // Determine the parent component
1782
+ const packageJsonF = pathLib.join(basePath, "package.json");
1783
+ if (fs.existsSync(packageJsonF)) {
1784
+ const pcs = await utils.parsePkgJson(packageJsonF);
1785
+ if (pcs.length) {
1786
+ parentComponent = pcs[0];
1787
+ parentComponent.type = "application";
1788
+ }
1789
+ } else {
1790
+ let dirName = pathLib.dirname(f);
1791
+ const tmpA = dirName.split(pathLib.sep);
1792
+ dirName = tmpA[tmpA.length - 1];
1793
+ parentComponent = {
1794
+ group: "",
1795
+ name: dirName,
1796
+ type: "application"
1797
+ };
1798
+ ppurl = new PackageURL(
1799
+ "application",
1800
+ parentComponent.group,
1801
+ parentComponent.name,
1802
+ parentComponent.version,
1803
+ null,
1804
+ null
1805
+ ).toString();
1806
+ parentComponent["bom-ref"] = ppurl;
1807
+ parentComponent["purl"] = ppurl;
1808
+ }
1809
+ // Parse yarn.lock if available. This check is after rush.json since
1810
+ // rush.js could include yarn.lock :(
1811
+ const parsedList = await utils.parseYarnLock(f);
1812
+ const dlist = parsedList.pkgList;
1813
+ if (dlist && dlist.length) {
1814
+ pkgList = pkgList.concat(dlist);
1815
+ }
1816
+ const rdeplist = [];
1817
+ if (parsedList.dependenciesList && parsedList.dependenciesList) {
1818
+ // Inject parent component to the dependency tree to make it complete
1819
+ // In case of yarn, yarn list command lists every root package as a direct dependency
1820
+ // The same logic is matched with this for loop although this is incorrect since even dev dependencies would get included here
1821
+ for (const dobj of parsedList.dependenciesList) {
1822
+ rdeplist.push(dobj.ref);
1823
+ }
1824
+ // Fixes: 212. Handle case where there are no package.json to determine the parent package
1825
+ if (Object.keys(parentComponent).length && parentComponent.name) {
1826
+ const ppurl = new PackageURL(
1827
+ "application",
1828
+ parentComponent.group,
1829
+ parentComponent.name,
1830
+ parentComponent.version,
1831
+ null,
1832
+ null
1833
+ ).toString();
1834
+ parsedList.dependenciesList.push({
1835
+ ref: decodeURIComponent(ppurl),
1836
+ dependsOn: rdeplist
1837
+ });
1838
+ }
1839
+ dependencies = mergeDependencies(
1840
+ dependencies,
1841
+ parsedList.dependenciesList
1842
+ );
1843
+ }
1844
+ }
1845
+ }
1846
+ if (!pkgList.length && fs.existsSync(pathLib.join(path, "node_modules"))) {
1847
+ const pkgJsonFiles = utils.getAllFiles(
1848
+ pathLib.join(path, "node_modules"),
1849
+ "**/package.json"
1850
+ );
1851
+ manifestFiles = manifestFiles.concat(pkgJsonFiles);
1852
+ for (let pkgjf of pkgJsonFiles) {
1853
+ const dlist = await utils.parsePkgJson(pkgjf);
1854
+ if (dlist && dlist.length) {
1855
+ pkgList = pkgList.concat(dlist);
1856
+ }
1857
+ }
1858
+ return buildBomNSData(options, pkgList, "npm", {
1859
+ allImports,
1860
+ src: path,
1861
+ filename: manifestFiles.join(", "),
1862
+ dependencies,
1863
+ parentComponent
1864
+ });
1865
+ }
1866
+ // Projects containing just min files or bower
1867
+ if (pkgList.length) {
1868
+ return buildBomNSData(options, pkgList, "npm", {
1869
+ allImports,
1870
+ src: path,
1871
+ filename: manifestFiles.join(", "),
1872
+ dependencies,
1873
+ parentComponent
1874
+ });
1875
+ }
1876
+ return {};
1877
+ };
1878
+
1879
+ /**
1880
+ * Function to create bom string for Python projects
1881
+ *
1882
+ * @param path to the project
1883
+ * @param options Parse options from the cli
1884
+ */
1885
+ const createPythonBom = async (path, options) => {
1886
+ let pkgList = [];
1887
+ let dlist = [];
1888
+ let metadataFilename = "";
1889
+ const pipenvMode = fs.existsSync(pathLib.join(path, "Pipfile"));
1890
+ const poetryFiles = utils.getAllFiles(
1891
+ path,
1892
+ (options.multiProject ? "**/" : "") + "poetry.lock"
1893
+ );
1894
+ const reqFiles = utils.getAllFiles(
1895
+ path,
1896
+ (options.multiProject ? "**/" : "") + "requirements.txt"
1897
+ );
1898
+ const reqDirFiles = utils.getAllFiles(
1899
+ path,
1900
+ (options.multiProject ? "**/" : "") + "requirements/*.txt"
1901
+ );
1902
+ const metadataFiles = utils.getAllFiles(
1903
+ path,
1904
+ (options.multiProject ? "**/site-packages/**/" : "") + "METADATA"
1905
+ );
1906
+ const whlFiles = utils.getAllFiles(
1907
+ path,
1908
+ (options.multiProject ? "**/" : "") + "*.whl"
1909
+ );
1910
+ const eggInfoFiles = utils.getAllFiles(
1911
+ path,
1912
+ (options.multiProject ? "**/" : "") + "*.egg-info"
1913
+ );
1914
+ const setupPy = pathLib.join(path, "setup.py");
1915
+ const requirementsMode =
1916
+ (reqFiles && reqFiles.length) || (reqDirFiles && reqDirFiles.length);
1917
+ const poetryMode = poetryFiles && poetryFiles.length;
1918
+ const setupPyMode = fs.existsSync(setupPy);
1919
+ // Poetry sets up its own virtual env containing site-packages so
1920
+ // we give preference to poetry lock file. Issue# 129
1921
+ if (poetryMode) {
1922
+ for (let f of poetryFiles) {
1923
+ const lockData = fs.readFileSync(f, { encoding: "utf-8" });
1924
+ const dlist = await utils.parsePoetrylockData(lockData);
1925
+ if (dlist && dlist.length) {
1926
+ pkgList = pkgList.concat(dlist);
1927
+ }
1928
+ }
1929
+ return buildBomNSData(options, pkgList, "pypi", {
1930
+ src: path,
1931
+ filename: poetryFiles.join(", ")
1932
+ });
1933
+ } else if (metadataFiles && metadataFiles.length) {
1934
+ // dist-info directories
1935
+ for (let mf of metadataFiles) {
1936
+ const mData = fs.readFileSync(mf, {
1937
+ encoding: "utf-8"
1938
+ });
1939
+ const dlist = utils.parseBdistMetadata(mData);
1940
+ if (dlist && dlist.length) {
1941
+ pkgList = pkgList.concat(dlist);
1942
+ }
1943
+ }
1944
+ }
1945
+ // .whl files. Zip file containing dist-info directory
1946
+ if (whlFiles && whlFiles.length) {
1947
+ for (let wf of whlFiles) {
1948
+ const mData = await utils.readZipEntry(wf, "METADATA");
1949
+ if (mData) {
1950
+ const dlist = utils.parseBdistMetadata(mData);
1951
+ if (dlist && dlist.length) {
1952
+ pkgList = pkgList.concat(dlist);
1953
+ }
1954
+ }
1955
+ }
1956
+ }
1957
+ // .egg-info files
1958
+ if (eggInfoFiles && eggInfoFiles.length) {
1959
+ for (let ef of eggInfoFiles) {
1960
+ dlist = utils.parseBdistMetadata(
1961
+ fs.readFileSync(ef, { encoding: "utf-8" })
1962
+ );
1963
+ if (dlist && dlist.length) {
1964
+ pkgList = pkgList.concat(dlist);
1965
+ }
1966
+ }
1967
+ }
1968
+ if (requirementsMode || pipenvMode || setupPyMode) {
1969
+ if (pipenvMode) {
1970
+ spawnSync("pipenv", ["install"], { cwd: path, encoding: "utf-8" });
1971
+ const piplockFile = pathLib.join(path, "Pipfile.lock");
1972
+ if (fs.existsSync(piplockFile)) {
1973
+ const lockData = JSON.parse(fs.readFileSync(piplockFile));
1974
+ dlist = await utils.parsePiplockData(lockData);
1975
+ if (dlist && dlist.length) {
1976
+ pkgList = pkgList.concat(dlist);
1977
+ }
1978
+ } else {
1979
+ console.error("Pipfile.lock not found at", path);
1980
+ options.failOnError && process.exit(1);
1981
+ }
1982
+ } else if (requirementsMode) {
1983
+ metadataFilename = "requirements.txt";
1984
+ if (reqFiles && reqFiles.length) {
1985
+ for (let f of reqFiles) {
1986
+ const reqData = fs.readFileSync(f, { encoding: "utf-8" });
1987
+ const dlist = await utils.parseReqFile(reqData);
1988
+ if (dlist && dlist.length) {
1989
+ pkgList = pkgList.concat(dlist);
1990
+ }
1991
+ }
1992
+ metadataFilename = reqFiles.join(", ");
1993
+ } else if (reqDirFiles && reqDirFiles.length) {
1994
+ for (let j in reqDirFiles) {
1995
+ const f = reqDirFiles[j];
1996
+ const reqData = fs.readFileSync(f, { encoding: "utf-8" });
1997
+ const dlist = await utils.parseReqFile(reqData);
1998
+ if (dlist && dlist.length) {
1999
+ pkgList = pkgList.concat(dlist);
2000
+ }
2001
+ }
2002
+ metadataFilename = reqDirFiles.join(", ");
2003
+ }
2004
+ } else if (setupPyMode) {
2005
+ const setupPyData = fs.readFileSync(setupPy, { encoding: "utf-8" });
2006
+ dlist = await utils.parseSetupPyFile(setupPyData);
2007
+ if (dlist && dlist.length) {
2008
+ pkgList = pkgList.concat(dlist);
2009
+ }
2010
+ }
2011
+ }
2012
+ if (pkgList.length) {
2013
+ return buildBomNSData(options, pkgList, "pypi", {
2014
+ src: path,
2015
+ filename: metadataFilename
2016
+ });
2017
+ }
2018
+ return {};
2019
+ };
2020
+
2021
+ /**
2022
+ * Function to create bom string for Go projects
2023
+ *
2024
+ * @param path to the project
2025
+ * @param options Parse options from the cli
2026
+ */
2027
+ const createGoBom = async (path, options) => {
2028
+ let pkgList = [];
2029
+ // Is this a binary file
2030
+ let maybeBinary = false;
2031
+ try {
2032
+ maybeBinary = fs.statSync(path).isFile();
2033
+ } catch (err) {
2034
+ maybeBinary = false;
2035
+ }
2036
+ if (maybeBinary) {
2037
+ const buildInfoData = binaryLib.getGoBuildInfo(path);
2038
+ const dlist = await utils.parseGoVersionData(buildInfoData);
2039
+ if (dlist && dlist.length) {
2040
+ pkgList = pkgList.concat(dlist);
2041
+ }
2042
+ // Since this pkg list is derived from the binary mark them as used.
2043
+ const allImports = {};
2044
+ for (let mpkg of pkgList) {
2045
+ let pkgFullName = `${mpkg.group}/${mpkg.name}`;
2046
+ allImports[pkgFullName] = true;
2047
+ }
2048
+ return buildBomNSData(options, pkgList, "golang", {
2049
+ allImports,
2050
+ src: path,
2051
+ filename: path
2052
+ });
2053
+ }
2054
+
2055
+ // Read in go.sum and merge all go.sum files.
2056
+ const gosumFiles = utils.getAllFiles(
2057
+ path,
2058
+ (options.multiProject ? "**/" : "") + "go.sum"
2059
+ );
2060
+
2061
+ // If USE_GOSUM is true, generate BOM components only using go.sum.
2062
+ const useGosum = process.env.USE_GOSUM == "true";
2063
+ if (useGosum && gosumFiles.length) {
2064
+ console.warn(
2065
+ "Using go.sum to generate BOMs for go projects may return an inaccurate representation of transitive dependencies.\nSee: https://github.com/golang/go/wiki/Modules#is-gosum-a-lock-file-why-does-gosum-include-information-for-module-versions-i-am-no-longer-using\n",
2066
+ "Set USE_GOSUM=false to generate BOMs using go.mod as the dependency source of truth."
2067
+ );
2068
+ for (let f of gosumFiles) {
2069
+ if (DEBUG_MODE) {
2070
+ console.log(`Parsing ${f}`);
2071
+ }
2072
+ const gosumData = fs.readFileSync(f, { encoding: "utf-8" });
2073
+ const dlist = await utils.parseGosumData(gosumData);
2074
+ if (dlist && dlist.length) {
2075
+ pkgList = pkgList.concat(dlist);
2076
+ }
2077
+ }
2078
+ return buildBomNSData(options, pkgList, "golang", {
2079
+ src: path,
2080
+ filename: gosumFiles.join(", ")
2081
+ });
2082
+ }
2083
+
2084
+ // If USE_GOSUM is false, generate BOM components using go.mod.
2085
+ const gosumMap = {};
2086
+ if (gosumFiles.length) {
2087
+ for (let f of gosumFiles) {
2088
+ if (DEBUG_MODE) {
2089
+ console.log(`Parsing ${f}`);
2090
+ }
2091
+ const gosumData = fs.readFileSync(f, { encoding: "utf-8" });
2092
+ const dlist = await utils.parseGosumData(gosumData);
2093
+ if (dlist && dlist.length) {
2094
+ dlist.forEach((pkg) => {
2095
+ gosumMap[`${pkg.group}/${pkg.name}/${pkg.version}`] = pkg._integrity;
2096
+ });
2097
+ }
2098
+ }
2099
+ }
2100
+
2101
+ // Read in data from Gopkg.lock files if they exist
2102
+ const gopkgLockFiles = utils.getAllFiles(
2103
+ path,
2104
+ (options.multiProject ? "**/" : "") + "Gopkg.lock"
2105
+ );
2106
+
2107
+ // Read in go.mod files and parse BOM components with checksums from gosumData
2108
+ const gomodFiles = utils.getAllFiles(
2109
+ path,
2110
+ (options.multiProject ? "**/" : "") + "go.mod"
2111
+ );
2112
+ if (gomodFiles.length) {
2113
+ // Use the go list -deps and go mod why commands to generate a good quality BoM for non-docker invocations
2114
+ if (!["docker", "oci", "os"].includes(options.projectType)) {
2115
+ for (let f of gomodFiles) {
2116
+ const basePath = pathLib.dirname(f);
2117
+ // Ignore vendor packages
2118
+ if (basePath.includes("vendor") || basePath.includes("build")) {
2119
+ continue;
2120
+ }
2121
+ if (DEBUG_MODE) {
2122
+ console.log("Executing go list -deps in", basePath);
2123
+ }
2124
+ const result = spawnSync(
2125
+ "go",
2126
+ [
2127
+ "list",
2128
+ "-deps",
2129
+ "-f",
2130
+ "'{{with .Module}}{{.Path}} {{.Version}}{{end}}'",
2131
+ "./..."
2132
+ ],
2133
+ { cwd: basePath, encoding: "utf-8", timeout: TIMEOUT_MS }
2134
+ );
2135
+ if (result.status !== 0 || result.error) {
2136
+ console.error(result.stdout, result.stderr);
2137
+ options.failOnError && process.exit(1);
2138
+ }
2139
+ const stdout = result.stdout;
2140
+ if (stdout) {
2141
+ const cmdOutput = Buffer.from(stdout).toString();
2142
+ const dlist = await utils.parseGoListDep(cmdOutput, gosumMap);
2143
+ if (dlist && dlist.length) {
2144
+ pkgList = pkgList.concat(dlist);
2145
+ }
2146
+ } else {
2147
+ console.error("go unexpectedly didn't return any output");
2148
+ options.failOnError && process.exit(1);
2149
+ }
2150
+ }
2151
+ const allImports = {};
2152
+ let circuitBreak = false;
2153
+ if (DEBUG_MODE) {
2154
+ console.log(
2155
+ `Attempting to detect required packages using "go mod why" command for ${pkgList.length} packages`
2156
+ );
2157
+ }
2158
+ // Using go mod why detect required packages
2159
+ for (let apkg of pkgList) {
2160
+ if (circuitBreak) {
2161
+ break;
2162
+ }
2163
+ let pkgFullName = `${apkg.group}/${apkg.name}`;
2164
+ if (DEBUG_MODE) {
2165
+ console.log(`go mod why -m -vendor ${pkgFullName}`);
2166
+ }
2167
+ const mresult = spawnSync(
2168
+ "go",
2169
+ ["mod", "why", "-m", "-vendor", pkgFullName],
2170
+ { cwd: path, encoding: "utf-8", timeout: TIMEOUT_MS }
2171
+ );
2172
+ if (mresult.status !== 0 || mresult.error) {
2173
+ if (DEBUG_MODE) {
2174
+ console.log(mresult.stdout, mresult.stderr);
2175
+ }
2176
+ circuitBreak = true;
2177
+ } else {
2178
+ const mstdout = mresult.stdout;
2179
+ if (mstdout) {
2180
+ const cmdOutput = Buffer.from(mstdout).toString();
2181
+ let whyPkg = utils.parseGoModWhy(cmdOutput);
2182
+ if (whyPkg == pkgFullName) {
2183
+ allImports[pkgFullName] = true;
2184
+ }
2185
+ }
2186
+ }
2187
+ }
2188
+ if (DEBUG_MODE) {
2189
+ console.log(`Required packages: ${Object.keys(allImports).length}`);
2190
+ }
2191
+ return buildBomNSData(options, pkgList, "golang", {
2192
+ allImports,
2193
+ src: path,
2194
+ filename: gomodFiles.join(", ")
2195
+ });
2196
+ }
2197
+ // Parse the gomod files manually. The resultant BoM would be incomplete
2198
+ if (!["docker", "oci", "os"].includes(options.projectType)) {
2199
+ console.log(
2200
+ "Manually parsing go.mod files. The resultant BoM would be incomplete."
2201
+ );
2202
+ }
2203
+ for (let f of gomodFiles) {
2204
+ if (DEBUG_MODE) {
2205
+ console.log(`Parsing ${f}`);
2206
+ }
2207
+ const gomodData = fs.readFileSync(f, { encoding: "utf-8" });
2208
+ const dlist = await utils.parseGoModData(gomodData, gosumMap);
2209
+ if (dlist && dlist.length) {
2210
+ pkgList = pkgList.concat(dlist);
2211
+ }
2212
+ }
2213
+ return buildBomNSData(options, pkgList, "golang", {
2214
+ src: path,
2215
+ filename: gomodFiles.join(", ")
2216
+ });
2217
+ } else if (gopkgLockFiles.length) {
2218
+ for (let f of gopkgLockFiles) {
2219
+ if (DEBUG_MODE) {
2220
+ console.log(`Parsing ${f}`);
2221
+ }
2222
+ const gopkgData = fs.readFileSync(f, {
2223
+ encoding: "utf-8"
2224
+ });
2225
+ const dlist = await utils.parseGopkgData(gopkgData);
2226
+ if (dlist && dlist.length) {
2227
+ pkgList = pkgList.concat(dlist);
2228
+ }
2229
+ }
2230
+ return buildBomNSData(options, pkgList, "golang", {
2231
+ src: path,
2232
+ filename: gopkgLockFiles.join(", ")
2233
+ });
2234
+ }
2235
+ return {};
2236
+ };
2237
+
2238
+ /**
2239
+ * Function to create bom string for Rust projects
2240
+ *
2241
+ * @param path to the project
2242
+ * @param options Parse options from the cli
2243
+ */
2244
+ const createRustBom = async (path, options) => {
2245
+ let pkgList = [];
2246
+ // Is this a binary file
2247
+ let maybeBinary = false;
2248
+ try {
2249
+ maybeBinary = fs.statSync(path).isFile();
2250
+ } catch (err) {
2251
+ maybeBinary = false;
2252
+ }
2253
+ if (maybeBinary) {
2254
+ const cargoData = binaryLib.getCargoAuditableInfo(path);
2255
+ const dlist = await utils.parseCargoAuditableData(cargoData);
2256
+ if (dlist && dlist.length) {
2257
+ pkgList = pkgList.concat(dlist);
2258
+ }
2259
+ // Since this pkg list is derived from the binary mark them as used.
2260
+ const allImports = {};
2261
+ for (let mpkg of pkgList) {
2262
+ let pkgFullName = `${mpkg.group}/${mpkg.name}`;
2263
+ allImports[pkgFullName] = true;
2264
+ }
2265
+ return buildBomNSData(options, pkgList, "cargo", {
2266
+ allImports,
2267
+ src: path,
2268
+ filename: path
2269
+ });
2270
+ }
2271
+ let cargoLockFiles = utils.getAllFiles(
2272
+ path,
2273
+ (options.multiProject ? "**/" : "") + "Cargo.lock"
2274
+ );
2275
+ const cargoFiles = utils.getAllFiles(
2276
+ path,
2277
+ (options.multiProject ? "**/" : "") + "Cargo.toml"
2278
+ );
2279
+ const cargoMode = cargoFiles.length;
2280
+ let cargoLockMode = cargoLockFiles.length;
2281
+ if (cargoMode && !cargoLockMode) {
2282
+ for (let f of cargoFiles) {
2283
+ if (DEBUG_MODE) {
2284
+ console.log(`Parsing ${f}`);
2285
+ }
2286
+ const cargoData = fs.readFileSync(f, { encoding: "utf-8" });
2287
+ const dlist = await utils.parseCargoTomlData(cargoData);
2288
+ if (dlist && dlist.length) {
2289
+ pkgList = pkgList.concat(dlist);
2290
+ }
2291
+ }
2292
+ return buildBomNSData(options, pkgList, "cargo", {
2293
+ src: path,
2294
+ filename: cargoFiles.join(", ")
2295
+ });
2296
+ }
2297
+ // Get the new lock files
2298
+ cargoLockFiles = utils.getAllFiles(
2299
+ path,
2300
+ (options.multiProject ? "**/" : "") + "Cargo.lock"
2301
+ );
2302
+ if (cargoLockFiles.length) {
2303
+ for (let f of cargoLockFiles) {
2304
+ if (DEBUG_MODE) {
2305
+ console.log(`Parsing ${f}`);
2306
+ }
2307
+ const cargoData = fs.readFileSync(f, { encoding: "utf-8" });
2308
+ const dlist = await utils.parseCargoData(cargoData);
2309
+ if (dlist && dlist.length) {
2310
+ pkgList = pkgList.concat(dlist);
2311
+ }
2312
+ }
2313
+ return buildBomNSData(options, pkgList, "cargo", {
2314
+ src: path,
2315
+ filename: cargoLockFiles.join(", ")
2316
+ });
2317
+ }
2318
+ return {};
2319
+ };
2320
+
2321
+ /**
2322
+ * Function to create bom string for Dart projects
2323
+ *
2324
+ * @param path to the project
2325
+ * @param options Parse options from the cli
2326
+ */
2327
+ const createDartBom = async (path, options) => {
2328
+ const pubFiles = utils.getAllFiles(
2329
+ path,
2330
+ (options.multiProject ? "**/" : "") + "pubspec.lock"
2331
+ );
2332
+ const pubSpecYamlFiles = utils.getAllFiles(
2333
+ path,
2334
+ (options.multiProject ? "**/" : "") + "pubspec.yaml"
2335
+ );
2336
+ let pkgList = [];
2337
+ if (pubFiles.length) {
2338
+ for (let f of pubFiles) {
2339
+ if (DEBUG_MODE) {
2340
+ console.log(`Parsing ${f}`);
2341
+ }
2342
+ const pubLockData = fs.readFileSync(f, { encoding: "utf-8" });
2343
+ const dlist = await utils.parsePubLockData(pubLockData);
2344
+ if (dlist && dlist.length) {
2345
+ pkgList = pkgList.concat(dlist);
2346
+ }
2347
+ }
2348
+ return buildBomNSData(options, pkgList, "pub", {
2349
+ src: path,
2350
+ filename: pubFiles.join(", ")
2351
+ });
2352
+ } else if (pubSpecYamlFiles.length) {
2353
+ for (let f of pubSpecYamlFiles) {
2354
+ if (DEBUG_MODE) {
2355
+ console.log(`Parsing ${f}`);
2356
+ }
2357
+ const pubYamlData = fs.readFileSync(f, { encoding: "utf-8" });
2358
+ const dlist = await utils.parsePubYamlData(pubYamlData);
2359
+ if (dlist && dlist.length) {
2360
+ pkgList = pkgList.concat(dlist);
2361
+ }
2362
+ }
2363
+ return buildBomNSData(options, pkgList, "pub", {
2364
+ src: path,
2365
+ filename: pubSpecYamlFiles.join(", ")
2366
+ });
2367
+ }
2368
+
2369
+ return {};
2370
+ };
2371
+
2372
+ /**
2373
+ * Function to create bom string for cpp projects
2374
+ *
2375
+ * @param path to the project
2376
+ * @param options Parse options from the cli
2377
+ */
2378
+ const createCppBom = async (path, options) => {
2379
+ const conanLockFiles = utils.getAllFiles(
2380
+ path,
2381
+ (options.multiProject ? "**/" : "") + "conan.lock"
2382
+ );
2383
+ const conanFiles = utils.getAllFiles(
2384
+ path,
2385
+ (options.multiProject ? "**/" : "") + "conanfile.txt"
2386
+ );
2387
+ let pkgList = [];
2388
+ if (conanLockFiles.length) {
2389
+ for (let f of conanLockFiles) {
2390
+ if (DEBUG_MODE) {
2391
+ console.log(`Parsing ${f}`);
2392
+ }
2393
+ const conanLockData = fs.readFileSync(f, { encoding: "utf-8" });
2394
+ const dlist = await utils.parseConanLockData(conanLockData);
2395
+ if (dlist && dlist.length) {
2396
+ pkgList = pkgList.concat(dlist);
2397
+ }
2398
+ }
2399
+ return buildBomNSData(options, pkgList, "conan", {
2400
+ src: path,
2401
+ filename: conanLockFiles.join(", ")
2402
+ });
2403
+ } else if (conanFiles.length) {
2404
+ for (let f of conanFiles) {
2405
+ if (DEBUG_MODE) {
2406
+ console.log(`Parsing ${f}`);
2407
+ }
2408
+ const conanData = fs.readFileSync(f, { encoding: "utf-8" });
2409
+ const dlist = await utils.parseConanData(conanData);
2410
+ if (dlist && dlist.length) {
2411
+ pkgList = pkgList.concat(dlist);
2412
+ }
2413
+ }
2414
+ return buildBomNSData(options, pkgList, "conan", {
2415
+ src: path,
2416
+ filename: conanFiles.join(", ")
2417
+ });
2418
+ }
2419
+
2420
+ return {};
2421
+ };
2422
+
2423
+ /**
2424
+ * Function to create bom string for clojure projects
2425
+ *
2426
+ * @param path to the project
2427
+ * @param options Parse options from the cli
2428
+ */
2429
+ const createClojureBom = async (path, options) => {
2430
+ const ednFiles = utils.getAllFiles(
2431
+ path,
2432
+ (options.multiProject ? "**/" : "") + "deps.edn"
2433
+ );
2434
+ const leinFiles = utils.getAllFiles(
2435
+ path,
2436
+ (options.multiProject ? "**/" : "") + "project.clj"
2437
+ );
2438
+ let pkgList = [];
2439
+ if (leinFiles.length) {
2440
+ let LEIN_ARGS = ["deps", ":tree-data"];
2441
+ if (process.env.LEIN_ARGS) {
2442
+ LEIN_ARGS = process.env.LEIN_ARGS.split(" ");
2443
+ }
2444
+ for (let f of leinFiles) {
2445
+ if (DEBUG_MODE) {
2446
+ console.log(`Parsing ${f}`);
2447
+ }
2448
+ const basePath = pathLib.dirname(f);
2449
+ console.log("Executing", LEIN_CMD, LEIN_ARGS.join(" "), "in", basePath);
2450
+ const result = spawnSync(LEIN_CMD, LEIN_ARGS, {
2451
+ cwd: basePath,
2452
+ encoding: "utf-8",
2453
+ timeout: TIMEOUT_MS
2454
+ });
2455
+ if (result.status !== 0 || result.error) {
2456
+ if (result.stderr) {
2457
+ console.error(result.stdout, result.stderr);
2458
+ options.failOnError && process.exit(1);
2459
+ }
2460
+ console.log(
2461
+ "Check if the correct version of lein is installed and available in PATH. Falling back to manual parsing."
2462
+ );
2463
+ if (DEBUG_MODE) {
2464
+ console.log(`Parsing ${f}`);
2465
+ }
2466
+ const leinData = fs.readFileSync(f, { encoding: "utf-8" });
2467
+ const dlist = utils.parseLeiningenData(leinData);
2468
+ if (dlist && dlist.length) {
2469
+ pkgList = pkgList.concat(dlist);
2470
+ }
2471
+ } else {
2472
+ const stdout = result.stdout;
2473
+ if (stdout) {
2474
+ const cmdOutput = Buffer.from(stdout).toString();
2475
+ const dlist = utils.parseLeinDep(cmdOutput);
2476
+ if (dlist && dlist.length) {
2477
+ pkgList = pkgList.concat(dlist);
2478
+ }
2479
+ } else {
2480
+ console.error("lein unexpectedly didn't return any output");
2481
+ options.failOnError && process.exit(1);
2482
+ }
2483
+ }
2484
+ }
2485
+ return buildBomNSData(options, pkgList, "clojars", {
2486
+ src: path,
2487
+ filename: leinFiles.join(", ")
2488
+ });
2489
+ } else if (ednFiles.length) {
2490
+ let CLJ_ARGS = ["-Stree"];
2491
+ if (process.env.CLJ_ARGS) {
2492
+ CLJ_ARGS = process.env.CLJ_ARGS.split(" ");
2493
+ }
2494
+ for (let f of ednFiles) {
2495
+ const basePath = pathLib.dirname(f);
2496
+ console.log("Executing", CLJ_CMD, CLJ_ARGS.join(" "), "in", basePath);
2497
+ const result = spawnSync(CLJ_CMD, CLJ_ARGS, {
2498
+ cwd: basePath,
2499
+ encoding: "utf-8",
2500
+ timeout: TIMEOUT_MS
2501
+ });
2502
+ if (result.status !== 0 || result.error) {
2503
+ if (result.stderr) {
2504
+ console.error(result.stdout, result.stderr);
2505
+ options.failOnError && process.exit(1);
2506
+ }
2507
+ console.log(
2508
+ "Check if the correct version of clojure cli is installed and available in PATH. Falling back to manual parsing."
2509
+ );
2510
+ if (DEBUG_MODE) {
2511
+ console.log(`Parsing ${f}`);
2512
+ }
2513
+ const ednData = fs.readFileSync(f, { encoding: "utf-8" });
2514
+ const dlist = utils.parseEdnData(ednData);
2515
+ if (dlist && dlist.length) {
2516
+ pkgList = pkgList.concat(dlist);
2517
+ }
2518
+ } else {
2519
+ const stdout = result.stdout;
2520
+ if (stdout) {
2521
+ const cmdOutput = Buffer.from(stdout).toString();
2522
+ const dlist = utils.parseCljDep(cmdOutput);
2523
+ if (dlist && dlist.length) {
2524
+ pkgList = pkgList.concat(dlist);
2525
+ }
2526
+ } else {
2527
+ console.error("clj unexpectedly didn't return any output");
2528
+ options.failOnError && process.exit(1);
2529
+ }
2530
+ }
2531
+ }
2532
+ return buildBomNSData(options, pkgList, "clojars", {
2533
+ src: path,
2534
+ filename: ednFiles.join(", ")
2535
+ });
2536
+ }
2537
+
2538
+ return {};
2539
+ };
2540
+
2541
+ /**
2542
+ * Function to create bom string for Haskell projects
2543
+ *
2544
+ * @param path to the project
2545
+ * @param options Parse options from the cli
2546
+ */
2547
+ const createHaskellBom = async (path, options) => {
2548
+ const cabalFiles = utils.getAllFiles(
2549
+ path,
2550
+ (options.multiProject ? "**/" : "") + "cabal.project.freeze"
2551
+ );
2552
+ let pkgList = [];
2553
+ if (cabalFiles.length) {
2554
+ for (let f of cabalFiles) {
2555
+ if (DEBUG_MODE) {
2556
+ console.log(`Parsing ${f}`);
2557
+ }
2558
+ const cabalData = fs.readFileSync(f, { encoding: "utf-8" });
2559
+ const dlist = await utils.parseCabalData(cabalData);
2560
+ if (dlist && dlist.length) {
2561
+ pkgList = pkgList.concat(dlist);
2562
+ }
2563
+ }
2564
+ return buildBomNSData(options, pkgList, "hackage", {
2565
+ src: path,
2566
+ filename: cabalFiles.join(", ")
2567
+ });
2568
+ }
2569
+ return {};
2570
+ };
2571
+
2572
+ /**
2573
+ * Function to create bom string for Elixir projects
2574
+ *
2575
+ * @param path to the project
2576
+ * @param options Parse options from the cli
2577
+ */
2578
+ const createElixirBom = async (path, options) => {
2579
+ const mixFiles = utils.getAllFiles(
2580
+ path,
2581
+ (options.multiProject ? "**/" : "") + "mix.lock"
2582
+ );
2583
+ let pkgList = [];
2584
+ if (mixFiles.length) {
2585
+ for (let f of mixFiles) {
2586
+ if (DEBUG_MODE) {
2587
+ console.log(`Parsing ${f}`);
2588
+ }
2589
+ const mixData = fs.readFileSync(f, { encoding: "utf-8" });
2590
+ const dlist = await utils.parseMixLockData(mixData);
2591
+ if (dlist && dlist.length) {
2592
+ pkgList = pkgList.concat(dlist);
2593
+ }
2594
+ }
2595
+ return buildBomNSData(options, pkgList, "hex", {
2596
+ src: path,
2597
+ filename: mixFiles.join(", ")
2598
+ });
2599
+ }
2600
+ return {};
2601
+ };
2602
+
2603
+ /**
2604
+ * Function to create bom string for GitHub action workflows
2605
+ *
2606
+ * @param path to the project
2607
+ * @param options Parse options from the cli
2608
+ */
2609
+ const createGitHubBom = async (path, options) => {
2610
+ const ghactionFiles = utils.getAllFiles(path, ".github/workflows/" + "*.yml");
2611
+ let pkgList = [];
2612
+ if (ghactionFiles.length) {
2613
+ for (let f of ghactionFiles) {
2614
+ if (DEBUG_MODE) {
2615
+ console.log(`Parsing ${f}`);
2616
+ }
2617
+ const ghwData = fs.readFileSync(f, { encoding: "utf-8" });
2618
+ const dlist = await utils.parseGitHubWorkflowData(ghwData);
2619
+ if (dlist && dlist.length) {
2620
+ pkgList = pkgList.concat(dlist);
2621
+ }
2622
+ }
2623
+ return buildBomNSData(options, pkgList, "github", {
2624
+ src: path,
2625
+ filename: ghactionFiles.join(", ")
2626
+ });
2627
+ }
2628
+ return {};
2629
+ };
2630
+
2631
+ /**
2632
+ * Function to create bom string for cloudbuild yaml
2633
+ *
2634
+ * @param path to the project
2635
+ * @param options Parse options from the cli
2636
+ */
2637
+ const createCloudBuildBom = async (path, options) => {
2638
+ const cbFiles = utils.getAllFiles(path, "cloudbuild.yml");
2639
+ let pkgList = [];
2640
+ if (cbFiles.length) {
2641
+ for (let f of cbFiles) {
2642
+ if (DEBUG_MODE) {
2643
+ console.log(`Parsing ${f}`);
2644
+ }
2645
+ const cbwData = fs.readFileSync(f, { encoding: "utf-8" });
2646
+ const dlist = await utils.parseCloudBuildData(cbwData);
2647
+ if (dlist && dlist.length) {
2648
+ pkgList = pkgList.concat(dlist);
2649
+ }
2650
+ }
2651
+ return buildBomNSData(options, pkgList, "cloudbuild", {
2652
+ src: path,
2653
+ filename: cbFiles.join(", ")
2654
+ });
2655
+ }
2656
+ return {};
2657
+ };
2658
+
2659
+ /**
2660
+ * Function to create bom string for the current OS using osquery
2661
+ *
2662
+ * @param path to the project
2663
+ * @param options Parse options from the cli
2664
+ */
2665
+ const createOSBom = async (path, options) => {
2666
+ console.warn(
2667
+ "About to generate SBoM for the current OS installation. This would take several minutes ..."
2668
+ );
2669
+ let pkgList = [];
2670
+ let bomData = {};
2671
+ for (const queryCategory of Object.keys(osQueries)) {
2672
+ const queryObj = osQueries[queryCategory];
2673
+ const results = binaryLib.executeOsQuery(queryObj.query);
2674
+ const dlist = utils.convertOSQueryResults(queryCategory, queryObj, results);
2675
+ if (dlist && dlist.length) {
2676
+ pkgList = pkgList.concat(dlist);
2677
+ }
2678
+ } // for
2679
+ if (pkgList.length) {
2680
+ bomData = buildBomNSData(options, pkgList, "", {
2681
+ src: "",
2682
+ filename: ""
2683
+ });
2684
+ }
2685
+ options.bomData = bomData;
2686
+ options.multiProject = true;
2687
+ options.installDeps = false;
2688
+ // Force the project type to os
2689
+ options.projectType = "os";
2690
+ options.lastWorkingDir = undefined;
2691
+ options.allLayersExplodedDir = isWin ? "C:\\" : "";
2692
+ const exportData = {
2693
+ lastWorkingDir: undefined,
2694
+ allLayersDir: options.allLayersExplodedDir,
2695
+ allLayersExplodedDir: options.allLayersExplodedDir
2696
+ };
2697
+ let pkgPathList = [];
2698
+ if (options.deep) {
2699
+ dockerLib.getPkgPathList(exportData, undefined);
2700
+ }
2701
+ return createMultiXBom(pkgPathList, options);
2702
+ };
2703
+
2704
+ /**
2705
+ * Function to create bom string for Jenkins plugins
2706
+ *
2707
+ * @param path to the project
2708
+ * @param options Parse options from the cli
2709
+ */
2710
+ const createJenkinsBom = async (path, options) => {
2711
+ let pkgList = [];
2712
+ const hpiFiles = utils.getAllFiles(
2713
+ path,
2714
+ (options.multiProject ? "**/" : "") + "*.hpi"
2715
+ );
2716
+ let tempDir = fs.mkdtempSync(pathLib.join(os.tmpdir(), "hpi-deps-"));
2717
+ if (hpiFiles.length) {
2718
+ for (let f of hpiFiles) {
2719
+ if (DEBUG_MODE) {
2720
+ console.log(`Parsing ${f}`);
2721
+ }
2722
+ const dlist = utils.extractJarArchive(f, tempDir);
2723
+ if (dlist && dlist.length) {
2724
+ pkgList = pkgList.concat(dlist);
2725
+ }
2726
+ }
2727
+ }
2728
+ const jsFiles = utils.getAllFiles(tempDir, "**/*.js");
2729
+ if (jsFiles.length) {
2730
+ for (let f of jsFiles) {
2731
+ if (DEBUG_MODE) {
2732
+ console.log(`Parsing ${f}`);
2733
+ }
2734
+ const dlist = await utils.parseMinJs(f);
2735
+ if (dlist && dlist.length) {
2736
+ pkgList = pkgList.concat(dlist);
2737
+ }
2738
+ }
2739
+ }
2740
+ // Clean up
2741
+ if (tempDir && tempDir.startsWith(os.tmpdir()) && fs.rmSync) {
2742
+ console.log(`Cleaning up ${tempDir}`);
2743
+ fs.rmSync(tempDir, { recursive: true, force: true });
2744
+ }
2745
+ return buildBomNSData(options, pkgList, "maven", {
2746
+ src: path,
2747
+ filename: hpiFiles.join(", "),
2748
+ nsMapping: {}
2749
+ });
2750
+ };
2751
+
2752
+ /**
2753
+ * Function to create bom string for Helm charts
2754
+ *
2755
+ * @param path to the project
2756
+ * @param options Parse options from the cli
2757
+ */
2758
+ const createHelmBom = async (path, options) => {
2759
+ let pkgList = [];
2760
+ const yamlFiles = utils.getAllFiles(
2761
+ path,
2762
+ (options.multiProject ? "**/" : "") + "*.yaml"
2763
+ );
2764
+ if (yamlFiles.length) {
2765
+ for (let f of yamlFiles) {
2766
+ if (DEBUG_MODE) {
2767
+ console.log(`Parsing ${f}`);
2768
+ }
2769
+ const helmData = fs.readFileSync(f, { encoding: "utf-8" });
2770
+ const dlist = await utils.parseHelmYamlData(helmData);
2771
+ if (dlist && dlist.length) {
2772
+ pkgList = pkgList.concat(dlist);
2773
+ }
2774
+ }
2775
+ return buildBomNSData(options, pkgList, "helm", {
2776
+ src: path,
2777
+ filename: yamlFiles.join(", ")
2778
+ });
2779
+ }
2780
+ return {};
2781
+ };
2782
+
2783
+ /**
2784
+ * Function to create bom string for docker compose
2785
+ *
2786
+ * @param path to the project
2787
+ * @param options Parse options from the cli
2788
+ */
2789
+ const createContainerSpecLikeBom = async (path, options) => {
2790
+ let services = [];
2791
+ let ociSpecs = [];
2792
+ let components = [];
2793
+ let componentsXmls = [];
2794
+ let parentComponent = {};
2795
+ let dependencies = [];
2796
+ let doneimages = [];
2797
+ let doneservices = [];
2798
+ let origProjectType = options.projectType;
2799
+ let dcFiles = utils.getAllFiles(
2800
+ path,
2801
+ (options.multiProject ? "**/" : "") + "*.yml"
2802
+ );
2803
+ const yamlFiles = utils.getAllFiles(
2804
+ path,
2805
+ (options.multiProject ? "**/" : "") + "*.yaml"
2806
+ );
2807
+ let oapiFiles = utils.getAllFiles(
2808
+ path,
2809
+ (options.multiProject ? "**/" : "") + "open*.json"
2810
+ );
2811
+ let oapiYamlFiles = utils.getAllFiles(
2812
+ path,
2813
+ (options.multiProject ? "**/" : "") + "open*.yaml"
2814
+ );
2815
+ if (oapiYamlFiles && oapiYamlFiles.length) {
2816
+ oapiFiles = oapiFiles.concat(oapiYamlFiles);
2817
+ }
2818
+ if (yamlFiles.length) {
2819
+ dcFiles = dcFiles.concat(yamlFiles);
2820
+ }
2821
+ // Privado.ai json files
2822
+ const privadoFiles = utils.getAllFiles(path, ".privado/" + "*.json");
2823
+ // parse yaml manifest files
2824
+ if (dcFiles.length) {
2825
+ for (let f of dcFiles) {
2826
+ if (DEBUG_MODE) {
2827
+ console.log(`Parsing ${f}`);
2828
+ }
2829
+ const dcData = fs.readFileSync(f, { encoding: "utf-8" });
2830
+ const imglist = await utils.parseContainerSpecData(dcData);
2831
+ if (imglist && imglist.length) {
2832
+ if (DEBUG_MODE) {
2833
+ console.log("Images identified in", f, "are", imglist);
2834
+ }
2835
+ for (const img of imglist) {
2836
+ let commonProperties = [
2837
+ {
2838
+ name: "SrcFile",
2839
+ value: f
2840
+ }
2841
+ ];
2842
+ if (img.image) {
2843
+ commonProperties.push({
2844
+ name: "oci:SrcImage",
2845
+ value: img.image
2846
+ });
2847
+ }
2848
+ if (img.service) {
2849
+ commonProperties.push({
2850
+ name: "ServiceName",
2851
+ value: img.service
2852
+ });
2853
+ }
2854
+
2855
+ // img could have .service, .ociSpec or .image
2856
+ if (img.ociSpec) {
2857
+ console.log(
2858
+ `NOTE: ${img.ociSpec} needs to built using docker or podman and referred with a name to get included in this SBoM.`
2859
+ );
2860
+ ociSpecs.push({
2861
+ group: "",
2862
+ name: img.ociSpec,
2863
+ version: "latest",
2864
+ properties: commonProperties
2865
+ });
2866
+ }
2867
+ if (img.service) {
2868
+ let version = "latest";
2869
+ let name = img.service;
2870
+ if (img.service.includes(":")) {
2871
+ const tmpA = img.service.split(":");
2872
+ if (tmpA && tmpA.length === 2) {
2873
+ name = tmpA[0];
2874
+ version = tmpA[1];
2875
+ }
2876
+ }
2877
+ const servbomRef = `urn:service:${name}:${version}`;
2878
+ if (!doneservices.includes(servbomRef)) {
2879
+ services.push({
2880
+ "bom-ref": servbomRef,
2881
+ name: name,
2882
+ version: version,
2883
+ group: "",
2884
+ properties: commonProperties
2885
+ });
2886
+ doneservices.push(servbomRef);
2887
+ }
2888
+ }
2889
+ if (img.image) {
2890
+ if (doneimages.includes(img.image)) {
2891
+ if (DEBUG_MODE) {
2892
+ console.log("Skipping", img.image);
2893
+ }
2894
+ continue;
2895
+ }
2896
+ if (DEBUG_MODE) {
2897
+ console.log(`Parsing image ${img.image}`);
2898
+ }
2899
+ const imageObj = dockerLib.parseImageName(img.image);
2900
+ const pkg = {
2901
+ name: imageObj.repo,
2902
+ version:
2903
+ imageObj.tag ||
2904
+ (imageObj.digest ? "sha256:" + imageObj.digest : "latest"),
2905
+ qualifiers: {},
2906
+ properties: commonProperties
2907
+ };
2908
+ if (imageObj.registry) {
2909
+ pkg["qualifiers"]["repository_url"] = imageObj.registry;
2910
+ }
2911
+ if (imageObj.platform) {
2912
+ pkg["qualifiers"]["platform"] = imageObj.platform;
2913
+ }
2914
+ // Create an entry for the oci image
2915
+ const imageBomData = buildBomNSData(options, [pkg], "oci", {
2916
+ src: img.image,
2917
+ filename: f,
2918
+ nsMapping: {}
2919
+ });
2920
+ if (
2921
+ imageBomData &&
2922
+ imageBomData.bomJson &&
2923
+ imageBomData.bomJson.components
2924
+ ) {
2925
+ components = components.concat(imageBomData.bomJson.components);
2926
+ componentsXmls = componentsXmls.concat(
2927
+ listComponents(
2928
+ options,
2929
+ {},
2930
+ imageBomData.bomJson.components,
2931
+ "oci",
2932
+ "xml"
2933
+ )
2934
+ );
2935
+ }
2936
+ const bomData = await createBom(img.image, { projectType: "oci" });
2937
+ doneimages.push(img.image);
2938
+ if (bomData) {
2939
+ if (bomData.components && bomData.components.length) {
2940
+ // Inject properties
2941
+ for (const co of bomData.components) {
2942
+ co.properties = commonProperties;
2943
+ }
2944
+ components = components.concat(bomData.components);
2945
+ }
2946
+ if (bomData.componentsXmls && bomData.componentsXmls.length) {
2947
+ componentsXmls = componentsXmls.concat(bomData.componentsXmls);
2948
+ }
2949
+ }
2950
+ } // img.image
2951
+ } // for img
2952
+ }
2953
+ } // for
2954
+ } // if
2955
+ // Parse openapi files
2956
+ if (oapiFiles.length) {
2957
+ for (let af of oapiFiles) {
2958
+ if (DEBUG_MODE) {
2959
+ console.log(`Parsing ${af}`);
2960
+ }
2961
+ const oaData = fs.readFileSync(af, { encoding: "utf-8" });
2962
+ const servlist = await utils.parseOpenapiSpecData(oaData);
2963
+ if (servlist && servlist.length) {
2964
+ // Inject SrcFile property
2965
+ for (const se of servlist) {
2966
+ se.properties = [
2967
+ {
2968
+ name: "SrcFile",
2969
+ value: af
2970
+ }
2971
+ ];
2972
+ }
2973
+ services = services.concat(servlist);
2974
+ }
2975
+ }
2976
+ }
2977
+ // Parse privado files
2978
+ if (privadoFiles.length) {
2979
+ console.log(
2980
+ "Enriching your SBoM with information from privado.ai scan reports"
2981
+ );
2982
+ let rows = [["Classification", "Flow"]];
2983
+ let config = {
2984
+ header: {
2985
+ alignment: "center",
2986
+ content: "Data Privacy Insights from privado.ai"
2987
+ },
2988
+ columns: [{ width: 50 }, { width: 10 }]
2989
+ };
2990
+ for (let f of privadoFiles) {
2991
+ if (DEBUG_MODE) {
2992
+ console.log(`Parsing ${f}`);
2993
+ }
2994
+ const servlist = utils.parsePrivadoFile(f);
2995
+ services = services.concat(servlist);
2996
+ if (servlist.length) {
2997
+ const aservice = servlist[0];
2998
+ if (aservice.data) {
2999
+ for (let d of aservice.data) {
3000
+ rows.push([d.classification, d.flow]);
3001
+ }
3002
+ console.log(table(rows, config));
3003
+ }
3004
+ if (aservice.endpoints) {
3005
+ rows = [["Leaky Endpoints"]];
3006
+ for (let e of aservice.endpoints) {
3007
+ rows.push([e]);
3008
+ }
3009
+ console.log(
3010
+ table(rows, {
3011
+ columnDefault: {
3012
+ width: 50
3013
+ }
3014
+ })
3015
+ );
3016
+ }
3017
+ }
3018
+ }
3019
+ }
3020
+ if (origProjectType === "universal") {
3021
+ // In case of universal, repeat to collect multiX Boms
3022
+ const mbomData = await createMultiXBom([path], {
3023
+ projectType: origProjectType,
3024
+ multiProject: true
3025
+ });
3026
+ if (mbomData) {
3027
+ if (mbomData.components && mbomData.components.length) {
3028
+ components = components.concat(mbomData.components);
3029
+ }
3030
+ if (mbomData.componentsXmls && mbomData.componentsXmls.length) {
3031
+ componentsXmls = componentsXmls.concat(mbomData.componentsXmls);
3032
+ }
3033
+ if (mbomData.bomJson) {
3034
+ if (mbomData.bomJson.dependencies) {
3035
+ dependencies = mergeDependencies(
3036
+ dependencies,
3037
+ mbomData.bomJson.dependencies
3038
+ );
3039
+ }
3040
+ if (mbomData.bomJson.services) {
3041
+ services = services.concat(mbomData.bomJson.services);
3042
+ }
3043
+ }
3044
+ if (DEBUG_MODE) {
3045
+ console.log(
3046
+ `BOM includes ${components.length} unfiltered components ${dependencies.length} dependencies so far`
3047
+ );
3048
+ }
3049
+ }
3050
+ }
3051
+ options.services = services;
3052
+ options.ociSpecs = ociSpecs;
3053
+ return dedupeBom(
3054
+ options,
3055
+ components,
3056
+ componentsXmls,
3057
+ parentComponent,
3058
+ dependencies
3059
+ );
3060
+ };
3061
+
3062
+ /**
3063
+ * Function to create bom string for php projects
3064
+ *
3065
+ * @param path to the project
3066
+ * @param options Parse options from the cli
3067
+ */
3068
+ const createPHPBom = async (path, options) => {
3069
+ const composerJsonFiles = utils.getAllFiles(
3070
+ path,
3071
+ (options.multiProject ? "**/" : "") + "composer.json"
3072
+ );
3073
+ let composerLockFiles = utils.getAllFiles(
3074
+ path,
3075
+ (options.multiProject ? "**/" : "") + "composer.lock"
3076
+ );
3077
+ let pkgList = [];
3078
+ const composerJsonMode = composerJsonFiles.length;
3079
+ const composerLockMode = composerLockFiles.length;
3080
+ // Create a composer.lock file for each composer.json file if needed.
3081
+ if (!composerLockMode && composerJsonMode && options.installDeps) {
3082
+ const versionResult = spawnSync("composer", ["--version"], {
3083
+ encoding: "utf-8"
3084
+ });
3085
+ if (versionResult.status !== 0 || versionResult.error) {
3086
+ console.error(
3087
+ "No composer version found. Check if composer is installed and available in PATH."
3088
+ );
3089
+ console.log(versionResult.error, versionResult.stderr);
3090
+ options.failOnError && process.exit(1);
3091
+ return {};
3092
+ }
3093
+ const composerVersion = versionResult.stdout.match(/version (\d)/)[1];
3094
+ if (DEBUG_MODE) {
3095
+ console.log("Detected composer version:", composerVersion);
3096
+ }
3097
+ for (let f of composerJsonFiles) {
3098
+ const basePath = pathLib.dirname(f);
3099
+ let args = [];
3100
+ if (composerVersion > 1) {
3101
+ console.log("Generating composer.lock in", basePath);
3102
+ args = ["update", "--no-install", "--ignore-platform-reqs"];
3103
+ } else {
3104
+ console.log("Executing 'composer install' in", basePath);
3105
+ args = ["install", "--ignore-platform-reqs"];
3106
+ }
3107
+ const result = spawnSync("composer", args, {
3108
+ cwd: basePath,
3109
+ encoding: "utf-8"
3110
+ });
3111
+ if (result.status !== 0 || result.error) {
3112
+ console.error("Error running composer:");
3113
+ console.log(result.error, result.stderr);
3114
+ options.failOnError && process.exit(1);
3115
+ }
3116
+ }
3117
+ }
3118
+ composerLockFiles = utils.getAllFiles(
3119
+ path,
3120
+ (options.multiProject ? "**/" : "") + "composer.lock"
3121
+ );
3122
+ if (composerLockFiles.length) {
3123
+ for (let f of composerLockFiles) {
3124
+ if (DEBUG_MODE) {
3125
+ console.log(`Parsing ${f}`);
3126
+ }
3127
+ let dlist = utils.parseComposerLock(f);
3128
+ if (dlist && dlist.length) {
3129
+ pkgList = pkgList.concat(dlist);
3130
+ }
3131
+ }
3132
+ return buildBomNSData(options, pkgList, "composer", {
3133
+ src: path,
3134
+ filename: composerLockFiles.join(", ")
3135
+ });
3136
+ }
3137
+ return {};
3138
+ };
3139
+
3140
+ /**
3141
+ * Function to create bom string for ruby projects
3142
+ *
3143
+ * @param path to the project
3144
+ * @param options Parse options from the cli
3145
+ */
3146
+ const createRubyBom = async (path, options) => {
3147
+ const gemFiles = utils.getAllFiles(
3148
+ path,
3149
+ (options.multiProject ? "**/" : "") + "Gemfile"
3150
+ );
3151
+ let gemLockFiles = utils.getAllFiles(
3152
+ path,
3153
+ (options.multiProject ? "**/" : "") + "Gemfile.lock"
3154
+ );
3155
+ let pkgList = [];
3156
+ const gemFileMode = gemFiles.length;
3157
+ let gemLockMode = gemLockFiles.length;
3158
+ if (gemFileMode && !gemLockMode && options.installDeps) {
3159
+ for (let f of gemFiles) {
3160
+ const basePath = pathLib.dirname(f);
3161
+ console.log("Executing 'bundle install' in", basePath);
3162
+ const result = spawnSync("bundle", ["install"], {
3163
+ cwd: basePath,
3164
+ encoding: "utf-8"
3165
+ });
3166
+ if (result.status !== 0 || result.error) {
3167
+ console.error(
3168
+ "Bundle install has failed. Check if bundle is installed and available in PATH."
3169
+ );
3170
+ console.log(result.error, result.stderr);
3171
+ options.failOnError && process.exit(1);
3172
+ }
3173
+ }
3174
+ }
3175
+ gemLockFiles = utils.getAllFiles(
3176
+ path,
3177
+ (options.multiProject ? "**/" : "") + "Gemfile.lock"
3178
+ );
3179
+ if (gemLockFiles.length) {
3180
+ for (let f of gemLockFiles) {
3181
+ if (DEBUG_MODE) {
3182
+ console.log(`Parsing ${f}`);
3183
+ }
3184
+ let gemLockData = fs.readFileSync(f, { encoding: "utf-8" });
3185
+ const dlist = await utils.parseGemfileLockData(gemLockData);
3186
+ if (dlist && dlist.length) {
3187
+ pkgList = pkgList.concat(dlist);
3188
+ }
3189
+ }
3190
+ return buildBomNSData(options, pkgList, "gem", {
3191
+ src: path,
3192
+ filename: gemLockFiles.join(", ")
3193
+ });
3194
+ }
3195
+ return {};
3196
+ };
3197
+
3198
+ /**
3199
+ * Function to create bom string for csharp projects
3200
+ *
3201
+ * @param path to the project
3202
+ * @param options Parse options from the cli
3203
+ */
3204
+ const createCsharpBom = async (path, options) => {
3205
+ let manifestFiles = [];
3206
+ const csProjFiles = utils.getAllFiles(
3207
+ path,
3208
+ (options.multiProject ? "**/" : "") + "*.csproj"
3209
+ );
3210
+ const pkgConfigFiles = utils.getAllFiles(
3211
+ path,
3212
+ (options.multiProject ? "**/" : "") + "packages.config"
3213
+ );
3214
+ const projAssetsFiles = utils.getAllFiles(
3215
+ path,
3216
+ (options.multiProject ? "**/" : "") + "project.assets.json"
3217
+ );
3218
+ const pkgLockFiles = utils.getAllFiles(
3219
+ path,
3220
+ (options.multiProject ? "**/" : "") + "packages.lock.json"
3221
+ );
3222
+ const nupkgFiles = utils.getAllFiles(
3223
+ path,
3224
+ (options.multiProject ? "**/" : "") + "*.nupkg"
3225
+ );
3226
+ let pkgList = [];
3227
+ if (nupkgFiles.length) {
3228
+ manifestFiles = manifestFiles.concat(nupkgFiles);
3229
+ for (let nf of nupkgFiles) {
3230
+ if (DEBUG_MODE) {
3231
+ console.log(`Parsing ${nf}`);
3232
+ }
3233
+ const dlist = await utils.parseNupkg(nf);
3234
+ if (dlist && dlist.length) {
3235
+ pkgList = pkgList.concat(dlist);
3236
+ }
3237
+ }
3238
+ }
3239
+ // project.assets.json parsing
3240
+ if (projAssetsFiles.length) {
3241
+ manifestFiles = manifestFiles.concat(projAssetsFiles);
3242
+ for (let af of projAssetsFiles) {
3243
+ if (DEBUG_MODE) {
3244
+ console.log(`Parsing ${af}`);
3245
+ }
3246
+ let pkgData = fs.readFileSync(af, { encoding: "utf-8" });
3247
+ const dlist = await utils.parseCsProjAssetsData(pkgData);
3248
+ if (dlist && dlist.length) {
3249
+ pkgList = pkgList.concat(dlist);
3250
+ }
3251
+ }
3252
+ } else if (pkgLockFiles.length) {
3253
+ manifestFiles = manifestFiles.concat(pkgLockFiles);
3254
+ // packages.lock.json from nuget
3255
+ for (let af of pkgLockFiles) {
3256
+ if (DEBUG_MODE) {
3257
+ console.log(`Parsing ${af}`);
3258
+ }
3259
+ let pkgData = fs.readFileSync(af, { encoding: "utf-8" });
3260
+ const dlist = await utils.parseCsPkgLockData(pkgData);
3261
+ if (dlist && dlist.length) {
3262
+ pkgList = pkgList.concat(dlist);
3263
+ }
3264
+ }
3265
+ } else if (pkgConfigFiles.length) {
3266
+ manifestFiles = manifestFiles.concat(pkgConfigFiles);
3267
+ // packages.config parsing
3268
+ for (let f of pkgConfigFiles) {
3269
+ if (DEBUG_MODE) {
3270
+ console.log(`Parsing ${f}`);
3271
+ }
3272
+ let pkgData = fs.readFileSync(f, { encoding: "utf-8" });
3273
+ // Remove byte order mark
3274
+ if (pkgData.charCodeAt(0) === 0xfeff) {
3275
+ pkgData = pkgData.slice(1);
3276
+ }
3277
+ const dlist = await utils.parseCsPkgData(pkgData);
3278
+ if (dlist && dlist.length) {
3279
+ pkgList = pkgList.concat(dlist);
3280
+ }
3281
+ }
3282
+ } else if (csProjFiles.length) {
3283
+ manifestFiles = manifestFiles.concat(csProjFiles);
3284
+ // .csproj parsing
3285
+ for (let f of csProjFiles) {
3286
+ if (DEBUG_MODE) {
3287
+ console.log(`Parsing ${f}`);
3288
+ }
3289
+ let csProjData = fs.readFileSync(f, { encoding: "utf-8" });
3290
+ // Remove byte order mark
3291
+ if (csProjData.charCodeAt(0) === 0xfeff) {
3292
+ csProjData = csProjData.slice(1);
3293
+ }
3294
+ const dlist = await utils.parseCsProjData(csProjData);
3295
+ if (dlist && dlist.length) {
3296
+ pkgList = pkgList.concat(dlist);
3297
+ }
3298
+ }
3299
+ }
3300
+ if (pkgList.length) {
3301
+ return buildBomNSData(options, pkgList, "nuget", {
3302
+ src: path,
3303
+ filename: manifestFiles.join(", ")
3304
+ });
3305
+ }
3306
+ return {};
3307
+ };
3308
+
3309
+ const mergeDependencies = (dependencies, newDependencies) => {
3310
+ const deps_map = {};
3311
+ let combinedDeps = dependencies.concat(newDependencies || []);
3312
+ for (const adep of combinedDeps) {
3313
+ if (!deps_map[adep.ref]) {
3314
+ deps_map[adep.ref] = new Set();
3315
+ }
3316
+ for (const eachDepends of adep["dependsOn"]) {
3317
+ deps_map[adep.ref].add(eachDepends);
3318
+ }
3319
+ }
3320
+ let retlist = [];
3321
+ for (const akey of Object.keys(deps_map)) {
3322
+ retlist.push({
3323
+ ref: akey,
3324
+ dependsOn: Array.from(deps_map[akey])
3325
+ });
3326
+ }
3327
+ return retlist;
3328
+ };
3329
+ exports.mergeDependencies = mergeDependencies;
3330
+
3331
+ const trimComponents = (components, format) => {
3332
+ const keyCache = {};
3333
+ const filteredComponents = [];
3334
+ for (let comp of components) {
3335
+ if (format === "xml" && comp.component) {
3336
+ if (!keyCache[comp.component.purl]) {
3337
+ keyCache[comp.component.purl] = true;
3338
+ filteredComponents.push(comp);
3339
+ }
3340
+ } else {
3341
+ if (!keyCache[comp.purl]) {
3342
+ keyCache[comp.purl] = true;
3343
+ filteredComponents.push(comp);
3344
+ }
3345
+ }
3346
+ }
3347
+ return filteredComponents;
3348
+ };
3349
+ exports.trimComponents = trimComponents;
3350
+
3351
+ const dedupeBom = (
3352
+ options,
3353
+ components,
3354
+ componentsXmls,
3355
+ parentComponent,
3356
+ dependencies
3357
+ ) => {
3358
+ if (!components) {
3359
+ return {};
3360
+ }
3361
+ if (!dependencies) {
3362
+ dependencies = [];
3363
+ }
3364
+ components = trimComponents(components, "json");
3365
+ componentsXmls = trimComponents(componentsXmls, "xml");
3366
+ if (DEBUG_MODE) {
3367
+ console.log(
3368
+ `BoM includes ${components.length} components and ${dependencies.length} dependencies after dedupe`
3369
+ );
3370
+ }
3371
+ const serialNum = "urn:uuid:" + uuidv4();
3372
+ return {
3373
+ options,
3374
+ parentComponent,
3375
+ components,
3376
+ componentsXmls,
3377
+ bomXml: buildBomXml(
3378
+ serialNum,
3379
+ parentComponent,
3380
+ componentsXmls,
3381
+ {
3382
+ dependencies: dependencies,
3383
+ services: options.services
3384
+ },
3385
+ options
3386
+ ),
3387
+ bomJson: {
3388
+ bomFormat: "CycloneDX",
3389
+ specVersion: "1.4",
3390
+ serialNumber: serialNum,
3391
+ version: 1,
3392
+ metadata: addMetadata(parentComponent, "json", options),
3393
+ components,
3394
+ services: options.services || [],
3395
+ dependencies
3396
+ }
3397
+ };
3398
+ };
3399
+ exports.dedupeBom = dedupeBom;
3400
+
3401
+ /**
3402
+ * Function to create bom string for all languages
3403
+ *
3404
+ * @param pathList list of to the project
3405
+ * @param options Parse options from the cli
3406
+ */
3407
+ const createMultiXBom = async (pathList, options) => {
3408
+ let components = [];
3409
+ let dependencies = [];
3410
+ let componentsXmls = [];
3411
+ let bomData = undefined;
3412
+ let parentComponent = determineParentComponent(options);
3413
+ if (
3414
+ ["docker", "oci", "container"].includes(options.projectType) &&
3415
+ options.allLayersExplodedDir
3416
+ ) {
3417
+ const { osPackages, allTypes } = binaryLib.getOSPackages(
3418
+ options.allLayersExplodedDir
3419
+ );
3420
+ if (DEBUG_MODE) {
3421
+ console.log(
3422
+ `Found ${osPackages.length} OS packages at ${options.allLayersExplodedDir}`
3423
+ );
3424
+ }
3425
+ if (allTypes && allTypes.length) {
3426
+ options.allOSComponentTypes = allTypes;
3427
+ }
3428
+ components = components.concat(osPackages);
3429
+ componentsXmls = componentsXmls.concat(
3430
+ listComponents(options, {}, osPackages, "", "xml")
3431
+ );
3432
+ }
3433
+ if (options.projectType === "os" && options.bomData) {
3434
+ bomData = options.bomData;
3435
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3436
+ if (DEBUG_MODE) {
3437
+ console.log(`Found ${bomData.bomJson.components.length} OS components`);
3438
+ }
3439
+ components = components.concat(bomData.bomJson.components);
3440
+ componentsXmls = componentsXmls.concat(
3441
+ listComponents(options, {}, bomData.bomJson.components, "", "xml")
3442
+ );
3443
+ }
3444
+ }
3445
+ for (let path of pathList) {
3446
+ if (DEBUG_MODE) {
3447
+ console.log("Scanning", path);
3448
+ }
3449
+ bomData = await createNodejsBom(path, options);
3450
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3451
+ if (DEBUG_MODE) {
3452
+ console.log(
3453
+ `Found ${bomData.bomJson.components.length} node.js packages at ${path}`
3454
+ );
3455
+ }
3456
+ components = components.concat(bomData.bomJson.components);
3457
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3458
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3459
+ parentComponent = bomData.parentComponent;
3460
+ }
3461
+ componentsXmls = componentsXmls.concat(
3462
+ listComponents(options, {}, bomData.bomJson.components, "npm", "xml")
3463
+ );
3464
+ }
3465
+ bomData = await createJavaBom(path, options);
3466
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3467
+ if (DEBUG_MODE) {
3468
+ console.log(
3469
+ `Found ${bomData.bomJson.components.length} java packages at ${path}`
3470
+ );
3471
+ }
3472
+ components = components.concat(bomData.bomJson.components);
3473
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3474
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3475
+ parentComponent = bomData.parentComponent;
3476
+ }
3477
+ componentsXmls = componentsXmls.concat(
3478
+ listComponents(options, {}, bomData.bomJson.components, "maven", "xml")
3479
+ );
3480
+ }
3481
+ bomData = await createPythonBom(path, options);
3482
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3483
+ if (DEBUG_MODE) {
3484
+ console.log(
3485
+ `Found ${bomData.bomJson.components.length} python packages at ${path}`
3486
+ );
3487
+ }
3488
+ components = components.concat(bomData.bomJson.components);
3489
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3490
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3491
+ parentComponent = bomData.parentComponent;
3492
+ }
3493
+ componentsXmls = componentsXmls.concat(
3494
+ listComponents(options, {}, bomData.bomJson.components, "pypi", "xml")
3495
+ );
3496
+ }
3497
+ bomData = await createGoBom(path, options);
3498
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3499
+ if (DEBUG_MODE) {
3500
+ console.log(
3501
+ `Found ${bomData.bomJson.components.length} go packages at ${path}`
3502
+ );
3503
+ }
3504
+ components = components.concat(bomData.bomJson.components);
3505
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3506
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3507
+ parentComponent = bomData.parentComponent;
3508
+ }
3509
+ componentsXmls = componentsXmls.concat(
3510
+ listComponents(options, {}, bomData.bomJson.components, "golang", "xml")
3511
+ );
3512
+ }
3513
+ bomData = await createRustBom(path, options);
3514
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3515
+ if (DEBUG_MODE) {
3516
+ console.log(
3517
+ `Found ${bomData.bomJson.components.length} rust packages at ${path}`
3518
+ );
3519
+ }
3520
+ components = components.concat(bomData.bomJson.components);
3521
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3522
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3523
+ parentComponent = bomData.parentComponent;
3524
+ }
3525
+ componentsXmls = componentsXmls.concat(
3526
+ listComponents(options, {}, bomData.bomJson.components, "cargo", "xml")
3527
+ );
3528
+ }
3529
+ bomData = await createPHPBom(path, options);
3530
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3531
+ if (DEBUG_MODE) {
3532
+ console.log(
3533
+ `Found ${bomData.bomJson.components.length} php packages at ${path}`
3534
+ );
3535
+ }
3536
+ components = components.concat(bomData.bomJson.components);
3537
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3538
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3539
+ parentComponent = bomData.parentComponent;
3540
+ }
3541
+ componentsXmls = componentsXmls.concat(
3542
+ listComponents(
3543
+ options,
3544
+ {},
3545
+ bomData.bomJson.components,
3546
+ "composer",
3547
+ "xml"
3548
+ )
3549
+ );
3550
+ }
3551
+ bomData = await createRubyBom(path, options);
3552
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3553
+ if (DEBUG_MODE) {
3554
+ console.log(
3555
+ `Found ${bomData.bomJson.components.length} ruby packages at ${path}`
3556
+ );
3557
+ }
3558
+ components = components.concat(bomData.bomJson.components);
3559
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3560
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3561
+ parentComponent = bomData.parentComponent;
3562
+ }
3563
+ componentsXmls = componentsXmls.concat(
3564
+ listComponents(options, {}, bomData.bomJson.components, "gem", "xml")
3565
+ );
3566
+ }
3567
+ bomData = await createCsharpBom(path, options);
3568
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3569
+ if (DEBUG_MODE) {
3570
+ console.log(
3571
+ `Found ${bomData.bomJson.components.length} csharp packages at ${path}`
3572
+ );
3573
+ }
3574
+ components = components.concat(bomData.bomJson.components);
3575
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3576
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3577
+ parentComponent = bomData.parentComponent;
3578
+ }
3579
+ componentsXmls = componentsXmls.concat(
3580
+ listComponents(options, {}, bomData.bomJson.components, "nuget", "xml")
3581
+ );
3582
+ }
3583
+ bomData = await createDartBom(path, options);
3584
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3585
+ if (DEBUG_MODE) {
3586
+ console.log(
3587
+ `Found ${bomData.bomJson.components.length} pub packages at ${path}`
3588
+ );
3589
+ }
3590
+ components = components.concat(bomData.bomJson.components);
3591
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3592
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3593
+ parentComponent = bomData.parentComponent;
3594
+ }
3595
+ componentsXmls = componentsXmls.concat(
3596
+ listComponents(options, {}, bomData.bomJson.components, "pub", "xml")
3597
+ );
3598
+ }
3599
+ bomData = await createHaskellBom(path, options);
3600
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3601
+ if (DEBUG_MODE) {
3602
+ console.log(
3603
+ `Found ${bomData.bomJson.components.length} hackage packages at ${path}`
3604
+ );
3605
+ }
3606
+ components = components.concat(bomData.bomJson.components);
3607
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3608
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3609
+ parentComponent = bomData.parentComponent;
3610
+ }
3611
+ componentsXmls = componentsXmls.concat(
3612
+ listComponents(
3613
+ options,
3614
+ {},
3615
+ bomData.bomJson.components,
3616
+ "hackage",
3617
+ "xml"
3618
+ )
3619
+ );
3620
+ }
3621
+ bomData = await createElixirBom(path, options);
3622
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3623
+ if (DEBUG_MODE) {
3624
+ console.log(
3625
+ `Found ${bomData.bomJson.components.length} mix packages at ${path}`
3626
+ );
3627
+ }
3628
+ components = components.concat(bomData.bomJson.components);
3629
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3630
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3631
+ parentComponent = bomData.parentComponent;
3632
+ }
3633
+ componentsXmls = componentsXmls.concat(
3634
+ listComponents(options, {}, bomData.bomJson.components, "hex", "xml")
3635
+ );
3636
+ }
3637
+ bomData = await createCppBom(path, options);
3638
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3639
+ if (DEBUG_MODE) {
3640
+ console.log(
3641
+ `Found ${bomData.bomJson.components.length} cpp packages at ${path}`
3642
+ );
3643
+ }
3644
+ components = components.concat(bomData.bomJson.components);
3645
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3646
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3647
+ parentComponent = bomData.parentComponent;
3648
+ }
3649
+ componentsXmls = componentsXmls.concat(
3650
+ listComponents(options, {}, bomData.bomJson.components, "conan", "xml")
3651
+ );
3652
+ }
3653
+ bomData = await createClojureBom(path, options);
3654
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3655
+ if (DEBUG_MODE) {
3656
+ console.log(
3657
+ `Found ${bomData.bomJson.components.length} clojure packages at ${path}`
3658
+ );
3659
+ }
3660
+ components = components.concat(bomData.bomJson.components);
3661
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3662
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3663
+ parentComponent = bomData.parentComponent;
3664
+ }
3665
+ componentsXmls = componentsXmls.concat(
3666
+ listComponents(
3667
+ options,
3668
+ {},
3669
+ bomData.bomJson.components,
3670
+ "clojars",
3671
+ "xml"
3672
+ )
3673
+ );
3674
+ }
3675
+ bomData = await createGitHubBom(path, options);
3676
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3677
+ if (DEBUG_MODE) {
3678
+ console.log(
3679
+ `Found ${bomData.bomJson.components.length} GitHub action packages at ${path}`
3680
+ );
3681
+ }
3682
+ components = components.concat(bomData.bomJson.components);
3683
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3684
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3685
+ parentComponent = bomData.parentComponent;
3686
+ }
3687
+ componentsXmls = componentsXmls.concat(
3688
+ listComponents(options, {}, bomData.bomJson.components, "github", "xml")
3689
+ );
3690
+ }
3691
+ bomData = await createCloudBuildBom(path, options);
3692
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3693
+ if (DEBUG_MODE) {
3694
+ console.log(
3695
+ `Found ${bomData.bomJson.components.length} CloudBuild configuration at ${path}`
3696
+ );
3697
+ }
3698
+ components = components.concat(bomData.bomJson.components);
3699
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3700
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3701
+ parentComponent = bomData.parentComponent;
3702
+ }
3703
+ componentsXmls = componentsXmls.concat(
3704
+ listComponents(
3705
+ options,
3706
+ {},
3707
+ bomData.bomJson.components,
3708
+ "cloudbuild",
3709
+ "xml"
3710
+ )
3711
+ );
3712
+ }
3713
+ // jar scanning is quite slow so this is limited to only deep scans
3714
+ if (options.deep) {
3715
+ bomData = createJarBom(path, options);
3716
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3717
+ if (DEBUG_MODE) {
3718
+ console.log(
3719
+ `Found ${bomData.bomJson.components.length} jar packages at ${path}`
3720
+ );
3721
+ }
3722
+ components = components.concat(bomData.bomJson.components);
3723
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3724
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3725
+ parentComponent = bomData.parentComponent;
3726
+ }
3727
+ componentsXmls = componentsXmls.concat(
3728
+ listComponents(
3729
+ options,
3730
+ {},
3731
+ bomData.bomJson.components,
3732
+ "maven",
3733
+ "xml"
3734
+ )
3735
+ );
3736
+ }
3737
+ }
3738
+ } // for
3739
+ if (options.lastWorkingDir && options.lastWorkingDir !== "") {
3740
+ bomData = createJarBom(options.lastWorkingDir, options);
3741
+ if (bomData && bomData.bomJson && bomData.bomJson.components) {
3742
+ if (DEBUG_MODE) {
3743
+ console.log(
3744
+ `Found ${bomData.bomJson.components.length} jar packages at ${options.lastWorkingDir}`
3745
+ );
3746
+ }
3747
+ components = components.concat(bomData.bomJson.components);
3748
+ dependencies = dependencies.concat(bomData.bomJson.dependencies);
3749
+ if (!parentComponent || !Object.keys(parentComponent).length) {
3750
+ parentComponent = bomData.parentComponent;
3751
+ }
3752
+ componentsXmls = componentsXmls.concat(
3753
+ listComponents(options, {}, bomData.bomJson.components, "maven", "xml")
3754
+ );
3755
+ }
3756
+ }
3757
+ return dedupeBom(
3758
+ options,
3759
+ components,
3760
+ componentsXmls,
3761
+ parentComponent,
3762
+ dependencies
3763
+ );
3764
+ };
3765
+
3766
+ /**
3767
+ * Function to create bom string for various languages
3768
+ *
3769
+ * @param path to the project
3770
+ * @param options Parse options from the cli
3771
+ */
3772
+ const createXBom = async (path, options) => {
3773
+ try {
3774
+ fs.accessSync(path, fs.constants.R_OK);
3775
+ } catch (err) {
3776
+ console.error(path, "is invalid");
3777
+ process.exit(1);
3778
+ }
3779
+ // node.js - package.json
3780
+ if (
3781
+ fs.existsSync(pathLib.join(path, "package.json")) ||
3782
+ fs.existsSync(pathLib.join(path, "rush.json")) ||
3783
+ fs.existsSync(pathLib.join(path, "yarn.lock"))
3784
+ ) {
3785
+ return await createNodejsBom(path, options);
3786
+ }
3787
+ // maven - pom.xml
3788
+ const pomFiles = utils.getAllFiles(
3789
+ path,
3790
+ (options.multiProject ? "**/" : "") + "pom.xml"
3791
+ );
3792
+ // gradle
3793
+ let gradleFiles = utils.getAllFiles(
3794
+ path,
3795
+ (options.multiProject ? "**/" : "") + "build.gradle*"
3796
+ );
3797
+ // scala sbt
3798
+ let sbtFiles = utils.getAllFiles(
3799
+ path,
3800
+ (options.multiProject ? "**/" : "") + "{build.sbt,Build.scala}*"
3801
+ );
3802
+ if (pomFiles.length || gradleFiles.length || sbtFiles.length) {
3803
+ return await createJavaBom(path, options);
3804
+ }
3805
+ // python
3806
+ const pipenvMode = fs.existsSync(pathLib.join(path, "Pipfile"));
3807
+ const poetryMode = fs.existsSync(pathLib.join(path, "poetry.lock"));
3808
+ const reqFiles = utils.getAllFiles(
3809
+ path,
3810
+ (options.multiProject ? "**/" : "") + "requirements.txt"
3811
+ );
3812
+ const reqDirFiles = utils.getAllFiles(
3813
+ path,
3814
+ (options.multiProject ? "**/" : "") + "requirements/*.txt"
3815
+ );
3816
+ const setupPy = pathLib.join(path, "setup.py");
3817
+ const requirementsMode =
3818
+ (reqFiles && reqFiles.length) || (reqDirFiles && reqDirFiles.length);
3819
+ const whlFiles = utils.getAllFiles(
3820
+ path,
3821
+ (options.multiProject ? "**/" : "") + "*.whl"
3822
+ );
3823
+ const setupPyMode = fs.existsSync(setupPy);
3824
+ if (
3825
+ requirementsMode ||
3826
+ pipenvMode ||
3827
+ poetryMode ||
3828
+ setupPyMode ||
3829
+ whlFiles.length
3830
+ ) {
3831
+ return await createPythonBom(path, options);
3832
+ }
3833
+ // go
3834
+ const gosumFiles = utils.getAllFiles(
3835
+ path,
3836
+ (options.multiProject ? "**/" : "") + "go.sum"
3837
+ );
3838
+ const gomodFiles = utils.getAllFiles(
3839
+ path,
3840
+ (options.multiProject ? "**/" : "") + "go.mod"
3841
+ );
3842
+ const gopkgLockFiles = utils.getAllFiles(
3843
+ path,
3844
+ (options.multiProject ? "**/" : "") + "Gopkg.lock"
3845
+ );
3846
+ if (gomodFiles.length || gosumFiles.length || gopkgLockFiles.length) {
3847
+ return await createGoBom(path, options);
3848
+ }
3849
+
3850
+ // rust
3851
+ const cargoLockFiles = utils.getAllFiles(
3852
+ path,
3853
+ (options.multiProject ? "**/" : "") + "Cargo.lock"
3854
+ );
3855
+ const cargoFiles = utils.getAllFiles(
3856
+ path,
3857
+ (options.multiProject ? "**/" : "") + "Cargo.toml"
3858
+ );
3859
+ if (cargoLockFiles.length || cargoFiles.length) {
3860
+ return await createRustBom(path, options);
3861
+ }
3862
+
3863
+ // php
3864
+ const composerJsonFiles = utils.getAllFiles(
3865
+ path,
3866
+ (options.multiProject ? "**/" : "") + "composer.json"
3867
+ );
3868
+ const composerLockFiles = utils.getAllFiles(
3869
+ path,
3870
+ (options.multiProject ? "**/" : "") + "composer.lock"
3871
+ );
3872
+ if (composerJsonFiles.length || composerLockFiles.length) {
3873
+ return await createPHPBom(path, options);
3874
+ }
3875
+
3876
+ // Ruby
3877
+ const gemFiles = utils.getAllFiles(
3878
+ path,
3879
+ (options.multiProject ? "**/" : "") + "Gemfile"
3880
+ );
3881
+ const gemLockFiles = utils.getAllFiles(
3882
+ path,
3883
+ (options.multiProject ? "**/" : "") + "Gemfile.lock"
3884
+ );
3885
+ if (gemFiles.length || gemLockFiles.length) {
3886
+ return await createRubyBom(path, options);
3887
+ }
3888
+
3889
+ // .Net
3890
+ const csProjFiles = utils.getAllFiles(
3891
+ path,
3892
+ (options.multiProject ? "**/" : "") + "*.csproj"
3893
+ );
3894
+ if (csProjFiles.length) {
3895
+ return await createCsharpBom(path, options);
3896
+ }
3897
+
3898
+ // Dart
3899
+ const pubFiles = utils.getAllFiles(
3900
+ path,
3901
+ (options.multiProject ? "**/" : "") + "pubspec.lock"
3902
+ );
3903
+ const pubSpecFiles = utils.getAllFiles(
3904
+ path,
3905
+ (options.multiProject ? "**/" : "") + "pubspec.yaml"
3906
+ );
3907
+ if (pubFiles.length || pubSpecFiles.length) {
3908
+ return await createDartBom(path, options);
3909
+ }
3910
+
3911
+ // Haskell
3912
+ const hackageFiles = utils.getAllFiles(
3913
+ path,
3914
+ (options.multiProject ? "**/" : "") + "cabal.project.freeze"
3915
+ );
3916
+ if (hackageFiles.length) {
3917
+ return await createHaskellBom(path, options);
3918
+ }
3919
+
3920
+ // Elixir
3921
+ const mixFiles = utils.getAllFiles(
3922
+ path,
3923
+ (options.multiProject ? "**/" : "") + "mix.lock"
3924
+ );
3925
+ if (mixFiles.length) {
3926
+ return await createElixirBom(path, options);
3927
+ }
3928
+
3929
+ // cpp
3930
+ const conanLockFiles = utils.getAllFiles(
3931
+ path,
3932
+ (options.multiProject ? "**/" : "") + "conan.lock"
3933
+ );
3934
+ const conanFiles = utils.getAllFiles(
3935
+ path,
3936
+ (options.multiProject ? "**/" : "") + "conanfile.txt"
3937
+ );
3938
+ if (conanLockFiles.length || conanFiles.length) {
3939
+ return await createCppBom(path, options);
3940
+ }
3941
+
3942
+ // clojure
3943
+ const ednFiles = utils.getAllFiles(
3944
+ path,
3945
+ (options.multiProject ? "**/" : "") + "deps.edn"
3946
+ );
3947
+ const leinFiles = utils.getAllFiles(
3948
+ path,
3949
+ (options.multiProject ? "**/" : "") + "project.clj"
3950
+ );
3951
+ if (ednFiles.length || leinFiles.length) {
3952
+ return await createClojureBom(path, options);
3953
+ }
3954
+
3955
+ // GitHub actions
3956
+ const ghactionFiles = utils.getAllFiles(path, ".github/workflows/" + "*.yml");
3957
+ if (ghactionFiles.length) {
3958
+ return await createGitHubBom(path, options);
3959
+ }
3960
+
3961
+ // Jenkins plugins
3962
+ const hpiFiles = utils.getAllFiles(
3963
+ path,
3964
+ (options.multiProject ? "**/" : "") + "*.hpi"
3965
+ );
3966
+ if (hpiFiles.length) {
3967
+ return await createJenkinsBom(path, options);
3968
+ }
3969
+
3970
+ // Helm charts
3971
+ const chartFiles = utils.getAllFiles(
3972
+ path,
3973
+ (options.multiProject ? "**/" : "") + "Chart.yaml"
3974
+ );
3975
+ const yamlFiles = utils.getAllFiles(
3976
+ path,
3977
+ (options.multiProject ? "**/" : "") + "values.yaml"
3978
+ );
3979
+ if (chartFiles.length || yamlFiles.length) {
3980
+ return await createHelmBom(path, options);
3981
+ }
3982
+
3983
+ // Docker compose, kubernetes and skaffold
3984
+ const dcFiles = utils.getAllFiles(
3985
+ path,
3986
+ (options.multiProject ? "**/" : "") + "docker-compose*.yml"
3987
+ );
3988
+ const skFiles = utils.getAllFiles(
3989
+ path,
3990
+ (options.multiProject ? "**/" : "") + "skaffold.yaml"
3991
+ );
3992
+ const deplFiles = utils.getAllFiles(
3993
+ path,
3994
+ (options.multiProject ? "**/" : "") + "deployment.yaml"
3995
+ );
3996
+ if (dcFiles.length || skFiles.length || deplFiles.length) {
3997
+ return await createContainerSpecLikeBom(path, options);
3998
+ }
3999
+
4000
+ // Google CloudBuild
4001
+ const cbFiles = utils.getAllFiles(
4002
+ path,
4003
+ (options.multiProject ? "**/" : "") + "cloudbuild.yaml"
4004
+ );
4005
+ if (cbFiles.length) {
4006
+ return await createCloudBuildBom(path, options);
4007
+ }
4008
+ };
4009
+
4010
+ /**
4011
+ * Function to create bom string for various languages
4012
+ *
4013
+ * @param path to the project
4014
+ * @param options Parse options from the cli
4015
+ */
4016
+ const createBom = async (path, options) => {
4017
+ let { projectType } = options;
4018
+ if (!projectType) {
4019
+ projectType = "";
4020
+ }
4021
+ projectType = projectType.toLowerCase();
4022
+ let exportData = undefined;
4023
+ let isContainerMode = false;
4024
+ // Docker and image archive support
4025
+ if (path.endsWith(".tar") || path.endsWith(".tar.gz")) {
4026
+ exportData = await dockerLib.exportArchive(path);
4027
+ if (!exportData) {
4028
+ console.log(
4029
+ `OS BOM generation has failed due to problems with exporting the image ${path}`
4030
+ );
4031
+ return {};
4032
+ }
4033
+ isContainerMode = true;
4034
+ } else if (
4035
+ projectType === "docker" ||
4036
+ projectType === "podman" ||
4037
+ projectType === "oci" ||
4038
+ path.startsWith("docker.io") ||
4039
+ path.startsWith("quay.io") ||
4040
+ path.startsWith("ghcr.io") ||
4041
+ path.startsWith("mcr.microsoft.com") ||
4042
+ path.includes("@sha256") ||
4043
+ path.includes(":latest")
4044
+ ) {
4045
+ exportData = await dockerLib.exportImage(path);
4046
+ if (!exportData) {
4047
+ console.log(
4048
+ "BOM generation has failed due to problems with exporting the image"
4049
+ );
4050
+ options.failOnError && process.exit(1);
4051
+ return {};
4052
+ }
4053
+ isContainerMode = true;
4054
+ } else if (projectType === "oci-dir") {
4055
+ isContainerMode = true;
4056
+ exportData = {
4057
+ inspectData: undefined,
4058
+ lastWorkingDir: "",
4059
+ allLayersDir: path,
4060
+ allLayersExplodedDir: path
4061
+ };
4062
+ if (fs.existsSync(pathLib.join(path, "all-layers"))) {
4063
+ exportData.allLayersDir = pathLib.join(path, "all-layers");
4064
+ }
4065
+ exportData.pkgPathList = dockerLib.getPkgPathList(exportData, undefined);
4066
+ }
4067
+ if (isContainerMode) {
4068
+ options.multiProject = true;
4069
+ options.installDeps = false;
4070
+ // Force the project type to docker
4071
+ options.projectType = "docker";
4072
+ // Pass the original path
4073
+ options.path = path;
4074
+ options.parentComponent = {};
4075
+ // Create parent component based on the inspect config
4076
+ const inspectData = exportData.inspectData;
4077
+ if (
4078
+ inspectData &&
4079
+ inspectData.RepoDigests &&
4080
+ inspectData.RepoTags &&
4081
+ Array.isArray(inspectData.RepoDigests) &&
4082
+ Array.isArray(inspectData.RepoTags) &&
4083
+ inspectData.RepoDigests.length &&
4084
+ inspectData.RepoTags.length
4085
+ ) {
4086
+ const repoTag = inspectData.RepoTags[0];
4087
+ if (repoTag) {
4088
+ const tmpA = repoTag.split(":");
4089
+ if (tmpA && tmpA.length === 2) {
4090
+ options.parentComponent = {
4091
+ name: tmpA[0],
4092
+ version: tmpA[1],
4093
+ type: "container",
4094
+ purl: "pkg:oci/" + inspectData.RepoDigests[0],
4095
+ _integrity: inspectData.RepoDigests[0].replace("sha256:", "sha256-")
4096
+ };
4097
+ }
4098
+ }
4099
+ }
4100
+ // Pass the entire export data about the image layers
4101
+ options.exportData = exportData;
4102
+ options.lastWorkingDir = exportData.lastWorkingDir;
4103
+ options.allLayersExplodedDir = exportData.allLayersExplodedDir;
4104
+ const bomData = await createMultiXBom(
4105
+ [...new Set(exportData.pkgPathList)],
4106
+ options
4107
+ );
4108
+ if (
4109
+ exportData.allLayersDir &&
4110
+ exportData.allLayersDir.startsWith(os.tmpdir())
4111
+ ) {
4112
+ if (DEBUG_MODE) {
4113
+ console.log(`Cleaning up ${exportData.allLayersDir}`);
4114
+ }
4115
+ try {
4116
+ if (fs.rmSync) {
4117
+ fs.rmSync(exportData.allLayersDir, { recursive: true, force: true });
4118
+ }
4119
+ } catch (err) {
4120
+ // continue regardless of error
4121
+ }
4122
+ }
4123
+ return bomData;
4124
+ }
4125
+ if (path.endsWith(".war")) {
4126
+ projectType = "java";
4127
+ }
4128
+ switch (projectType) {
4129
+ case "java":
4130
+ case "groovy":
4131
+ case "kotlin":
4132
+ case "scala":
4133
+ case "jvm":
4134
+ return await createJavaBom(path, options);
4135
+ case "jar":
4136
+ options.multiProject = true;
4137
+ return await createJarBom(path, options);
4138
+ case "gradle-index":
4139
+ case "gradle-cache":
4140
+ options.multiProject = true;
4141
+ return await createJarBom(GRADLE_CACHE_DIR, options);
4142
+ case "sbt-index":
4143
+ case "sbt-cache":
4144
+ options.multiProject = true;
4145
+ return await createJarBom(SBT_CACHE_DIR, options);
4146
+ case "maven-index":
4147
+ case "maven-cache":
4148
+ case "maven-repo":
4149
+ options.multiProject = true;
4150
+ return await createJarBom(
4151
+ pathLib.join(os.homedir(), ".m2", "repository"),
4152
+ options
4153
+ );
4154
+ case "nodejs":
4155
+ case "js":
4156
+ case "javascript":
4157
+ case "typescript":
4158
+ case "ts":
4159
+ return await createNodejsBom(path, options);
4160
+ case "python":
4161
+ case "py":
4162
+ options.multiProject = true;
4163
+ return await createPythonBom(path, options);
4164
+ case "go":
4165
+ case "golang":
4166
+ options.multiProject = true;
4167
+ return await createGoBom(path, options);
4168
+ case "rust":
4169
+ case "rust-lang":
4170
+ options.multiProject = true;
4171
+ return await createRustBom(path, options);
4172
+ case "php":
4173
+ options.multiProject = true;
4174
+ return await createPHPBom(path, options);
4175
+ case "ruby":
4176
+ options.multiProject = true;
4177
+ return await createRubyBom(path, options);
4178
+ case "csharp":
4179
+ case "netcore":
4180
+ case "dotnet":
4181
+ options.multiProject = true;
4182
+ return await createCsharpBom(path, options);
4183
+ case "dart":
4184
+ case "flutter":
4185
+ case "pub":
4186
+ options.multiProject = true;
4187
+ return await createDartBom(path, options);
4188
+ case "haskell":
4189
+ case "hackage":
4190
+ case "cabal":
4191
+ options.multiProject = true;
4192
+ return await createHaskellBom(path, options);
4193
+ case "elixir":
4194
+ case "hex":
4195
+ case "mix":
4196
+ options.multiProject = true;
4197
+ return await createElixirBom(path, options);
4198
+ case "c":
4199
+ case "cpp":
4200
+ case "c++":
4201
+ case "conan":
4202
+ options.multiProject = true;
4203
+ return await createCppBom(path, options);
4204
+ case "clojure":
4205
+ case "edn":
4206
+ case "clj":
4207
+ case "leiningen":
4208
+ options.multiProject = true;
4209
+ return await createClojureBom(path, options);
4210
+ case "github":
4211
+ case "actions":
4212
+ options.multiProject = true;
4213
+ return await createGitHubBom(path, options);
4214
+ case "os":
4215
+ case "osquery":
4216
+ case "windows":
4217
+ case "linux":
4218
+ options.multiProject = true;
4219
+ return await createOSBom(path, options);
4220
+ case "jenkins":
4221
+ options.multiProject = true;
4222
+ return await createJenkinsBom(path, options);
4223
+ case "helm":
4224
+ case "charts":
4225
+ options.multiProject = true;
4226
+ return await createHelmBom(path, options);
4227
+ case "helm-index":
4228
+ case "helm-repo":
4229
+ options.multiProject = true;
4230
+ return await createHelmBom(
4231
+ pathLib.join(os.homedir(), ".cache", "helm", "repository"),
4232
+ options
4233
+ );
4234
+ case "universal":
4235
+ case "docker-compose":
4236
+ case "swarm":
4237
+ case "tekton":
4238
+ case "kustomize":
4239
+ case "operator":
4240
+ case "skaffold":
4241
+ case "kubernetes":
4242
+ case "openshift":
4243
+ case "yaml-manifest":
4244
+ options.multiProject = true;
4245
+ return await createContainerSpecLikeBom(path, options);
4246
+ case "cloudbuild":
4247
+ options.multiProject = true;
4248
+ return await createCloudBuildBom(path, options);
4249
+ default:
4250
+ // In recurse mode return multi-language Bom
4251
+ // https://github.com/cyclonedx/cdxgen/issues/95
4252
+ if (options.multiProject) {
4253
+ return await createMultiXBom([path], options);
4254
+ } else {
4255
+ return await createXBom(path, options);
4256
+ }
4257
+ }
4258
+ };
4259
+ exports.createBom = createBom;
4260
+
4261
+ /**
4262
+ * Method to submit the generated bom to dependency-track or cyclonedx server
4263
+ *
4264
+ * @param args CLI args
4265
+ * @param bomContents BOM Xml
4266
+ */
4267
+ exports.submitBom = async (args, bomContents) => {
4268
+ let serverUrl = args.serverUrl + "/api/v1/bom";
4269
+ let encodedBomContents = Buffer.from(bomContents).toString("base64");
4270
+ if (encodedBomContents.startsWith("77u/")) {
4271
+ encodedBomContents = encodedBomContents.substring(4);
4272
+ }
4273
+ const bomPayload = {
4274
+ project: args.projectId,
4275
+ projectName: args.projectName,
4276
+ projectVersion: args.projectVersion,
4277
+ autoCreate: "true",
4278
+ bom: encodedBomContents
4279
+ };
4280
+ if (DEBUG_MODE) {
4281
+ console.log("Submitting BOM to", serverUrl);
4282
+ }
4283
+ return await got(serverUrl, {
4284
+ method: "PUT",
4285
+ headers: {
4286
+ "X-Api-Key": args.apiKey,
4287
+ "Content-Type": "application/json"
4288
+ },
4289
+ json: bomPayload,
4290
+ responseType: "json"
4291
+ }).json();
4292
+ };