@cyclonedx/cdxgen 12.2.0 → 12.2.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.
Files changed (34) hide show
  1. package/README.md +5 -2
  2. package/bin/cdxgen.js +19 -1
  3. package/lib/cli/index.js +122 -57
  4. package/lib/cli/index.poku.js +117 -0
  5. package/lib/helpers/analyzer.js +606 -3
  6. package/lib/helpers/analyzer.poku.js +230 -0
  7. package/lib/helpers/depsUtils.js +16 -0
  8. package/lib/helpers/depsUtils.poku.js +58 -1
  9. package/lib/helpers/display.js +4 -2
  10. package/lib/helpers/remote/dependency-track.js +84 -0
  11. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  12. package/lib/helpers/table.js +384 -0
  13. package/lib/helpers/table.poku.js +186 -0
  14. package/lib/helpers/utils.js +184 -10
  15. package/lib/helpers/utils.poku.js +118 -11
  16. package/lib/server/openapi.yaml +33 -0
  17. package/lib/server/server.js +10 -2
  18. package/lib/server/server.poku.js +209 -0
  19. package/lib/stages/postgen/auditBom.js +1 -2
  20. package/lib/validator/reporters/console.js +2 -2
  21. package/package.json +1 -2
  22. package/types/lib/cli/index.d.ts.map +1 -1
  23. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  24. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  25. package/types/lib/helpers/display.d.ts.map +1 -1
  26. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  27. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  28. package/types/lib/helpers/table.d.ts +6 -0
  29. package/types/lib/helpers/table.d.ts.map +1 -0
  30. package/types/lib/helpers/utils.d.ts +1 -0
  31. package/types/lib/helpers/utils.d.ts.map +1 -1
  32. package/types/lib/server/server.d.ts +1 -0
  33. package/types/lib/server/server.d.ts.map +1 -1
  34. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
