@cyclonedx/cdxgen 9.8.10 → 9.9.1

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/evinser.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  collectMvnDependencies
8
8
  } from "./utils.js";
9
9
  import { tmpdir } from "node:os";
10
- import path from "node:path";
10
+ import path, { basename } from "node:path";
11
11
  import fs from "node:fs";
12
12
  import * as db from "./db.js";
13
13
  import { PackageURL } from "packageurl-js";
@@ -22,10 +22,24 @@ const typePurlsCache = {};
22
22
  * @param {object} Command line options
23
23
  */
24
24
  export const prepareDB = async (options) => {
25
+ if (!options.dbPath.includes("memory") && !fs.existsSync(options.dbPath)) {
26
+ try {
27
+ fs.mkdirSync(options.dbPath, { recursive: true });
28
+ } catch (e) {
29
+ // ignore
30
+ }
31
+ }
25
32
  const dirPath = options._[0] || ".";
26
33
  const bomJsonFile = options.input;
27
34
  if (!fs.existsSync(bomJsonFile)) {
28
- console.log("Bom file doesn't exist");
35
+ console.log(
36
+ "Bom file doesn't exist. Check if cdxgen was invoked with the correct type argument."
37
+ );
38
+ if (!process.env.CDXGEN_DEBUG_MODE) {
39
+ console.log(
40
+ "Set the environment variable CDXGEN_DEBUG_MODE to debug to troubleshoot the issue further."
41
+ );
42
+ }
29
43
  return;
30
44
  }
31
45
  const bomJson = JSON.parse(fs.readFileSync(bomJsonFile, "utf8"));
@@ -54,8 +68,6 @@ export const prepareDB = async (options) => {
54
68
  if ((!usagesSlice && !namespaceSlice) || options.force) {
55
69
  if (comp.purl.startsWith("pkg:maven")) {
56
70
  hasMavenPkgs = true;
57
- } else if (isSlicingRequired(comp.purl)) {
58
- purlsToSlice[comp.purl] = true;
59
71
  }
60
72
  }
61
73
  }
@@ -71,7 +83,7 @@ export const prepareDB = async (options) => {
71
83
  }
72
84
  }
73
85
  for (const purl of Object.keys(purlsToSlice)) {
74
- await createAndStoreSlice(purl, purlsJars, Usages);
86
+ await createAndStoreSlice(purl, purlsJars, Usages, options);
75
87
  }
76
88
  return { sequelize, Namespaces, Usages, DataFlows };
77
89
  };
@@ -148,8 +160,13 @@ export const catalogGradleDeps = async (dirPath, purlsJars, Namespaces) => {
148
160
  );
149
161
  };
150
162
 
