@cyclonedx/cdxgen 12.3.2 → 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 (182) hide show
  1. package/README.md +70 -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 +171 -15
  20. package/data/rules/container-risk.yaml +14 -7
  21. package/data/rules/dependency-sources.yaml +76 -5
  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 +36 -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 +647 -127
  38. package/lib/cli/index.poku.js +1905 -187
  39. package/lib/evinser/evinser.js +14 -9
  40. package/lib/helpers/agentFormulationParser.js +6 -2
  41. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  42. package/lib/helpers/analyzer.js +1444 -38
  43. package/lib/helpers/analyzer.poku.js +409 -0
  44. package/lib/helpers/analyzerScope.js +712 -0
  45. package/lib/helpers/asarutils.js +1556 -0
  46. package/lib/helpers/asarutils.poku.js +443 -0
  47. package/lib/helpers/auditCategories.js +12 -0
  48. package/lib/helpers/auditCategories.poku.js +32 -0
  49. package/lib/helpers/cbomutils.js +271 -1
  50. package/lib/helpers/cbomutils.poku.js +248 -5
  51. package/lib/helpers/chromextutils.js +25 -3
  52. package/lib/helpers/chromextutils.poku.js +68 -0
  53. package/lib/helpers/ciParsers/githubActions.js +79 -0
  54. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  55. package/lib/helpers/communityAiConfigParser.js +15 -5
  56. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  57. package/lib/helpers/depsUtils.js +5 -0
  58. package/lib/helpers/depsUtils.poku.js +55 -0
  59. package/lib/helpers/display.js +336 -23
  60. package/lib/helpers/display.poku.js +179 -43
  61. package/lib/helpers/evidenceUtils.js +58 -0
  62. package/lib/helpers/evidenceUtils.poku.js +54 -0
  63. package/lib/helpers/exportUtils.js +9 -0
  64. package/lib/helpers/gtfobins.js +142 -8
  65. package/lib/helpers/gtfobins.poku.js +24 -1
  66. package/lib/helpers/hbom.js +710 -0
  67. package/lib/helpers/hbom.poku.js +496 -0
  68. package/lib/helpers/hbomAnalysis.js +268 -0
  69. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  70. package/lib/helpers/hbomLoader.js +35 -0
  71. package/lib/helpers/hostTopology.js +803 -0
  72. package/lib/helpers/hostTopology.poku.js +363 -0
  73. package/lib/helpers/inventoryStats.js +69 -0
  74. package/lib/helpers/inventoryStats.poku.js +86 -0
  75. package/lib/helpers/lolbas.js +19 -1
  76. package/lib/helpers/lolbas.poku.js +23 -0
  77. package/lib/helpers/mcpConfigParser.js +21 -5
  78. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  79. package/lib/helpers/osqueryTransform.js +47 -0
  80. package/lib/helpers/osqueryTransform.poku.js +47 -0
  81. package/lib/helpers/plugins.js +349 -0
  82. package/lib/helpers/plugins.poku.js +57 -0
  83. package/lib/helpers/propertySanitizer.js +121 -0
  84. package/lib/helpers/protobom.js +156 -45
  85. package/lib/helpers/protobom.poku.js +140 -5
  86. package/lib/helpers/remote/dependency-track.js +36 -3
  87. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  88. package/lib/helpers/source.js +24 -0
  89. package/lib/helpers/source.poku.js +32 -0
  90. package/lib/helpers/utils.js +2454 -198
  91. package/lib/helpers/utils.poku.js +1798 -74
  92. package/lib/managers/binary.e2e.poku.js +367 -0
  93. package/lib/managers/binary.js +2306 -350
  94. package/lib/managers/binary.poku.js +1700 -1
  95. package/lib/managers/docker.js +441 -95
  96. package/lib/managers/docker.poku.js +1479 -14
  97. package/lib/server/server.js +2 -24
  98. package/lib/server/server.poku.js +36 -1
  99. package/lib/stages/postgen/annotator.js +38 -0
  100. package/lib/stages/postgen/annotator.poku.js +107 -1
  101. package/lib/stages/postgen/auditBom.js +121 -18
  102. package/lib/stages/postgen/auditBom.poku.js +2967 -990
  103. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  104. package/lib/stages/postgen/postgen.js +192 -1
  105. package/lib/stages/postgen/postgen.poku.js +321 -0
  106. package/lib/stages/postgen/ruleEngine.js +116 -0
  107. package/lib/stages/pregen/envAudit.js +14 -3
  108. package/package.json +24 -21
  109. package/types/bin/hbom.d.ts +3 -0
  110. package/types/bin/hbom.d.ts.map +1 -0
  111. package/types/bin/repl.d.ts.map +1 -1
  112. package/types/lib/audit/index.d.ts +44 -0
  113. package/types/lib/audit/index.d.ts.map +1 -1
  114. package/types/lib/audit/reporters.d.ts +16 -0
  115. package/types/lib/audit/reporters.d.ts.map +1 -1
  116. package/types/lib/audit/targets.d.ts.map +1 -1
  117. package/types/lib/cli/index.d.ts +16 -0
  118. package/types/lib/cli/index.d.ts.map +1 -1
  119. package/types/lib/evinser/evinser.d.ts +4 -0
  120. package/types/lib/evinser/evinser.d.ts.map +1 -1
  121. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  122. package/types/lib/helpers/analyzer.d.ts +33 -0
  123. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  124. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  125. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  126. package/types/lib/helpers/asarutils.d.ts +34 -0
  127. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  128. package/types/lib/helpers/auditCategories.d.ts +5 -0
  129. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  130. package/types/lib/helpers/cbomutils.d.ts +3 -2
  131. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  132. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  133. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  134. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  135. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  136. package/types/lib/helpers/display.d.ts +1 -0
  137. package/types/lib/helpers/display.d.ts.map +1 -1
  138. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  139. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  141. package/types/lib/helpers/gtfobins.d.ts +8 -0
  142. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  143. package/types/lib/helpers/hbom.d.ts +49 -0
  144. package/types/lib/helpers/hbom.d.ts.map +1 -0
  145. package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
  146. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  147. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  148. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  149. package/types/lib/helpers/hostTopology.d.ts +12 -0
  150. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  151. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  152. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  153. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  154. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  155. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  156. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  157. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  158. package/types/lib/helpers/plugins.d.ts +58 -0
  159. package/types/lib/helpers/plugins.d.ts.map +1 -0
  160. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  161. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  162. package/types/lib/helpers/protobom.d.ts +3 -4
  163. package/types/lib/helpers/protobom.d.ts.map +1 -1
  164. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  165. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  166. package/types/lib/helpers/source.d.ts.map +1 -1
  167. package/types/lib/helpers/utils.d.ts +74 -8
  168. package/types/lib/helpers/utils.d.ts.map +1 -1
  169. package/types/lib/managers/binary.d.ts +5 -0
  170. package/types/lib/managers/binary.d.ts.map +1 -1
  171. package/types/lib/managers/docker.d.ts +3 -0
  172. package/types/lib/managers/docker.d.ts.map +1 -1
  173. package/types/lib/server/server.d.ts +2 -0
  174. package/types/lib/server/server.d.ts.map +1 -1
  175. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  176. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  177. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  178. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  179. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  180. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  182. 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
  });
