@cyclonedx/cdxgen 9.8.9 → 9.9.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/display.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { existsSync, readFileSync } from "fs";
1
2
  import { createStream, table } from "table";
2
3
 
3
4
  // https://github.com/yangshun/tree-node-cli/blob/master/src/index.js
@@ -277,3 +278,36 @@ const recursePrint = (depMap, subtree, level, shownList, treeGraphics) => {
277
278
  }
278
279
  }
279
280
  };
281
+
282
+ export const printReachables = (sliceArtefacts) => {
283
+ const reachablesSlicesFile = sliceArtefacts.reachablesSlicesFile;
284
+ if (!existsSync(reachablesSlicesFile)) {
285
+ return;
286
+ }
287
+ const purlCounts = {};
288
+ const reachablesSlices = JSON.parse(
289
+ readFileSync(reachablesSlicesFile, "utf-8")
290
+ );
291
+ for (const areachable of reachablesSlices.reachables || []) {
292
+ const purls = areachable.purls || [];
293
+ for (const apurl of purls) {
294
+ purlCounts[apurl] = (purlCounts[apurl] || 0) + 1;
295
+ }
296
+ }
297
+ const sortedPurls = Object.fromEntries(
298
+ Object.entries(purlCounts).sort(([, a], [, b]) => b - a)
299
+ );
300
+ const data = [["Package URL", "Reachable Flows"]];
301
+ for (const apurl of Object.keys(sortedPurls)) {
302
+ data.push([apurl, "" + sortedPurls[apurl]]);
303
+ }
304
+ const config = {
305
+ header: {
306
+ alignment: "center",
307
+ content: "Reachable Components\nGenerated with \u2665 by cdxgen"
308
+ }
309
+ };
310
+ if (data.length > 1) {
311
+ console.log(table(data, config));
312
+ }
313
+ };
package/docker.js CHANGED
@@ -323,6 +323,9 @@ export const parseImageName = (fullImageName) => {
323
323
  fullImageName = fullImageName.replace(":" + nameObj.tag, "");
324
324
  }
325
325
  }
326
+ if (fullImageName && fullImageName.startsWith("library/")) {
327
+ fullImageName = fullImageName.replace("library/", "");
328
+ }
326
329
  // The left over string is the repo name
327
330
  nameObj.repo = fullImageName;
328
331
  return nameObj;
