@cyclonedx/cdxgen 11.3.1 → 11.4.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 +1 -1
- package/bin/cdxgen.js +39 -17
- package/bin/evinse.js +1 -25
- package/bin/verify.js +25 -3
- package/data/bom-1.6.schema.json +87 -65
- package/data/bom-1.7.schema.json +5915 -0
- package/data/component-tags.json +1 -1
- package/data/spdx-licenses.json +209 -4
- package/data/spdx.schema.json +29 -1
- package/lib/cli/index.js +43 -32
- package/lib/helpers/envcontext.js +15 -15
- package/lib/helpers/utils.js +256 -108
- package/lib/helpers/utils.test.js +34 -7
- package/lib/managers/binary.js +19 -9
- package/lib/managers/docker.js +13 -13
- package/lib/managers/oci.js +70 -0
- package/lib/managers/piptree.js +113 -20
- package/lib/server/openapi.yaml +21 -3
- package/lib/server/server.js +38 -38
- package/lib/stages/postgen/annotator.js +4 -0
- package/lib/stages/postgen/postgen.js +27 -6
- package/lib/stages/pregen/pregen.js +3 -3
- package/package.json +65 -24
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +11 -3
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +2 -0
- package/types/lib/managers/oci.d.ts.map +1 -0
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
|
@@ -2,8 +2,8 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterAll, beforeAll, describe, expect, test } from "@jest/globals";
|
|
5
|
-
import { load as loadYaml } from "js-yaml";
|
|
6
5
|
import { parse } from "ssri";
|
|
6
|
+
import { parse as loadYaml } from "yaml";
|
|
7
7
|
import {
|
|
8
8
|
buildObjectForCocoaPod,
|
|
9
9
|
buildObjectForGradleModule,
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
parseGoModData,
|
|
57
57
|
parseGoModGraph,
|
|
58
58
|
parseGoModWhy,
|
|
59
|
+
parseGoModulesTxt,
|
|
59
60
|
parseGoVersionData,
|
|
60
61
|
parseGopkgData,
|
|
61
62
|
parseGosumData,
|
|
@@ -1178,6 +1179,7 @@ describe("go data with vcs", () => {
|
|
|
1178
1179
|
}, 120000);
|
|
1179
1180
|
|
|
1180
1181
|
test("parseGoModData", async () => {
|
|
1182
|
+
process.env.GO_FETCH_VCS = "false";
|
|
1181
1183
|
let retMap = await parseGoModData(null);
|
|
1182
1184
|
expect(retMap).toEqual({});
|
|
1183
1185
|
const gosumMap = {
|
|
@@ -1196,6 +1198,8 @@ describe("go data with vcs", () => {
|
|
|
1196
1198
|
gosumMap,
|
|
1197
1199
|
);
|
|
1198
1200
|
expect(retMap.pkgList.length).toEqual(6);
|
|
1201
|
+
// Doesn't reliably work in CI/CD due to rate limiting.
|
|
1202
|
+
/*
|
|
1199
1203
|
expect(retMap.pkgList).toEqual([
|
|
1200
1204
|
{
|
|
1201
1205
|
group: "",
|
|
@@ -1280,6 +1284,7 @@ describe("go data with vcs", () => {
|
|
|
1280
1284
|
],
|
|
1281
1285
|
},
|
|
1282
1286
|
]);
|
|
1287
|
+
*/
|
|
1283
1288
|
|
|
1284
1289
|
retMap.pkgList.forEach((d) => {
|
|
1285
1290
|
expect(d.license);
|
|
@@ -1311,6 +1316,24 @@ describe("go data with vcs", () => {
|
|
|
1311
1316
|
}, 120000);
|
|
1312
1317
|
});
|
|
1313
1318
|
|
|
1319
|
+
describe("go vendor modules tests", () => {
|
|
1320
|
+
test("parseGoModulesTxt", async () => {
|
|
1321
|
+
const gosumMap = {
|
|
1322
|
+
"cel.dev/expr@v0.18.0":
|
|
1323
|
+
"sha256-CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=",
|
|
1324
|
+
"github.com/AdaLogics/go-fuzz-headers@v0.0.0-20230811130428-ced1acdcaa24":
|
|
1325
|
+
"sha256-bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=",
|
|
1326
|
+
"github.com/Azure/go-ansiterm@v0.0.0-20230124172434-306776ec8161":
|
|
1327
|
+
"sha256-L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=",
|
|
1328
|
+
};
|
|
1329
|
+
const pkgList = await parseGoModulesTxt(
|
|
1330
|
+
"./test/data/modules.txt",
|
|
1331
|
+
gosumMap,
|
|
1332
|
+
);
|
|
1333
|
+
expect((await pkgList).length).toEqual(212);
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1314
1337
|
describe("go data with licenses", () => {
|
|
1315
1338
|
beforeAll(() => {
|
|
1316
1339
|
process.env.FETCH_LICENSE = "true";
|
|
@@ -1741,14 +1764,18 @@ test("parse cargo lock", async () => {
|
|
|
1741
1764
|
// The base64 package does not have an associated checksum. Make sure the
|
|
1742
1765
|
// function does not accidentally insert an undefined hashsum value.
|
|
1743
1766
|
const base64Package = dep_list.find((pkg) => pkg.name === "base64");
|
|
1744
|
-
expect(base64Package).
|
|
1767
|
+
expect(base64Package).toEqual(
|
|
1768
|
+
expect.not.objectContaining({ hashes: expect.any(String) }),
|
|
1769
|
+
);
|
|
1745
1770
|
});
|
|
1746
1771
|
|
|
1747
1772
|
test("parse cargo lock simple component representation", async () => {
|
|
1748
1773
|
// If asking for a simple representation, we should skip any extended attributes.
|
|
1749
|
-
const componentList = await parseCargoData("./test/Cargo.lock");
|
|
1774
|
+
const componentList = await parseCargoData("./test/Cargo.lock", true);
|
|
1750
1775
|
const firstPackage = componentList[0];
|
|
1751
|
-
expect(firstPackage).
|
|
1776
|
+
expect(firstPackage).toEqual(
|
|
1777
|
+
expect.not.objectContaining({ evidence: expect.any(Object) }),
|
|
1778
|
+
);
|
|
1752
1779
|
});
|
|
1753
1780
|
|
|
1754
1781
|
test("parse cargo lock lists last package", async () => {
|
|
@@ -2447,7 +2474,7 @@ test("parse github actions workflow data", () => {
|
|
|
2447
2474
|
let dep_list = parseGitHubWorkflowData(
|
|
2448
2475
|
readFileSync("./.github/workflows/nodejs.yml", { encoding: "utf-8" }),
|
|
2449
2476
|
);
|
|
2450
|
-
expect(dep_list.length).toEqual(
|
|
2477
|
+
expect(dep_list.length).toEqual(7);
|
|
2451
2478
|
expect(dep_list[0]).toEqual({
|
|
2452
2479
|
group: "actions",
|
|
2453
2480
|
name: "checkout",
|
|
@@ -3779,8 +3806,8 @@ test("parsePnpmLock", async () => {
|
|
|
3779
3806
|
expect(parsedList.dependenciesList).toHaveLength(462);
|
|
3780
3807
|
expect(parsedList.pkgList.filter((pkg) => !pkg.scope)).toHaveLength(3);
|
|
3781
3808
|
parsedList = await parsePnpmLock("./pnpm-lock.yaml");
|
|
3782
|
-
expect(parsedList.pkgList.length).toEqual(
|
|
3783
|
-
expect(parsedList.dependenciesList.length).toEqual(
|
|
3809
|
+
expect(parsedList.pkgList.length).toEqual(591);
|
|
3810
|
+
expect(parsedList.dependenciesList.length).toEqual(591);
|
|
3784
3811
|
expect(parsedList.pkgList[0]).toEqual({
|
|
3785
3812
|
group: "@ampproject",
|
|
3786
3813
|
name: "remapping",
|
package/lib/managers/binary.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
2
|
import {
|
|
4
3
|
existsSync,
|
|
5
4
|
lstatSync,
|
|
@@ -26,17 +25,27 @@ import {
|
|
|
26
25
|
isSpdxLicenseExpression,
|
|
27
26
|
multiChecksumFile,
|
|
28
27
|
safeMkdirSync,
|
|
28
|
+
safeSpawnSync,
|
|
29
29
|
} from "../helpers/utils.js";
|
|
30
30
|
|
|
31
31
|
const dirName = dirNameStr;
|
|
32
32
|
const isWin = _platform() === "win32";
|
|
33
33
|
|
|
34
|
+
function isMusl() {
|
|
35
|
+
const result = safeSpawnSync("ldd", ["--version"], {
|
|
36
|
+
encoding: "utf-8",
|
|
37
|
+
});
|
|
38
|
+
return result?.stdout?.includes("musl") || result?.stderr?.includes("musl");
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
let platform = _platform();
|
|
35
42
|
let extn = "";
|
|
36
43
|
let pluginsBinSuffix = "";
|
|
37
44
|
if (platform === "win32") {
|
|
38
45
|
platform = "windows";
|
|
39
46
|
extn = ".exe";
|
|
47
|
+
} else if (platform === "linux" && isMusl()) {
|
|
48
|
+
platform = "linuxmusl";
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
let arch = _arch();
|
|
@@ -58,7 +67,7 @@ switch (arch) {
|
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
// cdxgen plugins version
|
|
61
|
-
const CDXGEN_PLUGINS_VERSION = "1.6.
|
|
70
|
+
const CDXGEN_PLUGINS_VERSION = "1.6.12";
|
|
62
71
|
|
|
63
72
|
// Retrieve the cdxgen plugins directory
|
|
64
73
|
let CDXGEN_PLUGINS_DIR = process.env.CDXGEN_PLUGINS_DIR;
|
|
@@ -71,6 +80,7 @@ if (
|
|
|
71
80
|
) {
|
|
72
81
|
CDXGEN_PLUGINS_DIR = join(dirName, "plugins");
|
|
73
82
|
}
|
|
83
|
+
|
|
74
84
|
// Is there a non-empty local node_modules directory
|
|
75
85
|
if (
|
|
76
86
|
!CDXGEN_PLUGINS_DIR &&
|
|
@@ -113,7 +123,7 @@ if (!CDXGEN_PLUGINS_DIR) {
|
|
|
113
123
|
`Trying to find the global node_modules path with "pnpm root -g" command.`,
|
|
114
124
|
);
|
|
115
125
|
}
|
|
116
|
-
const result =
|
|
126
|
+
const result = safeSpawnSync(isWin ? "pnpm.cmd" : "pnpm", ["root", "-g"], {
|
|
117
127
|
encoding: "utf-8",
|
|
118
128
|
});
|
|
119
129
|
if (result) {
|
|
@@ -368,7 +378,7 @@ const COMMON_RUNTIMES = [
|
|
|
368
378
|
|
|
369
379
|
export function getCargoAuditableInfo(src) {
|
|
370
380
|
if (CARGO_AUDITABLE_BIN) {
|
|
371
|
-
const result =
|
|
381
|
+
const result = safeSpawnSync(CARGO_AUDITABLE_BIN, [src], {
|
|
372
382
|
encoding: "utf-8",
|
|
373
383
|
});
|
|
374
384
|
if (result.status !== 0 || result.error) {
|
|
@@ -394,7 +404,7 @@ export function getCargoAuditableInfo(src) {
|
|
|
394
404
|
*/
|
|
395
405
|
export function executeSourcekitten(args) {
|
|
396
406
|
if (SOURCEKITTEN_BIN) {
|
|
397
|
-
const result =
|
|
407
|
+
const result = safeSpawnSync(SOURCEKITTEN_BIN, args, {
|
|
398
408
|
encoding: "utf-8",
|
|
399
409
|
maxBuffer: MAX_BUFFER,
|
|
400
410
|
});
|
|
@@ -466,7 +476,7 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
466
476
|
if (DEBUG_MODE) {
|
|
467
477
|
console.log("Executing", TRIVY_BIN, args.join(" "));
|
|
468
478
|
}
|
|
469
|
-
const result =
|
|
479
|
+
const result = safeSpawnSync(TRIVY_BIN, args, {
|
|
470
480
|
encoding: "utf-8",
|
|
471
481
|
});
|
|
472
482
|
if (result.status !== 0 || result.error) {
|
|
@@ -912,7 +922,7 @@ export function executeOsQuery(query) {
|
|
|
912
922
|
if (DEBUG_MODE) {
|
|
913
923
|
console.log("Executing", OSQUERY_BIN, args.join(" "));
|
|
914
924
|
}
|
|
915
|
-
const result =
|
|
925
|
+
const result = safeSpawnSync(OSQUERY_BIN, args, {
|
|
916
926
|
encoding: "utf-8",
|
|
917
927
|
maxBuffer: 50 * 1024 * 1024,
|
|
918
928
|
timeout: 60 * 1000,
|
|
@@ -965,7 +975,7 @@ export function getDotnetSlices(src, slicesFile) {
|
|
|
965
975
|
if (DEBUG_MODE) {
|
|
966
976
|
console.log("Executing", DOSAI_BIN, args.join(" "));
|
|
967
977
|
}
|
|
968
|
-
const result =
|
|
978
|
+
const result = safeSpawnSync(DOSAI_BIN, args, {
|
|
969
979
|
encoding: "utf-8",
|
|
970
980
|
timeout: TIMEOUT_MS,
|
|
971
981
|
cwd: src,
|
|
@@ -1019,7 +1029,7 @@ export function getBinaryBom(src, binaryBomFile, deepMode) {
|
|
|
1019
1029
|
console.log("Executing", BLINT_BIN, args.join(" "));
|
|
1020
1030
|
}
|
|
1021
1031
|
const cwd = lstatSync(src).isDirectory() ? src : dirname(src);
|
|
1022
|
-
const result =
|
|
1032
|
+
const result = safeSpawnSync(BLINT_BIN, args, {
|
|
1023
1033
|
encoding: "utf-8",
|
|
1024
1034
|
timeout: TIMEOUT_MS,
|
|
1025
1035
|
cwd,
|
package/lib/managers/docker.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
2
|
import {
|
|
4
3
|
createReadStream,
|
|
5
4
|
lstatSync,
|
|
@@ -25,6 +24,7 @@ import {
|
|
|
25
24
|
getTmpDir,
|
|
26
25
|
safeExistsSync,
|
|
27
26
|
safeMkdirSync,
|
|
27
|
+
safeSpawnSync,
|
|
28
28
|
} from "../helpers/utils.js";
|
|
29
29
|
|
|
30
30
|
export const isWin = _platform() === "win32";
|
|
@@ -88,7 +88,7 @@ export function detectColima() {
|
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
90
90
|
if (_platform() === "darwin") {
|
|
91
|
-
const result =
|
|
91
|
+
const result = safeSpawnSync("colima", ["version"], {
|
|
92
92
|
encoding: "utf-8",
|
|
93
93
|
});
|
|
94
94
|
if (result.status !== 0 || result.error) {
|
|
@@ -133,7 +133,7 @@ export function detectRancherDesktop() {
|
|
|
133
133
|
);
|
|
134
134
|
// Is Rancher Desktop running
|
|
135
135
|
if (safeExistsSync(limactl) || safeExistsSync(limaHome)) {
|
|
136
|
-
const result =
|
|
136
|
+
const result = safeSpawnSync("rdctl", ["list-settings"], {
|
|
137
137
|
encoding: "utf-8",
|
|
138
138
|
});
|
|
139
139
|
if (result.status !== 0 || result.error) {
|
|
@@ -616,7 +616,10 @@ export const parseImageName = (fullImageName) => {
|
|
|
616
616
|
* @returns boolean true if we should use the cli. false otherwise
|
|
617
617
|
*/
|
|
618
618
|
const needsCliFallback = () => {
|
|
619
|
-
if (
|
|
619
|
+
if (
|
|
620
|
+
["true", "1"].includes(process.env.DOCKER_USE_CLI) ||
|
|
621
|
+
(_platform() === "darwin" && (detectRancherDesktop() || detectColima()))
|
|
622
|
+
) {
|
|
620
623
|
return true;
|
|
621
624
|
}
|
|
622
625
|
return (
|
|
@@ -658,17 +661,14 @@ export const getImage = async (fullImageName) => {
|
|
|
658
661
|
}
|
|
659
662
|
let needsPull = true;
|
|
660
663
|
// Let's check the local cache first
|
|
661
|
-
let result =
|
|
664
|
+
let result = safeSpawnSync(dockerCmd, ["images", "--format=json"], {
|
|
662
665
|
encoding: "utf-8",
|
|
663
666
|
});
|
|
664
667
|
if (result.status === 0 && result.stdout) {
|
|
665
668
|
for (const imgLine of result.stdout.split("\n")) {
|
|
666
669
|
try {
|
|
667
670
|
const imgObj = JSON.parse(Buffer.from(imgLine).toString());
|
|
668
|
-
if (
|
|
669
|
-
imgObj.Repository === fullImageName ||
|
|
670
|
-
imgObj?.Name?.endsWith(fullImageName)
|
|
671
|
-
) {
|
|
671
|
+
if (`${imgObj.Repository}:${imgObj.Tag}` === fullImageName) {
|
|
672
672
|
needsPull = false;
|
|
673
673
|
break;
|
|
674
674
|
}
|
|
@@ -678,7 +678,7 @@ export const getImage = async (fullImageName) => {
|
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
680
|
if (needsPull) {
|
|
681
|
-
result =
|
|
681
|
+
result = safeSpawnSync(dockerCmd, ["pull", fullImageName], {
|
|
682
682
|
encoding: "utf-8",
|
|
683
683
|
timeout: TIMEOUT_MS,
|
|
684
684
|
});
|
|
@@ -696,7 +696,7 @@ export const getImage = async (fullImageName) => {
|
|
|
696
696
|
}
|
|
697
697
|
}
|
|
698
698
|
}
|
|
699
|
-
result =
|
|
699
|
+
result = safeSpawnSync(dockerCmd, ["inspect", fullImageName], {
|
|
700
700
|
encoding: "utf-8",
|
|
701
701
|
});
|
|
702
702
|
if (result.status !== 0 || result.error) {
|
|
@@ -1160,7 +1160,7 @@ export const exportImage = async (fullImageName, options) => {
|
|
|
1160
1160
|
console.log(
|
|
1161
1161
|
`About to export image ${fullImageName} to ${imageTarFile} using ${dockerCmd} cli`,
|
|
1162
1162
|
);
|
|
1163
|
-
const result =
|
|
1163
|
+
const result = safeSpawnSync(
|
|
1164
1164
|
dockerCmd,
|
|
1165
1165
|
["save", "-o", imageTarFile, fullImageName],
|
|
1166
1166
|
{
|
|
@@ -1392,7 +1392,7 @@ export const getCredsFromHelper = (exeSuffix, serverAddress) => {
|
|
|
1392
1392
|
if (isWin) {
|
|
1393
1393
|
credHelperExe = `${credHelperExe}.exe`;
|
|
1394
1394
|
}
|
|
1395
|
-
const result =
|
|
1395
|
+
const result = safeSpawnSync(credHelperExe, ["get"], {
|
|
1396
1396
|
input: serverAddress,
|
|
1397
1397
|
encoding: "utf-8",
|
|
1398
1398
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
MAX_BUFFER,
|
|
5
|
+
getAllFiles,
|
|
6
|
+
getTmpDir,
|
|
7
|
+
isWin,
|
|
8
|
+
safeSpawnSync,
|
|
9
|
+
} from "../helpers/utils.js";
|
|
10
|
+
|
|
11
|
+
export function getBomWithOras(image, platform = undefined) {
|
|
12
|
+
let parameters = [
|
|
13
|
+
"discover",
|
|
14
|
+
"--format",
|
|
15
|
+
"json",
|
|
16
|
+
"--artifact-type",
|
|
17
|
+
"sbom/cyclonedx",
|
|
18
|
+
];
|
|
19
|
+
if (platform) {
|
|
20
|
+
parameters = parameters.concat(["--platform", platform]);
|
|
21
|
+
}
|
|
22
|
+
let result = safeSpawnSync("oras", parameters.concat([image]), {
|
|
23
|
+
encoding: "utf-8",
|
|
24
|
+
shell: isWin,
|
|
25
|
+
maxBuffer: MAX_BUFFER,
|
|
26
|
+
});
|
|
27
|
+
if (result.status !== 0 || result.error) {
|
|
28
|
+
console.log(
|
|
29
|
+
"Install oras by following the instructions at: https://oras.land/docs/installation",
|
|
30
|
+
);
|
|
31
|
+
if (result.stderr) {
|
|
32
|
+
console.log(result.stderr);
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (result.stdout) {
|
|
37
|
+
const out = Buffer.from(result.stdout).toString();
|
|
38
|
+
try {
|
|
39
|
+
const manifestObj = JSON.parse(out);
|
|
40
|
+
if (
|
|
41
|
+
manifestObj?.manifests?.length &&
|
|
42
|
+
Array.isArray(manifestObj.manifests) &&
|
|
43
|
+
manifestObj.manifests[0]?.reference
|
|
44
|
+
) {
|
|
45
|
+
const imageRef = manifestObj.manifests[0].reference;
|
|
46
|
+
const tmpDir = getTmpDir();
|
|
47
|
+
result = safeSpawnSync("oras", ["pull", imageRef, "-o", tmpDir], {
|
|
48
|
+
encoding: "utf-8",
|
|
49
|
+
shell: isWin,
|
|
50
|
+
maxBuffer: MAX_BUFFER,
|
|
51
|
+
});
|
|
52
|
+
if (result.status !== 0 || result.error) {
|
|
53
|
+
console.log(
|
|
54
|
+
`Unable to pull the SBOM attachment for ${imageRef} with oras!`,
|
|
55
|
+
);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const bomFiles = getAllFiles(tmpDir, "**/*.{bom,cdx}.json");
|
|
59
|
+
if (bomFiles.length) {
|
|
60
|
+
return JSON.parse(fs.readFileSync(bomFiles.pop(), "utf8"));
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.log(`${image} does not contain any SBOM attachment!`);
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.log(e);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
package/lib/managers/piptree.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
/**
|
|
3
2
|
* The idea behind this plugin came from the excellent pipdeptree package
|
|
4
3
|
* https://github.com/tox-dev/pipdeptree
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
writeFileSync,
|
|
14
13
|
} from "node:fs";
|
|
15
14
|
import { delimiter, join } from "node:path";
|
|
16
|
-
import { getTmpDir } from "../helpers/utils.js";
|
|
15
|
+
import { getTmpDir, safeSpawnSync } from "../helpers/utils.js";
|
|
17
16
|
|
|
18
17
|
const PIP_TREE_PLUGIN_CONTENT = `
|
|
19
18
|
import importlib.metadata as importlib_metadata
|
|
@@ -22,6 +21,16 @@ import sys
|
|
|
22
21
|
|
|
23
22
|
from pip._internal.metadata import pkg_resources
|
|
24
23
|
|
|
24
|
+
REQUIREMENT_MODULE_FOUND = False
|
|
25
|
+
try:
|
|
26
|
+
from packaging.requirements import Requirement
|
|
27
|
+
REQUIREMENT_MODULE_FOUND = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
try:
|
|
30
|
+
from pip._vendor.packaging.requirements import Requirement
|
|
31
|
+
REQUIREMENT_MODULE_FOUND = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
pass
|
|
25
34
|
|
|
26
35
|
def frozen_req_from_dist(dist):
|
|
27
36
|
try:
|
|
@@ -41,8 +50,8 @@ def frozen_req_from_dist(dist):
|
|
|
41
50
|
pass
|
|
42
51
|
|
|
43
52
|
|
|
44
|
-
def get_installed_distributions():
|
|
45
|
-
dists = pkg_resources.Environment.from_paths(
|
|
53
|
+
def get_installed_distributions(python_path=None):
|
|
54
|
+
dists = pkg_resources.Environment.from_paths(python_path).iter_installed_distributions(
|
|
46
55
|
local_only=False,
|
|
47
56
|
skip=(),
|
|
48
57
|
user_only=False,
|
|
@@ -50,7 +59,81 @@ def get_installed_distributions():
|
|
|
50
59
|
return [d._dist for d in dists]
|
|
51
60
|
|
|
52
61
|
|
|
53
|
-
def
|
|
62
|
+
def _get_extra_deps_from_dist(dist):
|
|
63
|
+
extra_deps = {}
|
|
64
|
+
if not dist:
|
|
65
|
+
return extra_deps
|
|
66
|
+
# all requirements, some of which may be extra-only:
|
|
67
|
+
reqs = dist.metadata.get_all('Requires-Dist') or []
|
|
68
|
+
# extras this package defines:
|
|
69
|
+
extras = dist.metadata.get_all('Provides-Extra') or []
|
|
70
|
+
for req_str in reqs:
|
|
71
|
+
req = Requirement(req_str)
|
|
72
|
+
if req.marker and 'extra' in str(req.marker):
|
|
73
|
+
# evaluate marker for each declared extra
|
|
74
|
+
for extra in extras:
|
|
75
|
+
if req.marker.evaluate({'extra': extra}):
|
|
76
|
+
extra_deps.setdefault(extra, []).append({"name": str(req.name), "versionSpecifiers": str(req.specifier), "url": str(req.url) if req.url else None})
|
|
77
|
+
return extra_deps
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_deps_from_extras(name_version_cache, name_dist_cache, extra_deps):
|
|
81
|
+
dependencies = []
|
|
82
|
+
if not extra_deps:
|
|
83
|
+
return dependencies
|
|
84
|
+
# Treat an extra with the name all as dependencies
|
|
85
|
+
all_deps = extra_deps.get("all", [])
|
|
86
|
+
for dep in all_deps:
|
|
87
|
+
dversion = name_version_cache.get(dep["name"])
|
|
88
|
+
if not dversion:
|
|
89
|
+
continue
|
|
90
|
+
dversionSpecifiers = dep.get("versionSpecifiers")
|
|
91
|
+
dpurl = f"""pkg:pypi/{dep["name"].lower()}@{dversion}"""
|
|
92
|
+
dextra_deps = _get_extra_deps_from_dist(name_dist_cache.get(dep["name"]))
|
|
93
|
+
ddependencies = _get_deps_from_extras(name_version_cache, name_dist_cache, dextra_deps)
|
|
94
|
+
dependencies.append({
|
|
95
|
+
"name": dep["name"],
|
|
96
|
+
"version": dversion,
|
|
97
|
+
"versionSpecifiers": dversionSpecifiers,
|
|
98
|
+
"purl": dpurl,
|
|
99
|
+
"extra_deps": dextra_deps,
|
|
100
|
+
"dependencies": ddependencies
|
|
101
|
+
})
|
|
102
|
+
return dependencies
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_installed_with_extras():
|
|
106
|
+
result = {}
|
|
107
|
+
if not REQUIREMENT_MODULE_FOUND:
|
|
108
|
+
return result
|
|
109
|
+
name_version_cache = {}
|
|
110
|
+
name_dist_cache = {}
|
|
111
|
+
for dist in importlib_metadata.distributions():
|
|
112
|
+
name = dist.metadata['Name']
|
|
113
|
+
version = dist.version or ""
|
|
114
|
+
name_version_cache[name] = version
|
|
115
|
+
name_dist_cache[name] = dist
|
|
116
|
+
for dist in importlib_metadata.distributions():
|
|
117
|
+
name = dist.metadata['Name']
|
|
118
|
+
version = dist.version or ""
|
|
119
|
+
# extras this package defines:
|
|
120
|
+
extras = dist.metadata.get_all('Provides-Extra') or []
|
|
121
|
+
# map each extra → its extra-only dependencies
|
|
122
|
+
extra_deps = _get_extra_deps_from_dist(dist)
|
|
123
|
+
purl = f"pkg:pypi/{name.lower()}@{version}"
|
|
124
|
+
dependencies = _get_deps_from_extras(name_version_cache, name_dist_cache, extra_deps)
|
|
125
|
+
result[purl] = {
|
|
126
|
+
'name': name,
|
|
127
|
+
'version': version,
|
|
128
|
+
'extras': extras,
|
|
129
|
+
'purl': purl,
|
|
130
|
+
'extra_deps': extra_deps,
|
|
131
|
+
"dependencies": dependencies
|
|
132
|
+
}
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def find_deps(idx, path, purl, reqs, global_installed, traverse_count):
|
|
54
137
|
freqs = []
|
|
55
138
|
for r in reqs:
|
|
56
139
|
d = idx.get(r.key)
|
|
@@ -58,17 +141,26 @@ def find_deps(idx, path, reqs, traverse_count):
|
|
|
58
141
|
continue
|
|
59
142
|
r.project_name = d.project_name if d is not None else r.project_name
|
|
60
143
|
if r.key in path:
|
|
144
|
+
print(f"Cycle detected: {' -> '.join(current_path)}")
|
|
61
145
|
continue
|
|
62
146
|
current_path = path + [r.key]
|
|
63
147
|
specs = sorted(r.specs, reverse=True)
|
|
64
148
|
specs_str = ",".join(["".join(sp) for sp in specs]) if specs else ""
|
|
65
149
|
dreqs = d.requires()
|
|
150
|
+
name = r.project_name
|
|
151
|
+
version = importlib_metadata.version(r.key)
|
|
152
|
+
purl = f"pkg:pypi/{name.lower()}@{version}"
|
|
153
|
+
extra_deps = global_installed.get(purl, {}).get("extra_deps", {})
|
|
154
|
+
dependencies = find_deps(idx, current_path, purl, dreqs, global_installed, traverse_count + 1) if dreqs and traverse_count < 200 else []
|
|
155
|
+
all_dependencies = global_installed.get(purl, {}).get("dependencies", [])
|
|
66
156
|
freqs.append(
|
|
67
157
|
{
|
|
68
|
-
"name":
|
|
69
|
-
"version":
|
|
158
|
+
"name": name,
|
|
159
|
+
"version": version,
|
|
70
160
|
"versionSpecifiers": specs_str,
|
|
71
|
-
|
|
161
|
+
'purl': purl,
|
|
162
|
+
"extra_deps": extra_deps,
|
|
163
|
+
"dependencies": dependencies + all_dependencies,
|
|
72
164
|
}
|
|
73
165
|
)
|
|
74
166
|
return freqs
|
|
@@ -77,7 +169,8 @@ def find_deps(idx, path, reqs, traverse_count):
|
|
|
77
169
|
def main(argv):
|
|
78
170
|
out_file = "piptree.json" if len(argv) < 2 else argv[-1]
|
|
79
171
|
tree = []
|
|
80
|
-
|
|
172
|
+
global_installed = get_installed_with_extras()
|
|
173
|
+
pkgs = get_installed_distributions(python_path=None)
|
|
81
174
|
idx = {p.key: p for p in pkgs}
|
|
82
175
|
traverse_count = 0
|
|
83
176
|
for p in pkgs:
|
|
@@ -93,22 +186,22 @@ def main(argv):
|
|
|
93
186
|
version = "latest"
|
|
94
187
|
if len(tmpA) == 2:
|
|
95
188
|
version = tmpA[1]
|
|
189
|
+
pkgName = name.split(" ")[0]
|
|
190
|
+
purl = f"pkg:pypi/{pkgName.lower()}@{version}"
|
|
191
|
+
extra_deps = global_installed.get(purl, {}).get("extra_deps", "")
|
|
192
|
+
all_dependencies = global_installed.get(purl, {}).get("dependencies", [])
|
|
193
|
+
dependencies = find_deps(idx, [p.key], purl, p.requires(), global_installed, traverse_count + 1)
|
|
96
194
|
tree.append(
|
|
97
195
|
{
|
|
98
|
-
"name":
|
|
196
|
+
"name": pkgName,
|
|
99
197
|
"version": version,
|
|
100
|
-
"
|
|
198
|
+
"purl": purl,
|
|
199
|
+
"extra_deps": extra_deps,
|
|
200
|
+
"dependencies": dependencies + all_dependencies,
|
|
101
201
|
}
|
|
102
202
|
)
|
|
103
|
-
all_deps = {}
|
|
104
|
-
for t in tree:
|
|
105
|
-
for d in t["dependencies"]:
|
|
106
|
-
all_deps[d["name"]] = True
|
|
107
|
-
trimmed_tree = [
|
|
108
|
-
t for t in tree if t["name"] not in all_deps
|
|
109
|
-
]
|
|
110
203
|
with open(out_file, mode="w", encoding="utf-8") as fp:
|
|
111
|
-
json.dump(
|
|
204
|
+
json.dump(tree, fp)
|
|
112
205
|
|
|
113
206
|
|
|
114
207
|
if __name__ == "__main__":
|
|
@@ -141,7 +234,7 @@ export const getTreeWithPlugin = (env, python_cmd, basePath) => {
|
|
|
141
234
|
env.PYTHONPATH = `${env.PYTHONPATH}${delimiter}${env.PIP_TARGET}`;
|
|
142
235
|
}
|
|
143
236
|
}
|
|
144
|
-
const result =
|
|
237
|
+
const result = safeSpawnSync(python_cmd, pipPluginArgs, {
|
|
145
238
|
cwd: basePath,
|
|
146
239
|
encoding: "utf-8",
|
|
147
240
|
env,
|
package/lib/server/openapi.yaml
CHANGED
|
@@ -216,12 +216,22 @@ components:
|
|
|
216
216
|
type: object
|
|
217
217
|
properties:
|
|
218
218
|
type:
|
|
219
|
-
type:
|
|
220
|
-
|
|
219
|
+
type: array
|
|
220
|
+
items:
|
|
221
|
+
type: string
|
|
222
|
+
description: Project Types
|
|
221
223
|
default: "universal"
|
|
222
224
|
externalDocs:
|
|
223
225
|
description: Single or comma separated values. See supported project types
|
|
224
226
|
url: https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES
|
|
227
|
+
excludeType:
|
|
228
|
+
type: array
|
|
229
|
+
items:
|
|
230
|
+
type: string
|
|
231
|
+
description: Exclude Types
|
|
232
|
+
externalDocs:
|
|
233
|
+
description: Project types to exclude
|
|
234
|
+
url: https://cyclonedx.github.io/cdxgen/#/PROJECT_TYPES
|
|
225
235
|
multiProject:
|
|
226
236
|
type: boolean
|
|
227
237
|
requiredOnly:
|
|
@@ -259,7 +269,7 @@ components:
|
|
|
259
269
|
specVersion:
|
|
260
270
|
type: string
|
|
261
271
|
description: CycloneDX Specification version to use
|
|
262
|
-
default: "1.
|
|
272
|
+
default: "1.6"
|
|
263
273
|
filter:
|
|
264
274
|
type: array
|
|
265
275
|
items:
|
|
@@ -304,6 +314,14 @@ components:
|
|
|
304
314
|
standard:
|
|
305
315
|
type: string
|
|
306
316
|
description: The list of standards which may consist of regulations, industry or organizational-specific standards, maturity models, best practices, or any other requirements which can be evaluated against or attested to. Choices are asvs-4.0.3, bsimm-v13, masvs-2.0.0, nist_ssdf-1.1, pcissc-secure-slc-1.1, scvs-1.0.0, ssaf-DRAFT-2023-11
|
|
317
|
+
minConfidence:
|
|
318
|
+
type: number
|
|
319
|
+
description: Minimum confidence needed for the identity of a component from 0 - 1, where 1 is 100% confidence.
|
|
320
|
+
technique:
|
|
321
|
+
type: array
|
|
322
|
+
items:
|
|
323
|
+
type: string
|
|
324
|
+
description: Analysis technique to use
|
|
307
325
|
CycloneDXSBOM:
|
|
308
326
|
type: object
|
|
309
327
|
externalDocs:
|