@@ -9,6 +9,7 @@ import {
9
9
  CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES,
10
10
  detectExtensionCapabilities,
11
11
  } from "./analyzer.js";
12
+ import { sanitizeBomPropertyValue } from "./propertySanitizer.js";
12
13
  import { isMac, isWin, safeExistsSync } from "./utils.js";
13
14
 
14
15
  /**
@@ -850,7 +851,12 @@ export function toComponent(extInfo) {
850
851
  const component = {
851
852
  name: extensionId,
852
853
  version: extInfo.version || "",
853
- description: extInfo.displayName || extInfo.description || "",
854
+ description: String(
855
+ sanitizeBomPropertyValue(
856
+ "cdx:chrome-extension:description",
857
+ extInfo.displayName || extInfo.description || "",
858
+ ) || "",
859
+ ),
854
860
  purl,
855
861
  "bom-ref": decodeURIComponent(purl),
856
862
  type: "application",
@@ -1035,8 +1041,24 @@ export function toComponent(extInfo) {
1035
1041
  if (extInfo.srcPath) {
1036
1042
  properties.push({ name: "SrcFile", value: extInfo.srcPath });
1037
1043
  }
1038
- if (properties.length) {
1039
- component.properties = properties;
1044
+ const sanitizedProperties = properties
1045
+ .map((property) => {
1046
+ const sanitizedValue = sanitizeBomPropertyValue(
1047
+ property.name,
1048
+ property.value,
1049
+ );
1050
+ if (
1051
+ sanitizedValue === undefined ||
1052
+ sanitizedValue === null ||
1053
+ sanitizedValue === ""
1054
+ ) {
1055
+ return undefined;
1056
+ }
1057
+ return { name: property.name, value: String(sanitizedValue) };
1058
+ })
1059
+ .filter(Boolean);
1060
+ if (sanitizedProperties.length) {
1061
+ component.properties = sanitizedProperties;
1040
1062
  }
1041
1063
  return component;
1042
1064
  }
@@ -142,6 +142,74 @@ describe("parseChromiumExtensionManifest", () => {
142
142
  assert.strictEqual(parsed.hasAutofill, false);
143
143
  });
144
144
 
145
+ it("sanitizes emitted URL properties before they enter the BOM", () => {
146
+ const extensionRoot = join(baseTempDir, "sanitized-extension");
147
+ const extensionId = "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii";
148
+ const extensionVersion = "1.0.0";
149
+ const versionDir = join(extensionRoot, extensionId, extensionVersion);
150
+ mkdirSync(versionDir, { recursive: true });
151
+ writeFileSync(
152
+ join(versionDir, "manifest.json"),
153
+ JSON.stringify({
154
+ manifest_version: 3,
155
+ name: "Sanitized URLs",
156
+ version: extensionVersion,
157
+ update_url: "https://user:pass@example.com/update.xml?token=abc#frag",
158
+ host_permissions: [
159
+ "https://user:pass@example.com/*?token=abc#frag",
160
+ "<all_urls>",
161
+ ],
162
+ externally_connectable: {
163
+ matches: ["https://user:pass@example.com/*?token=abc#frag"],
164
+ },
165
+ }),
166
+ "utf-8",
167
+ );
168
+
169
+ const result = collectChromeExtensionsFromPath(versionDir);
170
+
171
+ assert.strictEqual(
172
+ getProp(result.components[0], "cdx:chrome-extension:updateUrl"),
173
+ "https://example.com/update.xml",
174
+ );
175
+ assert.strictEqual(
176
+ getProp(result.components[0], "cdx:chrome-extension:hostPermissions"),
177
+ "https://example.com/*, <all_urls>",
178
+ );
179
+ assert.strictEqual(
180
+ getProp(
181
+ result.components[0],
182
+ "cdx:chrome-extension:externallyConnectableMatches",
183
+ ),
184
+ "https://example.com/*",
185
+ );
186
+ });
187
+
188
+ it("sanitizes emitted extension descriptions before they enter the BOM", () => {
189
+ const extensionRoot = join(baseTempDir, "sanitized-description-extension");
190
+ const extensionId = "jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj";
191
+ const extensionVersion = "1.0.0";
192
+ const versionDir = join(extensionRoot, extensionId, extensionVersion);
193
+ mkdirSync(versionDir, { recursive: true });
194
+ writeFileSync(
195
+ join(versionDir, "manifest.json"),
196
+ JSON.stringify({
197
+ manifest_version: 3,
198
+ description:
199
+ "Connect with Bearer sk_test_super_secret_value at https://user:pass@example.com/path?token=abc#frag",
200
+ version: extensionVersion,
201
+ }),
202
+ "utf-8",
203
+ );
204
+
205
+ const result = collectChromeExtensionsFromPath(versionDir);
206
+
207
+ assert.strictEqual(
208
+ result.components[0].description,
209
+ "Connect with [redacted] at https://example.com/path",
210
+ );
211
+ });
212
+
145
213
  it("should parse real manifest fixtures from Chrome, Chromium and Edge extensions", () => {
146
214
  const fixtureCases = [
147
215
  {