@cyclonedx/cdxgen 12.3.0 → 12.3.2
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 +15 -5
- package/bin/audit.js +7 -0
- package/bin/cdxgen.js +241 -81
- package/bin/repl.js +138 -0
- package/data/rules/ai-agent-governance.yaml +249 -0
- package/data/rules/dependency-sources.yaml +41 -0
- package/data/rules/mcp-servers.yaml +304 -0
- package/data/rules/package-integrity.yaml +123 -0
- package/lib/audit/index.js +353 -29
- package/lib/audit/index.poku.js +247 -7
- package/lib/audit/reporters.js +26 -0
- package/lib/audit/scoring.js +262 -13
- package/lib/audit/scoring.poku.js +179 -0
- package/lib/audit/targets.js +391 -2
- package/lib/audit/targets.poku.js +416 -3
- package/lib/cli/index.js +588 -45
- package/lib/cli/index.poku.js +735 -1
- package/lib/evinser/evinser.js +8 -5
- package/lib/helpers/agentFormulationParser.js +318 -0
- package/lib/helpers/aiInventory.js +262 -0
- package/lib/helpers/aiInventory.poku.js +111 -0
- package/lib/helpers/analyzer.js +1769 -0
- package/lib/helpers/analyzer.poku.js +284 -3
- package/lib/helpers/auditCategories.js +76 -0
- package/lib/helpers/ciParsers/githubActions.js +140 -16
- package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
- package/lib/helpers/communityAiConfigParser.js +672 -0
- package/lib/helpers/communityAiConfigParser.poku.js +63 -0
- package/lib/helpers/depsUtils.js +108 -0
- package/lib/helpers/depsUtils.poku.js +72 -1
- package/lib/helpers/display.js +325 -3
- package/lib/helpers/display.poku.js +301 -0
- package/lib/helpers/formulationParsers.js +28 -0
- package/lib/helpers/formulationParsers.poku.js +504 -1
- package/lib/helpers/jsonLike.js +102 -0
- package/lib/helpers/jsonLike.poku.js +34 -0
- package/lib/helpers/mcp.js +248 -0
- package/lib/helpers/mcp.poku.js +101 -0
- package/lib/helpers/mcpConfigParser.js +656 -0
- package/lib/helpers/mcpConfigParser.poku.js +126 -0
- package/lib/helpers/mcpDiscovery.js +84 -0
- package/lib/helpers/mcpDiscovery.poku.js +21 -0
- package/lib/helpers/protobom.js +3 -3
- package/lib/helpers/provenanceUtils.js +29 -4
- package/lib/helpers/provenanceUtils.poku.js +29 -3
- package/lib/helpers/registryProvenance.js +210 -0
- package/lib/helpers/registryProvenance.poku.js +144 -0
- package/lib/helpers/rustFormulationParser.js +330 -0
- package/lib/helpers/source.js +21 -2
- package/lib/helpers/source.poku.js +38 -0
- package/lib/helpers/utils.js +1331 -83
- package/lib/helpers/utils.poku.js +599 -188
- package/lib/helpers/vsixutils.js +12 -4
- package/lib/helpers/vsixutils.poku.js +34 -0
- package/lib/managers/binary.js +36 -12
- package/lib/managers/binary.poku.js +68 -0
- package/lib/managers/docker.js +59 -9
- package/lib/managers/docker.poku.js +61 -0
- package/lib/managers/piptree.js +12 -7
- package/lib/managers/piptree.poku.js +44 -0
- package/lib/stages/postgen/annotator.js +2 -1
- package/lib/stages/postgen/annotator.poku.js +15 -0
- package/lib/stages/postgen/auditBom.js +20 -6
- package/lib/stages/postgen/auditBom.poku.js +694 -1
- package/lib/stages/postgen/postgen.js +262 -11
- package/lib/stages/postgen/postgen.poku.js +306 -2
- package/lib/stages/postgen/ruleEngine.js +49 -1
- package/lib/stages/postgen/spdxConverter.poku.js +70 -0
- package/lib/stages/pregen/pregen.js +6 -4
- package/package.json +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts +12 -0
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -8
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
- package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
- package/types/lib/helpers/aiInventory.d.ts +23 -0
- package/types/lib/helpers/aiInventory.d.ts.map +1 -0
- package/types/lib/helpers/analyzer.d.ts +10 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/auditCategories.d.ts +12 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +8 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +17 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/jsonLike.d.ts +4 -0
- package/types/lib/helpers/jsonLike.d.ts.map +1 -0
- package/types/lib/helpers/mcp.d.ts +29 -0
- package/types/lib/helpers/mcp.d.ts.map +1 -0
- package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
- package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
- package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +5 -3
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
- package/types/lib/helpers/registryProvenance.d.ts +9 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
- package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
- package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +31 -1
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/vsixutils.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/piptree.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
package/lib/helpers/vsixutils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, join, resolve } from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
isMac,
|
|
14
14
|
isWin,
|
|
15
15
|
safeExistsSync,
|
|
16
|
+
safeExtractArchive,
|
|
17
|
+
safeMkdtempSync,
|
|
18
|
+
safeRmSync,
|
|
16
19
|
} from "./utils.js";
|
|
17
20
|
import { toVersRange } from "./versutils.js";
|
|
18
21
|
|
|
@@ -804,9 +807,14 @@ export async function extractVsixToTempDir(vsixFile) {
|
|
|
804
807
|
let tempDir;
|
|
805
808
|
let zip;
|
|
806
809
|
try {
|
|
807
|
-
tempDir =
|
|
810
|
+
tempDir = safeMkdtempSync(join(getTmpDir(), "vsix-deps-"));
|
|
808
811
|
zip = new StreamZip.async({ file: vsixFile });
|
|
809
|
-
await
|
|
812
|
+
const extracted = await safeExtractArchive(vsixFile, tempDir, async () => {
|
|
813
|
+
await zip.extract(null, tempDir);
|
|
814
|
+
});
|
|
815
|
+
if (!extracted) {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
810
818
|
// Most vsix files have content under extension/ subdirectory
|
|
811
819
|
const extensionSubDir = join(tempDir, "extension");
|
|
812
820
|
if (safeExistsSync(extensionSubDir)) {
|
|
@@ -854,7 +862,7 @@ export function cleanupTempDir(tempDir) {
|
|
|
854
862
|
dirBaseName.startsWith("vsix-deps-") &&
|
|
855
863
|
resolve(dirToRemove, "..") === expectedBase
|
|
856
864
|
) {
|
|
857
|
-
|
|
865
|
+
safeRmSync(dirToRemove, { recursive: true, force: true });
|
|
858
866
|
}
|
|
859
867
|
} catch (_e) {
|
|
860
868
|
// Best effort cleanup
|
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
+
import esmock from "esmock";
|
|
12
13
|
import { describe, it } from "poku";
|
|
14
|
+
import sinon from "sinon";
|
|
13
15
|
|
|
14
16
|
import {
|
|
15
17
|
cleanupTempDir,
|
|
@@ -40,6 +42,38 @@ describe("VSCODE_EXTENSION_PURL_TYPE", () => {
|
|
|
40
42
|
});
|
|
41
43
|
});
|
|
42
44
|
|
|
45
|
+
describe("extractVsixToTempDir()", () => {
|
|
46
|
+
it("returns undefined when dry-run blocks vsix extraction", async () => {
|
|
47
|
+
const safeExtractArchive = sinon.stub().resolves(false);
|
|
48
|
+
const zipClose = sinon.stub().resolves();
|
|
49
|
+
const { extractVsixToTempDir } = await esmock("./vsixutils.js", {
|
|
50
|
+
"node-stream-zip": {
|
|
51
|
+
default: {
|
|
52
|
+
async: sinon.stub().returns({
|
|
53
|
+
close: zipClose,
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
"./utils.js": {
|
|
58
|
+
DEBUG_MODE: false,
|
|
59
|
+
getTmpDir: sinon.stub().returns("/tmp"),
|
|
60
|
+
isMac: false,
|
|
61
|
+
isWin: false,
|
|
62
|
+
safeExistsSync: sinon.stub().returns(false),
|
|
63
|
+
safeExtractArchive,
|
|
64
|
+
safeMkdtempSync: sinon.stub().returns("/tmp/vsix-deps-test"),
|
|
65
|
+
safeRmSync: sinon.stub(),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const extractedDir = await extractVsixToTempDir("/tmp/sample.vsix");
|
|
70
|
+
|
|
71
|
+
assert.strictEqual(extractedDir, undefined);
|
|
72
|
+
sinon.assert.calledOnce(safeExtractArchive);
|
|
73
|
+
sinon.assert.calledOnce(zipClose);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
43
77
|
describe("getIdeExtensionDirs", () => {
|
|
44
78
|
it("should return an array of IDE configurations", () => {
|
|
45
79
|
const ides = getIdeExtensionDirs();
|
package/lib/managers/binary.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
lstatSync,
|
|
3
|
-
mkdtempSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
realpathSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
statSync,
|
|
8
|
-
} from "node:fs";
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
9
2
|
import { arch as _arch, platform as _platform, homedir } from "node:os";
|
|
10
3
|
import {
|
|
11
4
|
basename,
|
|
@@ -30,11 +23,15 @@ import {
|
|
|
30
23
|
extractPathEnv,
|
|
31
24
|
findLicenseId,
|
|
32
25
|
getTmpDir,
|
|
26
|
+
isDryRun,
|
|
33
27
|
isSpdxLicenseExpression,
|
|
34
28
|
multiChecksumFile,
|
|
29
|
+
recordActivity,
|
|
35
30
|
retrieveCdxgenPluginVersion,
|
|
36
31
|
safeExistsSync,
|
|
37
32
|
safeMkdirSync,
|
|
33
|
+
safeMkdtempSync,
|
|
34
|
+
safeRmSync,
|
|
38
35
|
safeSpawnSync,
|
|
39
36
|
} from "../helpers/utils.js";
|
|
40
37
|
import { getDirs } from "./containerutils.js";
|
|
@@ -451,6 +448,25 @@ export function executeSourcekitten(args) {
|
|
|
451
448
|
* @returns {Object} Metadata containing packages, dependencies, etc
|
|
452
449
|
*/
|
|
453
450
|
export async function getOSPackages(src, imageConfig) {
|
|
451
|
+
if (isDryRun) {
|
|
452
|
+
recordActivity({
|
|
453
|
+
kind: "container",
|
|
454
|
+
reason:
|
|
455
|
+
"Dry run mode blocks Trivy-based OS package generation because it executes external tools and writes temporary output.",
|
|
456
|
+
status: "blocked",
|
|
457
|
+
target: src,
|
|
458
|
+
});
|
|
459
|
+
return {
|
|
460
|
+
allTypes: new Set(),
|
|
461
|
+
binPaths: [],
|
|
462
|
+
bundledRuntimes: new Set(),
|
|
463
|
+
bundledSdks: new Set(),
|
|
464
|
+
dependenciesList: [],
|
|
465
|
+
executables: [],
|
|
466
|
+
osPackages: [],
|
|
467
|
+
sharedLibs: [],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
454
470
|
const pkgList = [];
|
|
455
471
|
const dependenciesList = [];
|
|
456
472
|
const allTypes = new Set();
|
|
@@ -491,7 +507,7 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
491
507
|
if (safeExistsSync(src)) {
|
|
492
508
|
imageType = "rootfs";
|
|
493
509
|
}
|
|
494
|
-
const tempDir =
|
|
510
|
+
const tempDir = safeMkdtempSync(join(getTmpDir(), "trivy-cdxgen-"));
|
|
495
511
|
const bomJsonFile = join(tempDir, "trivy-bom.json");
|
|
496
512
|
const args = [
|
|
497
513
|
imageType,
|
|
@@ -541,9 +557,7 @@ export async function getOSPackages(src, imageConfig) {
|
|
|
541
557
|
if (DEBUG_MODE) {
|
|
542
558
|
console.log(`Cleaning up ${tempDir}`);
|
|
543
559
|
}
|
|
544
|
-
|
|
545
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
546
|
-
}
|
|
560
|
+
safeRmSync(tempDir, { recursive: true, force: true });
|
|
547
561
|
}
|
|
548
562
|
const osReleaseData = {};
|
|
549
563
|
let osReleaseFile;
|
|
@@ -1026,6 +1040,16 @@ const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
|
|
|
1026
1040
|
};
|
|
1027
1041
|
|
|
1028
1042
|
export function executeOsQuery(query) {
|
|
1043
|
+
if (isDryRun) {
|
|
1044
|
+
recordActivity({
|
|
1045
|
+
kind: "osquery",
|
|
1046
|
+
reason:
|
|
1047
|
+
"Dry run mode blocks osquery execution and reports the query instead.",
|
|
1048
|
+
status: "blocked",
|
|
1049
|
+
target: query,
|
|
1050
|
+
});
|
|
1051
|
+
return undefined;
|
|
1052
|
+
}
|
|
1029
1053
|
if (OSQUERY_BIN) {
|
|
1030
1054
|
if (!query.endsWith(";")) {
|
|
1031
1055
|
query = `${query};`;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import esmock from "esmock";
|
|
2
|
+
import { assert, it } from "poku";
|
|
3
|
+
import sinon from "sinon";
|
|
4
|
+
|
|
5
|
+
async function loadBinaryModule({ utilsOverrides } = {}) {
|
|
6
|
+
return esmock("./binary.js", {
|
|
7
|
+
"../helpers/utils.js": {
|
|
8
|
+
adjustLicenseInformation: sinon.stub(),
|
|
9
|
+
collectExecutables: sinon.stub().returns([]),
|
|
10
|
+
collectSharedLibs: sinon.stub().returns([]),
|
|
11
|
+
DEBUG_MODE: false,
|
|
12
|
+
dirNameStr: "/tmp",
|
|
13
|
+
extractPathEnv: sinon.stub().returns([]),
|
|
14
|
+
findLicenseId: sinon.stub(),
|
|
15
|
+
getTmpDir: sinon.stub().returns("/tmp"),
|
|
16
|
+
isDryRun: false,
|
|
17
|
+
isSpdxLicenseExpression: sinon.stub().returns(false),
|
|
18
|
+
multiChecksumFile: sinon.stub(),
|
|
19
|
+
recordActivity: sinon.stub(),
|
|
20
|
+
retrieveCdxgenPluginVersion: sinon.stub().returns("1.0.0"),
|
|
21
|
+
safeExistsSync: sinon.stub().returns(false),
|
|
22
|
+
safeMkdirSync: sinon.stub(),
|
|
23
|
+
safeMkdtempSync: sinon.stub().returns("/tmp/trivy-cdxgen-test"),
|
|
24
|
+
safeRmSync: sinon.stub(),
|
|
25
|
+
safeSpawnSync: sinon
|
|
26
|
+
.stub()
|
|
27
|
+
.returns({ status: 1, stdout: "", stderr: "" }),
|
|
28
|
+
...utilsOverrides,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
it("executeOsQuery() reports a blocked dry-run activity", async () => {
|
|
34
|
+
const recordActivity = sinon.stub();
|
|
35
|
+
const { executeOsQuery } = await loadBinaryModule({
|
|
36
|
+
utilsOverrides: {
|
|
37
|
+
isDryRun: true,
|
|
38
|
+
recordActivity,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const result = executeOsQuery("select * from processes");
|
|
42
|
+
assert.strictEqual(result, undefined);
|
|
43
|
+
sinon.assert.calledWithMatch(recordActivity, {
|
|
44
|
+
kind: "osquery",
|
|
45
|
+
status: "blocked",
|
|
46
|
+
target: "select * from processes",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("getOSPackages() returns empty collections and reports a blocked dry-run activity", async () => {
|
|
51
|
+
const recordActivity = sinon.stub();
|
|
52
|
+
const { getOSPackages } = await loadBinaryModule({
|
|
53
|
+
utilsOverrides: {
|
|
54
|
+
isDryRun: true,
|
|
55
|
+
recordActivity,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const result = await getOSPackages("/tmp/rootfs", {});
|
|
59
|
+
assert.deepStrictEqual(result.osPackages, []);
|
|
60
|
+
assert.deepStrictEqual(result.dependenciesList, []);
|
|
61
|
+
assert.deepStrictEqual(result.binPaths, []);
|
|
62
|
+
assert.deepStrictEqual(Array.from(result.allTypes), []);
|
|
63
|
+
sinon.assert.calledWithMatch(recordActivity, {
|
|
64
|
+
kind: "container",
|
|
65
|
+
status: "blocked",
|
|
66
|
+
target: "/tmp/rootfs",
|
|
67
|
+
});
|
|
68
|
+
});
|
package/lib/managers/docker.js
CHANGED
|
@@ -2,11 +2,8 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import {
|
|
3
3
|
createReadStream,
|
|
4
4
|
lstatSync,
|
|
5
|
-
mkdtempSync,
|
|
6
5
|
readdirSync,
|
|
7
6
|
readFileSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
writeFileSync,
|
|
10
7
|
} from "node:fs";
|
|
11
8
|
import { platform as _platform, userInfo as _userInfo, homedir } from "node:os";
|
|
12
9
|
import { basename, join, resolve, win32 } from "node:path";
|
|
@@ -22,9 +19,14 @@ import {
|
|
|
22
19
|
extractPathEnv,
|
|
23
20
|
getAllFiles,
|
|
24
21
|
getTmpDir,
|
|
22
|
+
isDryRun,
|
|
23
|
+
recordActivity,
|
|
25
24
|
safeExistsSync,
|
|
26
25
|
safeMkdirSync,
|
|
26
|
+
safeMkdtempSync,
|
|
27
|
+
safeRmSync,
|
|
27
28
|
safeSpawnSync,
|
|
29
|
+
safeWriteSync,
|
|
28
30
|
} from "../helpers/utils.js";
|
|
29
31
|
import { getDirs, getOnlyDirs } from "./containerutils.js";
|
|
30
32
|
|
|
@@ -393,6 +395,16 @@ const getDefaultOptions = (forRegistry) => {
|
|
|
393
395
|
* daemon base URL, or `undefined`
|
|
394
396
|
*/
|
|
395
397
|
export const getConnection = async (options, forRegistry) => {
|
|
398
|
+
if (isDryRun) {
|
|
399
|
+
recordActivity({
|
|
400
|
+
kind: "network",
|
|
401
|
+
reason:
|
|
402
|
+
"Dry run mode blocks container daemon and registry HTTP requests.",
|
|
403
|
+
status: "blocked",
|
|
404
|
+
target: forRegistry || "container-daemon",
|
|
405
|
+
});
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
396
408
|
if (isContainerd || isNerdctl) {
|
|
397
409
|
return undefined;
|
|
398
410
|
}
|
|
@@ -507,6 +519,16 @@ export const getConnection = async (options, forRegistry) => {
|
|
|
507
519
|
* requests, raw Buffer for other methods, or `undefined` if no client is available
|
|
508
520
|
*/
|
|
509
521
|
export const makeRequest = async (path, method, forRegistry) => {
|
|
522
|
+
if (isDryRun) {
|
|
523
|
+
recordActivity({
|
|
524
|
+
kind: "network",
|
|
525
|
+
reason:
|
|
526
|
+
"Dry run mode blocks container daemon and registry HTTP requests.",
|
|
527
|
+
status: "blocked",
|
|
528
|
+
target: `${method} ${forRegistry || "container-daemon"}/${path}`,
|
|
529
|
+
});
|
|
530
|
+
return undefined;
|
|
531
|
+
}
|
|
510
532
|
const client = await getConnection({}, forRegistry);
|
|
511
533
|
if (!client) {
|
|
512
534
|
return undefined;
|
|
@@ -959,6 +981,16 @@ const EXTRACT_EXCLUDE_TYPES = new Set([
|
|
|
959
981
|
* empty or a non-fatal error was encountered
|
|
960
982
|
*/
|
|
961
983
|
export const extractTar = async (fullImageName, dir, options) => {
|
|
984
|
+
if (isDryRun) {
|
|
985
|
+
recordActivity({
|
|
986
|
+
kind: "untar",
|
|
987
|
+
reason:
|
|
988
|
+
"Dry run mode blocks untar and layer extraction operations because they create files on disk.",
|
|
989
|
+
status: "blocked",
|
|
990
|
+
target: `${fullImageName} -> ${dir}`,
|
|
991
|
+
});
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
962
994
|
try {
|
|
963
995
|
await stream.pipeline(
|
|
964
996
|
createReadStream(fullImageName),
|
|
@@ -1126,12 +1158,22 @@ const discoverManifestFromBlobs = (tempDir) => {
|
|
|
1126
1158
|
* Returns the location of the layers with additional packages related metadata
|
|
1127
1159
|
*/
|
|
1128
1160
|
export const exportArchive = async (fullImageName, options = {}) => {
|
|
1161
|
+
if (isDryRun) {
|
|
1162
|
+
recordActivity({
|
|
1163
|
+
kind: "container",
|
|
1164
|
+
reason:
|
|
1165
|
+
"Dry run mode blocks container archive expansion and layer materialization.",
|
|
1166
|
+
status: "blocked",
|
|
1167
|
+
target: fullImageName,
|
|
1168
|
+
});
|
|
1169
|
+
return undefined;
|
|
1170
|
+
}
|
|
1129
1171
|
if (!safeExistsSync(fullImageName)) {
|
|
1130
1172
|
console.log(`Unable to find container image archive ${fullImageName}`);
|
|
1131
1173
|
return undefined;
|
|
1132
1174
|
}
|
|
1133
1175
|
const manifest = {};
|
|
1134
|
-
const tempDir =
|
|
1176
|
+
const tempDir = safeMkdtempSync(join(getTmpDir(), "docker-images-"));
|
|
1135
1177
|
const allLayersExplodedDir = join(tempDir, "all-layers");
|
|
1136
1178
|
const blobsDir = join(tempDir, "blobs", "sha256");
|
|
1137
1179
|
safeMkdirSync(allLayersExplodedDir);
|
|
@@ -1163,7 +1205,7 @@ export const exportArchive = async (fullImageName, options = {}) => {
|
|
|
1163
1205
|
if (safeExistsSync(blobsDir)) {
|
|
1164
1206
|
const discoveredManifest = discoverManifestFromBlobs(tempDir);
|
|
1165
1207
|
if (discoveredManifest?.length) {
|
|
1166
|
-
|
|
1208
|
+
safeWriteSync(
|
|
1167
1209
|
synthesizedManifestFile,
|
|
1168
1210
|
JSON.stringify(discoveredManifest),
|
|
1169
1211
|
"utf-8",
|
|
@@ -1340,6 +1382,16 @@ export const extractFromManifest = async (
|
|
|
1340
1382
|
* Returns the location of the layers with additional packages related metadata
|
|
1341
1383
|
*/
|
|
1342
1384
|
export const exportImage = async (fullImageName, options) => {
|
|
1385
|
+
if (isDryRun) {
|
|
1386
|
+
recordActivity({
|
|
1387
|
+
kind: "container",
|
|
1388
|
+
reason:
|
|
1389
|
+
"Dry run mode blocks container image pull, save, and export operations.",
|
|
1390
|
+
status: "blocked",
|
|
1391
|
+
target: fullImageName,
|
|
1392
|
+
});
|
|
1393
|
+
return undefined;
|
|
1394
|
+
}
|
|
1343
1395
|
// Safely ignore local directories
|
|
1344
1396
|
if (
|
|
1345
1397
|
!fullImageName ||
|
|
@@ -1358,7 +1410,7 @@ export const exportImage = async (fullImageName, options) => {
|
|
|
1358
1410
|
if (tag === "" && digest === "") {
|
|
1359
1411
|
fullImageName = `${fullImageName}:latest`;
|
|
1360
1412
|
}
|
|
1361
|
-
const tempDir =
|
|
1413
|
+
const tempDir = safeMkdtempSync(join(getTmpDir(), "docker-images-"));
|
|
1362
1414
|
const allLayersExplodedDir = join(tempDir, "all-layers");
|
|
1363
1415
|
let manifestFile = join(tempDir, "manifest.json");
|
|
1364
1416
|
// Windows containers use index.json
|
|
@@ -1386,9 +1438,7 @@ export const exportImage = async (fullImageName, options) => {
|
|
|
1386
1438
|
if (DEBUG_MODE) {
|
|
1387
1439
|
console.log(`Cleaning up ${imageTarFile}`);
|
|
1388
1440
|
}
|
|
1389
|
-
|
|
1390
|
-
rmSync(imageTarFile, { force: true });
|
|
1391
|
-
}
|
|
1441
|
+
safeRmSync(imageTarFile, { force: true });
|
|
1392
1442
|
} else {
|
|
1393
1443
|
const client = await getConnection({}, registry);
|
|
1394
1444
|
try {
|
|
@@ -135,12 +135,18 @@ async function loadDockerModule({ clientResponse, utilsOverrides } = {}) {
|
|
|
135
135
|
};
|
|
136
136
|
const utilsStub = {
|
|
137
137
|
DEBUG_MODE: false,
|
|
138
|
+
createDryRunError: sinon.stub(),
|
|
138
139
|
extractPathEnv: sinon.stub().returns([]),
|
|
139
140
|
getAllFiles: sinon.stub().returns([]),
|
|
140
141
|
getTmpDir: sinon.stub().returns("/tmp"),
|
|
142
|
+
isDryRun: false,
|
|
143
|
+
recordActivity: sinon.stub(),
|
|
141
144
|
safeExistsSync: sinon.stub().returns(false),
|
|
142
145
|
safeMkdirSync: sinon.stub(),
|
|
146
|
+
safeMkdtempSync: sinon.stub().returns("/tmp/docker-images-test"),
|
|
147
|
+
safeRmSync: sinon.stub(),
|
|
143
148
|
safeSpawnSync: sinon.stub().returns({ status: 1, stdout: "", stderr: "" }),
|
|
149
|
+
safeWriteSync: sinon.stub(),
|
|
144
150
|
...utilsOverrides,
|
|
145
151
|
};
|
|
146
152
|
const dockerModule = await esmock("./docker.js", {
|
|
@@ -257,6 +263,61 @@ await it("docker getImage uses nerdctl when DOCKER_CMD is configured", async ()
|
|
|
257
263
|
}
|
|
258
264
|
});
|
|
259
265
|
|
|
266
|
+
await it("docker getConnection reports blocked network activity in dry-run mode", async () => {
|
|
267
|
+
const recordActivity = sinon.stub();
|
|
268
|
+
const { dockerModule } = await loadDockerModule({
|
|
269
|
+
utilsOverrides: {
|
|
270
|
+
isDryRun: true,
|
|
271
|
+
recordActivity,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
const conn = await dockerModule.getConnection({}, "docker.io");
|
|
275
|
+
assert.strictEqual(conn, undefined);
|
|
276
|
+
sinon.assert.calledWithMatch(recordActivity, {
|
|
277
|
+
kind: "network",
|
|
278
|
+
status: "blocked",
|
|
279
|
+
target: "docker.io",
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await it("docker extractTar reports a blocked untar activity in dry-run mode", async () => {
|
|
284
|
+
const recordActivity = sinon.stub();
|
|
285
|
+
const { dockerModule } = await loadDockerModule({
|
|
286
|
+
utilsOverrides: {
|
|
287
|
+
isDryRun: true,
|
|
288
|
+
recordActivity,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
const result = await dockerModule.extractTar(
|
|
292
|
+
"/tmp/image.tar",
|
|
293
|
+
"/tmp/out",
|
|
294
|
+
{},
|
|
295
|
+
);
|
|
296
|
+
assert.strictEqual(result, false);
|
|
297
|
+
sinon.assert.calledWithMatch(recordActivity, {
|
|
298
|
+
kind: "untar",
|
|
299
|
+
status: "blocked",
|
|
300
|
+
target: "/tmp/image.tar -> /tmp/out",
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await it("docker exportImage reports a blocked container activity in dry-run mode", async () => {
|
|
305
|
+
const recordActivity = sinon.stub();
|
|
306
|
+
const { dockerModule } = await loadDockerModule({
|
|
307
|
+
utilsOverrides: {
|
|
308
|
+
isDryRun: true,
|
|
309
|
+
recordActivity,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
const result = await dockerModule.exportImage("alpine:3.20", {});
|
|
313
|
+
assert.strictEqual(result, undefined);
|
|
314
|
+
sinon.assert.calledWithMatch(recordActivity, {
|
|
315
|
+
kind: "container",
|
|
316
|
+
status: "blocked",
|
|
317
|
+
target: "alpine:3.20",
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
260
321
|
await it("docker exportImage ignores local directories", async () => {
|
|
261
322
|
const imageData = await exportImage(".");
|
|
262
323
|
assert.strictEqual(imageData, undefined);
|
package/lib/managers/piptree.js
CHANGED
|
@@ -4,10 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* We use the internal pip api to construct the dependency tree for modern python + pip environments
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
8
|
import { delimiter, join } from "node:path";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getTmpDir,
|
|
12
|
+
safeExistsSync,
|
|
13
|
+
safeMkdtempSync,
|
|
14
|
+
safeRmSync,
|
|
15
|
+
safeSpawnSync,
|
|
16
|
+
safeWriteSync,
|
|
17
|
+
} from "../helpers/utils.js";
|
|
11
18
|
|
|
12
19
|
const PIP_TREE_PLUGIN_CONTENT = `
|
|
13
20
|
import importlib.metadata as importlib_metadata
|
|
@@ -226,11 +233,11 @@ if __name__ == "__main__":
|
|
|
226
233
|
*/
|
|
227
234
|
export const getTreeWithPlugin = (env, python_cmd, basePath) => {
|
|
228
235
|
let tree = [];
|
|
229
|
-
const tempDir =
|
|
236
|
+
const tempDir = safeMkdtempSync(join(getTmpDir(), "cdxgen-piptree-"));
|
|
230
237
|
const pipPlugin = join(tempDir, "piptree.py");
|
|
231
238
|
const pipTreeJson = join(tempDir, "piptree.json");
|
|
232
239
|
const pipPluginArgs = [pipPlugin, pipTreeJson];
|
|
233
|
-
|
|
240
|
+
safeWriteSync(pipPlugin, PIP_TREE_PLUGIN_CONTENT);
|
|
234
241
|
if (env.PIP_TARGET) {
|
|
235
242
|
if (!env.PYTHONPATH) {
|
|
236
243
|
env.PYTHONPATH = "";
|
|
@@ -255,8 +262,6 @@ export const getTreeWithPlugin = (env, python_cmd, basePath) => {
|
|
|
255
262
|
}),
|
|
256
263
|
);
|
|
257
264
|
}
|
|
258
|
-
|
|
259
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
260
|
-
}
|
|
265
|
+
safeRmSync(tempDir, { recursive: true, force: true });
|
|
261
266
|
return tree;
|
|
262
267
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import esmock from "esmock";
|
|
4
|
+
import { assert, it } from "poku";
|
|
5
|
+
import sinon from "sinon";
|
|
6
|
+
|
|
7
|
+
it("getTreeWithPlugin() reports dry-run temp-dir, write, execute, and cleanup activity", async () => {
|
|
8
|
+
const tempDir = path.join(path.sep, "tmp", "cdxgen-piptree-test");
|
|
9
|
+
const pluginFile = path.join(tempDir, "piptree.py");
|
|
10
|
+
const outputFile = path.join(tempDir, "piptree.json");
|
|
11
|
+
const safeMkdtempSync = sinon.stub().returns(tempDir);
|
|
12
|
+
const safeWriteSync = sinon.stub();
|
|
13
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
14
|
+
error: new Error("dry run"),
|
|
15
|
+
status: 1,
|
|
16
|
+
stderr: "",
|
|
17
|
+
stdout: "",
|
|
18
|
+
});
|
|
19
|
+
const safeRmSync = sinon.stub();
|
|
20
|
+
const { getTreeWithPlugin } = await esmock("./piptree.js", {
|
|
21
|
+
"../helpers/utils.js": {
|
|
22
|
+
getTmpDir: sinon.stub().returns("/tmp"),
|
|
23
|
+
safeExistsSync: sinon.stub().returns(false),
|
|
24
|
+
safeMkdtempSync,
|
|
25
|
+
safeRmSync,
|
|
26
|
+
safeSpawnSync,
|
|
27
|
+
safeWriteSync,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const result = getTreeWithPlugin({}, "python3", "/repo");
|
|
32
|
+
|
|
33
|
+
assert.deepStrictEqual(result, []);
|
|
34
|
+
sinon.assert.calledOnce(safeMkdtempSync);
|
|
35
|
+
sinon.assert.calledWithMatch(safeWriteSync, pluginFile, sinon.match.string);
|
|
36
|
+
sinon.assert.calledWith(safeSpawnSync, "python3", [pluginFile, outputFile], {
|
|
37
|
+
cwd: "/repo",
|
|
38
|
+
env: {},
|
|
39
|
+
});
|
|
40
|
+
sinon.assert.calledWith(safeRmSync, tempDir, {
|
|
41
|
+
force: true,
|
|
42
|
+
recursive: true,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -285,7 +285,8 @@ export function textualMetadata(bomJson) {
|
|
|
285
285
|
const { bomType, bomTypeDescription } = findBomType(bomJson);
|
|
286
286
|
const metadata = bomJson.metadata;
|
|
287
287
|
const lifecycles = metadata?.lifecycles || [];
|
|
288
|
-
const tlpClassification =
|
|
288
|
+
const tlpClassification =
|
|
289
|
+
metadata.distributionConstraints?.tlp || metadata.distribution;
|
|
289
290
|
const cryptoAssetsCount = bomJson?.components?.filter(
|
|
290
291
|
(c) => c.type === "cryptographic-asset",
|
|
291
292
|
).length;
|
|
@@ -311,3 +311,18 @@ it("extractTags tests", () => {
|
|
|
311
311
|
"security",
|
|
312
312
|
]);
|
|
313
313
|
});
|
|
314
|
+
|
|
315
|
+
it("textualMetadata includes the CycloneDX 1.7 TLP classification from distributionConstraints", () => {
|
|
316
|
+
assert.match(
|
|
317
|
+
textualMetadata({
|
|
318
|
+
bomFormat: "CycloneDX",
|
|
319
|
+
specVersion: "1.7",
|
|
320
|
+
metadata: {
|
|
321
|
+
distributionConstraints: {
|
|
322
|
+
tlp: "AMBER_AND_STRICT",
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
/TLP\) classification for this document is 'AMBER_AND_STRICT'/,
|
|
327
|
+
);
|
|
328
|
+
});
|
|
@@ -6,6 +6,10 @@ import { join, resolve } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
import { buildAnnotationText } from "../../helpers/annotationFormatter.js";
|
|
9
|
+
import {
|
|
10
|
+
expandBomAuditCategories,
|
|
11
|
+
validateBomAuditCategories,
|
|
12
|
+
} from "../../helpers/auditCategories.js";
|
|
9
13
|
import { table } from "../../helpers/table.js";
|
|
10
14
|
import {
|
|
11
15
|
DEBUG_MODE,
|
|
@@ -45,15 +49,17 @@ export async function auditBom(bomJson, options) {
|
|
|
45
49
|
}
|
|
46
50
|
let activeRules = rules;
|
|
47
51
|
if (options.bomAuditCategories) {
|
|
48
|
-
const categories =
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const { categories, expandedCategories } = validateBomAuditCategories(
|
|
53
|
+
options.bomAuditCategories,
|
|
54
|
+
rules,
|
|
55
|
+
);
|
|
52
56
|
if (categories.length > 0) {
|
|
53
|
-
activeRules = rules.filter((r) =>
|
|
57
|
+
activeRules = rules.filter((r) =>
|
|
58
|
+
expandedCategories.includes(r.category),
|
|
59
|
+
);
|
|
54
60
|
if (DEBUG_MODE) {
|
|
55
61
|
console.log(
|
|
56
|
-
`Filtering rules by categories: ${categories.join(", ")} (${activeRules.length} active)`,
|
|
62
|
+
`Filtering rules by categories: ${categories.join(", ")} -> ${expandBomAuditCategories(categories).join(", ")} (${activeRules.length} active)`,
|
|
57
63
|
);
|
|
58
64
|
}
|
|
59
65
|
}
|
|
@@ -159,6 +165,14 @@ export function formatAnnotations(findings, bomJson) {
|
|
|
159
165
|
value: f.attackTechniques.join(","),
|
|
160
166
|
});
|
|
161
167
|
}
|
|
168
|
+
if (f.standards && typeof f.standards === "object") {
|
|
169
|
+
for (const [standardName, entries] of Object.entries(f.standards)) {
|
|
170
|
+
properties.push({
|
|
171
|
+
name: `cdx:audit:standards:${standardName}`,
|
|
172
|
+
value: Array.isArray(entries) ? entries.join(",") : String(entries),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
162
176
|
if (f?.location?.purl) {
|
|
163
177
|
properties.push({
|
|
164
178
|
name: "cdx:audit:location:purl",
|