@cyclonedx/cdxgen 12.3.3 → 12.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.
Files changed (157) hide show
  1. package/README.md +64 -22
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +238 -116
  4. package/bin/convert.js +28 -13
  5. package/bin/hbom.js +490 -0
  6. package/bin/repl.js +580 -29
  7. package/bin/validate.js +34 -4
  8. package/bin/verify.js +40 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/predictive-audit-allowlist.json +11 -0
  13. package/data/queries-darwin.json +12 -1
  14. package/data/queries-win.json +7 -1
  15. package/data/queries.json +39 -2
  16. package/data/rules/ai-agent-governance.yaml +16 -0
  17. package/data/rules/asar-archives.yaml +150 -0
  18. package/data/rules/chrome-extensions.yaml +8 -0
  19. package/data/rules/ci-permissions.yaml +42 -18
  20. package/data/rules/container-risk.yaml +14 -7
  21. package/data/rules/dependency-sources.yaml +11 -0
  22. package/data/rules/hbom-compliance.yaml +325 -0
  23. package/data/rules/hbom-performance.yaml +307 -0
  24. package/data/rules/hbom-security.yaml +248 -0
  25. package/data/rules/host-topology.yaml +165 -0
  26. package/data/rules/mcp-servers.yaml +18 -3
  27. package/data/rules/obom-runtime.yaml +907 -22
  28. package/data/rules/package-integrity.yaml +14 -0
  29. package/data/rules/rootfs-hardening.yaml +179 -0
  30. package/data/rules/vscode-extensions.yaml +9 -0
  31. package/lib/audit/index.js +209 -8
  32. package/lib/audit/index.poku.js +332 -0
  33. package/lib/audit/reporters.js +222 -0
  34. package/lib/audit/targets.js +146 -1
  35. package/lib/audit/targets.poku.js +186 -0
  36. package/lib/cli/asar.poku.js +328 -0
  37. package/lib/cli/index.js +506 -88
  38. package/lib/cli/index.poku.js +1352 -212
  39. package/lib/evinser/evinser.js +14 -9
  40. package/lib/helpers/analyzer.js +1406 -29
  41. package/lib/helpers/analyzer.poku.js +342 -0
  42. package/lib/helpers/analyzerScope.js +712 -0
  43. package/lib/helpers/asarutils.js +1556 -0
  44. package/lib/helpers/asarutils.poku.js +443 -0
  45. package/lib/helpers/auditCategories.js +12 -0
  46. package/lib/helpers/auditCategories.poku.js +32 -0
  47. package/lib/helpers/cbomutils.js +271 -1
  48. package/lib/helpers/cbomutils.poku.js +248 -5
  49. package/lib/helpers/display.js +291 -1
  50. package/lib/helpers/display.poku.js +149 -0
  51. package/lib/helpers/evidenceUtils.js +58 -0
  52. package/lib/helpers/evidenceUtils.poku.js +54 -0
  53. package/lib/helpers/exportUtils.js +9 -0
  54. package/lib/helpers/gtfobins.js +142 -8
  55. package/lib/helpers/gtfobins.poku.js +24 -1
  56. package/lib/helpers/hbom.js +710 -0
  57. package/lib/helpers/hbom.poku.js +496 -0
  58. package/lib/helpers/hbomAnalysis.js +268 -0
  59. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  60. package/lib/helpers/hbomLoader.js +35 -0
  61. package/lib/helpers/hostTopology.js +803 -0
  62. package/lib/helpers/hostTopology.poku.js +363 -0
  63. package/lib/helpers/inventoryStats.js +69 -0
  64. package/lib/helpers/inventoryStats.poku.js +86 -0
  65. package/lib/helpers/lolbas.js +19 -1
  66. package/lib/helpers/lolbas.poku.js +23 -0
  67. package/lib/helpers/osqueryTransform.js +47 -0
  68. package/lib/helpers/osqueryTransform.poku.js +47 -0
  69. package/lib/helpers/plugins.js +349 -0
  70. package/lib/helpers/plugins.poku.js +57 -0
  71. package/lib/helpers/protobom.js +156 -45
  72. package/lib/helpers/protobom.poku.js +140 -5
  73. package/lib/helpers/remote/dependency-track.js +36 -3
  74. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  75. package/lib/helpers/source.js +24 -0
  76. package/lib/helpers/source.poku.js +32 -0
  77. package/lib/helpers/utils.js +1438 -93
  78. package/lib/helpers/utils.poku.js +846 -4
  79. package/lib/managers/binary.e2e.poku.js +367 -0
  80. package/lib/managers/binary.js +2293 -353
  81. package/lib/managers/binary.poku.js +1699 -1
  82. package/lib/managers/docker.js +201 -79
  83. package/lib/managers/docker.poku.js +337 -12
  84. package/lib/server/server.js +2 -27
  85. package/lib/stages/postgen/annotator.js +38 -0
  86. package/lib/stages/postgen/annotator.poku.js +107 -1
  87. package/lib/stages/postgen/auditBom.js +121 -18
  88. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  89. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  90. package/lib/stages/postgen/postgen.js +192 -1
  91. package/lib/stages/postgen/postgen.poku.js +321 -0
  92. package/lib/stages/postgen/ruleEngine.js +116 -0
  93. package/lib/stages/pregen/envAudit.js +14 -3
  94. package/package.json +23 -21
  95. package/types/bin/hbom.d.ts +3 -0
  96. package/types/bin/hbom.d.ts.map +1 -0
  97. package/types/bin/repl.d.ts.map +1 -1
  98. package/types/lib/audit/index.d.ts +44 -0
  99. package/types/lib/audit/index.d.ts.map +1 -1
  100. package/types/lib/audit/reporters.d.ts +16 -0
  101. package/types/lib/audit/reporters.d.ts.map +1 -1
  102. package/types/lib/audit/targets.d.ts.map +1 -1
  103. package/types/lib/cli/index.d.ts +16 -0
  104. package/types/lib/cli/index.d.ts.map +1 -1
  105. package/types/lib/evinser/evinser.d.ts +4 -0
  106. package/types/lib/evinser/evinser.d.ts.map +1 -1
  107. package/types/lib/helpers/analyzer.d.ts +33 -0
  108. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  109. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  110. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  111. package/types/lib/helpers/asarutils.d.ts +34 -0
  112. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  113. package/types/lib/helpers/auditCategories.d.ts +5 -0
  114. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  115. package/types/lib/helpers/cbomutils.d.ts +3 -2
  116. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  117. package/types/lib/helpers/display.d.ts.map +1 -1
  118. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  119. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  120. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  121. package/types/lib/helpers/gtfobins.d.ts +8 -0
  122. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  123. package/types/lib/helpers/hbom.d.ts +49 -0
  124. package/types/lib/helpers/hbom.d.ts.map +1 -0
  125. package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
  126. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  127. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  128. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  129. package/types/lib/helpers/hostTopology.d.ts +12 -0
  130. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  131. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  132. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  133. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  134. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  135. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  136. package/types/lib/helpers/plugins.d.ts +58 -0
  137. package/types/lib/helpers/plugins.d.ts.map +1 -0
  138. package/types/lib/helpers/protobom.d.ts +3 -4
  139. package/types/lib/helpers/protobom.d.ts.map +1 -1
  140. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  141. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  142. package/types/lib/helpers/source.d.ts.map +1 -1
  143. package/types/lib/helpers/utils.d.ts +45 -8
  144. package/types/lib/helpers/utils.d.ts.map +1 -1
  145. package/types/lib/managers/binary.d.ts +5 -0
  146. package/types/lib/managers/binary.d.ts.map +1 -1
  147. package/types/lib/managers/docker.d.ts.map +1 -1
  148. package/types/lib/server/server.d.ts +2 -1
  149. package/types/lib/server/server.d.ts.map +1 -1
  150. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  151. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  152. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  153. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  154. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  155. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  156. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  157. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -2,6 +2,11 @@ import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { executeOsQuery } from "../managers/binary.js";