@@ -0,0 +1,230 @@
1
+ import {
2
+ copyFileSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+
11
+ import { assert, describe, it } from "poku";
12
+
13
+ import { findJSImportsExports } from "./analyzer.js";
14
+
15
+ const baseTempDir = mkdtempSync(join(tmpdir(), "cdxgen-analyzer-poku-"));
16
+
17
+ process.on("exit", () => {
18
+ rmSync(baseTempDir, { recursive: true, force: true });
19
+ });
20
+
21
+ const createProject = (subDirName, entryContent) => {
22
+ const projectDir = join(baseTempDir, subDirName);
23
+ mkdirSync(projectDir, { recursive: true });
24
+ writeFileSync(join(projectDir, "index.js"), entryContent, {
25
+ encoding: "utf-8",
26
+ });
27
+ return projectDir;
28
+ };
29
+
30
+ const createProjectFromFixture = (subDirName, fixtureFileName) => {
31
+ const projectDir = join(baseTempDir, subDirName);
32
+ mkdirSync(projectDir, { recursive: true });
33
+ const fixturePath = new URL(
34
+ `../../test/data/${fixtureFileName}`,
35
+ import.meta.url,
36
+ );
37
+ copyFileSync(fixturePath, join(projectDir, fixtureFileName));
38
+ return projectDir;
39
+ };
40
+
41
+ describe("findJSImportsExports() wasm and wasi detection", () => {
42
+ it("captures wasm exports from WebAssembly.instantiate() flow", async () => {
43
+ const projectDir = createProject(
44
+ "instantiate-flow",
45
+ `import fs from "node:fs/promises";
46
+ const wasmBuffer = await fs.readFile("./add.wasm");
47
+ const wasmModule = await WebAssembly.instantiate(wasmBuffer);
48
+ const { add } = wasmModule.instance.exports;
49
+ console.log(add(5, 6));
50
+ `,
51
+ );
52
+
53
+ const { allImports } = await findJSImportsExports(projectDir, false);
54
+ assert.ok(allImports["add.wasm"], "expected add.wasm to be discovered");
55
+ const occurrences = Array.from(allImports["add.wasm"]);
56
+ assert.ok(
57
+ occurrences.some((occ) => occ.importedModules?.includes("add")),
58
+ "expected add export symbol to be tracked",
59
+ );
60
+ const addOccurrence = occurrences.find((occ) =>
61
+ occ.importedModules?.includes("add"),
62
+ );
63
+ assert.ok(addOccurrence, "expected add symbol occurrence to exist");
64
+ assert.ok(
65
+ addOccurrence.fileName?.includes("index.js"),
66
+ "expected source filename to be tracked",
67
+ );
68
+ assert.strictEqual(addOccurrence.lineNumber, 4);
69
+ assert.strictEqual(typeof addOccurrence.columnNumber, "number");
70
+ assert.ok(addOccurrence.columnNumber >= 0);
71
+ });
72
+
73
+ it("captures wasm exports from instantiateStreaming(fetch(new URL(...)))", async () => {
74
+ const projectDir = createProject(
75
+ "streaming-flow",
76
+ `const { instance } = await WebAssembly.instantiateStreaming(
77
+ fetch(new URL("./stream.wasm", import.meta.url)),
78
+ );
79
+ const { run } = instance.exports;
80
+ console.log(run());
81
+ `,
82
+ );
83
+
84
+ const { allImports } = await findJSImportsExports(projectDir, false);
85
+ assert.ok(
86
+ allImports["stream.wasm"],
87
+ "expected stream.wasm to be discovered",
88
+ );
89
+ const occurrences = Array.from(allImports["stream.wasm"]);
90
+ assert.ok(
91
+ occurrences.some((occ) => occ.importedModules?.includes("run")),
92
+ "expected run export symbol to be tracked",
93
+ );
94
+ });
95
+
96
+ it("does not treat arbitrary function calls with .wasm literals as wasm imports", async () => {
97
+ const projectDir = createProject(
98
+ "non-wasm-callee",
99
+ `doSomething("./ignored.wasm");
100
+ `,
101
+ );
102
+
103
+ const { allImports } = await findJSImportsExports(projectDir, false);
104
+ assert.ok(
105
+ !allImports["./ignored.wasm"] && !allImports["ignored.wasm"],
106
+ "expected non-wasm callee usage to be ignored",
107
+ );
108
+ });
109
+
110
+ it("captures wasi constructor and lifecycle API usage", async () => {
111
+ const projectDir = createProject(
112
+ "wasi-flow",
113
+ `import { WASI } from "node:wasi";
114
+ const wasi = new WASI({ version: "preview1" });
115
+ wasi.initialize(instance);
116
+ `,
117
+ );
118
+
119
+ const { allImports } = await findJSImportsExports(projectDir, false);
120
+ assert.ok(allImports["node:wasi"], "expected node:wasi to be discovered");
121
+ const occurrences = Array.from(allImports["node:wasi"]);
122
+ assert.ok(
123
+ occurrences.some((occ) => occ.importedModules?.includes("WASI")),
124
+ "expected WASI usage to be tracked",
125
+ );
126
+ assert.ok(
127
+ occurrences.some((occ) => occ.importedModules?.includes("initialize")),
128
+ "expected initialize API usage to be tracked",
129
+ );
130
+ });
131
+
132
+ it("captures wasi constructor alias invoked without new", async () => {
133
+ const projectDir = createProject(
134
+ "wasi-call-alias-flow",
135
+ `import { WASI as WasiCtor } from "node:wasi";
136
+ const wasi = WasiCtor({ version: "preview1" });
137
+ wasi.start(instance);
138
+ `,
139
+ );
140
+
141
+ const { allImports } = await findJSImportsExports(projectDir, false);
142
+ assert.ok(allImports["node:wasi"], "expected node:wasi to be discovered");
143
+ const occurrences = Array.from(allImports["node:wasi"]);
144
+ assert.ok(
145
+ occurrences.some((occ) => occ.importedModules?.includes("WASI")),
146
+ "expected WASI constructor alias usage to be tracked",
147
+ );
148
+ assert.ok(
149
+ occurrences.some((occ) => occ.importedModules?.includes("start")),
150
+ "expected start API usage to be tracked",
151
+ );
152
+ });
153
+
154
+ it("detects wasm import/export functions from libmagic wrapper fixture", async () => {
155
+ const projectDir = createProjectFromFixture(
156
+ "libmagic-wrapper",
157
+ "libmagic-wrapper.js",
158
+ );
159
+
160
+ const { allImports, allExports } = await findJSImportsExports(
161
+ projectDir,
162
+ false,
163
+ );
164
+ assert.ok(allImports.fs, "expected fs require import to be detected");
165
+ assert.ok(
166
+ allImports.crypto,
167
+ "expected crypto require import to be detected",
168
+ );
169
+ assert.ok(
170
+ allImports["libmagic-wrapper.wasm"],
171
+ "expected libmagic-wrapper.wasm to be detected",
172
+ );
173
+ assert.ok(
174
+ allExports["libmagic-wrapper.wasm"],
175
+ "expected libmagic-wrapper.wasm exports to be detected",
176
+ );
177
+
178
+ const wasmImportOccurrences = Array.from(
179
+ allImports["libmagic-wrapper.wasm"],
180
+ );
181
+ const wasmExportOccurrences = Array.from(
182
+ allExports["libmagic-wrapper.wasm"],
183
+ );
184
+
185
+ assert.ok(
186
+ wasmImportOccurrences.some(
187
+ (occ) =>
188
+ occ.fileName?.includes("libmagic-wrapper.js") &&
189
+ typeof occ.lineNumber === "number" &&
190
+ typeof occ.columnNumber === "number",
191
+ ),
192
+ "expected wasm import occurrences to include source location metadata",
193
+ );
194
+
195
+ const importedModules = new Set(
196
+ wasmImportOccurrences.flatMap((occ) => occ.importedModules || []),
197
+ );
198
+ for (const expectedImportedModule of [
199
+ "free",
200
+ "malloc",
201
+ "magic_wrapper_load",
202
+ "magic_wrapper_detect",
203
+ "_emscripten_stack_restore",
204
+ "_emscripten_stack_alloc",
205
+ "emscripten_stack_get_current",
206
+ "memory",
207
+ "__indirect_function_table",
208
+ ]) {
209
+ assert.ok(
210
+ importedModules.has(expectedImportedModule),
211
+ `expected imported wasm symbol ${expectedImportedModule}`,
212
+ );
213
+ }
214
+
215
+ const exportedModules = new Set(
216
+ wasmExportOccurrences.flatMap((occ) => occ.exportedModules || []),
217
+ );
218
+ for (const expectedExportedModule of [
219
+ "_free",
220
+ "_malloc",
221
+ "_magic_wrapper_load",
222
+ "_magic_wrapper_detect",
223
+ ]) {
224
+ assert.ok(
225
+ exportedModules.has(expectedExportedModule),
226
+ `expected exported wasm symbol ${expectedExportedModule}`,
227
+ );
228
+ }
229
+ });
230
+ });
@@ -119,6 +119,22 @@ export function trimComponents(components) {
119
119
  existingComponent.properties = comp.properties;
120
120
  }
121
121
  }