151
- export const createAndStoreSlice = async (purl, purlsJars, Usages) => {
152
- const retMap = createSlice(purl, purlsJars[purl], "usages");
163
+ export const createAndStoreSlice = async (
164
+ purl,
165
+ purlsJars,
166
+ Usages,
167
+ options = {}
168
+ ) => {
169
+ const retMap = createSlice(purl, purlsJars[purl], "usages", options);
153
170
  let sliceData = undefined;
154
171
  if (retMap && retMap.slicesFile && fs.existsSync(retMap.slicesFile)) {
155
172
  sliceData = await Usages.findOrCreate({
@@ -166,7 +183,12 @@ export const createAndStoreSlice = async (purl, purlsJars, Usages) => {
166
183
  return sliceData;
167
184
  };
168
185
 
169
- export const createSlice = (purlOrLanguage, filePath, sliceType = "usages") => {
186
+ export const createSlice = (
187
+ purlOrLanguage,
188
+ filePath,
189
+ sliceType = "usages",
190
+ options = {}
191
+ ) => {
170
192
  if (!filePath) {
171
193
  return;
172
194
  }
@@ -177,9 +199,18 @@ export const createSlice = (purlOrLanguage, filePath, sliceType = "usages") => {
177
199
  if (!language) {
178
200
  return undefined;
179
201
  }
180
- const tempDir = fs.mkdtempSync(path.join(tmpdir(), `atom-${sliceType}-`));
181
- const atomFile = path.join(tempDir, "app.atom");
182
- const slicesFile = path.join(tempDir, `${sliceType}.slices.json`);
202
+ let sliceOutputDir = fs.mkdtempSync(
203
+ path.join(tmpdir(), `atom-${sliceType}-`)
204
+ );
205
+ if (options && options.output) {
206
+ sliceOutputDir =
207
+ fs.existsSync(options.output) &&
208
+ fs.lstatSync(options.output).isDirectory()
209
+ ? path.basename(options.output)
210
+ : path.dirname(options.output);
211
+ }
212
+ const atomFile = path.join(sliceOutputDir, "app.atom");
213
+ const slicesFile = path.join(sliceOutputDir, `${sliceType}.slices.json`);
183
214
  const args = [
184
215
  sliceType,
185
216
  "-l",
@@ -204,7 +235,7 @@ export const createSlice = (purlOrLanguage, filePath, sliceType = "usages") => {
204
235
  );
205
236
  }
206
237
  return {
207
- tempDir,
238
+ tempDir: sliceOutputDir,
208
239
  slicesFile,
209
240
  atomFile
210
241
  };
@@ -231,17 +262,19 @@ export const initFromSbom = (components) => {
231
262
  const purlLocationMap = {};
232
263
  const purlImportsMap = {};
233
264
  for (const comp of components) {
234
- if (!comp || !comp.evidence || !comp.evidence.occurrences) {
265
+ if (!comp || !comp.evidence) {
235
266
  continue;
236
267
  }
237
- purlLocationMap[comp.purl] = new Set(
238
- comp.evidence.occurrences.map((v) => v.location)
239
- );
240
268
  (comp.properties || [])
241
269
  .filter((v) => v.name === "ImportedModules")
242
270
  .forEach((v) => {
243
271
  purlImportsMap[comp.purl] = (v.value || "").split(",");
244
272
  });
273
+ if (comp.evidence.occurrences) {
274
+ purlLocationMap[comp.purl] = new Set(
275
+ comp.evidence.occurrences.map((v) => v.location)
276
+ );
277
+ }
245
278
  }
246
279
  return {
247
280
  purlLocationMap,
@@ -260,8 +293,10 @@ export const analyzeProject = async (dbObjMap, options) => {
260
293
  const language = options.language;
261
294
  let usageSlice = undefined;
262
295
  let dataFlowSlice = undefined;
296
+ let reachablesSlice = undefined;
263
297
  let usagesSlicesFile = undefined;
264
298
  let dataFlowSlicesFile = undefined;
299
+ let reachablesSlicesFile = undefined;
265
300
  let dataFlowFrames = {};
266
301
  let servicesMap = {};
267
302
  let retMap = {};
@@ -278,7 +313,7 @@ export const analyzeProject = async (dbObjMap, options) => {
278
313
  usagesSlicesFile = options.usagesSlicesFile;
279
314
  } else {
280
315
  // Generate our own slices
281
- retMap = createSlice(language, dirPath, "usages");
316
+ retMap = createSlice(language, dirPath, "usages", options);
282
317
  if (retMap && retMap.slicesFile && fs.existsSync(retMap.slicesFile)) {
283
318
  usageSlice = JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8"));
284
319
  usagesSlicesFile = retMap.slicesFile;
@@ -310,7 +345,7 @@ export const analyzeProject = async (dbObjMap, options) => {
310
345
  fs.readFileSync(options.dataFlowSlicesFile, "utf-8")
311
346
  );
312
347
  } else {
313
- retMap = createSlice(language, dirPath, "data-flow");
348
+ retMap = createSlice(language, dirPath, "data-flow", options);
314
349
  if (retMap && retMap.slicesFile && fs.existsSync(retMap.slicesFile)) {
315
350
  dataFlowSlicesFile = retMap.slicesFile;
316
351
  dataFlowSlice = JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8"));
@@ -330,10 +365,36 @@ export const analyzeProject = async (dbObjMap, options) => {
330
365
  purlImportsMap
331
366
  );
332
367
  }
368
+ if (options.withReachables) {
369
+ if (
370
+ options.reachablesSlicesFile &&
371
+ fs.existsSync(options.reachablesSlicesFile)
372
+ ) {
373
+ reachablesSlicesFile = options.reachablesSlicesFile;
374
+ reachablesSlice = JSON.parse(
375
+ fs.readFileSync(options.reachablesSlicesFile, "utf-8")
376
+ );
377
+ } else {
378
+ retMap = createSlice(language, dirPath, "reachables", options);
379
+ if (retMap && retMap.slicesFile && fs.existsSync(retMap.slicesFile)) {
380
+ reachablesSlicesFile = retMap.slicesFile;
381
+ reachablesSlice = JSON.parse(
382
+ fs.readFileSync(retMap.slicesFile, "utf-8")
383
+ );
384
+ console.log(
385
+ `To speed up this step, cache ${reachablesSlicesFile} and invoke evinse with the --reachables-slices-file argument.`
386
+ );
387
+ }
388
+ }
389
+ }
390
+ if (reachablesSlice && Object.keys(reachablesSlice).length) {
391
+ dataFlowFrames = await collectReachableFrames(language, reachablesSlice);
392
+ }
333
393
  return {
334
394
  atomFile: retMap.atomFile,
335
395
  usagesSlicesFile,
336
396
  dataFlowSlicesFile,
397
+ reachablesSlicesFile,
337
398
  purlLocationMap,
338
399
  servicesMap,
339
400
  dataFlowFrames,
@@ -365,7 +426,8 @@ export const parseObjectSlices = async (
365
426
  if (
366
427
  !slice.fileName ||
367
428
  !slice.fileName.trim().length ||
368
- slice.fileName === "<empty>"
429
+ slice.fileName === "<empty>" ||
430
+ slice.fileName === "<unknown>"
369
431
  ) {
370
432
  continue;
371
433
  }
@@ -379,6 +441,7 @@ export const parseObjectSlices = async (
379
441
  );
380
442
  detectServicesFromUsages(language, slice, servicesMap);
381
443
  }
444
+ detectServicesFromUDT(language, usageSlice.userDefinedTypes, servicesMap);
382
445
  return {
383
446
  purlLocationMap,
384
447
  servicesMap,
@@ -428,10 +491,13 @@ export const parseSliceUsages = async (
428
491
  atype[0] !== false &&
429
492
  !isFilterableType(language, userDefinedTypesMap, atype[1])
430
493
  ) {
431
- if (!atype[1].includes("(")) {
494
+ if (!atype[1].includes("(") && !atype[1].includes(".py")) {
432
495
  typesToLookup.add(atype[1]);
433
496
  // Javascript calls can be resolved to a precise line number only from the call nodes
434
- if (language == "javascript" && ausageLine) {
497
+ if (
498
+ ["javascript", "js", "ts", "typescript"].includes(language) &&
499
+ ausageLine
500
+ ) {
435
501
  if (atype[1].includes(":")) {
436
502
  typesToLookup.add(atype[1].split("::")[0].replace(/:/g, "/"));
437
503
  }
@@ -456,7 +522,10 @@ export const parseSliceUsages = async (
456
522
  if (
457
523
  !isFilterableType(language, userDefinedTypesMap, acall?.resolvedMethod)
458
524
  ) {
459
- if (!acall?.resolvedMethod.includes("(")) {
525
+ if (
526
+ !acall?.resolvedMethod.includes("(") &&
527
+ !acall?.resolvedMethod.includes(".py")
528
+ ) {
460
529
  typesToLookup.add(acall?.resolvedMethod);
461
530
  // Javascript calls can be resolved to a precise line number only from the call nodes
462
531
  if (acall.lineNumber) {
@@ -484,7 +553,7 @@ export const parseSliceUsages = async (
484
553
  }
485
554
  for (const aparamType of acall?.paramTypes || []) {
486
555
  if (!isFilterableType(language, userDefinedTypesMap, aparamType)) {
487
- if (!aparamType.includes("(")) {
556
+ if (!aparamType.includes("(") && !aparamType.includes(".py")) {
488
557
  typesToLookup.add(aparamType);
489
558
  if (acall.lineNumber) {
490
559
  if (aparamType.includes(":")) {
@@ -533,16 +602,17 @@ export const parseSliceUsages = async (
533
602
  }
534
603
  } else {
535
604
  // Check the namespaces db
536
- const nsHits =
537
- typePurlsCache[atype] ||
538
- (await dbObjMap.Namespaces.findAll({
605
+ let nsHits = typePurlsCache[atype];
606
+ if (["java", "jar"].includes(language)) {
607
+ nsHits = await dbObjMap.Namespaces.findAll({
539
608
  attributes: ["purl"],
540
609
  where: {
541
610
  data: {
542
611
  [Op.like]: `%${atype}%`
543
612
  }
544
613
  }
545
- }));
614
+ });
615
+ }
546
616
  if (nsHits && nsHits.length) {
547
617
  for (const ns of nsHits) {
548
618
  if (!purlLocationMap[ns.purl]) {
@@ -565,16 +635,21 @@ export const isFilterableType = (
565
635
  ) => {
566
636
  if (
567
637
  !typeFullName ||
568
- ["ANY", "UNKNOWN", "VOID"].includes(typeFullName.toUpperCase())
638
+ ["ANY", "UNKNOWN", "VOID", "IMPORT"].includes(typeFullName.toUpperCase())
569
639
  ) {
570
640
  return true;
571
641
  }
572
- if (
573
- typeFullName.startsWith("<operator") ||
574
- typeFullName.startsWith("<unresolved") ||
575
- typeFullName.startsWith("<unknownFullName")
576
- ) {
577
- return true;
642
+ for (const ab of [
643
+ "<operator",
644
+ "<unresolved",
645
+ "<unknownFullName",
646
+ "__builtin",
647
+ "LAMBDA",
648
+ "../"
649
+ ]) {
650
+ if (typeFullName.startsWith(ab)) {
651
+ return true;
652
+ }
578
653
  }
579
654
  if (language && ["java", "jar"].includes(language)) {
580
655
  if (
@@ -590,7 +665,7 @@ export const isFilterableType = (
590
665
  return true;
591
666
  }
592
667
  }
593
- if (language === "javascript") {
668
+ if (["javascript", "js", "ts", "typescript"].includes(language)) {
594
669
  if (
595
670
  typeFullName.includes(".js") ||
596
671
  typeFullName.includes("=>") ||
@@ -598,13 +673,20 @@ export const isFilterableType = (
598
673
  typeFullName.startsWith("{ ") ||
599
674
  typeFullName.startsWith("JSON") ||
600
675
  typeFullName.startsWith("void:") ||
601
- typeFullName.startsWith("LAMBDA") ||
602
- typeFullName.startsWith("../") ||
603
676
  typeFullName.startsWith("node:")
604
677
  ) {
605
678
  return true;
606
679
  }
607
680
  }
681
+ if (["python", "py"].includes(language)) {
682
+ if (
683
+ typeFullName.startsWith("tmp") ||
684
+ typeFullName.startsWith("self.") ||
685
+ typeFullName.startsWith("_")
686
+ ) {
687
+ return true;
688
+ }
689
+ }
608
690
  if (userDefinedTypesMap[typeFullName]) {
609
691
  return true;
610
692
  }
@@ -668,6 +750,61 @@ export const detectServicesFromUsages = (language, slice, servicesMap = {}) => {
668
750
  }
669
751
  };
670
752
 
753
+ /**
754
+ * Method to detect services from user defined types in the usage slice
755
+ *
756
+ * @param {string} language Application language
757
+ * @param {array} userDefinedTypes User defined types
758
+ * @param {object} servicesMap Existing service map
759
+ */
760
+ export const detectServicesFromUDT = (
761
+ language,
762
+ userDefinedTypes,
763
+ servicesMap
764
+ ) => {
765
+ if (
766
+ ["python", "py"].includes(language) &&
767
+ userDefinedTypes &&
768
+ userDefinedTypes.length
769
+ ) {
770
+ for (const audt of userDefinedTypes) {
771
+ if (
772
+ audt.name.includes("route") ||
773
+ audt.name.includes("path") ||
774
+ audt.name.includes("url")
775
+ ) {
776
+ const fields = audt.fields || [];
777
+ if (
778
+ fields.length &&
779
+ fields[0] &&
780
+ fields[0].name &&
781
+ fields[0].name.length > 1
782
+ ) {
783
+ const endpoints = extractEndpoints(language, fields[0].name);
784
+ let serviceName = "service";
785
+ if (audt.fileName) {
786
+ serviceName = `${basename(
787
+ audt.fileName.replace(".py", "")
788
+ )}-service`;
789
+ }
790
+ if (!servicesMap[serviceName]) {
791
+ servicesMap[serviceName] = {
792
+ endpoints: new Set(),
793
+ authenticated: false,
794
+ xTrustBoundary: undefined
795
+ };
796
+ }
797
+ if (endpoints) {
798
+ for (const endpoint of endpoints) {
799
+ servicesMap[serviceName].endpoints.add(endpoint);
800
+ }
801
+ }
802
+ }
803
+ }
804
+ }
805
+ }
806
+ };
807
+
671
808
  export const constructServiceName = (language, slice) => {
672
809
  let serviceName = "service";
673
810
  if (slice?.fullName) {
@@ -706,7 +843,10 @@ export const extractEndpoints = (language, code) => {
706
843
  );
707
844
  }
708
845
  break;
846
+ case "js":
847
+ case "ts":
709
848
  case "javascript":
849
+ case "typescript":
710
850
  if (code.includes("app.") || code.includes("route")) {
711
851
  const matches = code.match(/['"](.*?)['"]/gi) || [];
712
852
  endpoints = matches
@@ -722,24 +862,18 @@ export const extractEndpoints = (language, code) => {
722
862
  );
723
863
  }
724
864
  break;
865
+ case "py":
866
+ case "python":
867
+ endpoints = (code.match(/['"](.*?)['"]/gi) || [])
868
+ .map((v) => v.replace(/["']/g, "").replace("\n", ""))
869
+ .filter((v) => v.length > 2);
870
+ break;
725
871
  default:
726
872
  break;
727
873
  }
728
874
  return endpoints;
729
875
  };
730
876
 
731
- /**
732
- * Function to determine if slicing is required for the given language's dependencies.
733
- * For performance reasons, we make java operate only with namespaces
734
- *
735
- * @param {string} purl
736
- * @returns
737
- */
738
- export const isSlicingRequired = (purl) => {
739
- const language = purlToLanguage(purl);
740
- return ["python"].includes(language);
741
- };
742
-
743
877
  /**
744
878
  * Method to create the SBOM with evidence file called evinse file.
745
879
  *
@@ -752,6 +886,7 @@ export const createEvinseFile = (sliceArtefacts, options) => {
752
886
  tempDir,
753
887
  usagesSlicesFile,
754
888
  dataFlowSlicesFile,
889
+ reachablesSlicesFile,
755
890
  purlLocationMap,
756
891
  servicesMap,
757
892
  dataFlowFrames
@@ -830,6 +965,14 @@ export const createEvinseFile = (sliceArtefacts, options) => {
830
965
  text: fs.readFileSync(dataFlowSlicesFile, "utf8")
831
966
  });
832
967
  }
968
+ if (reachablesSlicesFile && fs.existsSync(reachablesSlicesFile)) {
969
+ bomJson.annotations.push({
970
+ subjects: [bomJson.serialNumber],
971
+ annotator: { component: bomJson.metadata.tools.components[0] },
972
+ timestamp: new Date().toISOString(),
973
+ text: fs.readFileSync(reachablesSlicesFile, "utf8")
974
+ });
975
+ }
833
976
  }
834
977
  // Increment the version
835
978
  bomJson.version = (bomJson.version || 1) + 1;
@@ -889,7 +1032,10 @@ export const collectDataFlowFrames = async (
889
1032
  continue;
890
1033
  }
891
1034
  let typeFullName = theNode.typeFullName;
892
- if (language === "javascript" && typeFullName == "ANY") {
1035
+ if (
1036
+ ["javascript", "js", "ts", "typescript"].includes(language) &&
1037
+ typeFullName == "ANY"
1038
+ ) {
893
1039
  if (
894
1040
  theNode.code &&
895
1041
  (theNode.code.startsWith("new ") ||
@@ -915,16 +1061,17 @@ export const collectDataFlowFrames = async (
915
1061
  }
916
1062
  } else {
917
1063
  // Check the namespaces db
918
- const nsHits =
919
- typePurlsCache[typeFullName] ||
920
- (await dbObjMap.Namespaces.findAll({
1064
+ let nsHits = typePurlsCache[typeFullName];
1065
+ if (["java", "jar"].includes(language)) {
1066
+ nsHits = await dbObjMap.Namespaces.findAll({
921
1067
  attributes: ["purl"],
922
1068
  where: {
923
1069
  data: {
924
1070
  [Op.like]: `%${typeFullName}%`
925
1071
  }
926
1072
  }
927
- }));
1073
+ });
1074
+ }
928
1075
  if (nsHits && nsHits.length) {
929
1076
  for (const ns of nsHits) {
930
1077
  referredPurls.add(ns.purl);
@@ -973,6 +1120,49 @@ export const collectDataFlowFrames = async (
973
1120
  return dfFrames;
974
1121
  };
975
1122
 
1123
+ /**
1124
+ * Method to convert reachable slice into usable callstack frames
1125
+ * Implemented based on the logic proposed here - https://github.com/AppThreat/atom/blob/main/specification/docs/slices.md#data-flow-slice
1126
+ *
1127
+ * @param {string} language Application language
1128
+ * @param {object} reachablesSlice Reachables slice object from atom
1129
+ */
1130
+ export const collectReachableFrames = async (language, reachablesSlice) => {
1131
+ const reachableNodes = reachablesSlice?.reachables || [];
1132
+ // purl key and an array of frames array
1133
+ // CycloneDX 1.5 currently accepts only 1 frame as evidence
1134
+ // so this method is more future-proof
1135
+ const dfFrames = {};
1136
+ for (const anode of reachableNodes) {
1137
+ let aframe = [];
1138
+ let referredPurls = new Set(anode.purls || []);
1139
+ for (const fnode of anode.flows) {
1140
+ if (!fnode.parentFileName || fnode.parentFileName === "<unknown>") {
1141
+ continue;
1142
+ }
1143
+ aframe.push({
1144
+ package: fnode.parentPackageName,
1145
+ module: fnode.parentClassName || "",
1146
+ function: fnode.parentMethodName || "",
1147
+ line: fnode.lineNumber || undefined,
1148
+ column: fnode.columnNumber || undefined,
1149
+ fullFilename: fnode.parentFileName || ""
1150
+ });
1151
+ }
1152
+ referredPurls = Array.from(referredPurls);
1153
+ if (referredPurls.length) {
1154
+ for (const apurl of referredPurls) {
1155
+ if (!dfFrames[apurl]) {
1156
+ dfFrames[apurl] = [];
1157
+ }
1158
+ // Store this frame as an evidence for this purl
1159
+ dfFrames[apurl].push(aframe);
1160
+ }
1161
+ }
1162
+ }
1163
+ return dfFrames;
1164
+ };
1165
+
976
1166
  /**
977
1167
  * Method to pick a callstack frame as an evidence. This method is required since CycloneDX 1.5 accepts only a single frame as evidence.
978
1168
  *
@@ -1000,7 +1190,7 @@ export const getClassTypeFromSignature = (language, typeFullName) => {
1000
1190
  const tmpA = typeFullName.split(".");
1001
1191
  tmpA.pop();
1002
1192
  typeFullName = tmpA.join(".");
1003
- } else if (language === "javascript") {
1193
+ } else if (["javascript", "js", "ts", "typescript"].includes(language)) {
1004
1194
  typeFullName = typeFullName.replace("new: ", "").replace("await ", "");
1005
1195
  if (typeFullName.includes(":")) {
1006
1196
  const tmpA = typeFullName.split("::")[0].replace(/:/g, "/").split("/");
@@ -1009,6 +1199,15 @@ export const getClassTypeFromSignature = (language, typeFullName) => {
1009
1199
  }
1010
1200
  typeFullName = tmpA.join("/");
1011
1201
  }
1202
+ } else if (["python", "py"].includes(language)) {
1203
+ typeFullName = typeFullName
1204
+ .replace(".py:<module>", "")
1205
+ .replace(/\//g, ".")
1206
+ .replace(".<metaClassCallHandler>", "")
1207
+ .replace(".<fakeNew>", "")
1208
+ .replace(".<body>", "")
1209
+ .replace(".__iter__", "")
1210
+ .replace(".__init__", "");
1012
1211
  }
1013
1212
  if (
1014
1213
  typeFullName.startsWith("<unresolved") ||