@cyclonedx/cdxgen 12.2.1 → 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 (170) hide show
  1. package/README.md +239 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +513 -167
  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 +154 -11
  33. package/lib/cli/index.poku.js +251 -0
  34. package/lib/helpers/analyzer.js +446 -2
  35. package/lib/helpers/analyzer.poku.js +72 -1
  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/display.js +241 -59
  48. package/lib/helpers/display.poku.js +162 -2
  49. package/lib/helpers/exportUtils.js +123 -0
  50. package/lib/helpers/exportUtils.poku.js +60 -0
  51. package/lib/helpers/formulationParsers.js +69 -0
  52. package/lib/helpers/formulationParsers.poku.js +44 -0
  53. package/lib/helpers/gtfobins.js +189 -0
  54. package/lib/helpers/gtfobins.poku.js +49 -0
  55. package/lib/helpers/lolbas.js +267 -0
  56. package/lib/helpers/lolbas.poku.js +39 -0
  57. package/lib/helpers/osqueryTransform.js +84 -0
  58. package/lib/helpers/osqueryTransform.poku.js +49 -0
  59. package/lib/helpers/provenanceUtils.js +193 -0
  60. package/lib/helpers/provenanceUtils.poku.js +145 -0
  61. package/lib/helpers/pylockutils.js +281 -0
  62. package/lib/helpers/pylockutils.poku.js +48 -0
  63. package/lib/helpers/registryProvenance.js +793 -0
  64. package/lib/helpers/registryProvenance.poku.js +452 -0
  65. package/lib/helpers/source.js +1267 -0
  66. package/lib/helpers/source.poku.js +771 -0
  67. package/lib/helpers/spdxUtils.js +97 -0
  68. package/lib/helpers/spdxUtils.poku.js +70 -0
  69. package/lib/helpers/unicodeScan.js +147 -0
  70. package/lib/helpers/unicodeScan.poku.js +45 -0
  71. package/lib/helpers/utils.js +700 -128
  72. package/lib/helpers/utils.poku.js +877 -80
  73. package/lib/managers/binary.js +29 -5
  74. package/lib/managers/docker.js +179 -52
  75. package/lib/managers/docker.poku.js +327 -28
  76. package/lib/managers/oci.js +107 -23
  77. package/lib/managers/oci.poku.js +132 -0
  78. package/lib/server/openapi.yaml +17 -0
  79. package/lib/server/server.js +225 -336
  80. package/lib/server/server.poku.js +16 -10
  81. package/lib/stages/postgen/annotator.js +7 -0
  82. package/lib/stages/postgen/annotator.poku.js +40 -0
  83. package/lib/stages/postgen/auditBom.js +19 -3
  84. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  85. package/lib/stages/postgen/postgen.js +40 -0
  86. package/lib/stages/postgen/postgen.poku.js +47 -0
  87. package/lib/stages/postgen/ruleEngine.js +80 -2
  88. package/lib/stages/postgen/spdxConverter.js +796 -0
  89. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  90. package/lib/validator/bomValidator.js +232 -0
  91. package/lib/validator/bomValidator.poku.js +70 -0
  92. package/lib/validator/complianceRules.js +70 -7
  93. package/lib/validator/complianceRules.poku.js +30 -0
  94. package/lib/validator/reporters/annotations.js +2 -2
  95. package/lib/validator/reporters/console.js +11 -0
  96. package/lib/validator/reporters.poku.js +13 -0
  97. package/package.json +10 -7
  98. package/types/bin/audit.d.ts +3 -0
  99. package/types/bin/audit.d.ts.map +1 -0
  100. package/types/bin/convert.d.ts +3 -0
  101. package/types/bin/convert.d.ts.map +1 -0
  102. package/types/bin/repl.d.ts.map +1 -1
  103. package/types/lib/audit/index.d.ts +115 -0
  104. package/types/lib/audit/index.d.ts.map +1 -0
  105. package/types/lib/audit/progress.d.ts +27 -0
  106. package/types/lib/audit/progress.d.ts.map +1 -0
  107. package/types/lib/audit/reporters.d.ts +35 -0
  108. package/types/lib/audit/reporters.d.ts.map +1 -0
  109. package/types/lib/audit/scoring.d.ts +35 -0
  110. package/types/lib/audit/scoring.d.ts.map +1 -0
  111. package/types/lib/audit/targets.d.ts +63 -0
  112. package/types/lib/audit/targets.d.ts.map +1 -0
  113. package/types/lib/cli/index.d.ts +8 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/helpers/analyzer.d.ts +13 -0
  116. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  117. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  118. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  119. package/types/lib/helpers/bomUtils.d.ts +5 -0
  120. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  121. package/types/lib/helpers/chromextutils.d.ts +97 -0
  122. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  123. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  124. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  125. package/types/lib/helpers/containerRisk.d.ts +17 -0
  126. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  127. package/types/lib/helpers/display.d.ts +4 -1
  128. package/types/lib/helpers/display.d.ts.map +1 -1
  129. package/types/lib/helpers/exportUtils.d.ts +40 -0
  130. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  131. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  132. package/types/lib/helpers/gtfobins.d.ts +17 -0
  133. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  134. package/types/lib/helpers/lolbas.d.ts +16 -0
  135. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  136. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  137. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  138. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  139. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/pylockutils.d.ts +51 -0
  141. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  142. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  143. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  144. package/types/lib/helpers/source.d.ts +141 -0
  145. package/types/lib/helpers/source.d.ts.map +1 -0
  146. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  147. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  148. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  149. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  150. package/types/lib/helpers/utils.d.ts +29 -11
  151. package/types/lib/helpers/utils.d.ts.map +1 -1
  152. package/types/lib/managers/binary.d.ts.map +1 -1
  153. package/types/lib/managers/docker.d.ts.map +1 -1
  154. package/types/lib/managers/oci.d.ts.map +1 -1
  155. package/types/lib/server/server.d.ts +0 -36
  156. package/types/lib/server/server.d.ts.map +1 -1
  157. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  158. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  159. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  160. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  161. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  162. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  163. package/types/lib/validator/bomValidator.d.ts +1 -0
  164. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  165. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  166. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  167. package/types/bin/dependencies.d.ts +0 -3
  168. package/types/bin/dependencies.d.ts.map +0 -1
  169. package/types/bin/licenses.d.ts +0 -3
  170. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,493 @@