122
+ if (comp.hashes) {
123
+ if (existingComponent.hashes) {
124
+ for (const newhash of comp.hashes) {
125
+ if (
126
+ !existingComponent.hashes.find(
127
+ (hash) =>
128
+ hash.alg === newhash.alg && hash.content === newhash.content,
129
+ )
130
+ ) {
131
+ existingComponent.hashes.push(newhash);
132
+ }
133
+ }
134
+ } else {
135
+ existingComponent.hashes = comp.hashes;
136
+ }
137
+ }
122
138
  // Retain all component.evidence.identity
123
139
  if (comp?.evidence?.identity) {
124
140
  if (!existingComponent.evidence) {
@@ -1,6 +1,6 @@
1
1
  import { assert, describe, it } from "poku";
2
2
 
3
- import { mergeDependencies } from "./depsUtils.js";
3
+ import { mergeDependencies, trimComponents } from "./depsUtils.js";
4
4
 
5
5
  describe("mergeDependencies()", () => {
6
6
  it("merges two non-overlapping dependency arrays", () => {
@@ -148,3 +148,60 @@ describe("mergeDependencies()", () => {
148
148
  assert.ok(!entry.dependsOn.includes(null), "null must be filtered");
149
149
  });
150
150
  });
151
+
152
+ describe("trimComponents()", () => {
153
+ it("retains hashes from duplicate components", () => {
154
+ const components = [
155
+ {
156
+ name: "jquery",
157
+ version: "3.5.1",
158
+ purl: "pkg:npm/jquery@3.5.1",
159
+ type: "library",
160
+ properties: [{ name: "SrcFile", value: "Scripts/jquery.min.js" }],
161
+ },
162
+ {
163
+ name: "jquery",
164
+ version: "3.5.1",
165
+ purl: "pkg:npm/jquery@3.5.1",
166
+ type: "framework",
167
+ hashes: [{ alg: "SHA-512", content: "abc123" }],
168
+ properties: [{ name: "SrcFile", value: "package-lock.json" }],
169
+ },
170
+ ];
171
+ const result = trimComponents(components);
172
+ assert.strictEqual(result.length, 1);
173
+ assert.deepStrictEqual(result[0].hashes, [
174
+ { alg: "SHA-512", content: "abc123" },
175
+ ]);
176
+ });
177
+
178
+ it("merges and deduplicates hashes from duplicate components", () => {
179
+ const components = [
180
+ {
181
+ name: "jquery",
182
+ version: "3.5.1",
183
+ purl: "pkg:npm/jquery@3.5.1",
184
+ type: "library",
185
+ hashes: [{ alg: "SHA-512", content: "abc123" }],
186
+ properties: [{ name: "SrcFile", value: "Scripts/jquery.min.js" }],
187
+ },
188
+ {
189
+ name: "jquery",
190
+ version: "3.5.1",
191
+ purl: "pkg:npm/jquery@3.5.1",
192
+ type: "framework",
193
+ hashes: [
194
+ { alg: "SHA-512", content: "abc123" },
195
+ { alg: "SHA-256", content: "def456" },
196
+ ],
197
+ properties: [{ name: "SrcFile", value: "package-lock.json" }],
198
+ },
199
+ ];
200
+ const result = trimComponents(components);
201
+ assert.strictEqual(result.length, 1);
202
+ assert.deepStrictEqual(result[0].hashes, [
203
+ { alg: "SHA-512", content: "abc123" },
204
+ { alg: "SHA-256", content: "def456" },
205
+ ]);
206
+ });
207
+ });
@@ -2,8 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
4
 
5
- import { createStream, table } from "table";
6
-
5
+ import { createStream, table } from "./table.js";
7
6
  import { isSecureMode, safeExistsSync, toCamel } from "./utils.js";
8
7
 
9
8
  // https://github.com/yangshun/tree-node-cli/blob/master/src/index.js
@@ -91,6 +90,7 @@ export function printTable(
91
90
  ]);
92
91
  }
93
92
  }
93
+ stream.end();
94
94
  console.log();
95
95
  if (!filterTypes) {
96
96
  console.log(
@@ -135,6 +135,7 @@ export function printOSTable(bomJson) {
135
135
  (comp.tags || []).join(", "),
136
136
  ]);
137
137
  }
138
+ stream.end();
138
139
  console.log();
139
140
  }
