@cyclonedx/cdxgen 12.1.5 → 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 (193) hide show
  1. package/README.md +51 -40
  2. package/bin/cdxgen.js +194 -97
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +1 -1
  5. package/bin/sign.js +102 -0
  6. package/bin/validate.js +233 -0
  7. package/bin/verify.js +69 -28
  8. package/data/queries.json +1 -1
  9. package/data/rules/ci-permissions.yaml +186 -0
  10. package/data/rules/dependency-sources.yaml +123 -0
  11. package/data/rules/package-integrity.yaml +135 -0
  12. package/data/rules/vscode-extensions.yaml +228 -0
  13. package/lib/cli/index.js +449 -429
  14. package/lib/cli/index.poku.js +117 -0
  15. package/lib/evinser/db.js +137 -0
  16. package/lib/{helpers → evinser}/db.poku.js +2 -6
  17. package/lib/evinser/evinser.js +2 -14
  18. package/lib/helpers/analyzer.js +606 -3
  19. package/lib/helpers/analyzer.poku.js +230 -0
  20. package/lib/helpers/bomSigner.js +312 -0
  21. package/lib/helpers/bomSigner.poku.js +156 -0
  22. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  23. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  24. package/lib/helpers/ciParsers/circleCi.js +286 -0
  25. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  26. package/lib/helpers/ciParsers/common.js +24 -0
  27. package/lib/helpers/ciParsers/githubActions.js +636 -0
  28. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  29. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  30. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  31. package/lib/helpers/ciParsers/jenkins.js +181 -0
  32. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  33. package/lib/helpers/depsUtils.js +219 -0
  34. package/lib/helpers/depsUtils.poku.js +207 -0
  35. package/lib/helpers/display.js +426 -5
  36. package/lib/helpers/envcontext.js +18 -3
  37. package/lib/helpers/formulationParsers.js +351 -0
  38. package/lib/helpers/logger.js +14 -0
  39. package/lib/helpers/protobom.js +9 -9
  40. package/lib/helpers/pythonutils.js +9 -0
  41. package/lib/helpers/remote/dependency-track.js +84 -0
  42. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  43. package/lib/helpers/table.js +384 -0
  44. package/lib/helpers/table.poku.js +186 -0
  45. package/lib/helpers/utils.js +865 -416
  46. package/lib/helpers/utils.poku.js +172 -265
  47. package/lib/helpers/versutils.js +202 -0
  48. package/lib/helpers/versutils.poku.js +315 -0
  49. package/lib/helpers/vsixutils.js +1061 -0
  50. package/lib/helpers/vsixutils.poku.js +2247 -0
  51. package/lib/managers/binary.js +19 -19
  52. package/lib/managers/docker.js +108 -1
  53. package/lib/managers/oci.js +10 -0
  54. package/lib/managers/piptree.js +3 -9
  55. package/lib/parsers/npmrc.js +17 -13
  56. package/lib/parsers/npmrc.poku.js +41 -5
  57. package/lib/server/openapi.yaml +34 -1
  58. package/lib/server/server.js +50 -13
  59. package/lib/server/server.poku.js +332 -144
  60. package/lib/stages/postgen/annotator.js +1 -1
  61. package/lib/stages/postgen/auditBom.js +196 -0
  62. package/lib/stages/postgen/auditBom.poku.js +378 -0
  63. package/lib/stages/postgen/postgen.js +54 -1
  64. package/lib/stages/postgen/postgen.poku.js +90 -1
  65. package/lib/stages/postgen/ruleEngine.js +369 -0
  66. package/lib/stages/pregen/envAudit.js +299 -0
  67. package/lib/stages/pregen/envAudit.poku.js +572 -0
  68. package/lib/stages/pregen/pregen.js +12 -8
  69. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  70. package/lib/validator/complianceEngine.js +241 -0
  71. package/lib/validator/complianceEngine.poku.js +168 -0
  72. package/lib/validator/complianceRules.js +1610 -0
  73. package/lib/validator/complianceRules.poku.js +328 -0
  74. package/lib/validator/index.js +222 -0
  75. package/lib/validator/index.poku.js +144 -0
  76. package/lib/validator/reporters/annotations.js +121 -0
  77. package/lib/validator/reporters/console.js +149 -0
  78. package/lib/validator/reporters/index.js +41 -0
  79. package/lib/validator/reporters/json.js +37 -0
  80. package/lib/validator/reporters/sarif.js +184 -0
  81. package/lib/validator/reporters.poku.js +150 -0
  82. package/package.json +8 -9
  83. package/types/bin/sign.d.ts +3 -0
  84. package/types/bin/sign.d.ts.map +1 -0
  85. package/types/bin/validate.d.ts +3 -0
  86. package/types/bin/validate.d.ts.map +1 -0
  87. package/types/helpers/utils.d.ts +0 -1
  88. package/types/lib/cli/index.d.ts +49 -52
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/evinser/db.d.ts +34 -0
  91. package/types/lib/evinser/db.d.ts.map +1 -0
  92. package/types/lib/evinser/evinser.d.ts +63 -16
  93. package/types/lib/evinser/evinser.d.ts.map +1 -1
  94. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  95. package/types/lib/helpers/bomSigner.d.ts +27 -0
  96. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  97. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  98. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  99. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  100. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  101. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  102. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  103. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  104. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  105. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  106. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  107. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  108. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  109. package/types/lib/helpers/depsUtils.d.ts +21 -0
  110. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  111. package/types/lib/helpers/display.d.ts +111 -11
  112. package/types/lib/helpers/display.d.ts.map +1 -1
  113. package/types/lib/helpers/envcontext.d.ts +19 -7
  114. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  115. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  116. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  117. package/types/lib/helpers/logger.d.ts +15 -1
  118. package/types/lib/helpers/logger.d.ts.map +1 -1
  119. package/types/lib/helpers/protobom.d.ts +2 -2
  120. package/types/lib/helpers/pythonutils.d.ts +10 -1
  121. package/types/lib/helpers/pythonutils.d.ts.map +1 -1
  122. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  123. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  124. package/types/lib/helpers/table.d.ts +6 -0
  125. package/types/lib/helpers/table.d.ts.map +1 -0
  126. package/types/lib/helpers/utils.d.ts +533 -128
  127. package/types/lib/helpers/utils.d.ts.map +1 -1
  128. package/types/lib/helpers/versutils.d.ts +8 -0
  129. package/types/lib/helpers/versutils.d.ts.map +1 -0
  130. package/types/lib/helpers/vsixutils.d.ts +130 -0
  131. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  132. package/types/lib/managers/docker.d.ts +12 -31
  133. package/types/lib/managers/docker.d.ts.map +1 -1
  134. package/types/lib/managers/oci.d.ts +11 -1
  135. package/types/lib/managers/oci.d.ts.map +1 -1
  136. package/types/lib/managers/piptree.d.ts.map +1 -1
  137. package/types/lib/parsers/npmrc.d.ts +4 -1
  138. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  139. package/types/lib/server/server.d.ts +22 -2
  140. package/types/lib/server/server.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  142. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  144. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  145. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  146. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  147. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  148. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  149. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  150. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  151. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  152. package/types/lib/validator/complianceEngine.d.ts +66 -0
  153. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  154. package/types/lib/validator/complianceRules.d.ts +70 -0
  155. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  156. package/types/lib/validator/index.d.ts +70 -0
  157. package/types/lib/validator/index.d.ts.map +1 -0
  158. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  159. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  160. package/types/lib/validator/reporters/console.d.ts +30 -0
  161. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  162. package/types/lib/validator/reporters/index.d.ts +21 -0
  163. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  164. package/types/lib/validator/reporters/json.d.ts +11 -0
  165. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  166. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  167. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  168. package/lib/helpers/db.js +0 -162
  169. package/lib/stages/pregen/env-audit.js +0 -34
  170. package/lib/stages/pregen/env-audit.poku.js +0 -290
  171. package/types/helpers/db.d.ts +0 -35
  172. package/types/helpers/db.d.ts.map +0 -1
  173. package/types/lib/helpers/db.d.ts +0 -35
  174. package/types/lib/helpers/db.d.ts.map +0 -1
  175. package/types/lib/helpers/validator.d.ts.map +0 -1
  176. package/types/lib/stages/pregen/env-audit.d.ts +0 -2
  177. package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
  178. package/types/managers/binary.d.ts +0 -37
  179. package/types/managers/binary.d.ts.map +0 -1
  180. package/types/managers/docker.d.ts +0 -56
  181. package/types/managers/docker.d.ts.map +0 -1
  182. package/types/managers/oci.d.ts +0 -2
  183. package/types/managers/oci.d.ts.map +0 -1
  184. package/types/managers/piptree.d.ts +0 -2
  185. package/types/managers/piptree.d.ts.map +0 -1
  186. package/types/server/server.d.ts +0 -34
  187. package/types/server/server.d.ts.map +0 -1
  188. package/types/stages/postgen/annotator.d.ts +0 -27
  189. package/types/stages/postgen/annotator.d.ts.map +0 -1
  190. package/types/stages/postgen/postgen.d.ts +0 -51
  191. package/types/stages/postgen/postgen.d.ts.map +0 -1
  192. package/types/stages/pregen/pregen.d.ts +0 -59
  193. package/types/stages/pregen/pregen.d.ts.map +0 -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