1
+ import { strict as assert } from "node:assert";
2
+ import {
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ readFileSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ import { describe, it } from "poku";
14
+
15
+ import {
16
+ CHROME_EXTENSION_PURL_TYPE,
17
+ collectChromeExtensionsFromPath,
18
+ collectInstalledChromeExtensions,
19
+ compareChromiumExtensionVersions,
20
+ getChromiumExtensionDirs,
21
+ getChromiumProfiles,
22
+ inferChromiumContextFromManifest,
23
+ parseChromiumExtensionManifest,
24
+ } from "./chromextutils.js";
25
+
26
+ const baseTempDir = mkdtempSync(join(tmpdir(), "cdxgen-chromext-poku-"));
27
+ const fixtureDir = join(
28
+ dirname(fileURLToPath(import.meta.url)),
29
+ "..",
30
+ "..",
31
+ "test",
32
+ "data",
33
+ "chrome-extensions",
34
+ );
35
+
36
+ function getProp(component, propName) {
37
+ return component?.properties?.find((prop) => prop.name === propName)?.value;
38
+ }
39
+ process.on("exit", () => {
40
+ try {
41
+ rmSync(baseTempDir, { recursive: true, force: true });
42
+ } catch (_e) {
43
+ // Ignore cleanup errors
44
+ }
45
+ });
46
+
47
+ describe("CHROME_EXTENSION_PURL_TYPE", () => {
48
+ it("should be chrome-extension", () => {
49
+ assert.strictEqual(CHROME_EXTENSION_PURL_TYPE, "chrome-extension");
50
+ });
51
+ });
52
+
53
+ describe("getChromiumExtensionDirs", () => {
54
+ it("should include expected browser entries", () => {
55
+ const dirs = getChromiumExtensionDirs();
56
+ assert.ok(Array.isArray(dirs));
57
+ assert.ok(dirs.length > 0);
58
+ const browsers = dirs.map((entry) => entry.browser);
59
+ assert.ok(browsers.includes("Google Chrome"));
60
+ assert.ok(browsers.includes("Chromium"));
61
+ assert.ok(browsers.includes("Microsoft Edge"));
62
+ assert.ok(browsers.includes("Brave"));
63
+ assert.ok(browsers.includes("Vivaldi"));
64
+ });
65
+ });
66
+
67
+ describe("compareChromiumExtensionVersions", () => {
68
+ it("should compare 1-4 segment numeric versions", () => {
69
+ assert.strictEqual(compareChromiumExtensionVersions("1", "1.0"), 0);
70
+ assert.ok(compareChromiumExtensionVersions("1.2.9", "1.2.10") < 0);
71
+ assert.ok(compareChromiumExtensionVersions("6.0.2.3611", "6.0.2.999") > 0);
72
+ assert.strictEqual(compareChromiumExtensionVersions("2.0", "2.0"), 0);
73
+ });
74
+ });
75
+
76
+ describe("getChromiumProfiles", () => {
77
+ it("should use Local State profile info_cache when available", () => {
78
+ const userData = join(baseTempDir, "profiles-local-state");
79
+ mkdirSync(join(userData, "Default", "Extensions"), { recursive: true });
80
+ mkdirSync(join(userData, "Profile 1", "Extensions"), { recursive: true });
81
+ writeFileSync(
82
+ join(userData, "Local State"),
83
+ JSON.stringify({
84
+ profile: {
85
+ info_cache: {
86
+ Default: { name: "Person 1" },
87
+ "Profile 1": { name: "Person 2" },
88
+ },
89
+ },
90
+ }),
91
+ "utf-8",
92
+ );
93
+ const profiles = getChromiumProfiles(userData);
94
+ assert.deepStrictEqual(profiles.sort(), ["Default", "Profile 1"]);
95
+ });
96
+
97
+ it("should fallback to Default/Profile* directories when Local State is missing", () => {
98
+ const userData = join(baseTempDir, "profiles-fallback");
99
+ mkdirSync(join(userData, "Default", "Extensions"), { recursive: true });
100
+ mkdirSync(join(userData, "Profile 2", "Extensions"), { recursive: true });
101
+ const profiles = getChromiumProfiles(userData);
102
+ assert.deepStrictEqual(profiles.sort(), ["Default", "Profile 2"]);
103
+ });
104
+ });
105
+
106
+ describe("parseChromiumExtensionManifest", () => {
107
+ it("should parse known manifest fields", () => {
108
+ const manifestPath = join(baseTempDir, "manifest-test.json");
109
+ writeFileSync(
110
+ manifestPath,
111
+ JSON.stringify({
112
+ manifest_version: 3,
113
+ name: "Example Extension",
114
+ description: "Sample description",
115
+ version: "1.2.3",
116
+ update_url: "https://example.invalid/update.xml",
117
+ minimum_edge_version: "125.0.0.0",
118
+ edge_url_overrides: { newtab: "edge-newtab.html" },
119
+ }),
120
+ "utf-8",
121
+ );
122
+ const parsed = parseChromiumExtensionManifest(manifestPath);
123
+ assert.strictEqual(parsed.name, "Example Extension");
124
+ assert.strictEqual(parsed.description, "Sample description");
125
+ assert.strictEqual(parsed.version, "1.2.3");
126
+ assert.strictEqual(parsed.manifestVersion, 3);
127
+ assert.strictEqual(parsed.updateUrl, "https://example.invalid/update.xml");
128
+ assert.deepStrictEqual(parsed.permissions, []);
129
+ assert.deepStrictEqual(parsed.optionalPermissions, []);
130
+ assert.deepStrictEqual(parsed.hostPermissions, []);
131
+ assert.deepStrictEqual(parsed.optionalHostPermissions, []);
132
+ assert.deepStrictEqual(parsed.commands, []);
133
+ assert.deepStrictEqual(parsed.contentScriptsRunAt, []);
134
+ assert.deepStrictEqual(parsed.webAccessibleResourceMatches, []);
135
+ assert.deepStrictEqual(parsed.externallyConnectableMatches, []);
136
+ assert.strictEqual(parsed.minimumChromeVersion, "");
137
+ assert.strictEqual(parsed.minimumEdgeVersion, "125.0.0.0");
138
+ assert.deepStrictEqual(parsed.edgeUrlOverrides, {
139
+ newtab: "edge-newtab.html",
140
+ });
141
+ assert.strictEqual(parsed.storageManagedSchema, "");
142
+ assert.strictEqual(parsed.hasAutofill, false);
143
+ });
144
+
145
+ it("should parse real manifest fixtures from Chrome, Chromium and Edge extensions", () => {
146
+ const fixtureCases = [
147
+ {
148
+ file: "chrome-bitwarden-manifest.json",
149
+ version: "2026.4.0",
150
+ manifestVersion: 2,
151
+ },
152
+ {
153
+ file: "chromium-ublock-origin-manifest.json",
154
+ version: "1.15.11.0",
155
+ manifestVersion: 2,
156
+ },
157
+ {
158
+ file: "edge-dark-reader-manifest.json",
159
+ version: "4.9.124",
160
+ manifestVersion: 2,
161
+ },
162
+ {
163
+ file: "edge-duckduckgo-manifest.json",
164
+ version: "2026.1.12",
165
+ manifestVersion: 3,
166
+ },
167
+ {
168
+ file: "brave-core-manifest.json",
169
+ version: "1.0.0",
170
+ manifestVersion: 2,
171
+ },
172
+ {
173
+ file: "chrome-youtube-summary-chatgpt-manifest.json",
174
+ version: "1.0.4",
175
+ manifestVersion: 3,
176
+ },
177
+ {
178
+ file: "chrome-agentbrain-chatgpt-claude-manifest.json",
179
+ version: "0.1.0",
180
+ manifestVersion: 3,
181
+ },
182
+ {
183
+ file: "chrome-copilottts-manifest.json",
184
+ version: "1.0.0",
185
+ manifestVersion: 3,
186
+ },
187
+ ];
188
+ for (const fixtureCase of fixtureCases) {
189
+ const parsed = parseChromiumExtensionManifest(
190
+ join(fixtureDir, fixtureCase.file),
191
+ );
192
+ assert.ok(parsed);
193
+ assert.strictEqual(parsed.version, fixtureCase.version);
194
+ assert.strictEqual(parsed.manifestVersion, fixtureCase.manifestVersion);
195
+ assert.ok(parsed.name);
196
+ }
197
+ });
198
+ });
199
+
200
+ describe("collectInstalledChromeExtensions", () => {
201
+ it("should select highest version and suppress duplicate components", () => {
202
+ const browserDir = join(baseTempDir, "browser-data");
203
+ const extId = "abcdefghijklmnopqrstuvwxzyabcdef";
204
+ const extensionBase = join(browserDir, "Default", "Extensions", extId);
205
+ mkdirSync(join(extensionBase, "1.0.0"), { recursive: true });
206
+ mkdirSync(join(extensionBase, "2.1.0"), { recursive: true });
207
+ writeFileSync(
208
+ join(extensionBase, "1.0.0", "manifest.json"),
209
+ JSON.stringify({
210
+ manifest_version: 3,
211
+ name: "Demo extension",
212
+ description: "Version 1",
213
+ version: "1.0.0",
214
+ }),
215
+ "utf-8",
216
+ );
217
+ writeFileSync(
218
+ join(extensionBase, "2.1.0", "manifest.json"),
219
+ JSON.stringify({
220
+ manifest_version: 3,
221
+ name: "Demo extension",
222
+ description: "Version 2",
223
+ version: "2.1.0",
224
+ }),
225
+ "utf-8",
226
+ );
227
+
228
+ const components = collectInstalledChromeExtensions([
229
+ { browser: "Google Chrome", channel: "stable", dir: browserDir },
230
+ { browser: "Google Chrome", channel: "stable", dir: browserDir },
231
+ ]);
232
+ assert.strictEqual(components.length, 1);
233
+ assert.strictEqual(components[0].name, extId);
234
+ assert.strictEqual(components[0].version, "2.1.0");
235
+ assert.strictEqual(
236
+ components[0].purl,
237
+ `pkg:chrome-extension/${extId}@2.1.0`,
238
+ );
239
+ });
240
+ });
241
+
242
+ describe("collectChromeExtensionsFromPath", () => {
243
+ it("should parse extension-id dir and choose highest available version", () => {
244
+ const extensionRoot = join(baseTempDir, "single-extension");
245
+ const extensionId = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
246
+ const extensionIdDir = join(extensionRoot, extensionId);
247
+ mkdirSync(join(extensionIdDir, "1.0.0"), { recursive: true });
248
+ mkdirSync(join(extensionIdDir, "1.2.0"), { recursive: true });
249
+ writeFileSync(
250
+ join(extensionIdDir, "1.0.0", "manifest.json"),
251
+ JSON.stringify({
252
+ manifest_version: 3,
253
+ name: "Sample One",
254
+ version: "1.0.0",
255
+ }),
256
+ "utf-8",
257
+ );
258
+ writeFileSync(
259
+ join(extensionIdDir, "1.2.0", "manifest.json"),
260
+ JSON.stringify({
261
+ manifest_version: 3,
262
+ name: "Sample Two",
263
+ version: "1.2.0",
264
+ }),
265
+ "utf-8",
266
+ );
267
+ const result = collectChromeExtensionsFromPath(extensionIdDir);
268
+ assert.strictEqual(result.components.length, 1);
269
+ assert.strictEqual(result.components[0].name, extensionId);
270
+ assert.strictEqual(result.components[0].version, "1.2.0");
271
+ assert.strictEqual(result.extensionDirs.length, 1);
272
+ assert.ok(result.extensionDirs[0].endsWith(join(extensionId, "1.2.0")));
273
+ });
274
+
275
+ it("should parse a real fixture from a version directory path", () => {
276
+ const extensionRoot = join(baseTempDir, "real-fixture-extension");
277
+ const extensionId = "cccccccccccccccccccccccccccccccc";
278
+ const extensionVersion = "4.9.124";
279
+ const versionDir = join(extensionRoot, extensionId, extensionVersion);
280
+ mkdirSync(versionDir, { recursive: true });
281
+ writeFileSync(
282
+ join(versionDir, "manifest.json"),
283
+ readFileSync(join(fixtureDir, "edge-dark-reader-manifest.json"), "utf-8"),
284
+ "utf-8",
285
+ );
286
+ const result = collectChromeExtensionsFromPath(versionDir);
287
+ assert.strictEqual(result.components.length, 1);
288
+ assert.strictEqual(
289
+ result.components[0].purl,
290
+ `pkg:chrome-extension/${extensionId}@${extensionVersion}`,
291
+ );
292
+ assert.strictEqual(
293
+ getProp(result.components[0], "cdx:chrome-extension:permissions"),
294
+ "alarms, fontSettings, storage, tabs, <all_urls>",
295
+ );
296
+ assert.strictEqual(
297
+ getProp(result.components[0], "cdx:chrome-extension:commands"),
298
+ "toggle, addSite, switchEngine",
299
+ );
300
+ assert.strictEqual(
301
+ getProp(result.components[0], "cdx:chrome-extension:contentScriptsRunAt"),
302
+ "document_start",
303
+ );
304
+ assert.strictEqual(
305
+ getProp(result.components[0], "cdx:chrome-extension:hasAutofill"),
306
+ undefined,
307
+ );
308
+ assert.strictEqual(
309
+ getProp(
310
+ result.components[0],
311
+ "cdx:chrome-extension:storageManagedSchema",
312
+ ),
313
+ undefined,
314
+ );
315
+ });
316
+
317
+ it("should capture security-sensitive properties from a real edge manifest", () => {
318
+ const extensionRoot = join(baseTempDir, "real-edge-fixture-extension");
319
+ const extensionId = "dddddddddddddddddddddddddddddddd";
320
+ const extensionVersion = "2026.1.12";
321
+ const versionDir = join(extensionRoot, extensionId, extensionVersion);
322
+ mkdirSync(versionDir, { recursive: true });
323
+ writeFileSync(
324
+ join(versionDir, "manifest.json"),
325
+ readFileSync(join(fixtureDir, "edge-duckduckgo-manifest.json"), "utf-8"),
326
+ "utf-8",
327
+ );
328
+ const result = collectChromeExtensionsFromPath(versionDir);
329
+ assert.strictEqual(result.components.length, 1);
330
+ assert.strictEqual(
331
+ getProp(result.components[0], "cdx:chrome-extension:hostPermissions"),
332
+ "*://*/*, <all_urls>",
333
+ );
334
+ assert.strictEqual(
335
+ getProp(
336
+ result.components[0],
337
+ "cdx:chrome-extension:contentScriptsMatches",
338
+ ),
339
+ "<all_urls>",
340
+ );
341
+ assert.strictEqual(
342
+ getProp(result.components[0], "cdx:chrome-extension:optionalPermissions"),
343
+ "browsingData",
344
+ );
345
+ assert.strictEqual(
346
+ getProp(result.components[0], "cdx:chrome-extension:contentScriptsRunAt"),
347
+ "document_start",
348
+ );
349
+ assert.strictEqual(
350
+ getProp(
351
+ result.components[0],
352
+ "cdx:chrome-extension:storageManagedSchema",
353
+ ),
354
+ "managed-schema.json",
355
+ );
356
+ assert.strictEqual(
357
+ getProp(result.components[0], "cdx:chrome-extension:hasAutofill"),
358
+ "true",
359
+ );
360
+ assert.strictEqual(
361
+ getProp(
362
+ result.components[0],
363
+ "cdx:chrome-extension:minimumChromeVersion",
364
+ ),
365
+ "128.0",
366
+ );
367
+ assert.strictEqual(
368
+ getProp(
369
+ result.components[0],
370
+ "cdx:chrome-extension:optionalHostPermissions",
371
+ ),
372
+ undefined,
373
+ );
374
+ assert.strictEqual(
375
+ getProp(
376
+ result.components[0],
377
+ "cdx:chrome-extension:webAccessibleResourceMatches",
378
+ ),
379
+ "<all_urls>",
380
+ );
381
+ assert.strictEqual(
382
+ getProp(result.components[0], "cdx:chrome-extension:capability:network"),
383
+ "true",
384
+ );
385
+ assert.strictEqual(
386
+ getProp(
387
+ result.components[0],
388
+ "cdx:chrome-extension:capability:codeInjection",
389
+ ),
390
+ "true",
391
+ );
392
+ });
393
+
394
+ it("should capture brave-specific manifest fields with explicit property names", () => {
395
+ const extensionRoot = join(baseTempDir, "real-brave-fixture-extension");
396
+ const extensionId = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
397
+ const extensionVersion = "1.0.0";
398
+ const versionDir = join(extensionRoot, extensionId, extensionVersion);
399
+ mkdirSync(versionDir, { recursive: true });
400
+ writeFileSync(
401
+ join(versionDir, "manifest.json"),
402
+ readFileSync(join(fixtureDir, "brave-core-manifest.json"), "utf-8"),
403
+ "utf-8",
404
+ );
405
+ const result = collectChromeExtensionsFromPath(versionDir);
406
+ assert.strictEqual(result.components.length, 1);
407
+ assert.strictEqual(
408
+ getProp(
409
+ result.components[0],
410
+ "cdx:chrome-extension:brave:maybeBackground",
411
+ ),
412
+ "true",
413
+ );
414
+ assert.strictEqual(
415
+ getProp(result.components[0], "cdx:chrome-extension:brave:permissions"),
416
+ "settingsPrivate, webDiscovery",
417
+ );
418
+ });
419
+
420
+ it("should capture AI extension fixture properties for OpenAI/Anthropic/Copilot scenarios", () => {
421
+ const cases = [
422
+ {
423
+ fixture: "chrome-youtube-summary-chatgpt-manifest.json",
424
+ extensionId: "ffffffffffffffffffffffffffffffff",
425
+ extensionVersion: "1.0.4",
426
+ checks: [
427
+ ["cdx:chrome-extension:contentScriptsRunAt", undefined],
428
+ ["cdx:chrome-extension:capabilities", "codeInjection"],
429
+ ["cdx:chrome-extension:capability:codeInjection", "true"],
430
+ ],
431
+ },
432
+ {
433
+ fixture: "chrome-agentbrain-chatgpt-claude-manifest.json",
434
+ extensionId: "gggggggggggggggggggggggggggggggg",
435
+ extensionVersion: "0.1.0",
436
+ checks: [
437
+ [
438
+ "cdx:chrome-extension:hostPermissions",
439
+ "https://chat.openai.com/*, https://chatgpt.com/*, https://claude.ai/*, https://gemini.google.com/*, https://www.perplexity.ai/*, https://api.agentbrain.ch/*",
440
+ ],
441
+ ["cdx:chrome-extension:contentScriptsRunAt", "document_idle"],
442
+ ["cdx:chrome-extension:capabilities", "codeInjection"],
443
+ ["cdx:chrome-extension:capability:codeInjection", "true"],
444
+ ],
445
+ },
446
+ {
447
+ fixture: "chrome-copilottts-manifest.json",
448
+ extensionId: "hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh",
449
+ extensionVersion: "1.0.0",
450
+ checks: [
451
+ ["cdx:chrome-extension:permissions", "activeTab, scripting, storage"],
452
+ ["cdx:chrome-extension:contentScriptsRunAt", "document_idle"],
453
+ ["cdx:chrome-extension:capabilities", "codeInjection"],
454
+ ["cdx:chrome-extension:capability:codeInjection", "true"],
455
+ ],
456
+ },
457
+ ];
458
+ for (const fixtureCase of cases) {
459
+ const extensionRoot = join(
460
+ baseTempDir,
461
+ `ai-extension-${fixtureCase.extensionId}`,
462
+ );
463
+ const versionDir = join(
464
+ extensionRoot,
465
+ fixtureCase.extensionId,
466
+ fixtureCase.extensionVersion,
467
+ );
468
+ mkdirSync(versionDir, { recursive: true });
469
+ writeFileSync(
470
+ join(versionDir, "manifest.json"),
471
+ readFileSync(join(fixtureDir, fixtureCase.fixture), "utf-8"),
472
+ "utf-8",
473
+ );
474
+ const result = collectChromeExtensionsFromPath(versionDir);
475
+ assert.strictEqual(result.components.length, 1);
476
+ for (const [propName, expectedValue] of fixtureCase.checks) {
477
+ assert.strictEqual(
478
+ getProp(result.components[0], propName),
479
+ expectedValue,
480
+ `${fixtureCase.fixture} expected ${propName}`,
481
+ );
482
+ }
483
+ }
484
+ });
485
+ });
486
+
487
+ describe("inferChromiumContextFromManifest", () => {
488
+ it("should return empty context for paths outside known browser roots", () => {
489
+ const manifestPath = join(baseTempDir, "outside", "manifest.json");
490
+ const context = inferChromiumContextFromManifest(manifestPath);
491
+ assert.deepStrictEqual(context, {});
492
+ });
493
+ });