@cyclonedx/cdxgen 12.2.0 → 12.3.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 (181) hide show
  1. package/README.md +242 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +532 -168
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +276 -68
  33. package/lib/cli/index.poku.js +368 -0
  34. package/lib/helpers/analyzer.js +1052 -5
  35. package/lib/helpers/analyzer.poku.js +301 -0
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/depsUtils.js +16 -0
  48. package/lib/helpers/depsUtils.poku.js +58 -1
  49. package/lib/helpers/display.js +245 -61
  50. package/lib/helpers/display.poku.js +162 -2
  51. package/lib/helpers/exportUtils.js +123 -0
  52. package/lib/helpers/exportUtils.poku.js +60 -0
  53. package/lib/helpers/formulationParsers.js +69 -0
  54. package/lib/helpers/formulationParsers.poku.js +44 -0
  55. package/lib/helpers/gtfobins.js +189 -0
  56. package/lib/helpers/gtfobins.poku.js +49 -0
  57. package/lib/helpers/lolbas.js +267 -0
  58. package/lib/helpers/lolbas.poku.js +39 -0
  59. package/lib/helpers/osqueryTransform.js +84 -0
  60. package/lib/helpers/osqueryTransform.poku.js +49 -0
  61. package/lib/helpers/provenanceUtils.js +193 -0
  62. package/lib/helpers/provenanceUtils.poku.js +145 -0
  63. package/lib/helpers/pylockutils.js +281 -0
  64. package/lib/helpers/pylockutils.poku.js +48 -0
  65. package/lib/helpers/registryProvenance.js +793 -0
  66. package/lib/helpers/registryProvenance.poku.js +452 -0
  67. package/lib/helpers/remote/dependency-track.js +84 -0
  68. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  69. package/lib/helpers/source.js +1267 -0
  70. package/lib/helpers/source.poku.js +771 -0
  71. package/lib/helpers/spdxUtils.js +97 -0
  72. package/lib/helpers/spdxUtils.poku.js +70 -0
  73. package/lib/helpers/table.js +384 -0
  74. package/lib/helpers/table.poku.js +186 -0
  75. package/lib/helpers/unicodeScan.js +147 -0
  76. package/lib/helpers/unicodeScan.poku.js +45 -0
  77. package/lib/helpers/utils.js +882 -136
  78. package/lib/helpers/utils.poku.js +995 -91
  79. package/lib/managers/binary.js +29 -5
  80. package/lib/managers/docker.js +179 -52
  81. package/lib/managers/docker.poku.js +327 -28
  82. package/lib/managers/oci.js +107 -23
  83. package/lib/managers/oci.poku.js +132 -0
  84. package/lib/server/openapi.yaml +50 -0
  85. package/lib/server/server.js +228 -331
  86. package/lib/server/server.poku.js +220 -5
  87. package/lib/stages/postgen/annotator.js +7 -0
  88. package/lib/stages/postgen/annotator.poku.js +40 -0
  89. package/lib/stages/postgen/auditBom.js +20 -5
  90. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  91. package/lib/stages/postgen/postgen.js +40 -0
  92. package/lib/stages/postgen/postgen.poku.js +47 -0
  93. package/lib/stages/postgen/ruleEngine.js +80 -2
  94. package/lib/stages/postgen/spdxConverter.js +796 -0
  95. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  96. package/lib/validator/bomValidator.js +232 -0
  97. package/lib/validator/bomValidator.poku.js +70 -0
  98. package/lib/validator/complianceRules.js +70 -7
  99. package/lib/validator/complianceRules.poku.js +30 -0
  100. package/lib/validator/reporters/annotations.js +2 -2
  101. package/lib/validator/reporters/console.js +13 -2
  102. package/lib/validator/reporters.poku.js +13 -0
  103. package/package.json +10 -8
  104. package/types/bin/audit.d.ts +3 -0
  105. package/types/bin/audit.d.ts.map +1 -0
  106. package/types/bin/convert.d.ts +3 -0
  107. package/types/bin/convert.d.ts.map +1 -0
  108. package/types/bin/repl.d.ts.map +1 -1
  109. package/types/lib/audit/index.d.ts +115 -0
  110. package/types/lib/audit/index.d.ts.map +1 -0
  111. package/types/lib/audit/progress.d.ts +27 -0
  112. package/types/lib/audit/progress.d.ts.map +1 -0
  113. package/types/lib/audit/reporters.d.ts +35 -0
  114. package/types/lib/audit/reporters.d.ts.map +1 -0
  115. package/types/lib/audit/scoring.d.ts +35 -0
  116. package/types/lib/audit/scoring.d.ts.map +1 -0
  117. package/types/lib/audit/targets.d.ts +63 -0
  118. package/types/lib/audit/targets.d.ts.map +1 -0
  119. package/types/lib/cli/index.d.ts +8 -0
  120. package/types/lib/cli/index.d.ts.map +1 -1
  121. package/types/lib/helpers/analyzer.d.ts +13 -0
  122. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  123. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  124. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  125. package/types/lib/helpers/bomUtils.d.ts +5 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  127. package/types/lib/helpers/chromextutils.d.ts +97 -0
  128. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  129. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  130. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  131. package/types/lib/helpers/containerRisk.d.ts +17 -0
  132. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  133. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  134. package/types/lib/helpers/display.d.ts +4 -1
  135. package/types/lib/helpers/display.d.ts.map +1 -1
  136. package/types/lib/helpers/exportUtils.d.ts +40 -0
  137. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  138. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  139. package/types/lib/helpers/gtfobins.d.ts +17 -0
  140. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  141. package/types/lib/helpers/lolbas.d.ts +16 -0
  142. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  143. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  144. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  145. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  146. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  147. package/types/lib/helpers/pylockutils.d.ts +51 -0
  148. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  149. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  150. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  151. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  152. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  153. package/types/lib/helpers/source.d.ts +141 -0
  154. package/types/lib/helpers/source.d.ts.map +1 -0
  155. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  156. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  157. package/types/lib/helpers/table.d.ts +6 -0
  158. package/types/lib/helpers/table.d.ts.map +1 -0
  159. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  160. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  161. package/types/lib/helpers/utils.d.ts +30 -11
  162. package/types/lib/helpers/utils.d.ts.map +1 -1
  163. package/types/lib/managers/binary.d.ts.map +1 -1
  164. package/types/lib/managers/docker.d.ts.map +1 -1
  165. package/types/lib/managers/oci.d.ts.map +1 -1
  166. package/types/lib/server/server.d.ts +0 -35
  167. package/types/lib/server/server.d.ts.map +1 -1
  168. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  170. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  171. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  172. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  173. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  174. package/types/lib/validator/bomValidator.d.ts +1 -0
  175. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  176. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  177. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  178. package/types/bin/dependencies.d.ts +0 -3
  179. package/types/bin/dependencies.d.ts.map +0 -1
  180. package/types/bin/licenses.d.ts +0 -3
  181. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,1153 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
