@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.
- package/README.md +242 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +532 -168
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +276 -68
- package/lib/cli/index.poku.js +368 -0
- package/lib/helpers/analyzer.js +1052 -5
- package/lib/helpers/analyzer.poku.js +301 -0
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/depsUtils.js +16 -0
- package/lib/helpers/depsUtils.poku.js +58 -1
- package/lib/helpers/display.js +245 -61
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +882 -136
- package/lib/helpers/utils.poku.js +995 -91
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +50 -0
- package/lib/server/server.js +228 -331
- package/lib/server/server.poku.js +220 -5
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +20 -5
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +13 -2
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -8
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
- package/types/lib/helpers/table.d.ts +6 -0
- package/types/lib/helpers/table.d.ts.map +1 -0
- package/types/lib/helpers/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +30 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -35
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- 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
|
+
}
|