@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/README.md +54 -36
- package/analyzer.js +6 -2
- package/bin/cdxgen.js +65 -30
- package/bin/evinse.js +67 -4
- package/bin/verify.js +2 -0
- package/binary.js +20 -2
- package/data/README.md +1 -0
- package/data/frameworks-list.json +128 -0
- package/display.js +34 -0
- package/docker.js +64 -5
- package/evinser.js +109 -10
- package/index.js +103 -91
- package/package.json +7 -6
- package/postgen.js +92 -0
- package/postgen.test.js +70 -0
- package/server.js +18 -11
- package/utils.js +407 -177
- package/utils.test.js +81 -7
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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 (
|
|
152
|
-
|
|
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 = (
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
*
|