5
+ import { detectJsCryptoInventory } from "./analyzer.js";
6
+ import {
7
+ createOccurrenceEvidence,
8
+ formatOccurrenceEvidence,
9
+ } from "./evidenceUtils.js";
5
10
  import { convertOSQueryResults, dirNameStr } from "./utils.js";
6
11
 
7
12
  const cbomosDbQueries = JSON.parse(
@@ -47,10 +52,275 @@ function cleanStr(str) {
47
52
  return str.toLowerCase().replace(/[^0-9a-z ]/gi, "");
48
53
  }
49
54
 
55
+ function normalizeDetectedCryptoAlgorithmName(name, primitive, keyLength) {
56
+ const trimmedName = String(name || "").trim();
57
+ if (!trimmedName) {
58
+ return undefined;
59
+ }
60
+ const upperName = trimmedName.toUpperCase();
61
+ const compactName = upperName.replace(/[^A-Z0-9]/g, "");
62
+ const lowerName = trimmedName.toLowerCase();
63
+ const normalizedLower = lowerName.replace(/[ _]/g, "-");
64
+ const jwtMappings = {
65
+ ES256: "ecdsaWithSHA256",
66
+ ES384: "ecdsaWithSHA384",
67
+ ES512: "ecdsaWithSHA512",
68
+ HS256: "hmacWithSHA256",
69
+ HS384: "hmacWithSHA384",
70
+ HS512: "hmacWithSHA512",
71
+ RS256: "sha256WithRSAEncryption",
72
+ RS384: "sha384WithRSAEncryption",
73
+ RS512: "sha512WithRSAEncryption",
74
+ };
75
+ if (jwtMappings[upperName]) {
76
+ return jwtMappings[upperName];
77
+ }
78
+ if (/^A(128|192|256)GCM$/.test(compactName)) {
79
+ return `aes${compactName.slice(1, 4)}-GCM`;
80
+ }
81
+ if (/^SHA(1|224|256|384|512)$/.test(compactName)) {
82
+ return `sha-${compactName.slice(3)}`;
83
+ }
84
+ if (/^SHA3(224|256|384|512)$/.test(compactName)) {
85
+ return `sha3-${compactName.slice(4)}`;
86
+ }
87
+ if (compactName === "PBKDF2") {
88
+ return "PBKDF2";
89
+ }
90
+ if (compactName === "SCRYPT") {
91
+ return "scrypt";
92
+ }
93
+ if (
94
+ normalizedLower === "aes-gcm" &&
95
+ typeof keyLength === "number" &&
96
+ [128, 192, 256].includes(keyLength)
97
+ ) {
98
+ return `aes${keyLength}-GCM`;
99
+ }
100
+ if (
101
+ normalizedLower === "aes-cbc" &&
102
+ typeof keyLength === "number" &&
103
+ [128, 192, 256].includes(keyLength)
104
+ ) {
105
+ return `aes${keyLength}-CBC`;
106
+ }
107
+ if (
108
+ normalizedLower === "aes-ctr" &&
109
+ typeof keyLength === "number" &&
110
+ [128, 192, 256].includes(keyLength)
111
+ ) {
112
+ return `aes${keyLength}-CTR`;
113
+ }
114
+ if (cbomCryptoOids[trimmedName]) {
115
+ return trimmedName;
116
+ }
117
+ if (cbomCryptoOids[normalizedLower]) {
118
+ return normalizedLower;
119
+ }
120
+ if (primitive === "algorithm" && cbomCryptoOids[upperName]) {
121
+ return upperName;
122
+ }
123
+ return trimmedName;
124
+ }
125
+
126
+ function cryptoAlgorithmBomRef(name, oid) {
127
+ const version = oid || cleanStr(name) || "unknown";
128
+ return `crypto/algorithm/${encodeURIComponent(name)}@${version}`;
129
+ }
130
+
131
+ function usageLocationValue(usage) {
132
+ if (typeof usage?.lineNumber !== "number") {
133
+ return undefined;
134
+ }
135
+ return `${usage.fileName || "<inline>"}:${usage.lineNumber}${typeof usage.columnNumber === "number" ? `:${usage.columnNumber}` : ""}`;
136
+ }
137
+
138
+ function mergeAlgorithmComponentEvidence(component, usage, options) {
139
+ if (!options?.evidence) {
140
+ return component;
141
+ }
142
+ const locationValue = usageLocationValue(usage);
143
+ const occurrence = createOccurrenceEvidence(usage.fileName || "<inline>", {
144
+ additionalContext: usage.primitive,
145
+ ...(typeof usage.lineNumber === "number" ? { line: usage.lineNumber } : {}),
146
+ ...(usage.source ? { symbol: usage.source } : {}),
147
+ });
148
+ const evidence = component.evidence || {};
149
+ const identity = Array.isArray(evidence.identity)
150
+ ? (evidence.identity[0] ?? {
151
+ field: "name",
152
+ confidence: 1,
153
+ concludedValue: component.name,
154
+ methods: [],
155
+ })
156
+ : (evidence.identity ?? {
157
+ field: "name",
158
+ confidence: 1,
159
+ concludedValue: component.name,
160
+ methods: [],
161
+ });
162
+ const methodValue = occurrence
163
+ ? formatOccurrenceEvidence(occurrence)
164
+ : locationValue || component.name;
165
+ if (
166
+ !identity.methods?.some(
167
+ (method) =>
168
+ method.technique === "source-code-analysis" &&
169
+ method.value === methodValue,
170
+ )
171
+ ) {
172
+ identity.methods = identity.methods || [];
173
+ identity.methods.push({
174
+ technique: "source-code-analysis",
175
+ confidence: 1,
176
+ value: methodValue,
177
+ });
178
+ }
179
+ evidence.identity = identity;
180
+ if (occurrence) {
181
+ const occurrences = evidence.occurrences || [];
182
+ if (
183
+ !occurrences.some(
184
+ (existingOccurrence) =>
185
+ formatOccurrenceEvidence(existingOccurrence) ===
186
+ formatOccurrenceEvidence(occurrence),
187
+ )
188
+ ) {
189
+ occurrences.push(occurrence);
190
+ }
191
+ evidence.occurrences = occurrences;
192
+ }
193
+ component.evidence = evidence;
194
+ return component;
195
+ }
196
+
197
+ function normalizeCryptoComponentEvidence(component, options) {
198
+ if (!component?.evidence?.identity) {
199
+ return component;
200
+ }
201
+ if (
202
+ options?.specVersion >= 1.6 &&
203
+ !Array.isArray(component.evidence.identity)
204
+ ) {
205
+ component.evidence.identity = [component.evidence.identity];
206
+ }
207
+ if (
208
+ options?.specVersion === 1.5 &&
209
+ Array.isArray(component.evidence.identity)
210
+ ) {
211
+ component.evidence.identity = component.evidence.identity[0];
212
+ }
213
+ return component;
214
+ }
215
+
216
+ function mergeAlgorithmComponentUsage(component, usage, src, options) {
217
+ const sourceFile = usage.fileName ? join(src, usage.fileName) : undefined;
218
+ const properties = component.properties || [];
219
+ const locationValue = usageLocationValue(usage);
220
+ if (sourceFile) {
221
+ if (
222
+ !properties.some(
223
+ (property) =>
224
+ property.name === "SrcFile" && property.value === sourceFile,
225
+ )
226
+ ) {
227
+ properties.push({ name: "SrcFile", value: sourceFile });
228
+ }
229
+ }
230
+ if (usage.primitive) {
231
+ if (
232
+ !properties.some(
233
+ (property) =>
234
+ property.name === "cdx:crypto:primitive" &&
235
+ property.value === usage.primitive,
236
+ )
237
+ ) {
238
+ properties.push({ name: "cdx:crypto:primitive", value: usage.primitive });
239
+ }
240
+ }
241
+ if (usage.source) {
242
+ if (
243
+ !properties.some(
244
+ (property) =>
245
+ property.name === "cdx:crypto:sourceType" &&
246
+ property.value === `js-ast:${usage.source}`,
247
+ )
248
+ ) {
249
+ properties.push({
250
+ name: "cdx:crypto:sourceType",
251
+ value: `js-ast:${usage.source}`,
252
+ });
253
+ }
254
+ }
255
+ if (locationValue) {
256
+ if (
257
+ !properties.some(
258
+ (property) =>
259
+ property.name === "cdx:crypto:sourceLocation" &&
260
+ property.value === locationValue,
261
+ )
262
+ ) {
263
+ properties.push({
264
+ name: "cdx:crypto:sourceLocation",
265
+ value: locationValue,
266
+ });
267
+ }
268
+ }
269
+ component.properties = properties;
270
+ mergeAlgorithmComponentEvidence(component, usage, options);
271
+ return component;
272
+ }
273
+
274
+ export async function collectSourceCryptoComponents(src, options = {}) {
275
+ const inventory = await detectJsCryptoInventory(src, Boolean(options.deep));
276
+ const componentsByRef = new Map();
277
+ for (const usage of inventory.algorithms || []) {
278
+ const normalizedName = normalizeDetectedCryptoAlgorithmName(
279
+ usage.name,
280
+ usage.primitive,
281
+ usage.keyLength,
282
+ );
283
+ if (!normalizedName) {
284
+ continue;
285
+ }
286
+ const algorithmMetadata =
287
+ cbomCryptoOids[normalizedName] || cbomCryptoOids[usage.name];
288
+ if (!algorithmMetadata?.oid) {
289
+ continue;
290
+ }
291
+ const componentName = algorithmMetadata ? normalizedName : usage.name;
292
+ const bomRef = cryptoAlgorithmBomRef(componentName, algorithmMetadata?.oid);
293
+ const component = componentsByRef.get(bomRef) || {
294
+ type: "cryptographic-asset",
295
+ name: componentName,
296
+ "bom-ref": bomRef,
297
+ description:
298
+ algorithmMetadata?.description ||
299
+ `${usage.primitive || "cryptographic"} algorithm detected in source analysis`,
300
+ cryptoProperties: {
301
+ assetType: "algorithm",
302
+ ...(algorithmMetadata?.oid ? { oid: algorithmMetadata.oid } : {}),
303
+ },
304
+ properties: [],
305
+ };
306
+ mergeAlgorithmComponentUsage(component, usage, src, options);
307
+ componentsByRef.set(bomRef, component);
308
+ }
309
+ const components = Array.from(componentsByRef.values());
310
+ components.forEach((component) => {
311
+ normalizeCryptoComponentEvidence(component, options);
312
+ });
313
+ return components.sort((left, right) =>
314
+ `${left.name}:${left["bom-ref"]}`.localeCompare(
315
+ `${right.name}:${right["bom-ref"]}`,
316
+ ),
317
+ );
318
+ }
319
+
50
320
  /**
51
321
  * Find crypto algorithm in the given code snippet
52
322
  *
53
- * @param {String} Code snippet
323
+ * @param {string} code Code snippet
54
324
  * @returns {Array} Arary of crypto algorithm objects with oid and description
55
325
  */
56
326
  export function findCryptoAlgos(code) {
@@ -1,8 +1,251 @@
1
- import { assert, it } from "poku";
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
2
4
 
3
- import { collectOSCryptoLibs } from "./cbomutils.js";
5
+ import { assert, describe, it } from "poku";
4
6
 
5
- it("cbom utils tests", () => {
6
- const cryptoLibs = collectOSCryptoLibs();
7
- assert.ok(cryptoLibs);
7
+ import {
8
+ collectOSCryptoLibs,
9
+ collectSourceCryptoComponents,
10
+ } from "./cbomutils.js";
11
+
12
+ describe("cbom utils", () => {
13
+ it("collectOSCryptoLibs() returns a result set", () => {
14
+ const cryptoLibs = collectOSCryptoLibs();
15
+ assert.ok(cryptoLibs);
16
+ });
17
+
18
+ it("collectSourceCryptoComponents() extracts algorithms from JS source", async () => {
19
+ const projectDir = mkdtempSync(join(tmpdir(), "cdxgen-cbom-source-"));
20
+ try {
21
+ writeFileSync(
22
+ join(projectDir, "index.js"),
23
+ [
24
+ "import { createHash, webcrypto } from 'node:crypto';",
25
+ "import jwt from 'jsonwebtoken';",
26
+ "const subtle = webcrypto.subtle;",
27
+ "const digest = 'sha256';",
28
+ "const signingAlgorithm = 'Ed25519';",
29
+ "const profile = { name: 'AES-GCM', length: 256 };",
30
+ "createHash(digest);",
31
+ "subtle.generateKey(profile, true, ['encrypt']);",
32
+ "jwt.sign({ sub: '123' }, 'secret', { algorithm: 'RS256' });",
33
+ ].join("\n"),
34
+ "utf-8",
35
+ );
36
+ const components = await collectSourceCryptoComponents(projectDir, {
37
+ deep: false,
38
+ evidence: true,
39
+ specVersion: 1.7,
40
+ });
41
+ const names = components.map((component) => component.name);
42
+ const sha256Component = components.find(
43
+ (component) => component.name === "sha-256",
44
+ );
45
+ assert.ok(names.includes("sha-256"));
46
+ assert.ok(names.includes("aes256-GCM"));
47
+ assert.ok(names.includes("Ed25519"));
48
+ assert.ok(names.includes("sha256WithRSAEncryption"));
49
+ assert.ok(!names.includes("hmac"));
50
+ assert.ok(sha256Component);
51
+ assert.ok(Array.isArray(sha256Component.evidence.identity));
52
+ assert.strictEqual(sha256Component.evidence.identity[0].field, "name");
53
+ assert.strictEqual(
54
+ sha256Component.evidence.identity[0].concludedValue,
55
+ "sha-256",
56
+ );
57
+ assert.ok(
58
+ sha256Component.evidence.identity[0].methods.some(
59
+ (method) => method.technique === "source-code-analysis",
60
+ ),
61
+ );
62
+ assert.ok(
63
+ sha256Component.evidence.occurrences.some(
64
+ (occurrence) =>
65
+ occurrence.location === "index.js" && occurrence.line === 7,
66
+ ),
67
+ );
68
+ const sha256Occurrence = sha256Component.evidence.occurrences.find(
69
+ (occurrence) =>
70
+ occurrence.location === "index.js" && occurrence.line === 7,
71
+ );
72
+ assert.ok(sha256Occurrence);
73
+ assert.strictEqual(sha256Occurrence.additionalContext, "hash");
74
+ assert.strictEqual(sha256Occurrence.symbol, "node:crypto.createHash");
75
+ assert.ok(!Object.hasOwn(sha256Occurrence, "offset"));
76
+ assert.ok(
77
+ components.every(
78
+ (component) => component.cryptoProperties?.oid?.length,
79
+ ),
80
+ );
81
+ assert.ok(
82
+ components.some((component) =>
83
+ component.properties.some(
84
+ (property) =>
85
+ property.name === "cdx:crypto:sourceType" &&
86
+ property.value.startsWith("js-ast:"),
87
+ ),
88
+ ),
89
+ );
90
+ } finally {
91
+ rmSync(projectDir, { recursive: true, force: true });
92
+ }
93
+ });
94
+
95
+ it("collectSourceCryptoComponents() keeps branch-derived evidence for dynamic crypto values", async () => {
96
+ const projectDir = mkdtempSync(join(tmpdir(), "cdxgen-cbom-branches-"));
97
+ try {
98
+ writeFileSync(
99
+ join(projectDir, "dynamic-branches.mjs"),
100
+ [
101
+ "import crypto, { createHash, webcrypto } from 'node:crypto';",
102
+ "import jwt from 'jsonwebtoken';",
103
+ "const subtle = webcrypto.subtle;",
104
+ "const digestName = process.env.CDXGEN_TEST_DIGEST || 'sha384';",
105
+ "const keyProfiles = globalThis.__legacyCipher",
106
+ " ? { active: { name: 'AES-CBC', length: 256 } }",
107
+ " : { active: { name: 'AES-GCM', length: 256 } };",
108
+ "const signingAlgorithm = globalThis.__legacySignature ? 'RS256' : 'RS512';",
109
+ "const jwtOptions = globalThis.__jwtOptions ?? { algorithm: signingAlgorithm };",
110
+ "createHash(digestName);",
111
+ "await subtle.generateKey(keyProfiles.active, true, ['encrypt', 'decrypt']);",
112
+ "jwt.sign({ sub: '123' }, 'secret', jwtOptions);",
113
+ "export function signPayload(payload, privateKey, alg) {",
114
+ " let hashAlg = null;",
115
+ " if (alg === 'RS256' || alg === 'RS512') {",
116
+ " hashAlg = alg.replace('RS', 'SHA');",
117
+ " return crypto.sign(hashAlg, Buffer.from(payload, 'utf8'), { key: privateKey });",
118
+ " }",
119
+ " if (alg !== 'RS384') {",
120
+ " return crypto.sign('SHA-224', Buffer.from(payload, 'utf8'), { key: privateKey });",
121
+ " } else {",
122
+ " hashAlg = alg.replace('RS', 'SHA');",
123
+ " return crypto.sign(hashAlg, Buffer.from(payload, 'utf8'), { key: privateKey });",
124
+ " }",
125
+ "}",
126
+ "export function signPayloadWithSwitch(payload, privateKey, alg) {",
127
+ " switch (alg) {",
128
+ " case 'RS256':",
129
+ " case 'RS512':",
130
+ " return crypto.sign(alg.replace('RS', 'SHA'), Buffer.from(payload, 'utf8'), { key: privateKey });",
131
+ " case 'RS384':",
132
+ " return crypto.sign(alg.replace('RS', 'SHA'), Buffer.from(payload, 'utf8'), { key: privateKey });",
133
+ " default:",
134
+ " return crypto.sign('SHA-224', Buffer.from(payload, 'utf8'), { key: privateKey });",
135
+ " }",
136
+ "}",
137
+ "export function signPayloadWithSwitchDefault(payload, privateKey) {",
138
+ " const alg = globalThis.__preferLegacy ? 'RS256' : 'RS384';",
139
+ " switch (alg) {",
140
+ " case 'RS256':",
141
+ " return crypto.sign(alg.replace('RS', 'SHA'), Buffer.from(payload, 'utf8'), { key: privateKey });",
142
+ " default:",
143
+ " return crypto.sign(alg.replace('RS', 'SHA'), Buffer.from(payload, 'utf8'), { key: privateKey });",
144
+ " }",
145
+ "}",
146
+ ].join("\n"),
147
+ "utf-8",
148
+ );
149
+ const components = await collectSourceCryptoComponents(projectDir, {
150
+ deep: false,
151
+ evidence: true,
152
+ specVersion: 1.7,
153
+ });
154
+ const names = components.map((component) => component.name);
155
+ const sha384Component = components.find(
156
+ (component) => component.name === "sha-384",
157
+ );
158
+
159
+ assert.ok(names.includes("sha-384"));
160
+ assert.ok(names.includes("sha-224"));
161
+ assert.ok(names.includes("sha-256"));
162
+ assert.ok(names.includes("sha-512"));
163
+ assert.ok(names.includes("aes256-CBC"));
164
+ assert.ok(names.includes("aes256-GCM"));
165
+ assert.ok(names.includes("sha256WithRSAEncryption"));
166
+ assert.ok(names.includes("sha512WithRSAEncryption"));
167
+ assert.ok(sha384Component);
168
+ assert.ok(
169
+ sha384Component.evidence.occurrences.some(
170
+ (occurrence) =>
171
+ occurrence.location === "dynamic-branches.mjs" &&
172
+ occurrence.line === 10 &&
173
+ occurrence.symbol === "node:crypto.createHash" &&
174
+ occurrence.additionalContext === "hash",
175
+ ),
176
+ );
177
+ assert.ok(
178
+ sha384Component.properties.some(
179
+ (property) =>
180
+ property.name === "cdx:crypto:sourceLocation" &&
181
+ property.value === "dynamic-branches.mjs:10:0",
182
+ ),
183
+ );
184
+ assert.ok(
185
+ sha384Component.properties.some(
186
+ (property) =>
187
+ property.name === "cdx:crypto:sourceType" &&
188
+ property.value === "js-ast:node:crypto.sign",
189
+ ),
190
+ );
191
+ assert.ok(
192
+ sha384Component.evidence.occurrences.some(
193
+ (occurrence) =>
194
+ occurrence.location === "dynamic-branches.mjs" &&
195
+ occurrence.symbol === "node:crypto.sign" &&
196
+ occurrence.additionalContext === "signature",
197
+ ),
198
+ );
199
+ assert.ok(
200
+ components.some(
201
+ (component) =>
202
+ component.name === "sha-256" &&
203
+ component.properties.some(
204
+ (property) =>
205
+ property.name === "cdx:crypto:sourceType" &&
206
+ property.value === "js-ast:node:crypto.sign",
207
+ ) &&
208
+ component.evidence.occurrences.some(
209
+ (occurrence) =>
210
+ occurrence.location === "dynamic-branches.mjs" &&
211
+ occurrence.symbol === "node:crypto.sign" &&
212
+ occurrence.additionalContext === "signature",
213
+ ),
214
+ ),
215
+ );
216
+ assert.ok(
217
+ components.some(
218
+ (component) =>
219
+ component.name === "sha-512" &&
220
+ component.properties.some(
221
+ (property) =>
222
+ property.name === "cdx:crypto:sourceType" &&
223
+ property.value === "js-ast:node:crypto.sign",
224
+ ) &&
225
+ component.evidence.occurrences.some(
226
+ (occurrence) =>
227
+ occurrence.location === "dynamic-branches.mjs" &&
228
+ occurrence.line === 30 &&
229
+ occurrence.symbol === "node:crypto.sign" &&
230
+ occurrence.additionalContext === "signature",
231
+ ),
232
+ ),
233
+ );
234
+ assert.ok(
235
+ components.some(
236
+ (component) =>
237
+ component.name === "sha-384" &&
238
+ component.evidence.occurrences.some(
239
+ (occurrence) =>
240
+ occurrence.location === "dynamic-branches.mjs" &&
241
+ occurrence.symbol === "node:crypto.sign" &&
242
+ occurrence.additionalContext === "signature" &&
243
+ occurrence.line > 30,
244
+ ),
245
+ ),
246
+ );
247
+ } finally {
248
+ rmSync(projectDir, { recursive: true, force: true });
249
+ }
250
+ });
8
251
  });