@@ -333,7 +336,9 @@ export const parseImageName = (fullImageName) => {
333
336
  */
334
337
  export const getImage = async (fullImageName) => {
335
338
  let localData = undefined;
339
+ let pullData = undefined;
336
340
  const { repo, tag, digest } = parseImageName(fullImageName);
341
+ let repoWithTag = `${repo}:${tag !== "" ? tag : ":latest"}`;
337
342
  // Fetch only the latest tag if none is specified
338
343
  if (tag === "" && digest === "") {
339
344
  fullImageName = fullImageName + ":latest";
@@ -379,6 +384,14 @@ export const getImage = async (fullImageName) => {
379
384
  }
380
385
  }
381
386
  }
387
+ try {
388
+ localData = await makeRequest(`images/${repoWithTag}/json`);
389
+ if (localData) {
390
+ return localData;
391
+ }
392
+ } catch (err) {
393
+ // ignore
394
+ }
382
395
  try {
383
396
  localData = await makeRequest(`images/${repo}/json`);
384
397
  } catch (err) {
@@ -397,7 +410,7 @@ export const getImage = async (fullImageName) => {
397
410
  }
398
411
  // If the data is not available locally
399
412
  try {
400
- const pullData = await makeRequest(
413
+ pullData = await makeRequest(
401
414
  `images/create?fromImage=${fullImageName}`,
402
415
  "POST"
403
416
  );
@@ -415,15 +428,42 @@ export const getImage = async (fullImageName) => {
415
428
  return undefined;
416
429
  }
417
430
  } catch (err) {
418
- // continue regardless of error
431
+ try {
432
+ if (DEBUG_MODE) {
433
+ console.log(`Re-trying the pull with the name ${repoWithTag}.`);
434
+ }
435
+ pullData = await makeRequest(
436
+ `images/create?fromImage=${repoWithTag}`,
437
+ "POST"
438
+ );
439
+ } catch (err) {
440
+ // continue regardless of error
441
+ }
419
442
  }
420
443
  try {
421
444
  if (DEBUG_MODE) {
422
- console.log(`Trying with ${repo}`);
445
+ console.log(`Trying with ${repoWithTag}`);
446
+ }
447
+ localData = await makeRequest(`images/${repoWithTag}/json`);
448
+ if (localData) {
449
+ return localData;
423
450
  }
424
- localData = await makeRequest(`images/${repo}/json`);
425
451
  } catch (err) {
426
452
  try {
453
+ if (DEBUG_MODE) {
454
+ console.log(`Trying with ${repo}`);
455
+ }
456
+ localData = await makeRequest(`images/${repo}/json`);
457
+ if (localData) {
458
+ return localData;
459
+ }
460
+ } catch (err) {
461
+ // continue regardless of error
462
+ }
463
+ try {
464
+ if (DEBUG_MODE) {
465
+ console.log(`Trying with ${fullImageName}`);
466
+ }
427
467
  localData = await makeRequest(`images/${fullImageName}/json`);
428
468
  } catch (err) {
429
469
  // continue regardless of error
@@ -701,7 +741,26 @@ export const exportImage = async (fullImageName) => {
701
741
  })
702
742
  );
703
743
  } catch (err) {
704
- console.error(err);
744
+ if (localData && localData.Id) {
745
+ console.log(`Retrying with ${localData.Id}`);
746
+ try {
747
+ await stream.pipeline(
748
+ client.stream(`images/${localData.Id}/get`),
749
+ x({
750
+ sync: true,
751
+ preserveOwner: false,
752
+ noMtime: true,
753
+ noChmod: true,
754
+ strict: true,
755
+ C: tempDir,
756
+ portable: true,
757
+ onwarn: () => {}
758
+ })
759
+ );
760
+ } catch (err) {
761
+ console.log(err);
762
+ }
763
+ }
705
764
  }
706
765
  }
707
766
  // Continue with extracting the layers
package/evinser.js CHANGED
@@ -71,7 +71,7 @@ export const prepareDB = async (options) => {
71
71
  }
72
72
  }
73
73
  for (const purl of Object.keys(purlsToSlice)) {
74
- await createAndStoreSlice(purl, purlsJars, Usages);
74
+ await createAndStoreSlice(purl, purlsJars, Usages, options);
75
75
  }
76
76
  return { sequelize, Namespaces, Usages, DataFlows };
77
77
  };
@@ -148,8 +148,13 @@ export const catalogGradleDeps = async (dirPath, purlsJars, Namespaces) => {
148
148
  );
149
149
  };
150
150
 