+ import process from "node:process";
5
+
6
+ import { PackageURL } from "packageurl-js";
7
+
8
+ import {
9
+ CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES,
10
+ detectExtensionCapabilities,
11
+ } from "./analyzer.js";
12
+ import { isMac, isWin, safeExistsSync } from "./utils.js";
13
+
14
+ /**
15
+ * The purl type for Chrome extensions as defined by the packageurl spec.
16
+ */
17
+ export const CHROME_EXTENSION_PURL_TYPE = "chrome-extension";
18
+
19
+ const CHROME_EXTENSION_ID_REGEX = /^[a-z]{32}$/i;
20
+ const BRAVE_SPECIFIC_PERMISSIONS = ["webDiscovery", "settingsPrivate"];
21
+ /**
22
+ * Per-process cache for extension source capability scans.
23
+ *
24
+ * Entries are keyed by resolved extension directory and populated on first scan.
25
+ * Values are reused during a single cdxgen run to avoid repeated Babel AST scans
26
+ * for the same extension directory. The cache is intentionally process-local and
27
+ * naturally discarded when the process exits.
28
+ */
29
+ const extensionDirCapabilityCache = new Map();
30
+
31
+ /**
32
+ * Infer high-risk extension capabilities from manifest fields and permission hints.
33
+ *
34
+ * @param {Object} manifestData Parsed manifest-derived data
35
+ * @returns {Object<string, boolean>} Capability booleans keyed by
36
+ * CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES entries; unknown/extra keys are ignored.
37
+ */
38
+ function inferChromiumCapabilitySignals(manifestData) {
39
+ const permissions = [
40
+ ...(manifestData?.permissions || []),
41
+ ...(manifestData?.optionalPermissions || []),
42
+ ]
43
+ .filter(Boolean)
44
+ .map((permission) => permission.toLowerCase());
45
+ const hostPermissions = [
46
+ ...(manifestData?.hostPermissions || []),
47
+ ...(manifestData?.optionalHostPermissions || []),
48
+ ]
49
+ .filter(Boolean)
50
+ .map((permission) => permission.toLowerCase());
51
+ const commandNames = (manifestData?.commands || [])
52
+ .filter(Boolean)
53
+ .map((commandName) => commandName.toLowerCase());
54
+ const contentScripts = Array.isArray(manifestData?.contentScripts)
55
+ ? manifestData.contentScripts
56
+ : [];
57
+ const contentScriptPaths = contentScripts
58
+ .flatMap((script) => [
59
+ ...(Array.isArray(script?.js) ? script.js : []),
60
+ ...(Array.isArray(script?.css) ? script.css : []),
61
+ ])
62
+ .filter((entry) => typeof entry === "string")
63
+ .map((entry) => entry.toLowerCase());
64
+ const allSignals = [
65
+ ...permissions,
66
+ ...hostPermissions,
67
+ ...commandNames,
68
+ ...contentScriptPaths,
69
+ ];
70
+ const hasBroadHosts = hostPermissions.some(
71
+ (permission) =>
72
+ permission === "<all_urls>" ||
73
+ permission === "*://*/*" ||
74
+ permission.startsWith("file://"),
75
+ );
76
+ const hasContentScripts = contentScripts.length > 0;
77
+ const hasWebAccessibleResources = Array.isArray(
78
+ manifestData?.webAccessibleResources,
79
+ )
80
+ ? manifestData.webAccessibleResources.length > 0
81
+ : false;
82
+ return {
83
+ fileAccess:
84
+ allSignals.some((signal) =>
85
+ [
86
+ "filesystem",
87
+ "downloads",
88
+ "filebrowserhandler",
89
+ "filemanagerprivate",
90
+ "file://",
91
+ ].some((token) => signal.includes(token)),
92
+ ) || Boolean(manifestData?.fileBrowserHandlers),
93
+ deviceAccess: allSignals.some((signal) =>
94
+ ["usb", "hid", "serial", "nfc", "mediagalleries", "bluetooth"].some(
95
+ (token) => signal.includes(token),
96
+ ),
97
+ ),
98
+ network:
99
+ allSignals.some((signal) =>
100
+ [
101
+ "webrequest",
102
+ "declarativenetrequest",
103
+ "proxy",
104
+ "webnavigation",
105
+ "socket",
106
+ "cookies",
107
+ ].some((token) => signal.includes(token)),
108
+ ) ||
109
+ hasBroadHosts ||
110
+ hasWebAccessibleResources,
111
+ bluetooth: allSignals.some((signal) => signal.includes("bluetooth")),
112
+ accessibility: allSignals.some((signal) =>
113
+ ["accessibility", "automation", "screenreader"].some((token) =>
114
+ signal.includes(token),
115
+ ),
116
+ ),
117
+ codeInjection:
118
+ allSignals.some((signal) =>
119
+ [
120
+ "scripting",
121
+ "userscripts",
122
+ "debugger",
123
+ "tabs",
124
+ "execute",
125
+ "inject",
126
+ ].some((token) => signal.includes(token)),
127
+ ) || hasContentScripts,
128
+ fingerprinting: allSignals.some((signal) =>
129
+ ["history", "fonts", "fontsettings", "webgl", "canvas", "cookies"].some(
130
+ (token) => signal.includes(token),
131
+ ),
132
+ ),
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Merge one or more capability maps into a normalized set of boolean flags.
138
+ *
139
+ * Performs logical OR across known capability keys only; unknown keys are ignored.
140
+ *
141
+ * @param {...Object<string, boolean>} capabilityMaps Capability maps from manifest/code analysis
142
+ * @returns {Object<string, boolean>} Merged capability map
143
+ */
144
+ function mergeCapabilitySignals(...capabilityMaps) {
145
+ const merged = {};
146
+ for (const capabilityName of CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES) {
147
+ merged[capabilityName] = false;
148
+ }
149
+ for (const capabilityMap of capabilityMaps) {
150
+ for (const capabilityName of CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES) {
151
+ if (capabilityMap?.[capabilityName]) {
152
+ merged[capabilityName] = true;
153
+ }
154
+ }
155
+ }
156
+ return merged;
157
+ }
158
+
159
+ /**
160
+ * Detect extension capabilities from source code with per-directory caching.
161
+ *
162
+ * @param {string} extensionDir Extension directory
163
+ * @returns {Object<string, boolean>} Capability signal map for
164
+ * CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES where each value is boolean.
165
+ * Uses detectExtensionCapabilities(extensionDir, false), where false excludes
166
+ * node_modules/deep scanning for performance.
167
+ */
168
+ function detectCachedExtensionCapabilities(extensionDir) {
169
+ const cacheKey = resolve(extensionDir);
170
+ if (extensionDirCapabilityCache.has(cacheKey)) {
171
+ return extensionDirCapabilityCache.get(cacheKey);
172
+ }
173
+ const codeCapabilityScan = detectExtensionCapabilities(cacheKey, false);
174
+ const codeCapabilities = {};
175
+ for (const capabilityName of CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES) {
176
+ codeCapabilities[capabilityName] =
177
+ codeCapabilityScan.capabilities.includes(capabilityName);
178
+ }
179
+ extensionDirCapabilityCache.set(cacheKey, codeCapabilities);
180
+ return codeCapabilities;
181
+ }
182
+
183
+ /**
184
+ * Discover known Chromium-based browser user-data directories.
185
+ *
186
+ * @returns {Array<{browser: string, channel: string, dir: string}>}
187
+ */
188
+ export function getChromiumExtensionDirs() {
189
+ const home = homedir();
190
+ const localAppData =
191
+ process.env.LOCALAPPDATA || join(home, "AppData", "Local");
192
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME || join(home, ".config");
193
+ const dirs = [
194
+ // Google Chrome
195
+ {
196
+ browser: "Google Chrome",
197
+ channel: "stable",
198
+ dir: isWin
199
+ ? join(localAppData, "Google", "Chrome", "User Data")
200
+ : isMac
201
+ ? join(home, "Library", "Application Support", "Google", "Chrome")
202
+ : join(xdgConfigHome, "google-chrome"),
203
+ },
204
+ {
205
+ browser: "Google Chrome",
206
+ channel: "beta",
207
+ dir: isWin
208
+ ? join(localAppData, "Google", "Chrome Beta", "User Data")
209
+ : isMac
210
+ ? join(
211
+ home,
212
+ "Library",
213
+ "Application Support",
214
+ "Google",
215
+ "Chrome Beta",
216
+ )
217
+ : join(xdgConfigHome, "google-chrome-beta"),
218
+ },
219
+ {
220
+ browser: "Google Chrome",
221
+ channel: "dev",
222
+ dir: isWin
223
+ ? join(localAppData, "Google", "Chrome Dev", "User Data")
224
+ : isMac
225
+ ? join(home, "Library", "Application Support", "Google", "Chrome Dev")
226
+ : join(xdgConfigHome, "google-chrome-unstable"),
227
+ },
228
+ {
229
+ browser: "Google Chrome",
230
+ channel: "canary",
231
+ dir: isWin
232
+ ? join(localAppData, "Google", "Chrome SxS", "User Data")
233
+ : isMac
234
+ ? join(
235
+ home,
236
+ "Library",
237
+ "Application Support",
238
+ "Google",
239
+ "Chrome Canary",
240
+ )
241
+ : "",
242
+ },
243
+ // Chromium
244
+ {
245
+ browser: "Chromium",
246
+ channel: "stable",
247
+ dir: isWin
248
+ ? join(localAppData, "Chromium", "User Data")
249
+ : isMac
250
+ ? join(home, "Library", "Application Support", "Chromium")
251
+ : join(xdgConfigHome, "chromium"),
252
+ },
253
+ // Microsoft Edge
254
+ {
255
+ browser: "Microsoft Edge",
256
+ channel: "stable",
257
+ dir: isWin
258
+ ? join(localAppData, "Microsoft", "Edge", "User Data")
259
+ : isMac
260
+ ? join(home, "Library", "Application Support", "Microsoft Edge")
261
+ : join(xdgConfigHome, "microsoft-edge"),
262
+ },
263
+ {
264
+ browser: "Microsoft Edge",
265
+ channel: "beta",
266
+ dir: isWin
267
+ ? join(localAppData, "Microsoft", "Edge Beta", "User Data")
268
+ : isMac
269
+ ? join(home, "Library", "Application Support", "Microsoft Edge Beta")
270
+ : join(xdgConfigHome, "microsoft-edge-beta"),
271
+ },
272
+ {
273
+ browser: "Microsoft Edge",
274
+ channel: "dev",
275
+ dir: isWin
276
+ ? join(localAppData, "Microsoft", "Edge Dev", "User Data")
277
+ : isMac
278
+ ? join(home, "Library", "Application Support", "Microsoft Edge Dev")
279
+ : join(xdgConfigHome, "microsoft-edge-dev"),
280
+ },
281
+ {
282
+ browser: "Microsoft Edge",
283
+ channel: "canary",
284
+ dir: isWin
285
+ ? join(localAppData, "Microsoft", "Edge SxS", "User Data")
286
+ : isMac
287
+ ? join(
288
+ home,
289
+ "Library",
290
+ "Application Support",
291
+ "Microsoft Edge Canary",
292
+ )
293
+ : "",
294
+ },
295
+ // Brave
296
+ {
297
+ browser: "Brave",
298
+ channel: "stable",
299
+ dir: isWin
300
+ ? join(localAppData, "BraveSoftware", "Brave-Browser", "User Data")
301
+ : isMac
302
+ ? join(
303
+ home,
304
+ "Library",
305
+ "Application Support",
306
+ "BraveSoftware",
307
+ "Brave-Browser",
308
+ )
309
+ : join(xdgConfigHome, "BraveSoftware", "Brave-Browser"),
310
+ },
311
+ {
312
+ browser: "Brave",
313
+ channel: "beta",
314
+ dir: isWin
315
+ ? join(localAppData, "BraveSoftware", "Brave-Browser-Beta", "User Data")
316
+ : isMac
317
+ ? join(
318
+ home,
319
+ "Library",
320
+ "Application Support",
321
+ "BraveSoftware",
322
+ "Brave-Browser-Beta",
323
+ )
324
+ : join(xdgConfigHome, "BraveSoftware", "Brave-Browser-Beta"),
325
+ },
326
+ {
327
+ browser: "Brave",
328
+ channel: "dev",
329
+ dir: isWin
330
+ ? join(localAppData, "BraveSoftware", "Brave-Browser-Dev", "User Data")
331
+ : isMac
332
+ ? join(
333
+ home,
334
+ "Library",
335
+ "Application Support",
336
+ "BraveSoftware",
337
+ "Brave-Browser-Dev",
338
+ )
339
+ : join(xdgConfigHome, "BraveSoftware", "Brave-Browser-Dev"),
340
+ },
341
+ {
342
+ browser: "Brave",
343
+ channel: "nightly",
344
+ dir: isWin
345
+ ? join(
346
+ localAppData,
347
+ "BraveSoftware",
348
+ "Brave-Browser-Nightly",
349
+ "User Data",
350
+ )
351
+ : isMac
352
+ ? join(
353
+ home,
354
+ "Library",
355
+ "Application Support",
356
+ "BraveSoftware",
357
+ "Brave-Browser-Nightly",
358
+ )
359
+ : join(xdgConfigHome, "BraveSoftware", "Brave-Browser-Nightly"),
360
+ },
361
+ // Vivaldi
362
+ {
363
+ browser: "Vivaldi",
364
+ channel: "stable",
365
+ dir: isWin
366
+ ? join(localAppData, "Vivaldi", "User Data")
367
+ : isMac
368
+ ? join(home, "Library", "Application Support", "Vivaldi")
369
+ : join(xdgConfigHome, "vivaldi"),
370
+ },
371
+ {
372
+ browser: "Vivaldi",
373
+ channel: "snapshot",
374
+ dir: isWin
375
+ ? join(localAppData, "Vivaldi Snapshot", "User Data")
376
+ : isMac
377
+ ? join(home, "Library", "Application Support", "Vivaldi Snapshot")
378
+ : join(xdgConfigHome, "vivaldi-snapshot"),
379
+ },
380
+ ];
381
+ return dirs.filter((entry) => entry.dir);
382
+ }
383
+
384
+ /**
385
+ * Discover existing Chromium-based browser user-data directories.
386
+ *
387
+ * @returns {Array<{browser: string, channel: string, dir: string}>}
388
+ */
389
+ export function discoverChromiumExtensionDirs() {
390
+ const found = [];
391
+ const seen = new Set();
392
+ for (const browserDir of getChromiumExtensionDirs()) {
393
+ if (safeExistsSync(browserDir.dir) && !seen.has(browserDir.dir)) {
394
+ seen.add(browserDir.dir);
395
+ found.push(browserDir);
396
+ }
397
+ }
398
+ return found;
399
+ }
400
+
401
+ /**
402
+ * Compare Chromium extension versions with numeric dot-separated semantics.
403
+ *
404
+ * @param {string} leftVersion Left version
405
+ * @param {string} rightVersion Right version
406
+ * @returns {number} Negative when left<right, positive when left>right, zero when equal
407
+ */
408
+ export function compareChromiumExtensionVersions(leftVersion, rightVersion) {
409
+ const leftParts = String(leftVersion || "")
410
+ .split(".")
411
+ .map((part) => Number.parseInt(part, 10));
412
+ const rightParts = String(rightVersion || "")
413
+ .split(".")
414
+ .map((part) => Number.parseInt(part, 10));
415
+ const maxLength = Math.max(leftParts.length, rightParts.length);
416
+ for (let i = 0; i < maxLength; i++) {
417
+ const leftRawPart = leftParts[i];
418
+ const rightRawPart = rightParts[i];
419
+ const leftPart =
420
+ leftRawPart === undefined || Number.isNaN(leftRawPart) ? 0 : leftRawPart;
421
+ const rightPart =
422
+ rightRawPart === undefined || Number.isNaN(rightRawPart)
423
+ ? 0
424
+ : rightRawPart;
425
+ if (leftPart !== rightPart) {
426
+ return leftPart - rightPart;
427
+ }
428
+ }
429
+ return 0;
430
+ }
431
+
432
+ /**
433
+ * Read profile names from Chromium user-data directory.
434
+ *
435
+ * @param {string} userDataDir Browser user-data directory
436
+ * @returns {string[]} Profile directory names
437
+ */
438
+ export function getChromiumProfiles(userDataDir) {
439
+ const profiles = [];
440
+ const localStateFile = join(userDataDir, "Local State");
441
+ if (safeExistsSync(localStateFile)) {
442
+ try {
443
+ const localState = JSON.parse(readFileSync(localStateFile, "utf-8"));
444
+ const infoCache = localState?.profile?.info_cache;
445
+ if (infoCache && typeof infoCache === "object") {
446
+ for (const profileName of Object.keys(infoCache)) {
447
+ if (safeExistsSync(join(userDataDir, profileName, "Extensions"))) {
448
+ profiles.push(profileName);
449
+ }
450
+ }
451
+ }
452
+ const lastUsed = localState?.profile?.last_used;
453
+ if (
454
+ lastUsed &&
455
+ safeExistsSync(join(userDataDir, lastUsed, "Extensions")) &&
456
+ !profiles.includes(lastUsed)
457
+ ) {
458
+ profiles.push(lastUsed);
459
+ }
460
+ } catch (_err) {
461
+ // Ignore malformed Local State and fallback to directory scan
462
+ }
463
+ }
464
+ if (profiles.length) {
465
+ return profiles;
466
+ }
467
+ try {
468
+ const profileDirs = readdirSync(userDataDir, { withFileTypes: true })
469
+ .filter((entry) => entry.isDirectory())
470
+ .map((entry) => entry.name)
471
+ .filter((name) => name === "Default" || /^Profile \d+$/.test(name))
472
+ .filter((name) => safeExistsSync(join(userDataDir, name, "Extensions")));
473
+ if (profileDirs.length) {
474
+ return profileDirs;
475
+ }
476
+ } catch (_err) {
477
+ // Ignore directory scan errors
478
+ }
479
+ return safeExistsSync(join(userDataDir, "Default", "Extensions"))
480
+ ? ["Default"]
481
+ : [];
482
+ }
483
+
484
+ /**
485
+ * Parse a Chromium extension manifest file.
486
+ *
487
+ * @param {string} manifestFile Absolute path to manifest.json
488
+ * @returns {Object|undefined} Parsed manifest metadata
489
+ */
490
+ export function parseChromiumExtensionManifest(manifestFile) {
491
+ if (!safeExistsSync(manifestFile)) {
492
+ return undefined;
493
+ }
494
+ try {
495
+ const manifest = JSON.parse(readFileSync(manifestFile, "utf-8"));
496
+ const permissions = Array.isArray(manifest.permissions)
497
+ ? manifest.permissions.filter((value) => typeof value === "string")
498
+ : [];
499
+ const optionalPermissions = Array.isArray(manifest.optional_permissions)
500
+ ? manifest.optional_permissions.filter(
501
+ (value) => typeof value === "string",
502
+ )
503
+ : [];
504
+ const declaredHostPermissions = Array.isArray(manifest.host_permissions)
505
+ ? manifest.host_permissions.filter((value) => typeof value === "string")
506
+ : [];
507
+ const optionalHostPermissions = Array.isArray(
508
+ manifest.optional_host_permissions,
509
+ )
510
+ ? manifest.optional_host_permissions.filter(
511
+ (value) => typeof value === "string",
512
+ )
513
+ : [];
514
+ const commands =
515
+ manifest.commands && typeof manifest.commands === "object"
516
+ ? Object.keys(manifest.commands).filter(Boolean)
517
+ : [];
518
+ const contentScriptsRunAt = Array.isArray(manifest.content_scripts)
519
+ ? [
520
+ ...new Set(
521
+ manifest.content_scripts
522
+ .map((script) => script?.run_at)
523
+ .filter((value) => typeof value === "string"),
524
+ ),
525
+ ]
526
+ : [];
527
+ const contentScriptsMatches = Array.isArray(manifest.content_scripts)
528
+ ? [
529
+ ...new Set(
530
+ manifest.content_scripts
531
+ .flatMap((script) =>
532
+ Array.isArray(script?.matches) ? script.matches : [],
533
+ )
534
+ .filter((value) => typeof value === "string"),
535
+ ),
536
+ ]
537
+ : [];
538
+ const hostPermissions = [
539
+ ...new Set([...declaredHostPermissions, ...contentScriptsMatches]),
540
+ ];
541
+ const hasAutofillInContentScripts = Array.isArray(manifest.content_scripts)
542
+ ? manifest.content_scripts.some((script) => {
543
+ if (!script || typeof script !== "object") {
544
+ return false;
545
+ }
546
+ const jsEntries = Array.isArray(script.js) ? script.js : [];
547
+ const cssEntries = Array.isArray(script.css) ? script.css : [];
548
+ const hasAutofillInJs = jsEntries.some(
549
+ (entry) =>
550
+ typeof entry === "string" &&
551
+ entry.toLowerCase().includes("autofill"),
552
+ );
553
+ const hasAutofillInCss = cssEntries.some(
554
+ (entry) =>
555
+ typeof entry === "string" &&
556
+ entry.toLowerCase().includes("autofill"),
557
+ );
558
+ return hasAutofillInJs || hasAutofillInCss;
559
+ })
560
+ : false;
561
+ const hasAutofill =
562
+ permissions.some((permission) =>
563
+ permission.toLowerCase().includes("autofill"),
564
+ ) ||
565
+ optionalPermissions.some((permission) =>
566
+ permission.toLowerCase().includes("autofill"),
567
+ ) ||
568
+ hasAutofillInContentScripts ||
569
+ commands.some((commandName) =>
570
+ commandName.toLowerCase().includes("autofill"),
571
+ );
572
+ let contentSecurityPolicy = "";
573
+ if (typeof manifest.content_security_policy === "string") {
574
+ contentSecurityPolicy = manifest.content_security_policy;
575
+ } else if (
576
+ manifest.content_security_policy &&
577
+ typeof manifest.content_security_policy === "object"
578
+ ) {
579
+ contentSecurityPolicy = JSON.stringify(manifest.content_security_policy);
580
+ }
581
+ const webAccessibleResourceMatches = Array.isArray(
582
+ manifest.web_accessible_resources,
583
+ )
584
+ ? [
585
+ ...new Set(
586
+ manifest.web_accessible_resources
587
+ .flatMap((entry) => {
588
+ if (typeof entry === "string") {
589
+ return [];
590
+ }
591
+ const matches = Array.isArray(entry?.matches)
592
+ ? entry.matches
593
+ : [];
594
+ return matches.filter((value) => typeof value === "string");
595
+ })
596
+ .filter(Boolean),
597
+ ),
598
+ ]
599
+ : [];
600
+ const externallyConnectableMatches = Array.isArray(
601
+ manifest.externally_connectable?.matches,
602
+ )
603
+ ? manifest.externally_connectable.matches.filter(
604
+ (value) => typeof value === "string",
605
+ )
606
+ : [];
607
+ const capabilities = inferChromiumCapabilitySignals({
608
+ permissions,
609
+ optionalPermissions,
610
+ hostPermissions,
611
+ optionalHostPermissions,
612
+ commands,
613
+ contentScripts: manifest.content_scripts,
614
+ fileBrowserHandlers: manifest.file_browser_handlers,
615
+ webAccessibleResources: manifest.web_accessible_resources,
616
+ });
617
+ return {
618
+ name: manifest.name || "",
619
+ description: manifest.description || "",
620
+ version: manifest.version || "",
621
+ versionName: manifest.version_name || "",
622
+ manifestVersion: manifest.manifest_version,
623
+ updateUrl: manifest.update_url || "",
624
+ minimumChromeVersion: manifest.minimum_chrome_version || "",
625
+ minimumEdgeVersion: manifest.minimum_edge_version || "",
626
+ incognito: manifest.incognito || "",
627
+ offlineEnabled:
628
+ typeof manifest.offline_enabled === "boolean"
629
+ ? manifest.offline_enabled
630
+ : undefined,
631
+ permissions,
632
+ optionalPermissions,
633
+ hostPermissions,
634
+ optionalHostPermissions,
635
+ commands,
636
+ contentScriptsRunAt,
637
+ contentScriptsMatches,
638
+ contentSecurityPolicy,
639
+ storageManagedSchema: manifest?.storage?.managed_schema || "",
640
+ webAccessibleResourceMatches,
641
+ externallyConnectableMatches,
642
+ edgeUrlOverrides: manifest.edge_url_overrides || undefined,
643
+ braveMaybeBackground:
644
+ manifest.MAYBE_background &&
645
+ typeof manifest.MAYBE_background === "object",
646
+ bravePermissions: permissions.filter((permission) =>
647
+ BRAVE_SPECIFIC_PERMISSIONS.includes(permission),
648
+ ),
649
+ capabilities,
650
+ hasAutofill,
651
+ };
652
+ } catch (_err) {
653
+ return undefined;
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Infer browser context from a resolved Chromium extension manifest path.
659
+ *
660
+ * @param {string} manifestFile Absolute path to manifest.json
661
+ * @returns {{browser?: string, channel?: string, profile?: string, profilePath?: string}}
662
+ */
663
+ export function inferChromiumContextFromManifest(manifestFile) {
664
+ const resolvedManifest = resolve(manifestFile);
665
+ for (const browserDir of getChromiumExtensionDirs()) {
666
+ const resolvedBrowserDir = resolve(browserDir.dir);
667
+ const browserRootPrefix = `${resolvedBrowserDir}${sep}`;
668
+ if (!resolvedManifest.startsWith(browserRootPrefix)) {
669
+ continue;
670
+ }
671
+ const rel = relative(resolvedBrowserDir, resolvedManifest);
672
+ const relParts = rel.split(sep);
673
+ if (
674
+ relParts.length >= 5 &&
675
+ relParts[0] &&
676
+ relParts[1] === "Extensions" &&
677
+ CHROME_EXTENSION_ID_REGEX.test(relParts[2]) &&
678
+ relParts[4] === "manifest.json"
679
+ ) {
680
+ return {
681
+ browser: browserDir.browser,
682
+ channel: browserDir.channel,
683
+ profile: relParts[0],
684
+ profilePath: join(resolvedBrowserDir, relParts[0]),
685
+ };
686
+ }
687
+ }
688
+ return {};
689
+ }
690
+
691
+ /**
692
+ * Pick the latest installed version directory for an extension-id directory.
693
+ *
694
+ * @param {string} extensionIdDir Path to `<...>/Extensions/<extension-id>`
695
+ * @returns {string|undefined} Absolute path to the latest version directory
696
+ */
697
+ function getLatestExtensionVersionDir(extensionIdDir) {
698
+ if (!safeExistsSync(extensionIdDir)) {
699
+ return undefined;
700
+ }
701
+ let versionDirs;
702
+ try {
703
+ versionDirs = readdirSync(extensionIdDir, { withFileTypes: true })
704
+ .filter((entry) => entry.isDirectory())
705
+ .map((entry) => entry.name);
706
+ } catch (_err) {
707
+ return undefined;
708
+ }
709
+ if (!versionDirs.length) {
710
+ return undefined;
711
+ }
712
+ versionDirs.sort(compareChromiumExtensionVersions);
713
+ return join(extensionIdDir, versionDirs[versionDirs.length - 1]);
714
+ }
715
+
716
+ /**
717
+ * Convert a manifest file path into a CycloneDX component and extension dir.
718
+ *
719
+ * @param {string} manifestFile Absolute path to manifest.json
720
+ * @returns {{component?: Object, extensionDir?: string}}
721
+ */
722
+ function parseChromeExtensionFromManifestPath(manifestFile) {
723
+ if (!safeExistsSync(manifestFile)) {
724
+ return {};
725
+ }
726
+ const extensionDir = dirname(manifestFile);
727
+ const extensionId = basename(dirname(extensionDir)).toLowerCase();
728
+ if (!CHROME_EXTENSION_ID_REGEX.test(extensionId)) {
729
+ return {};
730
+ }
731
+ const versionFromPath = basename(extensionDir);
732
+ const manifest = parseChromiumExtensionManifest(manifestFile);
733
+ const codeCapabilities = detectCachedExtensionCapabilities(extensionDir);
734
+ const context = inferChromiumContextFromManifest(manifestFile);
735
+ return {
736
+ component: toComponent({
737
+ extensionId,
738
+ version: manifest?.version || versionFromPath,
739
+ displayName: manifest?.name || "",
740
+ description: manifest?.description || "",
741
+ manifestVersion: manifest?.manifestVersion,
742
+ updateUrl: manifest?.updateUrl || "",
743
+ permissions: manifest?.permissions || [],
744
+ optionalPermissions: manifest?.optionalPermissions || [],
745
+ hostPermissions: manifest?.hostPermissions || [],
746
+ optionalHostPermissions: manifest?.optionalHostPermissions || [],
747
+ commands: manifest?.commands || [],
748
+ contentScriptsRunAt: manifest?.contentScriptsRunAt || [],
749
+ contentScriptsMatches: manifest?.contentScriptsMatches || [],
750
+ contentSecurityPolicy: manifest?.contentSecurityPolicy || "",
751
+ storageManagedSchema: manifest?.storageManagedSchema || "",
752
+ minimumChromeVersion: manifest?.minimumChromeVersion || "",
753
+ minimumEdgeVersion: manifest?.minimumEdgeVersion || "",
754
+ versionName: manifest?.versionName || "",
755
+ incognito: manifest?.incognito || "",
756
+ offlineEnabled: manifest?.offlineEnabled,
757
+ webAccessibleResourceMatches:
758
+ manifest?.webAccessibleResourceMatches || [],
759
+ externallyConnectableMatches:
760
+ manifest?.externallyConnectableMatches || [],
761
+ edgeUrlOverrides: manifest?.edgeUrlOverrides,
762
+ braveMaybeBackground: manifest?.braveMaybeBackground || false,
763
+ bravePermissions: manifest?.bravePermissions || [],
764
+ capabilities: mergeCapabilitySignals(
765
+ manifest?.capabilities || {},
766
+ codeCapabilities,
767
+ ),
768
+ hasAutofill: manifest?.hasAutofill || false,
769
+ srcPath: manifestFile,
770
+ ...context,
771
+ }),
772
+ extensionDir,
773
+ };
774
+ }
775
+
776
+ /**
777
+ * Collect one directly specified extension from a path.
778
+ *
779
+ * Supported path forms:
780
+ * - `<...>/manifest.json`
781
+ * - `<...>/<extension-id>/<version>/manifest.json`
782
+ * - `<...>/<version>/` (contains manifest.json)
783
+ * - `<...>/<extension-id>/` (contains version subdirectories)
784
+ *
785
+ * Note: a standalone `<...>/<version>/` directory is not sufficient unless its
786
+ * parent directory name is the extension id, because the parser derives the
787
+ * extension id from the version directory's parent path.
788
+ *
789
+ * @param {string} extensionPath Candidate extension path
790
+ * @returns {{components: Object[], extensionDirs: string[]}}
791
+ */
792
+ export function collectChromeExtensionsFromPath(extensionPath) {
793
+ if (!extensionPath || !safeExistsSync(extensionPath)) {
794
+ return { components: [], extensionDirs: [] };
795
+ }
796
+ const resolvedPath = resolve(extensionPath);
797
+ const manifestCandidates = [];
798
+ const extensionDirs = [];
799
+ const seenManifestFiles = new Set();
800
+ const name = basename(resolvedPath);
801
+ if (name === "manifest.json") {
802
+ manifestCandidates.push(resolvedPath);
803
+ } else if (safeExistsSync(join(resolvedPath, "manifest.json"))) {
804
+ manifestCandidates.push(join(resolvedPath, "manifest.json"));
805
+ } else if (CHROME_EXTENSION_ID_REGEX.test(name)) {
806
+ const latestVersionDir = getLatestExtensionVersionDir(resolvedPath);
807
+ if (latestVersionDir) {
808
+ manifestCandidates.push(join(latestVersionDir, "manifest.json"));
809
+ }
810
+ }
811
+ const components = [];
812
+ const seenBomRefs = new Set();
813
+ for (const manifestFile of manifestCandidates) {
814
+ if (seenManifestFiles.has(manifestFile)) {
815
+ continue;
816
+ }
817
+ seenManifestFiles.add(manifestFile);
818
+ const { component, extensionDir } =
819
+ parseChromeExtensionFromManifestPath(manifestFile);
820
+ if (extensionDir && !extensionDirs.includes(extensionDir)) {
821
+ extensionDirs.push(extensionDir);
822
+ }
823
+ if (component?.["bom-ref"] && !seenBomRefs.has(component["bom-ref"])) {
824
+ seenBomRefs.add(component["bom-ref"]);
825
+ components.push(component);
826
+ }
827
+ }
828
+ return { components, extensionDirs };
829
+ }
830
+
831
+ /**
832
+ * Convert parsed Chromium extension metadata into a CycloneDX component object.
833
+ *
834
+ * @param {Object} extInfo Extension metadata
835
+ * @returns {Object|undefined} CycloneDX component object or undefined
836
+ */
837
+ export function toComponent(extInfo) {
838
+ if (!extInfo?.extensionId) {
839
+ return undefined;
840
+ }
841
+ const extensionId = extInfo.extensionId.toLowerCase();
842
+ const purl = new PackageURL(
843
+ CHROME_EXTENSION_PURL_TYPE,
844
+ null,
845
+ extensionId,
846
+ extInfo.version || null,
847
+ null,
848
+ null,
849
+ ).toString();
850
+ const component = {
851
+ name: extensionId,
852
+ version: extInfo.version || "",
853
+ description: extInfo.displayName || extInfo.description || "",
854
+ purl,
855
+ "bom-ref": decodeURIComponent(purl),
856
+ type: "application",
857
+ };
858
+ const properties = [];
859
+ if (extInfo.browser) {
860
+ properties.push({
861
+ name: "cdx:chrome-extension:browser",
862
+ value: extInfo.browser,
863
+ });
864
+ }
865
+ if (extInfo.channel) {
866
+ properties.push({
867
+ name: "cdx:chrome-extension:channel",
868
+ value: extInfo.channel,
869
+ });
870
+ }
871
+ if (extInfo.profile) {
872
+ properties.push({
873
+ name: "cdx:chrome-extension:profile",
874
+ value: extInfo.profile,
875
+ });
876
+ }
877
+ if (extInfo.profilePath) {
878
+ properties.push({
879
+ name: "cdx:chrome-extension:profilePath",
880
+ value: extInfo.profilePath,
881
+ });
882
+ }
883
+ if (extInfo.manifestVersion !== undefined) {
884
+ properties.push({
885
+ name: "cdx:chrome-extension:manifestVersion",
886
+ value: String(extInfo.manifestVersion),
887
+ });
888
+ }
889
+ if (extInfo.updateUrl) {
890
+ properties.push({
891
+ name: "cdx:chrome-extension:updateUrl",
892
+ value: extInfo.updateUrl,
893
+ });
894
+ }
895
+ if (extInfo.permissions?.length) {
896
+ properties.push({
897
+ name: "cdx:chrome-extension:permissions",
898
+ value: extInfo.permissions.join(", "),
899
+ });
900
+ }
901
+ if (extInfo.optionalPermissions?.length) {
902
+ properties.push({
903
+ name: "cdx:chrome-extension:optionalPermissions",
904
+ value: extInfo.optionalPermissions.join(", "),
905
+ });
906
+ }
907
+ if (extInfo.hostPermissions?.length) {
908
+ properties.push({
909
+ name: "cdx:chrome-extension:hostPermissions",
910
+ value: extInfo.hostPermissions.join(", "),
911
+ });
912
+ }
913
+ if (extInfo.optionalHostPermissions?.length) {
914
+ properties.push({
915
+ name: "cdx:chrome-extension:optionalHostPermissions",
916
+ value: extInfo.optionalHostPermissions.join(", "),
917
+ });
918
+ }
919
+ if (extInfo.commands?.length) {
920
+ properties.push({
921
+ name: "cdx:chrome-extension:commands",
922
+ value: extInfo.commands.join(", "),
923
+ });
924
+ }
925
+ if (extInfo.contentScriptsRunAt?.length) {
926
+ properties.push({
927
+ name: "cdx:chrome-extension:contentScriptsRunAt",
928
+ value: extInfo.contentScriptsRunAt.join(", "),
929
+ });
930
+ }
931
+ if (extInfo.contentScriptsMatches?.length) {
932
+ properties.push({
933
+ name: "cdx:chrome-extension:contentScriptsMatches",
934
+ value: extInfo.contentScriptsMatches.join(", "),
935
+ });
936
+ }
937
+ if (extInfo.contentSecurityPolicy) {
938
+ properties.push({
939
+ name: "cdx:chrome-extension:contentSecurityPolicy",
940
+ value: extInfo.contentSecurityPolicy,
941
+ });
942
+ }
943
+ if (extInfo.storageManagedSchema) {
944
+ properties.push({
945
+ name: "cdx:chrome-extension:storageManagedSchema",
946
+ value: extInfo.storageManagedSchema,
947
+ });
948
+ }
949
+ if (extInfo.minimumChromeVersion) {
950
+ properties.push({
951
+ name: "cdx:chrome-extension:minimumChromeVersion",
952
+ value: extInfo.minimumChromeVersion,
953
+ });
954
+ }
955
+ if (extInfo.versionName) {
956
+ properties.push({
957
+ name: "cdx:chrome-extension:versionName",
958
+ value: extInfo.versionName,
959
+ });
960
+ }
961
+ if (extInfo.incognito) {
962
+ properties.push({
963
+ name: "cdx:chrome-extension:incognito",
964
+ value: extInfo.incognito,
965
+ });
966
+ }
967
+ if (typeof extInfo.offlineEnabled === "boolean") {
968
+ properties.push({
969
+ name: "cdx:chrome-extension:offlineEnabled",
970
+ value: String(extInfo.offlineEnabled),
971
+ });
972
+ }
973
+ if (extInfo.webAccessibleResourceMatches?.length) {
974
+ properties.push({
975
+ name: "cdx:chrome-extension:webAccessibleResourceMatches",
976
+ value: extInfo.webAccessibleResourceMatches.join(", "),
977
+ });
978
+ }
979
+ if (extInfo.externallyConnectableMatches?.length) {
980
+ properties.push({
981
+ name: "cdx:chrome-extension:externallyConnectableMatches",
982
+ value: extInfo.externallyConnectableMatches.join(", "),
983
+ });
984
+ }
985
+ if (extInfo.minimumEdgeVersion) {
986
+ properties.push({
987
+ name: "cdx:chrome-extension:edge:minimumVersion",
988
+ value: extInfo.minimumEdgeVersion,
989
+ });
990
+ }
991
+ if (extInfo.edgeUrlOverrides) {
992
+ properties.push({
993
+ name: "cdx:chrome-extension:edge:urlOverrides",
994
+ value:
995
+ typeof extInfo.edgeUrlOverrides === "string"
996
+ ? extInfo.edgeUrlOverrides
997
+ : JSON.stringify(extInfo.edgeUrlOverrides),
998
+ });
999
+ }
1000
+ if (extInfo.braveMaybeBackground) {
1001
+ properties.push({
1002
+ name: "cdx:chrome-extension:brave:maybeBackground",
1003
+ value: "true",
1004
+ });
1005
+ }
1006
+ if (extInfo.bravePermissions?.length) {
1007
+ properties.push({
1008
+ name: "cdx:chrome-extension:brave:permissions",
1009
+ value: extInfo.bravePermissions.join(", "),
1010
+ });
1011
+ }
1012
+ if (extInfo.capabilities) {
1013
+ const capabilityNames = CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES.filter(
1014
+ (capabilityName) => extInfo.capabilities?.[capabilityName],
1015
+ );
1016
+ if (capabilityNames.length) {
1017
+ properties.push({
1018
+ name: "cdx:chrome-extension:capabilities",
1019
+ value: capabilityNames.join(", "),
1020
+ });
1021
+ for (const capabilityName of capabilityNames) {
1022
+ properties.push({
1023
+ name: `cdx:chrome-extension:capability:${capabilityName}`,
1024
+ value: "true",
1025
+ });
1026
+ }
1027
+ }
1028
+ }
1029
+ if (extInfo.hasAutofill) {
1030
+ properties.push({
1031
+ name: "cdx:chrome-extension:hasAutofill",
1032
+ value: "true",
1033
+ });
1034
+ }
1035
+ if (extInfo.srcPath) {
1036
+ properties.push({ name: "SrcFile", value: extInfo.srcPath });
1037
+ }
1038
+ if (properties.length) {
1039
+ component.properties = properties;
1040
+ }
1041
+ return component;
1042
+ }
1043
+
1044
+ /**
1045
+ * Collect installed Chromium extension components from discovered browser directories.
1046
+ *
1047
+ * @param {Array<{browser: string, channel: string, dir: string}>} browserDirs Browser directories
1048
+ * @returns {Object[]} Array of CycloneDX component objects
1049
+ */
1050
+ export function collectInstalledChromeExtensions(browserDirs) {
1051
+ const installMap = new Map();
1052
+ for (const browserDir of browserDirs) {
1053
+ const profiles = getChromiumProfiles(browserDir.dir);
1054
+ for (const profileName of profiles) {
1055
+ const profilePath = join(browserDir.dir, profileName);
1056
+ const extensionsDir = join(profilePath, "Extensions");
1057
+ if (!safeExistsSync(extensionsDir)) {
1058
+ continue;
1059
+ }
1060
+ let extensionEntries;
1061
+ try {
1062
+ extensionEntries = readdirSync(extensionsDir, { withFileTypes: true });
1063
+ } catch (_err) {
1064
+ continue;
1065
+ }
1066
+ for (const extensionEntry of extensionEntries) {
1067
+ if (!extensionEntry.isDirectory()) {
1068
+ continue;
1069
+ }
1070
+ const extensionId = extensionEntry.name.toLowerCase();
1071
+ if (!CHROME_EXTENSION_ID_REGEX.test(extensionId)) {
1072
+ continue;
1073
+ }
1074
+ const versionRoot = join(extensionsDir, extensionEntry.name);
1075
+ let versionEntries;
1076
+ try {
1077
+ versionEntries = readdirSync(versionRoot, { withFileTypes: true })
1078
+ .filter((entry) => entry.isDirectory())
1079
+ .map((entry) => entry.name);
1080
+ } catch (_err) {
1081
+ continue;
1082
+ }
1083
+ if (!versionEntries.length) {
1084
+ continue;
1085
+ }
1086
+ versionEntries.sort(compareChromiumExtensionVersions);
1087
+ const version = versionEntries[versionEntries.length - 1];
1088
+ const manifestPath = join(versionRoot, version, "manifest.json");
1089
+ const manifest = parseChromiumExtensionManifest(manifestPath);
1090
+ const extensionDir = join(versionRoot, version);
1091
+ const codeCapabilities =
1092
+ detectCachedExtensionCapabilities(extensionDir);
1093
+ const extInfo = {
1094
+ extensionId,
1095
+ version: manifest?.version || version,
1096
+ displayName: manifest?.name || "",
1097
+ description: manifest?.description || "",
1098
+ manifestVersion: manifest?.manifestVersion,
1099
+ updateUrl: manifest?.updateUrl || "",
1100
+ permissions: manifest?.permissions || [],
1101
+ optionalPermissions: manifest?.optionalPermissions || [],
1102
+ hostPermissions: manifest?.hostPermissions || [],
1103
+ optionalHostPermissions: manifest?.optionalHostPermissions || [],
1104
+ commands: manifest?.commands || [],
1105
+ contentScriptsRunAt: manifest?.contentScriptsRunAt || [],
1106
+ contentSecurityPolicy: manifest?.contentSecurityPolicy || "",
1107
+ storageManagedSchema: manifest?.storageManagedSchema || "",
1108
+ minimumChromeVersion: manifest?.minimumChromeVersion || "",
1109
+ minimumEdgeVersion: manifest?.minimumEdgeVersion || "",
1110
+ versionName: manifest?.versionName || "",
1111
+ incognito: manifest?.incognito || "",
1112
+ offlineEnabled: manifest?.offlineEnabled,
1113
+ webAccessibleResourceMatches:
1114
+ manifest?.webAccessibleResourceMatches || [],
1115
+ externallyConnectableMatches:
1116
+ manifest?.externallyConnectableMatches || [],
1117
+ edgeUrlOverrides: manifest?.edgeUrlOverrides,
1118
+ braveMaybeBackground: manifest?.braveMaybeBackground || false,
1119
+ bravePermissions: manifest?.bravePermissions || [],
1120
+ capabilities: mergeCapabilitySignals(
1121
+ manifest?.capabilities || {},
1122
+ codeCapabilities,
1123
+ ),
1124
+ hasAutofill: manifest?.hasAutofill || false,
1125
+ browser: browserDir.browser,
1126
+ channel: browserDir.channel,
1127
+ profile: profileName,
1128
+ profilePath,
1129
+ srcPath: manifestPath,
1130
+ };
1131
+ const key = `${browserDir.browser}|${browserDir.channel}|${profileName}|${extensionId}`;
1132
+ const existing = installMap.get(key);
1133
+ if (
1134
+ !existing ||
1135
+ compareChromiumExtensionVersions(existing.version, extInfo.version) <
1136
+ 0
1137
+ ) {
1138
+ installMap.set(key, extInfo);
1139
+ }
1140
+ }
1141
+ }
1142
+ }
1143
+ const components = [];
1144
+ const seen = new Set();
1145
+ for (const extInfo of installMap.values()) {
1146
+ const component = toComponent(extInfo);
1147
+ if (component && !seen.has(component["bom-ref"])) {
1148
+ seen.add(component["bom-ref"]);
1149
+ components.push(component);
1150
+ }
1151
+ }
1152
+ return components;
1153
+ }