+ });
@@ -0,0 +1,312 @@
1
+ import crypto from "node:crypto";
2
+
3
+ /**
4
+ * Lightweight, deterministic JSON Canonicalizer (similar to RFC 8785).
5
+ * Required by JSF to ensure the signature payload remains identical across systems.
6
+ *
7
+ * @param {any} value - The JSON object/value to canonicalize
8
+ * @returns {string} - Canonicalized JSON string
9
+ */
10
+ function canonicalize(value) {
11
+ if (value === null || typeof value !== "object") {
12
+ return JSON.stringify(value);
13
+ }
14
+ if (Array.isArray(value)) {
15
+ return `[${value.map(canonicalize).join(",")}]`;
16
+ }
17
+ const keys = Object.keys(value).sort();
18
+ let str = "{";
19
+ for (let i = 0; i < keys.length; i++) {
20
+ if (i > 0) str += ",";
21
+ str += `${JSON.stringify(keys[i])}:${canonicalize(value[keys[i]])}`;
22
+ }
23
+ str += "}";
24
+ return str;
25
+ }
26
+
27
+ /**
28
+ * Creates a JSF-compliant signature block.
29
+ *
30
+ * @param {Object} payload - The object to sign (e.g., BOM, component)
31
+ * @param {string|Buffer|crypto.KeyObject} privateKey - The signing key
32
+ * @param {string} alg - JSF algorithm identifier
33
+ * @param {Object} [publicKeyJwk] - Optional JWK representation of the public key
34
+ * @param {string} keyId - Key ID
35
+ *
36
+ * @returns {Object} - JSF signature block
37
+ */
38
+ function createSignatureBlock(
39
+ payload,
40
+ privateKey,
41
+ alg,
42
+ publicKeyJwk = null,
43
+ keyId = null,
44
+ ) {
45
+ // Exclude existing signatures from the canonicalized payload as per JSF
46
+ const { signature, ...dataToSign } = payload;
47
+ const canonicalData = canonicalize(dataToSign);
48
+
49
+ let hashAlg;
50
+ const signOptions = { key: privateKey };
51
+
52
+ // Handle HMAC (Symmetric)
53
+ if (alg.startsWith("HS")) {
54
+ const hash = alg.replace("HS", "sha");
55
+ const value = crypto
56
+ .createHmac(hash, privateKey)
57
+ .update(canonicalData, "utf8")
58
+ .digest("base64url");
59
+ const block = { algorithm: alg, value };
60
+ if (publicKeyJwk) {
61
+ block.publicKey = publicKeyJwk;
62
+ }
63
+ if (keyId) {
64
+ block.keyId = keyId;
65
+ }
66
+ return block;
67
+ }
68
+
69
+ // Handle Asymmetric Algorithms
70
+ if (alg.startsWith("RS")) {
71
+ hashAlg = alg.replace("RS", "SHA");
72
+ } else if (alg.startsWith("PS")) {
73
+ hashAlg = alg.replace("PS", "SHA");
74
+ signOptions.padding = crypto.constants.RSA_PKCS1_PSS_PADDING;
75
+ signOptions.saltLength = crypto.constants.RSA_PSS_SALTLEN_AUTO;
76
+ } else if (alg.startsWith("ES")) {
77
+ hashAlg = alg.replace("ES", "SHA");
78
+ // Standard JWA format requires IEEE P1363 (R || S) instead of ASN.1 DER
79
+ signOptions.dsaEncoding = "ieee-p1363";
80
+ } else if (alg === "Ed25519" || alg === "Ed448") {
81
+ // Native EdDSA algorithms do not require a separate hash algorithm definition
82
+ hashAlg = null;
83
+ } else {
84
+ throw new Error(`Unsupported JSF algorithm: ${alg}`);
85
+ }
86
+ const sigBuffer = crypto.sign(
87
+ hashAlg,
88
+ Buffer.from(canonicalData, "utf8"),
89
+ signOptions,
90
+ );
91
+ const block = { algorithm: alg, value: sigBuffer.toString("base64url") };
92
+ if (publicKeyJwk) {
93
+ block.publicKey = publicKeyJwk;
94
+ }
95
+ if (keyId) {
96
+ block.keyId = keyId;
97
+ }
98
+ return block;
99
+ }
100
+
101
+ /**
102
+ * Appends or replaces a signature on a target object based on the configured mode.
103
+ */
104
+ function addSignature(target, sigBlock, mode) {
105
+ if (!target.signature) {
106
+ target.signature = sigBlock;
107
+ return;
108
+ }
109
+ if (mode === "chain") {
110
+ if (target.signature.chain) {
111
+ target.signature.chain.push(sigBlock);
112
+ } else if (target.signature.signers) {
113
+ throw new Error("Cannot mix signature chains and multi-signers.");
114
+ } else {
115
+ target.signature = { chain: [target.signature, sigBlock] };
116
+ }
117
+ } else if (mode === "signers") {
118
+ if (target.signature.signers) {
119
+ target.signature.signers.push(sigBlock);
120
+ } else if (target.signature.chain) {
121
+ throw new Error("Cannot mix signature chains and multi-signers.");
122
+ } else {
123
+ target.signature = { signers: [target.signature, sigBlock] };
124
+ }
125
+ } else {
126
+ target.signature = sigBlock;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Recursively applies signatures to the BOM and its granular components.
132
+ *
133
+ * @param {Object} bomJson - CycloneDX BOM Object
134
+ * @param {Object} options - Signing options { privateKey, algorithm, mode, ... }
135
+ * @returns {Object} - Signed BOM Object
136
+ */
137
+ export function signBom(bomJson, options = {}) {
138
+ const {
139
+ privateKey,
140
+ algorithm = "RS512",
141
+ publicKeyJwk = null,
142
+ keyId = null,
143
+ mode = "replace", // Supports: 'replace', 'chain', 'signers'
144
+ signComponents = true,
145
+ signServices = true,
146
+ signAnnotations = true,
147
+ } = options;
148
+
149
+ if (!privateKey) {
150
+ throw new Error("privateKey is required for signing");
151
+ }
152
+ if (signComponents && Array.isArray(bomJson.components)) {
153
+ for (const comp of bomJson.components) {
154
+ addSignature(
155
+ comp,
156
+ createSignatureBlock(comp, privateKey, algorithm, publicKeyJwk, keyId),
157
+ mode,
158
+ );
159
+ }
160
+ }
161
+ if (signServices && Array.isArray(bomJson.services)) {
162
+ for (const svc of bomJson.services) {
163
+ addSignature(
164
+ svc,
165
+ createSignatureBlock(svc, privateKey, algorithm, publicKeyJwk, keyId),
166
+ mode,
167
+ );
168
+ }
169
+ }
170
+ if (signAnnotations && Array.isArray(bomJson.annotations)) {
171
+ for (const ann of bomJson.annotations) {
172
+ addSignature(
173
+ ann,
174
+ createSignatureBlock(ann, privateKey, algorithm, publicKeyJwk, keyId),
175
+ mode,
176
+ );
177
+ }
178
+ }
179
+ addSignature(
180
+ bomJson,
181
+ createSignatureBlock(bomJson, privateKey, algorithm, publicKeyJwk, keyId),
182
+ mode,
183
+ );
184
+ return bomJson;
185
+ }
186
+
187
+ /**
188
+ * Validates a single JSF signature block against the payload.
189
+ *
190
+ * @param {Object} payload - The payload to verify
191
+ * @param {string|crypto.KeyObject} publicKey - The public key corresponding to the signature
192
+ * @param {Object} sigBlock Signature
193
+ *
194
+ * @returns {boolean|Object} - Signature block if signature is valid. False otherwise.
195
+ */
196
+ function verifySignatureBlock(payload, publicKey, sigBlock) {
197
+ const { signature, ...dataToVerify } = payload;
198
+ const canonicalData = canonicalize(dataToVerify);
199
+
200
+ const { algorithm: alg, value } = sigBlock;
201
+
202
+ if (alg.startsWith("HS")) {
203
+ const hash = alg.replace("HS", "sha");
204
+ const expected = crypto
205
+ .createHmac(hash, publicKey)
206
+ .update(canonicalData, "utf8")
207
+ .digest("base64url");
208
+ const isValid = expected === value;
209
+ return isValid ? sigBlock : false;
210
+ }
211
+
212
+ const verifyOptions = { key: publicKey };
213
+ let hashAlg;
214
+
215
+ if (alg.startsWith("RS")) {
216
+ hashAlg = alg.replace("RS", "SHA");
217
+ } else if (alg.startsWith("PS")) {
218
+ hashAlg = alg.replace("PS", "SHA");
219
+ verifyOptions.padding = crypto.constants.RSA_PKCS1_PSS_PADDING;
220
+ verifyOptions.saltLength = crypto.constants.RSA_PSS_SALTLEN_AUTO;
221
+ } else if (alg.startsWith("ES")) {
222
+ hashAlg = alg.replace("ES", "SHA");
223
+ verifyOptions.dsaEncoding = "ieee-p1363";
224
+ } else if (alg === "Ed25519" || alg === "Ed448") {
225
+ hashAlg = null;
226
+ } else {
227
+ console.warn(`${alg} is unknown.`);
228
+ return false;
229
+ }
230
+
231
+ const isValid = crypto.verify(
232
+ hashAlg,
233
+ Buffer.from(canonicalData, "utf8"),
234
+ verifyOptions,
235
+ Buffer.from(value, "base64url"),
236
+ );
237
+ return isValid ? sigBlock : false;
238
+ }
239
+
240
+ /**
241
+ * Verifies the integrity of a specific element node (e.g., BOM root, Component, Service, Annotation).
242
+ * Resolves standard JSF signatures, multisignature (signers), and chains.
243
+ *
244
+ * @param {Object} node - The BOM or granular object to verify
245
+ * @param {string|crypto.KeyObject} publicKey - The public key corresponding to the signature
246
+ * @returns {boolean|Object} - Signature block if signature is valid. False otherwise.
247
+ */
248
+ export function verifyNode(node, publicKey) {
249
+ if (!node?.signature) {
250
+ return false;
251
+ }
252
+ const sigTarget = node.signature;
253
+ if (sigTarget.signers) {
254
+ for (const sig of sigTarget.signers) {
255
+ const match = verifySignatureBlock(node, publicKey, sig);
256
+ if (match) {
257
+ return match;
258
+ }
259
+ }
260
+ return false;
261
+ }
262
+ if (sigTarget.chain) {
263
+ for (const sig of sigTarget.chain) {
264
+ const match = verifySignatureBlock(node, publicKey, sig);
265
+ if (match) {
266
+ return match;
267
+ }
268
+ }
269
+ return false;
270
+ }
271
+ return verifySignatureBlock(node, publicKey, sigTarget);
272
+ }
273
+
274
+ /**
275
+ * Verifies the integrity of a BOM's top-level signature, as well as nested components, services, and annotations.
276
+ * Returns true only if the root signature is valid AND all signed nested elements are valid.
277
+ *
278
+ * @param {Object} bom - CycloneDX BOM Object
279
+ * @param {string|crypto.KeyObject} publicKey - The public key corresponding to the signature
280
+ * @returns {boolean|Object} - Signature block if signature is valid. False otherwise.
281
+ */
282
+ export function verifyBom(bom, publicKey) {
283
+ if (!bom?.signature) {
284
+ return false;
285
+ }
286
+ const rootMatch = verifyNode(bom, publicKey);
287
+ if (!rootMatch) {
288
+ return false;
289
+ }
290
+ if (Array.isArray(bom.components)) {
291
+ for (const comp of bom.components) {
292
+ if (comp.signature && !verifyNode(comp, publicKey)) {
293
+ return false;
294
+ }
295
+ }
296
+ }
297
+ if (Array.isArray(bom.services)) {
298
+ for (const svc of bom.services) {
299
+ if (svc.signature && !verifyNode(svc, publicKey)) {
300
+ return false;
301
+ }
302
+ }
303
+ }
304
+ if (Array.isArray(bom.annotations)) {
305
+ for (const ann of bom.annotations) {
306
+ if (ann.signature && !verifyNode(ann, publicKey)) {
307
+ return false;
308
+ }
309
+ }
310
+ }
311
+ return rootMatch;
312
+ }
@@ -0,0 +1,156 @@
1
+ import assert from "node:assert";
2
+ import crypto from "node:crypto";
3
+
4
+ import { describe, it } from "poku";
5
+
6
+ import { signBom, verifyBom, verifyNode } from "./bomSigner.js";
7
+
8
+ const rsaKeys = crypto.generateKeyPairSync("rsa", {
9
+ modulusLength: 2048,
10
+ publicKeyEncoding: { type: "spki", format: "pem" },
11
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
12
+ });
13
+
14
+ const ecKeys = crypto.generateKeyPairSync("ec", {
15
+ namedCurve: "prime256v1",
16
+ publicKeyEncoding: { type: "spki", format: "pem" },
17
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
18
+ });
19
+
20
+ const generateMockBom = () => ({
21
+ bomFormat: "CycloneDX",
22
+ specVersion: "1.6",
23
+ components: [{ type: "library", name: "cdxgen", version: "1.0.0" }],
24
+ services: [{ name: "acme-service", endpoints: ["https://appthreat.com"] }],
25
+ annotations: [{ subject: "ref-1", annotator: { name: "System" } }],
26
+ });
27
+
28
+ describe("bomSigner Tests", async () => {
29
+ it("Test basic RS512 Signature & Verification", () => {
30
+ const bomRsa = generateMockBom();
31
+ const signedRsa = signBom(bomRsa, {
32
+ privateKey: rsaKeys.privateKey,
33
+ algorithm: "RS512",
34
+ });
35
+ assert.ok(signedRsa.signature, "Root BOM should be signed");
36
+ assert.strictEqual(signedRsa.signature.algorithm, "RS512");
37
+ assert.ok(
38
+ signedRsa.components[0].signature,
39
+ "Granular component should be signed",
40
+ );
41
+ assert.ok(
42
+ signedRsa.services[0].signature,
43
+ "Granular service should be signed",
44
+ );
45
+ assert.ok(
46
+ signedRsa.annotations[0].signature,
47
+ "Granular annotation should be signed",
48
+ );
49
+ assert.ok(verifyBom(signedRsa, rsaKeys.publicKey));
50
+ });
51
+
52
+ it("Test ECDSA (ES256) Signature & Verification (JWA IEEE P1363 Format Compliance)", () => {
53
+ const bomEc = generateMockBom();
54
+ const signedEc = signBom(bomEc, {
55
+ privateKey: ecKeys.privateKey,
56
+ algorithm: "ES256",
57
+ });
58
+ assert.strictEqual(signedEc.signature.algorithm, "ES256");
59
+ assert.ok(verifyBom(signedEc, ecKeys.publicKey));
60
+ const signedRsa = signBom(bomEc, {
61
+ privateKey: rsaKeys.privateKey,
62
+ algorithm: "RS512",
63
+ });
64
+ assert.strictEqual(
65
+ verifyBom(signedRsa, ecKeys.publicKey),
66
+ false,
67
+ "Verification must fail with the wrong public key",
68
+ );
69
+ });
70
+
71
+ it("Test Multi-Signature Support (`signers`)", () => {
72
+ const bomMulti = generateMockBom();
73
+
74
+ // 1st Pass: First signer signs the whole BOM including inner elements
75
+ signBom(bomMulti, {
76
+ privateKey: rsaKeys.privateKey,
77
+ algorithm: "RS512",
78
+ mode: "signers",
79
+ });
80
+
81
+ assert.ok(
82
+ bomMulti.signature.algorithm,
83
+ "Initial signature block takes root format",
84
+ );
85
+
86
+ // 2nd Pass: Second signer ONLY co-signs the root BOM.
87
+ signBom(bomMulti, {
88
+ privateKey: ecKeys.privateKey,
89
+ algorithm: "ES256",
90
+ mode: "signers",
91
+ signComponents: false,
92
+ signServices: false,
93
+ signAnnotations: false,
94
+ });
95
+
96
+ assert.ok(
97
+ Array.isArray(bomMulti.signature.signers),
98
+ "Signature should be converted to signers array",
99
+ );
100
+
101
+ assert.strictEqual(
102
+ bomMulti.signature.signers.length,
103
+ 2,
104
+ "Should contain exactly two signers",
105
+ );
106
+
107
+ // RSA key signed EVERYTHING (root + components), so full deep verifyBom passes
108
+ assert.ok(verifyBom(bomMulti, rsaKeys.publicKey));
109
+
110
+ // EC key ONLY signed the root.
111
+ assert.ok(verifyNode(bomMulti, ecKeys.publicKey));
112
+ });
113
+
114
+ it("Test Signature Chain Support (`chain`)", () => {
115
+ const bomChain = generateMockBom();
116
+
117
+ signBom(bomChain, {
118
+ privateKey: rsaKeys.privateKey,
119
+ algorithm: "RS512",
120
+ mode: "chain",
121
+ });
122
+
123
+ signBom(bomChain, {
124
+ privateKey: ecKeys.privateKey,
125
+ algorithm: "ES256",
126
+ mode: "chain",
127
+ signComponents: false,
128
+ signServices: false,
129
+ signAnnotations: false,
130
+ });
131
+
132
+ assert.ok(
133
+ Array.isArray(bomChain.signature.chain),
134
+ "Signature should be converted to chain array",
135
+ );
136
+
137
+ assert.strictEqual(bomChain.signature.chain.length, 2);
138
+
139
+ // RSA key signed everything, verifyBom works
140
+ assert.ok(verifyBom(bomChain, rsaKeys.publicKey));
141
+
142
+ // EC key only chained the root, verifyNode strictly checks the root
143
+ assert.ok(verifyNode(bomChain, ecKeys.publicKey));
144
+ });
145
+
146
+ it("Test HMAC Symmetric Signature (HS256)", () => {
147
+ const symmetricKey = crypto.randomBytes(32).toString("hex");
148
+ const bomHmac = generateMockBom();
149
+ const signedHmac = signBom(bomHmac, {
150
+ privateKey: symmetricKey,
151
+ algorithm: "HS256",
152
+ });
153
+ assert.strictEqual(signedHmac.signature.algorithm, "HS256");
154
+ assert.ok(verifyBom(signedHmac, symmetricKey));
155
+ });
156
+ });