151
- export const createAndStoreSlice = async (purl, purlsJars, Usages) => {
152
- const retMap = createSlice(purl, purlsJars[purl], "usages");
151
+ export const createAndStoreSlice = async (
152
+ purl,
153
+ purlsJars,
154
+ Usages,
155
+ options = {}
156
+ ) => {
157
+ const retMap = createSlice(purl, purlsJars[purl], "usages", options);
153
158
  let sliceData = undefined;
154
159
  if (retMap && retMap.slicesFile && fs.existsSync(retMap.slicesFile)) {
155
160
  sliceData = await Usages.findOrCreate({
@@ -166,7 +171,12 @@ export const createAndStoreSlice = async (purl, purlsJars, Usages) => {
166
171
  return sliceData;
167
172
  };
168
173
 
169
- export const createSlice = (purlOrLanguage, filePath, sliceType = "usages") => {
174
+ export const createSlice = (
175
+ purlOrLanguage,
176
+ filePath,
177
+ sliceType = "usages",
178
+ options = {}
179
+ ) => {
170
180
  if (!filePath) {
171
181
  return;
172
182
  }
@@ -177,9 +187,18 @@ export const createSlice = (purlOrLanguage, filePath, sliceType = "usages") => {
177
187
  if (!language) {
178
188
  return undefined;
179
189
  }
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`);
190
+ let sliceOutputDir = fs.mkdtempSync(
191
+ path.join(tmpdir(), `atom-${sliceType}-`)
192
+ );
193
+ if (options && options.output) {
194
+ sliceOutputDir =
195
+ fs.existsSync(options.output) &&
196
+ fs.lstatSync(options.output).isDirectory()
197
+ ? path.basename(options.output)
198
+ : path.dirname(options.output);
199
+ }
200
+ const atomFile = path.join(sliceOutputDir, "app.atom");
201
+ const slicesFile = path.join(sliceOutputDir, `${sliceType}.slices.json`);
183
202
  const args = [
184
203
  sliceType,
185
204
  "-l",
@@ -204,7 +223,7 @@ export const createSlice = (purlOrLanguage, filePath, sliceType = "usages") => {
204
223
  );
205
224
  }
206
225
  return {
207
- tempDir,
226
+ tempDir: sliceOutputDir,
208
227
  slicesFile,
209
228
  atomFile
210
229
  };
@@ -260,8 +279,10 @@ export const analyzeProject = async (dbObjMap, options) => {
260
279
  const language = options.language;
261
280
  let usageSlice = undefined;
262
281
  let dataFlowSlice = undefined;
282
+ let reachablesSlice = undefined;
263
283
  let usagesSlicesFile = undefined;
264
284
  let dataFlowSlicesFile = undefined;
285
+ let reachablesSlicesFile = undefined;
265
286
  let dataFlowFrames = {};
266
287
  let servicesMap = {};
267
288
  let retMap = {};
@@ -278,7 +299,7 @@ export const analyzeProject = async (dbObjMap, options) => {
278
299
  usagesSlicesFile = options.usagesSlicesFile;
279
300
  } else {
280
301
  // Generate our own slices
281
- retMap = createSlice(language, dirPath, "usages");
302
+ retMap = createSlice(language, dirPath, "usages", options);
282
303
  if (retMap && retMap.slicesFile && fs.existsSync(retMap.slicesFile)) {
283
304
  usageSlice = JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8"));
284
305
  usagesSlicesFile = retMap.slicesFile;
@@ -310,7 +331,7 @@ export const analyzeProject = async (dbObjMap, options) => {
310
331
  fs.readFileSync(options.dataFlowSlicesFile, "utf-8")
311
332
  );
312
333
  } else {
313
- retMap = createSlice(language, dirPath, "data-flow");
334
+ retMap = createSlice(language, dirPath, "data-flow", options);
314
335
  if (retMap && retMap.slicesFile && fs.existsSync(retMap.slicesFile)) {
315
336
  dataFlowSlicesFile = retMap.slicesFile;
316
337
  dataFlowSlice = JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8"));
@@ -330,10 +351,36 @@ export const analyzeProject = async (dbObjMap, options) => {
330
351
  purlImportsMap
331
352
  );
332
353
  }
354
+ if (options.withReachables) {
355
+ if (
356
+ options.reachablesSlicesFile &&
357
+ fs.existsSync(options.reachablesSlicesFile)
358
+ ) {
359
+ reachablesSlicesFile = options.reachablesSlicesFile;
360
+ reachablesSlice = JSON.parse(
361
+ fs.readFileSync(options.reachablesSlicesFile, "utf-8")
362
+ );
363
+ } else {
364
+ retMap = createSlice(language, dirPath, "reachables", options);
365
+ if (retMap && retMap.slicesFile && fs.existsSync(retMap.slicesFile)) {
366
+ reachablesSlicesFile = retMap.slicesFile;
367
+ reachablesSlice = JSON.parse(
368
+ fs.readFileSync(retMap.slicesFile, "utf-8")
369
+ );
370
+ console.log(
371
+ `To speed up this step, cache ${reachablesSlicesFile} and invoke evinse with the --reachables-slices-file argument.`
372
+ );
373
+ }
374
+ }
375
+ }
376
+ if (reachablesSlice && Object.keys(reachablesSlice).length) {
377
+ dataFlowFrames = await collectReachableFrames(language, reachablesSlice);
378
+ }
333
379
  return {
334
380
  atomFile: retMap.atomFile,
335
381
  usagesSlicesFile,
336
382
  dataFlowSlicesFile,
383
+ reachablesSlicesFile,
337
384
  purlLocationMap,
338
385
  servicesMap,
339
386
  dataFlowFrames,
@@ -752,6 +799,7 @@ export const createEvinseFile = (sliceArtefacts, options) => {
752
799
  tempDir,
753
800
  usagesSlicesFile,
754
801
  dataFlowSlicesFile,
802
+ reachablesSlicesFile,
755
803
  purlLocationMap,
756
804
  servicesMap,
757
805
  dataFlowFrames
@@ -830,6 +878,14 @@ export const createEvinseFile = (sliceArtefacts, options) => {
830
878
  text: fs.readFileSync(dataFlowSlicesFile, "utf8")
831
879
  });
832
880
  }
881
+ if (reachablesSlicesFile && fs.existsSync(reachablesSlicesFile)) {
882
+ bomJson.annotations.push({
883
+ subjects: [bomJson.serialNumber],
884
+ annotator: { component: bomJson.metadata.tools.components[0] },
885
+ timestamp: new Date().toISOString(),
886
+ text: fs.readFileSync(reachablesSlicesFile, "utf8")
887
+ });
888
+ }
833
889
  }
834
890
  // Increment the version
835
891
  bomJson.version = (bomJson.version || 1) + 1;
@@ -973,6 +1029,49 @@ export const collectDataFlowFrames = async (
973
1029
  return dfFrames;
974
1030
  };
975
1031
 
1032
+ /**
1033
+ * Method to convert reachable slice into usable callstack frames
1034
+ * Implemented based on the logic proposed here - https://github.com/AppThreat/atom/blob/main/specification/docs/slices.md#data-flow-slice
1035
+ *
1036
+ * @param {string} language Application language
1037
+ * @param {object} reachablesSlice Reachables slice object from atom
1038
+ */
1039
+ export const collectReachableFrames = async (language, reachablesSlice) => {
1040
+ const reachableNodes = reachablesSlice?.reachables || [];
1041
+ // purl key and an array of frames array
1042
+ // CycloneDX 1.5 currently accepts only 1 frame as evidence
1043
+ // so this method is more future-proof
1044
+ const dfFrames = {};
1045
+ for (const anode of reachableNodes) {
1046
+ let aframe = [];
1047
+ let referredPurls = new Set(anode.purls || []);
1048
+ for (const fnode of anode.flows) {
1049
+ if (!fnode.parentFileName || fnode.parentFileName === "<unknown>") {
1050
+ continue;
1051
+ }
1052
+ aframe.push({
1053
+ package: fnode.parentPackageName,
1054
+ module: fnode.parentClassName || "",
1055
+ function: fnode.parentMethodName || "",
1056
+ line: fnode.lineNumber || undefined,
1057
+ column: fnode.columnNumber || undefined,
1058
+ fullFilename: fnode.parentFileName || ""
1059
+ });
1060
+ }
1061
+ referredPurls = Array.from(referredPurls);
1062
+ if (referredPurls.length) {
1063
+ for (const apurl of referredPurls) {
1064
+ if (!dfFrames[apurl]) {
1065
+ dfFrames[apurl] = [];
1066
+ }
1067
+ // Store this frame as an evidence for this purl
1068
+ dfFrames[apurl].push(aframe);
1069
+ }
1070
+ }
1071
+ }
1072
+ return dfFrames;
1073
+ };
1074
+
976
1075
  /**
977
1076
  * 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
1077
  *