@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/README.md +43 -41
- package/analyzer.js +6 -2
- package/bin/cdxgen.js +78 -17
- package/bin/evinse.js +44 -22
- package/bin/verify.js +2 -0
- package/binary.js +20 -2
- package/data/README.md +1 -0
- package/data/frameworks-list.json +146 -0
- package/data/lic-mapping.json +44 -5
- package/data/pypi-pkg-aliases.json +6 -0
- package/display.js +34 -0
- package/docker.js +64 -5
- package/evinser.js +254 -55
- package/index.js +128 -90
- package/package.json +5 -5
- package/server.js +34 -21
- package/utils.js +695 -262
- package/utils.test.js +81 -7
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(
|
|
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 (
|
|
152
|
-
|
|
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 = (
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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 (
|
|
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") ||
|