140
141
  /**
@@ -253,6 +254,7 @@ export function printOccurrences(bomJson) {
253
254
  stream.write(row);
254
255
  }
255
256
  }
257
+ stream.end();
256
258
  console.log();
257
259
  }
258
260
 
@@ -0,0 +1,84 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ /**
4
+ * Returns the Dependency-Track BOM API URL.
5
+ *
6
+ * @param {string} serverUrl Dependency-Track server URL
7
+ * @returns {string} API URL to submit BOM payload
8
+ */
9
+ export function getDependencyTrackBomUrl(serverUrl) {
10
+ return `${serverUrl.replace(/\/$/, "")}/api/v1/bom`;
11
+ }
12
+
13
+ /**
14
+ * Build the payload for Dependency-Track BOM submission.
15
+ *
16
+ * @param {Object} args CLI/server arguments
17
+ * @param {Object} bomContents BOM Json
18
+ * @returns {Object | undefined} payload object if project coordinates are valid
19
+ */
20
+ export function buildDependencyTrackBomPayload(args, bomContents) {
21
+ let encodedBomContents = Buffer.from(JSON.stringify(bomContents)).toString(
22
+ "base64",
23
+ );
24
+ if (encodedBomContents.startsWith("77u/")) {
25
+ encodedBomContents = encodedBomContents.substring(4);
26
+ }
27
+ const autoCreate =
28
+ typeof args.autoCreate === "boolean"
29
+ ? args.autoCreate
30
+ : args.autoCreate !== "false";
31
+ const bomPayload = {
32
+ autoCreate: String(autoCreate),
33
+ bom: encodedBomContents,
34
+ };
35
+ if (
36
+ typeof args.projectId !== "undefined" ||
37
+ typeof args.projectName !== "undefined"
38
+ ) {
39
+ if (typeof args.projectId !== "undefined") {
40
+ bomPayload.project = args.projectId;
41
+ }
42
+ if (typeof args.projectName !== "undefined") {
43
+ bomPayload.projectName = args.projectName;
44
+ }
45
+ // Dependency-Track submissions use "main" as fallback when no version is provided.
46
+ bomPayload.projectVersion = args.projectVersion || "main";
47
+ } else {
48
+ return undefined;
49
+ }
50
+ const parentProjectId = args.parentProjectId || args.parentUUID;
51
+ const hasParentUuidMode = typeof parentProjectId !== "undefined";
52
+ const hasParentName = typeof args.parentProjectName !== "undefined";
53
+ const hasParentVersion = typeof args.parentProjectVersion !== "undefined";
54
+ const hasParentCoordsMode = hasParentName || hasParentVersion;
55
+ if (hasParentUuidMode && hasParentCoordsMode) {
56
+ return undefined;
57
+ }
58
+ if (!hasParentUuidMode && hasParentName !== hasParentVersion) {
59
+ return undefined;
60
+ }
61
+ if (hasParentUuidMode) {
62
+ bomPayload.parentUUID = parentProjectId;
63
+ }
64
+ if (hasParentName && hasParentVersion) {
65
+ bomPayload.parentName = args.parentProjectName;
66
+ bomPayload.parentVersion = args.parentProjectVersion;
67
+ }
68
+ if (
69
+ typeof args.isLatest === "boolean" ||
70
+ args.isLatest === "true" ||
71
+ args.isLatest === "false"
72
+ ) {
73
+ bomPayload.isLatest =
74
+ typeof args.isLatest === "boolean"
75
+ ? args.isLatest
76
+ : args.isLatest === "true";
77
+ }
78
+ if (typeof args.projectTag !== "undefined") {
79
+ bomPayload.projectTags = (
80
+ Array.isArray(args.projectTag) ? args.projectTag : [args.projectTag]
81
+ ).map((tag) => ({ name: tag }));
82
+ }
83
+ return bomPayload;
84
+ }
@@ -0,0 +1,119 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ buildDependencyTrackBomPayload,
5
+ getDependencyTrackBomUrl,
6
+ } from "./dependency-track.js";
7
+
8
+ describe("Dependency-Track helper tests", () => {
9
+ it("returns submission URL without trailing slash duplication", () => {
10
+ assert.strictEqual(
11
+ getDependencyTrackBomUrl("https://dtrack.example.com/"),
12
+ "https://dtrack.example.com/api/v1/bom",
13
+ );
14
+ assert.strictEqual(
15
+ getDependencyTrackBomUrl("https://dtrack.example.com"),
16
+ "https://dtrack.example.com/api/v1/bom",
17
+ );
18
+ });
19
+
20
+ it("builds payload with parentUUID and tags", () => {
21
+ const payload = buildDependencyTrackBomPayload(
22
+ {
23
+ projectName: "child",
24
+ projectVersion: "1.0.0",
25
+ parentProjectId: "d9628844-5f04-4ca7-88a2-64eb6bc64db0",
26
+ projectTag: ["tag1", "tag2"],
27
+ },
28
+ { bom: "test" },
29
+ );
30
+ assert.deepStrictEqual(payload, {
31
+ autoCreate: "true",
32
+ bom: "eyJib20iOiJ0ZXN0In0=",
33
+ parentUUID: "d9628844-5f04-4ca7-88a2-64eb6bc64db0",
34
+ projectName: "child",
35
+ projectTags: [{ name: "tag1" }, { name: "tag2" }],
36
+ projectVersion: "1.0.0",
37
+ });
38
+ });
39
+
40
+ it("builds payload with parentName and parentVersion", () => {
41
+ const payload = buildDependencyTrackBomPayload(
42
+ {
43
+ projectName: "child",
44
+ projectVersion: "1.0.0",
45
+ parentProjectName: "parent",
46
+ parentProjectVersion: "2.0.0",
47
+ },
48
+ { bom: "test2" },
49
+ );
50
+ assert.deepStrictEqual(payload, {
51
+ autoCreate: "true",
52
+ bom: "eyJib20iOiJ0ZXN0MiJ9",
53
+ parentName: "parent",
54
+ parentVersion: "2.0.0",
55
+ projectName: "child",
56
+ projectVersion: "1.0.0",
57
+ });
58
+ });
59
+
60
+ it("returns undefined when project identity is missing", () => {
61
+ const payload = buildDependencyTrackBomPayload({}, { bom: "test3" });
62
+ assert.strictEqual(payload, undefined);
63
+ });
64
+
65
+ it("supports configurable autoCreate and isLatest", () => {
66
+ const payload = buildDependencyTrackBomPayload(
67
+ {
68
+ autoCreate: false,
69
+ isLatest: true,
70
+ projectName: "child",
71
+ },
72
+ { bom: "test4" },
73
+ );
74
+ assert.deepStrictEqual(payload, {
75
+ autoCreate: "false",
76
+ bom: "eyJib20iOiJ0ZXN0NCJ9",
77
+ isLatest: true,
78
+ projectName: "child",
79
+ projectVersion: "main",
80
+ });
81
+ });
82
+
83
+ it("defaults projectVersion to main when only projectName is provided", () => {
84
+ const payload = buildDependencyTrackBomPayload(
85
+ { projectName: "child" },
86
+ { bom: "test5" },
87
+ );
88
+ assert.deepStrictEqual(payload, {
89
+ autoCreate: "true",
90
+ bom: "eyJib20iOiJ0ZXN0NSJ9",
91
+ projectName: "child",
92
+ projectVersion: "main",
93
+ });
94
+ });
95
+
96
+ it("returns undefined when parent UUID and parent name/version are both provided", () => {
97
+ const payload = buildDependencyTrackBomPayload(
98
+ {
99
+ parentProjectId: "d9628844-5f04-4ca7-88a2-64eb6bc64db0",
100
+ parentProjectName: "parent",
101
+ parentProjectVersion: "1.0.0",
102
+ projectName: "child",
103
+ },
104
+ { bom: "test6" },
105
+ );
106
+ assert.strictEqual(payload, undefined);
107
+ });
108
+
109
+ it("returns undefined when parent name/version mode is incomplete", () => {
110
+ const payload = buildDependencyTrackBomPayload(
111
+ {
112
+ parentProjectName: "parent",
113
+ projectName: "child",
114
+ },
115
+ { bom: "test7" },
116
+ );
117
+ assert.strictEqual(payload, undefined);
118
+ });
119
+ });