@cyclonedx/cdxgen 12.3.3 → 12.4.1
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 +69 -25
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +270 -127
- package/bin/convert.js +34 -15
- package/bin/hbom.js +495 -0
- package/bin/repl.js +592 -37
- package/bin/validate.js +31 -4
- package/bin/verify.js +18 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -0
- package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
- package/data/predictive-audit-allowlist.json +11 -0
- package/data/queries-darwin.json +12 -1
- package/data/queries-win.json +7 -1
- package/data/queries.json +39 -2
- package/data/rules/ai-agent-governance.yaml +16 -0
- package/data/rules/asar-archives.yaml +150 -0
- package/data/rules/chrome-extensions.yaml +8 -0
- package/data/rules/ci-permissions.yaml +42 -18
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +11 -0
- package/data/rules/hbom-compliance.yaml +325 -0
- package/data/rules/hbom-performance.yaml +307 -0
- package/data/rules/hbom-security.yaml +248 -0
- package/data/rules/host-topology.yaml +165 -0
- package/data/rules/mcp-servers.yaml +18 -3
- package/data/rules/obom-runtime.yaml +907 -22
- package/data/rules/package-integrity.yaml +14 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +210 -8
- package/lib/audit/index.poku.js +332 -0
- package/lib/audit/reporters.js +222 -0
- package/lib/audit/targets.js +146 -1
- package/lib/audit/targets.poku.js +186 -0
- package/lib/cli/asar.poku.js +328 -0
- package/lib/cli/index.js +527 -99
- package/lib/cli/index.poku.js +1469 -212
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/analyzer.js +1406 -29
- package/lib/helpers/analyzer.poku.js +342 -0
- package/lib/helpers/analyzerScope.js +712 -0
- package/lib/helpers/asarutils.js +1556 -0
- package/lib/helpers/asarutils.poku.js +443 -0
- package/lib/helpers/auditCategories.js +12 -0
- package/lib/helpers/auditCategories.poku.js +32 -0
- package/lib/helpers/bomUtils.js +155 -1
- package/lib/helpers/bomUtils.poku.js +79 -1
- package/lib/helpers/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/display.js +291 -1
- package/lib/helpers/display.poku.js +149 -0
- package/lib/helpers/evidenceUtils.js +58 -0
- package/lib/helpers/evidenceUtils.poku.js +54 -0
- package/lib/helpers/exportUtils.js +9 -0
- package/lib/helpers/gtfobins.js +142 -8
- package/lib/helpers/gtfobins.poku.js +24 -1
- package/lib/helpers/hbom.js +710 -0
- package/lib/helpers/hbom.poku.js +496 -0
- package/lib/helpers/hbomAnalysis.js +268 -0
- package/lib/helpers/hbomAnalysis.poku.js +249 -0
- package/lib/helpers/hbomLoader.js +35 -0
- package/lib/helpers/hostTopology.js +803 -0
- package/lib/helpers/hostTopology.poku.js +363 -0
- package/lib/helpers/inventoryStats.js +69 -0
- package/lib/helpers/inventoryStats.poku.js +86 -0
- package/lib/helpers/lolbas.js +19 -1
- package/lib/helpers/lolbas.poku.js +23 -0
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +350 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/protobom.js +209 -45
- package/lib/helpers/protobom.poku.js +183 -5
- package/lib/helpers/protobomLoader.js +43 -0
- package/lib/helpers/protobomLoader.poku.js +31 -0
- package/lib/helpers/remote/dependency-track.js +36 -3
- package/lib/helpers/remote/dependency-track.poku.js +44 -0
- package/lib/helpers/source.js +24 -0
- package/lib/helpers/source.poku.js +32 -0
- package/lib/helpers/utils.js +1438 -93
- package/lib/helpers/utils.poku.js +846 -4
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2293 -353
- package/lib/managers/binary.poku.js +1699 -1
- package/lib/managers/docker.js +201 -79
- package/lib/managers/docker.poku.js +337 -12
- package/lib/server/server.js +4 -28
- package/lib/stages/postgen/annotator.js +38 -0
- package/lib/stages/postgen/annotator.poku.js +107 -1
- package/lib/stages/postgen/auditBom.js +121 -18
- package/lib/stages/postgen/auditBom.poku.js +1366 -31
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +406 -8
- package/lib/stages/postgen/postgen.poku.js +484 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- package/lib/validator/bomValidator.js +90 -38
- package/lib/validator/bomValidator.poku.js +90 -0
- package/lib/validator/complianceRules.js +4 -2
- package/lib/validator/index.poku.js +14 -0
- package/package.json +23 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- package/types/bin/repl.d.ts +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +44 -0
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts +16 -0
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +16 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +4 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +33 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/analyzerScope.d.ts +11 -0
- package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
- package/types/lib/helpers/asarutils.d.ts +34 -0
- package/types/lib/helpers/asarutils.d.ts.map +1 -0
- package/types/lib/helpers/auditCategories.d.ts +5 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -1
- package/types/lib/helpers/bomUtils.d.ts +10 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/evidenceUtils.d.ts +8 -0
- package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +8 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -1
- package/types/lib/helpers/hbom.d.ts +49 -0
- package/types/lib/helpers/hbom.d.ts.map +1 -0
- package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
- package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
- package/types/lib/helpers/hbomLoader.d.ts +7 -0
- package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
- package/types/lib/helpers/hostTopology.d.ts +12 -0
- package/types/lib/helpers/hostTopology.d.ts.map +1 -0
- package/types/lib/helpers/inventoryStats.d.ts +11 -0
- package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -1
- package/types/lib/helpers/osqueryTransform.d.ts +3 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts +58 -0
- package/types/lib/helpers/plugins.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +5 -4
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/protobomLoader.d.ts +17 -0
- package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +45 -8
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +5 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -1
- 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 +26 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +2 -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/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
- package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/data/spdx-model-v3.0.1.jsonld +0 -15999
|
@@ -0,0 +1,1556 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
openSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readSync,
|
|
8
|
+
statSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
12
|
+
import process from "node:process";
|
|
13
|
+
|
|
14
|
+
import { PackageURL } from "packageurl-js";
|
|
15
|
+
import { xml2js } from "xml-js";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
analyzeJsCapabilitiesSource,
|
|
19
|
+
analyzeSuspiciousJsSource,
|
|
20
|
+
} from "./analyzer.js";
|
|
21
|
+
import { thoughtLog } from "./logger.js";
|
|
22
|
+
import { sanitizeBomPropertyValue } from "./propertySanitizer.js";
|
|
23
|
+
import {
|
|
24
|
+
DEBUG_MODE,
|
|
25
|
+
getTmpDir,
|
|
26
|
+
isDryRun,
|
|
27
|
+
recordActivity,
|
|
28
|
+
safeCopyFileSync,
|
|
29
|
+
safeExistsSync,
|
|
30
|
+
safeExtractArchive,
|
|
31
|
+
safeMkdirSync,
|
|
32
|
+
safeMkdtempSync,
|
|
33
|
+
safeRmSync,
|
|
34
|
+
safeWriteSync,
|
|
35
|
+
} from "./utils.js";
|
|
36
|
+
|
|
37
|
+
const ASAR_JS_ANALYSIS_EXTENSIONS = new Set([
|
|
38
|
+
".cjs",
|
|
39
|
+
".cts",
|
|
40
|
+
".js",
|
|
41
|
+
".jsx",
|
|
42
|
+
".mjs",
|
|
43
|
+
".mts",
|
|
44
|
+
".ts",
|
|
45
|
+
".tsx",
|
|
46
|
+
]);
|
|
47
|
+
const ASAR_LOCKFILE_NAMES = new Set([
|
|
48
|
+
"npm-shrinkwrap.json",
|
|
49
|
+
"package-lock.json",
|
|
50
|
+
"pnpm-lock.yaml",
|
|
51
|
+
"yarn.lock",
|
|
52
|
+
]);
|
|
53
|
+
const ASAR_LIFECYCLE_SCRIPT_NAMES = new Set([
|
|
54
|
+
"install",
|
|
55
|
+
"postinstall",
|
|
56
|
+
"preinstall",
|
|
57
|
+
"prepare",
|
|
58
|
+
"prepublish",
|
|
59
|
+
]);
|
|
60
|
+
const PICKLE_UINT32_SIZE = 4;
|
|
61
|
+
const PICKLE_SIZE_PICKLE_BYTES = 8;
|
|
62
|
+
const MAX_ASAR_HEADER_BYTES = 64 * 1024 * 1024;
|
|
63
|
+
const MAX_ASAR_ENTRY_BYTES = 256 * 1024 * 1024;
|
|
64
|
+
const MAX_ASAR_OFFSET = BigInt(Number.MAX_SAFE_INTEGER);
|
|
65
|
+
const MAX_ASAR_HEADER_DEPTH = 256;
|
|
66
|
+
const MAX_ELECTRON_APP_BUNDLE_SEARCH_DEPTH = 20;
|
|
67
|
+
|
|
68
|
+
function addSanitizedProperty(properties, name, value) {
|
|
69
|
+
if (value === undefined || value === null || value === "") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const sanitizedValue = sanitizeBomPropertyValue(name, value);
|
|
73
|
+
if (
|
|
74
|
+
sanitizedValue === undefined ||
|
|
75
|
+
sanitizedValue === null ||
|
|
76
|
+
sanitizedValue === ""
|
|
77
|
+
) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
properties.push({
|
|
81
|
+
name,
|
|
82
|
+
value: String(sanitizedValue),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toArchiveOccurrence(archivePath, entryPath) {
|
|
87
|
+
return `${archivePath}#/${entryPath.replaceAll("\\", "/")}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeArchiveRelativePath(entryPath) {
|
|
91
|
+
return String(entryPath || "")
|
|
92
|
+
.replaceAll("\\", "/")
|
|
93
|
+
.replace(/^\/+/, "");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isPathWithin(baseDir, candidatePath) {
|
|
97
|
+
const relativePath = relative(resolve(baseDir), resolve(candidatePath))
|
|
98
|
+
.replaceAll("\\", "/")
|
|
99
|
+
.replace(/^\/+/, "");
|
|
100
|
+
return (
|
|
101
|
+
relativePath === "" ||
|
|
102
|
+
(!relativePath.startsWith("..") && !relativePath.split("/").includes(".."))
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseScopedPackageName(packageName) {
|
|
107
|
+
if (!packageName || typeof packageName !== "string") {
|
|
108
|
+
return { group: "", name: "" };
|
|
109
|
+
}
|
|
110
|
+
if (packageName.startsWith("@")) {
|
|
111
|
+
const [group, ...nameParts] = packageName.slice(1).split("/");
|
|
112
|
+
return {
|
|
113
|
+
group,
|
|
114
|
+
name: nameParts.join("/"),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { group: "", name: packageName };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createAsarPackagePurl(packageName, version) {
|
|
121
|
+
const parsedName = parseScopedPackageName(packageName);
|
|
122
|
+
if (!parsedName.name) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return new PackageURL(
|
|
126
|
+
"npm",
|
|
127
|
+
parsedName.group || null,
|
|
128
|
+
parsedName.name,
|
|
129
|
+
version || null,
|
|
130
|
+
null,
|
|
131
|
+
null,
|
|
132
|
+
).toString();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createGenericArchivePurl(archivePath, version) {
|
|
136
|
+
return new PackageURL(
|
|
137
|
+
"generic",
|
|
138
|
+
null,
|
|
139
|
+
basename(archivePath, ".asar"),
|
|
140
|
+
version || null,
|
|
141
|
+
{ type: "asar" },
|
|
142
|
+
null,
|
|
143
|
+
).toString();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseAsarJson(headerString) {
|
|
147
|
+
return JSON.parse(headerString, (_key, value) => {
|
|
148
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
const nullPrototypeObject = Object.create(null);
|
|
152
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
153
|
+
nullPrototypeObject[entryKey] = entryValue;
|
|
154
|
+
}
|
|
155
|
+
return nullPrototypeObject;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function validateHeaderEntry(entry, entryPath, depth = 0) {
|
|
160
|
+
if (depth > MAX_ASAR_HEADER_DEPTH) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`ASAR header nesting exceeds ${MAX_ASAR_HEADER_DEPTH} levels at ${entryPath || "/"}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
166
|
+
throw new Error(`Invalid ASAR entry at ${entryPath}`);
|
|
167
|
+
}
|
|
168
|
+
const hasFiles = Object.hasOwn(entry, "files");
|
|
169
|
+
const hasLink = Object.hasOwn(entry, "link");
|
|
170
|
+
const hasOffset = Object.hasOwn(entry, "offset");
|
|
171
|
+
const hasSize = Object.hasOwn(entry, "size");
|
|
172
|
+
if (hasLink) {
|
|
173
|
+
if (
|
|
174
|
+
typeof entry.link !== "string" ||
|
|
175
|
+
!entry.link ||
|
|
176
|
+
hasFiles ||
|
|
177
|
+
entry.unpacked === true ||
|
|
178
|
+
hasOffset ||
|
|
179
|
+
hasSize
|
|
180
|
+
) {
|
|
181
|
+
throw new Error(`Invalid ASAR symlink entry at ${entryPath}`);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (hasFiles) {
|
|
186
|
+
if (
|
|
187
|
+
!entry.files ||
|
|
188
|
+
typeof entry.files !== "object" ||
|
|
189
|
+
Array.isArray(entry.files) ||
|
|
190
|
+
entry.unpacked === true ||
|
|
191
|
+
hasOffset ||
|
|
192
|
+
hasSize
|
|
193
|
+
) {
|
|
194
|
+
throw new Error(`Invalid ASAR directory entry at ${entryPath}`);
|
|
195
|
+
}
|
|
196
|
+
for (const [name, child] of Object.entries(entry.files)) {
|
|
197
|
+
if (
|
|
198
|
+
!name ||
|
|
199
|
+
name === "." ||
|
|
200
|
+
name === ".." ||
|
|
201
|
+
name === "__proto__" ||
|
|
202
|
+
name === "constructor" ||
|
|
203
|
+
name === "prototype" ||
|
|
204
|
+
name.includes("/") ||
|
|
205
|
+
name.includes("\\")
|
|
206
|
+
) {
|
|
207
|
+
throw new Error(`Invalid ASAR child name "${name}" at ${entryPath}`);
|
|
208
|
+
}
|
|
209
|
+
validateHeaderEntry(child, `${entryPath}/${name}`, depth + 1);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (entry.unpacked === true) {
|
|
214
|
+
if (
|
|
215
|
+
hasOffset ||
|
|
216
|
+
typeof entry.size !== "number" ||
|
|
217
|
+
!Number.isSafeInteger(entry.size) ||
|
|
218
|
+
entry.size < 0 ||
|
|
219
|
+
entry.size > MAX_ASAR_ENTRY_BYTES
|
|
220
|
+
) {
|
|
221
|
+
throw new Error(`Invalid ASAR unpacked file entry at ${entryPath}`);
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (
|
|
226
|
+
typeof entry.offset !== "string" ||
|
|
227
|
+
!/^\d+$/.test(entry.offset) ||
|
|
228
|
+
typeof entry.size !== "number" ||
|
|
229
|
+
!Number.isSafeInteger(entry.size) ||
|
|
230
|
+
entry.size < 0 ||
|
|
231
|
+
entry.size > MAX_ASAR_ENTRY_BYTES
|
|
232
|
+
) {
|
|
233
|
+
throw new Error(`Invalid ASAR file entry at ${entryPath}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function parseAsarHeaderString(headerBuffer) {
|
|
238
|
+
if (headerBuffer.length < PICKLE_UINT32_SIZE * 2) {
|
|
239
|
+
throw new Error("ASAR header pickle is too small.");
|
|
240
|
+
}
|
|
241
|
+
const payloadSize = headerBuffer.readUInt32LE(0);
|
|
242
|
+
if (payloadSize > headerBuffer.length - PICKLE_UINT32_SIZE) {
|
|
243
|
+
throw new Error("ASAR header payload exceeds archive header size.");
|
|
244
|
+
}
|
|
245
|
+
const stringLength = headerBuffer.readInt32LE(PICKLE_UINT32_SIZE);
|
|
246
|
+
if (stringLength < 0 || stringLength > payloadSize - PICKLE_UINT32_SIZE) {
|
|
247
|
+
throw new Error("ASAR header string length is invalid.");
|
|
248
|
+
}
|
|
249
|
+
return headerBuffer.toString(
|
|
250
|
+
"utf8",
|
|
251
|
+
PICKLE_UINT32_SIZE * 2,
|
|
252
|
+
PICKLE_UINT32_SIZE * 2 + stringLength,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function readAsarArchiveHeaderSync(archivePath) {
|
|
257
|
+
const fd = openSync(archivePath, "r");
|
|
258
|
+
try {
|
|
259
|
+
const sizeBuffer = Buffer.alloc(PICKLE_SIZE_PICKLE_BYTES);
|
|
260
|
+
const sizeRead = readSync(fd, sizeBuffer, 0, PICKLE_SIZE_PICKLE_BYTES, 0);
|
|
261
|
+
if (sizeRead !== PICKLE_SIZE_PICKLE_BYTES) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Unable to read ASAR header size from ${archivePath}: expected ${PICKLE_SIZE_PICKLE_BYTES} bytes, got ${sizeRead}`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const headerPickleSize = sizeBuffer.readUInt32LE(PICKLE_UINT32_SIZE);
|
|
267
|
+
if (
|
|
268
|
+
headerPickleSize < PICKLE_UINT32_SIZE * 2 ||
|
|
269
|
+
headerPickleSize > MAX_ASAR_HEADER_BYTES
|
|
270
|
+
) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Unsupported ASAR header size ${headerPickleSize} for ${archivePath}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const headerBuffer = Buffer.alloc(headerPickleSize);
|
|
276
|
+
const headerRead = readSync(
|
|
277
|
+
fd,
|
|
278
|
+
headerBuffer,
|
|
279
|
+
0,
|
|
280
|
+
headerPickleSize,
|
|
281
|
+
PICKLE_SIZE_PICKLE_BYTES,
|
|
282
|
+
);
|
|
283
|
+
if (headerRead !== headerPickleSize) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Unable to read ASAR header from ${archivePath}: expected ${headerPickleSize} bytes, got ${headerRead}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const headerString = parseAsarHeaderString(headerBuffer);
|
|
289
|
+
const header = parseAsarJson(headerString);
|
|
290
|
+
if (!header?.files || typeof header.files !== "object") {
|
|
291
|
+
throw new Error(`Invalid ASAR header root for ${archivePath}`);
|
|
292
|
+
}
|
|
293
|
+
validateHeaderEntry(header, "", 0);
|
|
294
|
+
return {
|
|
295
|
+
archiveDataOffset: BigInt(PICKLE_SIZE_PICKLE_BYTES + headerPickleSize),
|
|
296
|
+
header,
|
|
297
|
+
headerSize: headerPickleSize,
|
|
298
|
+
headerString,
|
|
299
|
+
};
|
|
300
|
+
} finally {
|
|
301
|
+
closeSync(fd);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function listAsarEntries(archivePath) {
|
|
306
|
+
const parsedHeader = readAsarArchiveHeaderSync(archivePath);
|
|
307
|
+
const entries = [];
|
|
308
|
+
const visitEntries = (filesNode, currentPath = "") => {
|
|
309
|
+
for (const [name, child] of Object.entries(filesNode || {})) {
|
|
310
|
+
const childPath = currentPath ? `${currentPath}/${name}` : name;
|
|
311
|
+
if (child?.files) {
|
|
312
|
+
entries.push({
|
|
313
|
+
path: childPath,
|
|
314
|
+
type: "directory",
|
|
315
|
+
unpacked: child.unpacked === true,
|
|
316
|
+
});
|
|
317
|
+
visitEntries(child.files, childPath);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (child?.link) {
|
|
321
|
+
entries.push({
|
|
322
|
+
link: child.link,
|
|
323
|
+
path: childPath,
|
|
324
|
+
type: "link",
|
|
325
|
+
unpacked: child.unpacked === true,
|
|
326
|
+
});
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
entries.push({
|
|
330
|
+
executable: child?.executable === true,
|
|
331
|
+
integrity: child?.integrity,
|
|
332
|
+
offset: child?.offset,
|
|
333
|
+
path: childPath,
|
|
334
|
+
size: Number(child?.size || 0),
|
|
335
|
+
type: "file",
|
|
336
|
+
unpacked: child?.unpacked === true,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
visitEntries(parsedHeader.header.files);
|
|
341
|
+
return {
|
|
342
|
+
...parsedHeader,
|
|
343
|
+
entries: entries.sort((left, right) => left.path.localeCompare(right.path)),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function resolveUnpackedEntryPath(archivePath, entryPath) {
|
|
348
|
+
const unpackedBaseDir = `${archivePath}.unpacked`;
|
|
349
|
+
const normalizedEntryPath = normalizeArchiveRelativePath(entryPath);
|
|
350
|
+
const resolvedEntryPath = resolve(
|
|
351
|
+
unpackedBaseDir,
|
|
352
|
+
...normalizedEntryPath.split("/"),
|
|
353
|
+
);
|
|
354
|
+
if (!isPathWithin(unpackedBaseDir, resolvedEntryPath)) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Unpacked ASAR entry path escapes archive root: ${normalizedEntryPath}`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return resolvedEntryPath;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function resolveArchiveLinkPath(entryPath, linkTarget) {
|
|
363
|
+
if (!linkTarget || typeof linkTarget !== "string") {
|
|
364
|
+
throw new Error(`Invalid ASAR symlink target for ${entryPath}`);
|
|
365
|
+
}
|
|
366
|
+
const normalizedEntryPath = normalizeArchiveRelativePath(entryPath);
|
|
367
|
+
const archiveRoot = "/__asar_root__";
|
|
368
|
+
const resolvedLinkPath = resolve(
|
|
369
|
+
archiveRoot,
|
|
370
|
+
dirname(normalizedEntryPath),
|
|
371
|
+
linkTarget,
|
|
372
|
+
);
|
|
373
|
+
if (!isPathWithin(archiveRoot, resolvedLinkPath)) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`ASAR symlink ${entryPath} target escapes archive root: ${linkTarget}`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const archiveRelativeLinkPath = normalizeArchiveRelativePath(
|
|
379
|
+
relative(archiveRoot, resolvedLinkPath),
|
|
380
|
+
);
|
|
381
|
+
if (!archiveRelativeLinkPath || archiveRelativeLinkPath.startsWith("..")) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`ASAR symlink ${entryPath} target escapes archive root: ${linkTarget}`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return archiveRelativeLinkPath;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function validateArchiveSymlinkEntries(entries) {
|
|
390
|
+
const symlinkTargets = new Map();
|
|
391
|
+
for (const entry of entries) {
|
|
392
|
+
if (entry.type !== "link") {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
symlinkTargets.set(
|
|
396
|
+
normalizeArchiveRelativePath(entry.path),
|
|
397
|
+
resolveArchiveLinkPath(entry.path, entry.link),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const visitedPaths = new Set();
|
|
401
|
+
const visitingPaths = new Set();
|
|
402
|
+
const detectCycle = (entryPath) => {
|
|
403
|
+
if (visitedPaths.has(entryPath)) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (visitingPaths.has(entryPath)) {
|
|
407
|
+
throw new Error(`Circular ASAR symlink chain detected at ${entryPath}`);
|
|
408
|
+
}
|
|
409
|
+
const linkTarget = symlinkTargets.get(entryPath);
|
|
410
|
+
if (!linkTarget) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
visitingPaths.add(entryPath);
|
|
414
|
+
detectCycle(linkTarget);
|
|
415
|
+
visitingPaths.delete(entryPath);
|
|
416
|
+
visitedPaths.add(entryPath);
|
|
417
|
+
};
|
|
418
|
+
for (const entryPath of symlinkTargets.keys()) {
|
|
419
|
+
detectCycle(entryPath);
|
|
420
|
+
}
|
|
421
|
+
return symlinkTargets;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function readPackedEntryBuffer(archivePath, archiveDataOffset, entry, fd) {
|
|
425
|
+
const archiveFd = fd ?? openSync(archivePath, "r");
|
|
426
|
+
try {
|
|
427
|
+
if (!Number.isSafeInteger(entry.size) || entry.size < 0) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`Invalid packed ASAR entry size ${entry.size} for ${entry.path}`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (entry.size > MAX_ASAR_ENTRY_BYTES) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`ASAR entry ${entry.path} exceeds the maximum supported size of ${MAX_ASAR_ENTRY_BYTES} bytes`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const absoluteOffset =
|
|
438
|
+
archiveDataOffset + BigInt(String(entry.offset || "0"));
|
|
439
|
+
if (absoluteOffset > MAX_ASAR_OFFSET) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`ASAR entry ${entry.path} offset exceeds the safe read limit`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
const buffer = Buffer.alloc(entry.size);
|
|
445
|
+
const bytesRead = readSync(
|
|
446
|
+
archiveFd,
|
|
447
|
+
buffer,
|
|
448
|
+
0,
|
|
449
|
+
entry.size,
|
|
450
|
+
Number(absoluteOffset),
|
|
451
|
+
);
|
|
452
|
+
if (bytesRead !== entry.size) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Unable to read complete ASAR entry ${entry.path} from ${archivePath}: expected ${entry.size} bytes, got ${bytesRead}`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
return buffer;
|
|
458
|
+
} finally {
|
|
459
|
+
if (fd === undefined) {
|
|
460
|
+
closeSync(archiveFd);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function readAsarEntryBufferSync(archivePath, archiveDataOffset, entry, fd) {
|
|
466
|
+
if (entry.unpacked) {
|
|
467
|
+
return readFileSync(resolveUnpackedEntryPath(archivePath, entry.path));
|
|
468
|
+
}
|
|
469
|
+
return readPackedEntryBuffer(archivePath, archiveDataOffset, entry, fd);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function sha256Buffer(buffer) {
|
|
473
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function readXmlText(node) {
|
|
477
|
+
return (node?.elements || [])
|
|
478
|
+
.filter((child) => ["text", "cdata"].includes(child?.type))
|
|
479
|
+
.map((child) => child.text || child.cdata || "")
|
|
480
|
+
.join("");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function parsePlistElement(node) {
|
|
484
|
+
if (!node || node.type !== "element") {
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
switch (node.name) {
|
|
488
|
+
case "array":
|
|
489
|
+
return (node.elements || [])
|
|
490
|
+
.filter((child) => child?.type === "element")
|
|
491
|
+
.map((child) => parsePlistElement(child));
|
|
492
|
+
case "data":
|
|
493
|
+
case "date":
|
|
494
|
+
case "string":
|
|
495
|
+
return readXmlText(node);
|
|
496
|
+
case "dict": {
|
|
497
|
+
const plistObject = Object.create(null);
|
|
498
|
+
const childElements = (node.elements || []).filter(
|
|
499
|
+
(child) => child?.type === "element",
|
|
500
|
+
);
|
|
501
|
+
for (let index = 0; index < childElements.length - 1; index += 2) {
|
|
502
|
+
const keyElement = childElements[index];
|
|
503
|
+
const valueElement = childElements[index + 1];
|
|
504
|
+
if (keyElement?.name !== "key" || !valueElement) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const keyName = readXmlText(keyElement);
|
|
508
|
+
if (!keyName) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
plistObject[keyName] = parsePlistElement(valueElement);
|
|
512
|
+
}
|
|
513
|
+
return plistObject;
|
|
514
|
+
}
|
|
515
|
+
case "false":
|
|
516
|
+
return false;
|
|
517
|
+
case "integer":
|
|
518
|
+
return Number.parseInt(readXmlText(node), 10);
|
|
519
|
+
case "true":
|
|
520
|
+
return true;
|
|
521
|
+
default:
|
|
522
|
+
return readXmlText(node);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function parsePlistFile(plistPath) {
|
|
527
|
+
const plistXml = readFileSync(plistPath, "utf8");
|
|
528
|
+
const plistJson = xml2js(plistXml, {
|
|
529
|
+
compact: false,
|
|
530
|
+
ignoreCdata: false,
|
|
531
|
+
ignoreComment: true,
|
|
532
|
+
ignoreDoctype: true,
|
|
533
|
+
ignoreInstruction: true,
|
|
534
|
+
trim: true,
|
|
535
|
+
});
|
|
536
|
+
const plistElement = plistJson?.elements?.find(
|
|
537
|
+
(element) => element?.type === "element" && element?.name === "plist",
|
|
538
|
+
);
|
|
539
|
+
const rootElement = plistElement?.elements?.find(
|
|
540
|
+
(element) => element?.type === "element",
|
|
541
|
+
);
|
|
542
|
+
return parsePlistElement(rootElement);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function findEnclosingElectronInfoPlist(archivePath) {
|
|
546
|
+
let currentDir = dirname(resolve(archivePath));
|
|
547
|
+
let currentDepth = 0;
|
|
548
|
+
while (currentDir && currentDir !== dirname(currentDir)) {
|
|
549
|
+
if (currentDepth >= MAX_ELECTRON_APP_BUNDLE_SEARCH_DEPTH) {
|
|
550
|
+
thoughtLog(
|
|
551
|
+
"Stopping Electron app bundle search after",
|
|
552
|
+
MAX_ELECTRON_APP_BUNDLE_SEARCH_DEPTH,
|
|
553
|
+
"levels for",
|
|
554
|
+
archivePath,
|
|
555
|
+
);
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
if (basename(currentDir).endsWith(".app")) {
|
|
559
|
+
const infoPlistPath = join(currentDir, "Contents", "Info.plist");
|
|
560
|
+
if (safeExistsSync(infoPlistPath)) {
|
|
561
|
+
return {
|
|
562
|
+
appDir: currentDir,
|
|
563
|
+
infoPlistPath,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
currentDepth += 1;
|
|
568
|
+
currentDir = dirname(currentDir);
|
|
569
|
+
}
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function collectAsarSigningInfo(archivePath, headerString) {
|
|
574
|
+
const bundleInfo = findEnclosingElectronInfoPlist(archivePath);
|
|
575
|
+
if (!bundleInfo?.infoPlistPath) {
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
const archiveRelativePath = relative(
|
|
579
|
+
join(bundleInfo.appDir, "Contents"),
|
|
580
|
+
resolve(archivePath),
|
|
581
|
+
).replaceAll("\\", "/");
|
|
582
|
+
if (!archiveRelativePath || archiveRelativePath.startsWith("..")) {
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
let plistData;
|
|
586
|
+
try {
|
|
587
|
+
plistData = parsePlistFile(bundleInfo.infoPlistPath);
|
|
588
|
+
} catch {
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
const asarIntegrityRecord =
|
|
592
|
+
plistData?.ElectronAsarIntegrity?.[archiveRelativePath];
|
|
593
|
+
if (!asarIntegrityRecord || typeof asarIntegrityRecord !== "object") {
|
|
594
|
+
return undefined;
|
|
595
|
+
}
|
|
596
|
+
const computedHash = sha256Buffer(Buffer.from(headerString, "utf8"));
|
|
597
|
+
const algorithm = String(asarIntegrityRecord.algorithm || "").toUpperCase();
|
|
598
|
+
const declaredHash = String(asarIntegrityRecord.hash || "").toLowerCase();
|
|
599
|
+
return {
|
|
600
|
+
algorithm,
|
|
601
|
+
archiveRelativePath,
|
|
602
|
+
computedHash,
|
|
603
|
+
declaredHash,
|
|
604
|
+
infoPlistPath: bundleInfo.infoPlistPath,
|
|
605
|
+
scope: "header-only",
|
|
606
|
+
source: "electron-info-plist",
|
|
607
|
+
verified:
|
|
608
|
+
algorithm === "SHA256" &&
|
|
609
|
+
/^[a-f0-9]{64}$/i.test(declaredHash) &&
|
|
610
|
+
declaredHash === computedHash,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function createAsarSigningComponent(archivePath, signingInfo, options = {}) {
|
|
615
|
+
const specVersionNumber = Number(options.specVersion || 0);
|
|
616
|
+
const componentType =
|
|
617
|
+
specVersionNumber > 0 && specVersionNumber < 1.6
|
|
618
|
+
? "data"
|
|
619
|
+
: "cryptographic-asset";
|
|
620
|
+
const properties = [{ name: "SrcFile", value: archivePath }];
|
|
621
|
+
addSanitizedProperty(
|
|
622
|
+
properties,
|
|
623
|
+
"cdx:asar:signingAlgorithm",
|
|
624
|
+
signingInfo.algorithm,
|
|
625
|
+
);
|
|
626
|
+
addSanitizedProperty(
|
|
627
|
+
properties,
|
|
628
|
+
"cdx:asar:signingDeclaredHash",
|
|
629
|
+
signingInfo.declaredHash,
|
|
630
|
+
);
|
|
631
|
+
addSanitizedProperty(
|
|
632
|
+
properties,
|
|
633
|
+
"cdx:asar:headerHash",
|
|
634
|
+
signingInfo.computedHash,
|
|
635
|
+
);
|
|
636
|
+
addSanitizedProperty(
|
|
637
|
+
properties,
|
|
638
|
+
"cdx:asar:signingSource",
|
|
639
|
+
signingInfo.source,
|
|
640
|
+
);
|
|
641
|
+
addSanitizedProperty(properties, "cdx:asar:signingScope", signingInfo.scope);
|
|
642
|
+
addSanitizedProperty(
|
|
643
|
+
properties,
|
|
644
|
+
"cdx:asar:signingVerified",
|
|
645
|
+
String(signingInfo.verified),
|
|
646
|
+
);
|
|
647
|
+
addSanitizedProperty(
|
|
648
|
+
properties,
|
|
649
|
+
"cdx:asar:signingArchivePath",
|
|
650
|
+
signingInfo.archiveRelativePath,
|
|
651
|
+
);
|
|
652
|
+
const component = {
|
|
653
|
+
"bom-ref": `crypto/asar-signature/${encodeURIComponent(archivePath)}@sha256:${signingInfo.declaredHash || signingInfo.computedHash}`,
|
|
654
|
+
hashes: [
|
|
655
|
+
{
|
|
656
|
+
alg: "SHA-256",
|
|
657
|
+
content: signingInfo.declaredHash || signingInfo.computedHash,
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
name: `${basename(archivePath)} asar integrity record`,
|
|
661
|
+
properties,
|
|
662
|
+
type: componentType,
|
|
663
|
+
version: signingInfo.declaredHash || signingInfo.computedHash,
|
|
664
|
+
};
|
|
665
|
+
if (componentType === "cryptographic-asset") {
|
|
666
|
+
component.cryptoProperties = {
|
|
667
|
+
assetType: "related-crypto-material",
|
|
668
|
+
relatedCryptoMaterialProperties: {
|
|
669
|
+
type: "digest",
|
|
670
|
+
value: signingInfo.declaredHash || signingInfo.computedHash,
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
return component;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function toFileComponentRef(archivePath, entryPath) {
|
|
678
|
+
return `file:${archivePath}#/${normalizeArchiveRelativePath(entryPath)}`;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function inferPrimaryPackagePath(entries) {
|
|
682
|
+
const packageEntries = entries
|
|
683
|
+
.filter(
|
|
684
|
+
(entry) =>
|
|
685
|
+
entry.type === "file" &&
|
|
686
|
+
basename(entry.path) === "package.json" &&
|
|
687
|
+
!entry.path.includes("/node_modules/"),
|
|
688
|
+
)
|
|
689
|
+
.sort((left, right) => left.path.length - right.path.length);
|
|
690
|
+
return packageEntries[0]?.path;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function inferMainEntryFlags(packageJson) {
|
|
694
|
+
const properties = [];
|
|
695
|
+
addSanitizedProperty(properties, "cdx:asar:main", packageJson?.main);
|
|
696
|
+
addSanitizedProperty(properties, "cdx:asar:module", packageJson?.module);
|
|
697
|
+
addSanitizedProperty(properties, "cdx:asar:browser", packageJson?.browser);
|
|
698
|
+
addSanitizedProperty(
|
|
699
|
+
properties,
|
|
700
|
+
"cdx:asar:productName",
|
|
701
|
+
packageJson?.productName,
|
|
702
|
+
);
|
|
703
|
+
const lifecycleScripts = Object.keys(packageJson?.scripts || {}).filter(
|
|
704
|
+
(name) => ASAR_LIFECYCLE_SCRIPT_NAMES.has(name),
|
|
705
|
+
);
|
|
706
|
+
if (lifecycleScripts.length) {
|
|
707
|
+
addSanitizedProperty(
|
|
708
|
+
properties,
|
|
709
|
+
"cdx:asar:lifecycleScripts",
|
|
710
|
+
lifecycleScripts.join(", "),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
return properties;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function toArchiveVirtualPath(extractedDir, archivePath, candidatePath) {
|
|
717
|
+
if (!candidatePath || typeof candidatePath !== "string") {
|
|
718
|
+
return candidatePath;
|
|
719
|
+
}
|
|
720
|
+
const normalizedExtractedDir = resolve(extractedDir);
|
|
721
|
+
const normalizedCandidate = resolve(candidatePath);
|
|
722
|
+
if (!isPathWithin(normalizedExtractedDir, normalizedCandidate)) {
|
|
723
|
+
return candidatePath;
|
|
724
|
+
}
|
|
725
|
+
const relativePath = relative(normalizedExtractedDir, normalizedCandidate);
|
|
726
|
+
return `${archivePath}#/${relativePath.replaceAll("\\", "/")}`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function rewriteExtractedArchivePaths(
|
|
730
|
+
subject,
|
|
731
|
+
extractedDir,
|
|
732
|
+
archivePath,
|
|
733
|
+
) {
|
|
734
|
+
if (!subject || typeof subject !== "object") {
|
|
735
|
+
return subject;
|
|
736
|
+
}
|
|
737
|
+
if (Array.isArray(subject)) {
|
|
738
|
+
subject.forEach((entry) => {
|
|
739
|
+
rewriteExtractedArchivePaths(entry, extractedDir, archivePath);
|
|
740
|
+
});
|
|
741
|
+
return subject;
|
|
742
|
+
}
|
|
743
|
+
if (subject.properties?.length) {
|
|
744
|
+
subject.properties.forEach((property) => {
|
|
745
|
+
if (typeof property?.value === "string") {
|
|
746
|
+
property.value = toArchiveVirtualPath(
|
|
747
|
+
extractedDir,
|
|
748
|
+
archivePath,
|
|
749
|
+
property.value,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
if (subject.evidence?.identity?.methods?.length) {
|
|
755
|
+
subject.evidence.identity.methods.forEach((method) => {
|
|
756
|
+
if (typeof method?.value === "string") {
|
|
757
|
+
method.value = toArchiveVirtualPath(
|
|
758
|
+
extractedDir,
|
|
759
|
+
archivePath,
|
|
760
|
+
method.value,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
if (subject.evidence?.occurrences?.length) {
|
|
766
|
+
subject.evidence.occurrences.forEach((occurrence) => {
|
|
767
|
+
if (typeof occurrence?.location === "string") {
|
|
768
|
+
occurrence.location = toArchiveVirtualPath(
|
|
769
|
+
extractedDir,
|
|
770
|
+
archivePath,
|
|
771
|
+
occurrence.location,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
if (subject.components?.length) {
|
|
777
|
+
rewriteExtractedArchivePaths(subject.components, extractedDir, archivePath);
|
|
778
|
+
}
|
|
779
|
+
return subject;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function collectArchiveSummaryProperties(
|
|
783
|
+
archivePath,
|
|
784
|
+
summary,
|
|
785
|
+
primaryPackageJson,
|
|
786
|
+
primaryPackagePath,
|
|
787
|
+
) {
|
|
788
|
+
const properties = [{ name: "SrcFile", value: archivePath }];
|
|
789
|
+
addSanitizedProperty(properties, "cdx:file:kind", "asar-archive");
|
|
790
|
+
addSanitizedProperty(
|
|
791
|
+
properties,
|
|
792
|
+
"cdx:asar:entryCount",
|
|
793
|
+
`${summary.entryCount}`,
|
|
794
|
+
);
|
|
795
|
+
addSanitizedProperty(
|
|
796
|
+
properties,
|
|
797
|
+
"cdx:asar:fileCount",
|
|
798
|
+
`${summary.fileCount}`,
|
|
799
|
+
);
|
|
800
|
+
addSanitizedProperty(
|
|
801
|
+
properties,
|
|
802
|
+
"cdx:asar:directoryCount",
|
|
803
|
+
`${summary.directoryCount}`,
|
|
804
|
+
);
|
|
805
|
+
addSanitizedProperty(
|
|
806
|
+
properties,
|
|
807
|
+
"cdx:asar:symlinkCount",
|
|
808
|
+
`${summary.symlinkCount}`,
|
|
809
|
+
);
|
|
810
|
+
addSanitizedProperty(
|
|
811
|
+
properties,
|
|
812
|
+
"cdx:asar:jsFileCount",
|
|
813
|
+
`${summary.jsFileCount}`,
|
|
814
|
+
);
|
|
815
|
+
addSanitizedProperty(
|
|
816
|
+
properties,
|
|
817
|
+
"cdx:asar:packageJsonCount",
|
|
818
|
+
`${summary.packageJsonCount}`,
|
|
819
|
+
);
|
|
820
|
+
addSanitizedProperty(
|
|
821
|
+
properties,
|
|
822
|
+
"cdx:asar:lockfileCount",
|
|
823
|
+
`${summary.lockfileCount}`,
|
|
824
|
+
);
|
|
825
|
+
if (summary.nestedArchiveCount > 0) {
|
|
826
|
+
addSanitizedProperty(properties, "cdx:asar:hasNestedArchives", "true");
|
|
827
|
+
addSanitizedProperty(
|
|
828
|
+
properties,
|
|
829
|
+
"cdx:asar:nestedArchiveCount",
|
|
830
|
+
`${summary.nestedArchiveCount}`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
if (summary.unpackedFileCount > 0) {
|
|
834
|
+
addSanitizedProperty(properties, "cdx:asar:hasUnpackedEntries", "true");
|
|
835
|
+
addSanitizedProperty(
|
|
836
|
+
properties,
|
|
837
|
+
"cdx:asar:unpackedFileCount",
|
|
838
|
+
`${summary.unpackedFileCount}`,
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
if (summary.nativeAddonCount > 0) {
|
|
842
|
+
addSanitizedProperty(properties, "cdx:asar:hasNativeAddons", "true");
|
|
843
|
+
addSanitizedProperty(
|
|
844
|
+
properties,
|
|
845
|
+
"cdx:asar:nativeAddonCount",
|
|
846
|
+
`${summary.nativeAddonCount}`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (summary.integrityMismatchCount > 0) {
|
|
850
|
+
addSanitizedProperty(properties, "cdx:asar:hasIntegrityMismatch", "true");
|
|
851
|
+
addSanitizedProperty(
|
|
852
|
+
properties,
|
|
853
|
+
"cdx:asar:integrityMismatchCount",
|
|
854
|
+
`${summary.integrityMismatchCount}`,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (summary.capabilities.length) {
|
|
858
|
+
addSanitizedProperty(
|
|
859
|
+
properties,
|
|
860
|
+
"cdx:asar:capabilities",
|
|
861
|
+
summary.capabilities.join(", "),
|
|
862
|
+
);
|
|
863
|
+
summary.capabilities.forEach((capability) => {
|
|
864
|
+
addSanitizedProperty(
|
|
865
|
+
properties,
|
|
866
|
+
`cdx:asar:capability:${capability}`,
|
|
867
|
+
"true",
|
|
868
|
+
);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
if (summary.executionIndicators.length) {
|
|
872
|
+
addSanitizedProperty(
|
|
873
|
+
properties,
|
|
874
|
+
"cdx:asar:executionIndicators",
|
|
875
|
+
summary.executionIndicators.join(", "),
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (summary.networkIndicators.length) {
|
|
879
|
+
addSanitizedProperty(
|
|
880
|
+
properties,
|
|
881
|
+
"cdx:asar:networkIndicators",
|
|
882
|
+
summary.networkIndicators.join(", "),
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
if (summary.obfuscationIndicators.length) {
|
|
886
|
+
addSanitizedProperty(
|
|
887
|
+
properties,
|
|
888
|
+
"cdx:asar:obfuscationIndicators",
|
|
889
|
+
summary.obfuscationIndicators.join(", "),
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (summary.hasEval) {
|
|
893
|
+
addSanitizedProperty(properties, "cdx:asar:hasEval", "true");
|
|
894
|
+
}
|
|
895
|
+
if (summary.hasDynamicFetch) {
|
|
896
|
+
addSanitizedProperty(properties, "cdx:asar:hasDynamicFetch", "true");
|
|
897
|
+
}
|
|
898
|
+
if (summary.hasDynamicImport) {
|
|
899
|
+
addSanitizedProperty(properties, "cdx:asar:hasDynamicImport", "true");
|
|
900
|
+
}
|
|
901
|
+
if (summary.headerHash) {
|
|
902
|
+
addSanitizedProperty(properties, "cdx:asar:headerHash", summary.headerHash);
|
|
903
|
+
}
|
|
904
|
+
if (summary.signingInfo) {
|
|
905
|
+
addSanitizedProperty(properties, "cdx:asar:hasSigningMetadata", "true");
|
|
906
|
+
addSanitizedProperty(
|
|
907
|
+
properties,
|
|
908
|
+
"cdx:asar:signingAlgorithm",
|
|
909
|
+
summary.signingInfo.algorithm,
|
|
910
|
+
);
|
|
911
|
+
addSanitizedProperty(
|
|
912
|
+
properties,
|
|
913
|
+
"cdx:asar:signingDeclaredHash",
|
|
914
|
+
summary.signingInfo.declaredHash,
|
|
915
|
+
);
|
|
916
|
+
addSanitizedProperty(
|
|
917
|
+
properties,
|
|
918
|
+
"cdx:asar:signingSource",
|
|
919
|
+
summary.signingInfo.source,
|
|
920
|
+
);
|
|
921
|
+
addSanitizedProperty(
|
|
922
|
+
properties,
|
|
923
|
+
"cdx:asar:signingScope",
|
|
924
|
+
summary.signingInfo.scope,
|
|
925
|
+
);
|
|
926
|
+
addSanitizedProperty(
|
|
927
|
+
properties,
|
|
928
|
+
"cdx:asar:signingVerified",
|
|
929
|
+
String(summary.signingInfo.verified),
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
if (primaryPackagePath) {
|
|
933
|
+
addSanitizedProperty(
|
|
934
|
+
properties,
|
|
935
|
+
"cdx:asar:primaryManifest",
|
|
936
|
+
`${archivePath}#/${primaryPackagePath}`,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
if (primaryPackageJson) {
|
|
940
|
+
inferMainEntryFlags(primaryPackageJson).forEach((property) => {
|
|
941
|
+
properties.push(property);
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return properties;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function createArchiveParentComponent(
|
|
948
|
+
archivePath,
|
|
949
|
+
summary,
|
|
950
|
+
primaryPackageJson,
|
|
951
|
+
primaryPackagePath,
|
|
952
|
+
) {
|
|
953
|
+
const archivePurl =
|
|
954
|
+
createAsarPackagePurl(
|
|
955
|
+
primaryPackageJson?.name,
|
|
956
|
+
primaryPackageJson?.version,
|
|
957
|
+
) || createGenericArchivePurl(archivePath, primaryPackageJson?.version);
|
|
958
|
+
const component = {
|
|
959
|
+
"bom-ref": decodeURIComponent(archivePurl),
|
|
960
|
+
description:
|
|
961
|
+
primaryPackageJson?.description ||
|
|
962
|
+
`Electron ASAR archive ${basename(archivePath)}`,
|
|
963
|
+
name:
|
|
964
|
+
primaryPackageJson?.productName ||
|
|
965
|
+
primaryPackageJson?.name ||
|
|
966
|
+
basename(archivePath, ".asar"),
|
|
967
|
+
purl: archivePurl,
|
|
968
|
+
type: "application",
|
|
969
|
+
version: primaryPackageJson?.version || "",
|
|
970
|
+
};
|
|
971
|
+
const parsedName = parseScopedPackageName(primaryPackageJson?.name);
|
|
972
|
+
if (parsedName.group) {
|
|
973
|
+
component.group = parsedName.group;
|
|
974
|
+
}
|
|
975
|
+
if (primaryPackageJson?.author) {
|
|
976
|
+
component.author =
|
|
977
|
+
typeof primaryPackageJson.author === "string"
|
|
978
|
+
? primaryPackageJson.author
|
|
979
|
+
: primaryPackageJson.author?.name || "";
|
|
980
|
+
}
|
|
981
|
+
if (primaryPackageJson?.license) {
|
|
982
|
+
component.license = primaryPackageJson.license;
|
|
983
|
+
}
|
|
984
|
+
if (primaryPackageJson?.repository) {
|
|
985
|
+
const repositoryUrl =
|
|
986
|
+
typeof primaryPackageJson.repository === "string"
|
|
987
|
+
? primaryPackageJson.repository
|
|
988
|
+
: primaryPackageJson.repository?.url;
|
|
989
|
+
if (repositoryUrl) {
|
|
990
|
+
component.externalReferences = component.externalReferences || [];
|
|
991
|
+
component.externalReferences.push({
|
|
992
|
+
type: "vcs",
|
|
993
|
+
url: repositoryUrl,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (primaryPackageJson?.homepage) {
|
|
998
|
+
component.externalReferences = component.externalReferences || [];
|
|
999
|
+
component.externalReferences.push({
|
|
1000
|
+
type: "website",
|
|
1001
|
+
url: primaryPackageJson.homepage,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
component.properties = collectArchiveSummaryProperties(
|
|
1005
|
+
archivePath,
|
|
1006
|
+
summary,
|
|
1007
|
+
primaryPackageJson,
|
|
1008
|
+
primaryPackagePath,
|
|
1009
|
+
);
|
|
1010
|
+
component.evidence = {
|
|
1011
|
+
identity: {
|
|
1012
|
+
confidence: 1,
|
|
1013
|
+
field: "purl",
|
|
1014
|
+
methods: [
|
|
1015
|
+
{
|
|
1016
|
+
confidence: 1,
|
|
1017
|
+
technique: primaryPackagePath ? "manifest-analysis" : "filename",
|
|
1018
|
+
value: primaryPackagePath
|
|
1019
|
+
? `${archivePath}#/${primaryPackagePath}`
|
|
1020
|
+
: archivePath,
|
|
1021
|
+
},
|
|
1022
|
+
],
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
return component;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function createArchiveEntryComponent(
|
|
1029
|
+
archivePath,
|
|
1030
|
+
entry,
|
|
1031
|
+
computedHash,
|
|
1032
|
+
jsAnalysis,
|
|
1033
|
+
suspiciousAnalysis,
|
|
1034
|
+
) {
|
|
1035
|
+
const archiveLocation = toArchiveOccurrence(archivePath, entry.path);
|
|
1036
|
+
const properties = [{ name: "SrcFile", value: archivePath }];
|
|
1037
|
+
addSanitizedProperty(properties, "cdx:file:kind", "asar-entry");
|
|
1038
|
+
addSanitizedProperty(properties, "cdx:asar:path", entry.path);
|
|
1039
|
+
addSanitizedProperty(properties, "cdx:asar:size", `${entry.size || 0}`);
|
|
1040
|
+
addSanitizedProperty(
|
|
1041
|
+
properties,
|
|
1042
|
+
"cdx:asar:unpacked",
|
|
1043
|
+
String(entry.unpacked === true),
|
|
1044
|
+
);
|
|
1045
|
+
if (entry.offset !== undefined) {
|
|
1046
|
+
addSanitizedProperty(properties, "cdx:asar:offset", entry.offset);
|
|
1047
|
+
}
|
|
1048
|
+
if (entry.executable) {
|
|
1049
|
+
addSanitizedProperty(properties, "cdx:asar:executable", "true");
|
|
1050
|
+
}
|
|
1051
|
+
if (entry.link) {
|
|
1052
|
+
addSanitizedProperty(properties, "cdx:asar:linkTarget", entry.link);
|
|
1053
|
+
}
|
|
1054
|
+
if (entry.integrity?.algorithm) {
|
|
1055
|
+
addSanitizedProperty(
|
|
1056
|
+
properties,
|
|
1057
|
+
"cdx:asar:integrityAlgorithm",
|
|
1058
|
+
entry.integrity.algorithm,
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
if (entry.integrity?.hash) {
|
|
1062
|
+
addSanitizedProperty(
|
|
1063
|
+
properties,
|
|
1064
|
+
"cdx:asar:declaredIntegrityHash",
|
|
1065
|
+
entry.integrity.hash,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
if (entry.integrity?.blockSize) {
|
|
1069
|
+
addSanitizedProperty(
|
|
1070
|
+
properties,
|
|
1071
|
+
"cdx:asar:integrityBlockSize",
|
|
1072
|
+
`${entry.integrity.blockSize}`,
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (Array.isArray(entry.integrity?.blocks)) {
|
|
1076
|
+
addSanitizedProperty(
|
|
1077
|
+
properties,
|
|
1078
|
+
"cdx:asar:integrityBlockCount",
|
|
1079
|
+
`${entry.integrity.blocks.length}`,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
if (computedHash && entry.integrity?.hash) {
|
|
1083
|
+
addSanitizedProperty(
|
|
1084
|
+
properties,
|
|
1085
|
+
"cdx:asar:integrityVerified",
|
|
1086
|
+
String(computedHash === String(entry.integrity.hash).toLowerCase()),
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (jsAnalysis?.capabilities?.length) {
|
|
1090
|
+
addSanitizedProperty(
|
|
1091
|
+
properties,
|
|
1092
|
+
"cdx:asar:js:capabilities",
|
|
1093
|
+
jsAnalysis.capabilities.join(", "),
|
|
1094
|
+
);
|
|
1095
|
+
jsAnalysis.capabilities.forEach((capability) => {
|
|
1096
|
+
addSanitizedProperty(
|
|
1097
|
+
properties,
|
|
1098
|
+
`cdx:asar:js:capability:${capability}`,
|
|
1099
|
+
"true",
|
|
1100
|
+
);
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
if (jsAnalysis?.hasEval) {
|
|
1104
|
+
addSanitizedProperty(properties, "cdx:asar:js:hasEval", "true");
|
|
1105
|
+
}
|
|
1106
|
+
if (jsAnalysis?.hasDynamicFetch) {
|
|
1107
|
+
addSanitizedProperty(properties, "cdx:asar:js:hasDynamicFetch", "true");
|
|
1108
|
+
}
|
|
1109
|
+
if (jsAnalysis?.hasDynamicImport) {
|
|
1110
|
+
addSanitizedProperty(properties, "cdx:asar:js:hasDynamicImport", "true");
|
|
1111
|
+
}
|
|
1112
|
+
if (jsAnalysis?.indicatorMap?.fileAccess?.length) {
|
|
1113
|
+
addSanitizedProperty(
|
|
1114
|
+
properties,
|
|
1115
|
+
"cdx:asar:js:fileAccessIndicators",
|
|
1116
|
+
jsAnalysis.indicatorMap.fileAccess.join(", "),
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
if (jsAnalysis?.indicatorMap?.network?.length) {
|
|
1120
|
+
addSanitizedProperty(
|
|
1121
|
+
properties,
|
|
1122
|
+
"cdx:asar:js:networkIndicators",
|
|
1123
|
+
jsAnalysis.indicatorMap.network.join(", "),
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
if (jsAnalysis?.indicatorMap?.hardware?.length) {
|
|
1127
|
+
addSanitizedProperty(
|
|
1128
|
+
properties,
|
|
1129
|
+
"cdx:asar:js:hardwareIndicators",
|
|
1130
|
+
jsAnalysis.indicatorMap.hardware.join(", "),
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
if (suspiciousAnalysis?.executionIndicators?.length) {
|
|
1134
|
+
addSanitizedProperty(
|
|
1135
|
+
properties,
|
|
1136
|
+
"cdx:asar:js:executionIndicators",
|
|
1137
|
+
suspiciousAnalysis.executionIndicators.join(", "),
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
if (suspiciousAnalysis?.obfuscationIndicators?.length) {
|
|
1141
|
+
addSanitizedProperty(
|
|
1142
|
+
properties,
|
|
1143
|
+
"cdx:asar:js:obfuscationIndicators",
|
|
1144
|
+
suspiciousAnalysis.obfuscationIndicators.join(", "),
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
"bom-ref": toFileComponentRef(archivePath, entry.path),
|
|
1149
|
+
evidence: {
|
|
1150
|
+
identity: {
|
|
1151
|
+
confidence: 1,
|
|
1152
|
+
field: "name",
|
|
1153
|
+
methods: [
|
|
1154
|
+
{
|
|
1155
|
+
confidence: 1,
|
|
1156
|
+
technique: "filename",
|
|
1157
|
+
value: archiveLocation,
|
|
1158
|
+
},
|
|
1159
|
+
],
|
|
1160
|
+
},
|
|
1161
|
+
occurrences: [{ location: archiveLocation }],
|
|
1162
|
+
},
|
|
1163
|
+
hashes: computedHash
|
|
1164
|
+
? [{ alg: "SHA-256", content: computedHash }]
|
|
1165
|
+
: undefined,
|
|
1166
|
+
name: basename(entry.path),
|
|
1167
|
+
properties,
|
|
1168
|
+
type: "file",
|
|
1169
|
+
version: computedHash || undefined,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Parse an Electron ASAR archive and emit inventory, metadata, and optional
|
|
1175
|
+
* signing information.
|
|
1176
|
+
*
|
|
1177
|
+
* @param {string} archivePath Absolute or relative path to an ASAR archive
|
|
1178
|
+
* @param {Object} [options={}] Parse options
|
|
1179
|
+
* @param {string} [options.asarVirtualPath] Virtual archive identity to use in
|
|
1180
|
+
* BOM references and evidence for nested ASAR recursion
|
|
1181
|
+
* @param {number} [options.specVersion] CycloneDX spec version used to choose
|
|
1182
|
+
* compatible component types
|
|
1183
|
+
* @returns {Promise<Object>} Parsed archive analysis result
|
|
1184
|
+
*/
|
|
1185
|
+
export async function parseAsarArchive(archivePath, options = {}) {
|
|
1186
|
+
const resolvedArchivePath = resolve(archivePath);
|
|
1187
|
+
const parsedArchive = listAsarEntries(resolvedArchivePath);
|
|
1188
|
+
const archiveIdentityPath =
|
|
1189
|
+
typeof options?.asarVirtualPath === "string" && options.asarVirtualPath
|
|
1190
|
+
? options.asarVirtualPath
|
|
1191
|
+
: resolvedArchivePath;
|
|
1192
|
+
const signingInfo = collectAsarSigningInfo(
|
|
1193
|
+
resolvedArchivePath,
|
|
1194
|
+
parsedArchive.headerString,
|
|
1195
|
+
);
|
|
1196
|
+
const summary = {
|
|
1197
|
+
capabilities: new Set(),
|
|
1198
|
+
directoryCount: 0,
|
|
1199
|
+
entryCount: parsedArchive.entries.length,
|
|
1200
|
+
executionIndicators: new Set(),
|
|
1201
|
+
fileCount: 0,
|
|
1202
|
+
headerHash: sha256Buffer(Buffer.from(parsedArchive.headerString, "utf8")),
|
|
1203
|
+
hasDynamicFetch: false,
|
|
1204
|
+
hasDynamicImport: false,
|
|
1205
|
+
hasEval: false,
|
|
1206
|
+
integrityMismatchCount: 0,
|
|
1207
|
+
jsFileCount: 0,
|
|
1208
|
+
lockfileCount: 0,
|
|
1209
|
+
nativeAddonCount: 0,
|
|
1210
|
+
nestedArchiveCount: 0,
|
|
1211
|
+
networkIndicators: new Set(),
|
|
1212
|
+
obfuscationIndicators: new Set(),
|
|
1213
|
+
packageJsonCount: 0,
|
|
1214
|
+
signingInfo,
|
|
1215
|
+
symlinkCount: 0,
|
|
1216
|
+
unpackedFileCount: 0,
|
|
1217
|
+
};
|
|
1218
|
+
const components = [];
|
|
1219
|
+
const dependencies = [];
|
|
1220
|
+
const packageManifestPaths = [];
|
|
1221
|
+
let primaryPackageJson;
|
|
1222
|
+
const primaryPackagePath = inferPrimaryPackagePath(parsedArchive.entries);
|
|
1223
|
+
let archiveFd;
|
|
1224
|
+
try {
|
|
1225
|
+
for (const entry of parsedArchive.entries) {
|
|
1226
|
+
if (entry.type === "directory") {
|
|
1227
|
+
summary.directoryCount += 1;
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
if (entry.type === "link") {
|
|
1231
|
+
summary.symlinkCount += 1;
|
|
1232
|
+
components.push(
|
|
1233
|
+
createArchiveEntryComponent(archiveIdentityPath, entry, undefined),
|
|
1234
|
+
);
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
summary.fileCount += 1;
|
|
1238
|
+
if (entry.unpacked) {
|
|
1239
|
+
summary.unpackedFileCount += 1;
|
|
1240
|
+
}
|
|
1241
|
+
if (basename(entry.path) === "package.json") {
|
|
1242
|
+
summary.packageJsonCount += 1;
|
|
1243
|
+
packageManifestPaths.push(entry.path);
|
|
1244
|
+
}
|
|
1245
|
+
if (ASAR_LOCKFILE_NAMES.has(basename(entry.path))) {
|
|
1246
|
+
summary.lockfileCount += 1;
|
|
1247
|
+
}
|
|
1248
|
+
if (extname(entry.path) === ".asar") {
|
|
1249
|
+
summary.nestedArchiveCount += 1;
|
|
1250
|
+
}
|
|
1251
|
+
if (extname(entry.path) === ".node") {
|
|
1252
|
+
summary.nativeAddonCount += 1;
|
|
1253
|
+
}
|
|
1254
|
+
let computedHash;
|
|
1255
|
+
let fileBuffer;
|
|
1256
|
+
let jsAnalysis;
|
|
1257
|
+
let suspiciousAnalysis;
|
|
1258
|
+
try {
|
|
1259
|
+
if (!entry.unpacked && archiveFd === undefined) {
|
|
1260
|
+
archiveFd = openSync(resolvedArchivePath, "r");
|
|
1261
|
+
}
|
|
1262
|
+
fileBuffer = readAsarEntryBufferSync(
|
|
1263
|
+
resolvedArchivePath,
|
|
1264
|
+
parsedArchive.archiveDataOffset,
|
|
1265
|
+
entry,
|
|
1266
|
+
archiveFd,
|
|
1267
|
+
);
|
|
1268
|
+
computedHash = sha256Buffer(fileBuffer);
|
|
1269
|
+
if (
|
|
1270
|
+
entry.integrity?.hash &&
|
|
1271
|
+
computedHash !== String(entry.integrity.hash).toLowerCase()
|
|
1272
|
+
) {
|
|
1273
|
+
summary.integrityMismatchCount += 1;
|
|
1274
|
+
}
|
|
1275
|
+
if (
|
|
1276
|
+
entry.path === primaryPackagePath &&
|
|
1277
|
+
basename(entry.path) === "package.json"
|
|
1278
|
+
) {
|
|
1279
|
+
try {
|
|
1280
|
+
primaryPackageJson = JSON.parse(fileBuffer.toString("utf8"));
|
|
1281
|
+
} catch {
|
|
1282
|
+
// Ignore malformed package metadata and fall back to archive name.
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
thoughtLog("Error reading ASAR entry", entry.path, error.message);
|
|
1287
|
+
throw error;
|
|
1288
|
+
}
|
|
1289
|
+
if (ASAR_JS_ANALYSIS_EXTENSIONS.has(extname(entry.path))) {
|
|
1290
|
+
summary.jsFileCount += 1;
|
|
1291
|
+
const sourceBuffer =
|
|
1292
|
+
fileBuffer ||
|
|
1293
|
+
(entry.unpacked
|
|
1294
|
+
? readFileSync(
|
|
1295
|
+
resolveUnpackedEntryPath(resolvedArchivePath, entry.path),
|
|
1296
|
+
)
|
|
1297
|
+
: undefined);
|
|
1298
|
+
if (sourceBuffer) {
|
|
1299
|
+
const sourceText = sourceBuffer.toString("utf8");
|
|
1300
|
+
jsAnalysis = analyzeJsCapabilitiesSource(sourceText);
|
|
1301
|
+
suspiciousAnalysis = analyzeSuspiciousJsSource(sourceText);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
jsAnalysis?.capabilities?.forEach((capability) => {
|
|
1305
|
+
summary.capabilities.add(capability);
|
|
1306
|
+
});
|
|
1307
|
+
suspiciousAnalysis?.executionIndicators?.forEach((indicator) => {
|
|
1308
|
+
summary.executionIndicators.add(indicator);
|
|
1309
|
+
if (indicator === "eval") {
|
|
1310
|
+
summary.hasEval = true;
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
suspiciousAnalysis?.networkIndicators?.forEach((indicator) => {
|
|
1314
|
+
summary.networkIndicators.add(indicator);
|
|
1315
|
+
});
|
|
1316
|
+
suspiciousAnalysis?.obfuscationIndicators?.forEach((indicator) => {
|
|
1317
|
+
summary.obfuscationIndicators.add(indicator);
|
|
1318
|
+
});
|
|
1319
|
+
if (jsAnalysis?.hasDynamicFetch) {
|
|
1320
|
+
summary.hasDynamicFetch = true;
|
|
1321
|
+
}
|
|
1322
|
+
if (jsAnalysis?.hasDynamicImport) {
|
|
1323
|
+
summary.hasDynamicImport = true;
|
|
1324
|
+
}
|
|
1325
|
+
components.push(
|
|
1326
|
+
createArchiveEntryComponent(
|
|
1327
|
+
archiveIdentityPath,
|
|
1328
|
+
entry,
|
|
1329
|
+
computedHash,
|
|
1330
|
+
jsAnalysis,
|
|
1331
|
+
suspiciousAnalysis,
|
|
1332
|
+
),
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
} finally {
|
|
1336
|
+
if (archiveFd !== undefined) {
|
|
1337
|
+
closeSync(archiveFd);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const normalizedSummary = {
|
|
1341
|
+
...summary,
|
|
1342
|
+
capabilities: Array.from(summary.capabilities).sort(),
|
|
1343
|
+
executionIndicators: Array.from(summary.executionIndicators).sort(),
|
|
1344
|
+
networkIndicators: Array.from(summary.networkIndicators).sort(),
|
|
1345
|
+
obfuscationIndicators: Array.from(summary.obfuscationIndicators).sort(),
|
|
1346
|
+
};
|
|
1347
|
+
const parentComponent = createArchiveParentComponent(
|
|
1348
|
+
archiveIdentityPath,
|
|
1349
|
+
normalizedSummary,
|
|
1350
|
+
primaryPackageJson,
|
|
1351
|
+
primaryPackagePath,
|
|
1352
|
+
);
|
|
1353
|
+
if (signingInfo) {
|
|
1354
|
+
const signingComponent = createAsarSigningComponent(
|
|
1355
|
+
archiveIdentityPath,
|
|
1356
|
+
signingInfo,
|
|
1357
|
+
options,
|
|
1358
|
+
);
|
|
1359
|
+
components.push(signingComponent);
|
|
1360
|
+
if (parentComponent?.["bom-ref"] && signingComponent?.["bom-ref"]) {
|
|
1361
|
+
dependencies.push({
|
|
1362
|
+
ref: parentComponent["bom-ref"],
|
|
1363
|
+
dependsOn: [signingComponent["bom-ref"]],
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
recordActivity({
|
|
1368
|
+
capability: "archive-analysis",
|
|
1369
|
+
kind: "read",
|
|
1370
|
+
reason: `Cataloged ${normalizedSummary.fileCount} ASAR file entr${normalizedSummary.fileCount === 1 ? "y" : "ies"} from ${resolvedArchivePath}.`,
|
|
1371
|
+
status: "completed",
|
|
1372
|
+
target: resolvedArchivePath,
|
|
1373
|
+
});
|
|
1374
|
+
return {
|
|
1375
|
+
components,
|
|
1376
|
+
dependencies,
|
|
1377
|
+
entries: parsedArchive.entries,
|
|
1378
|
+
packageManifestPaths,
|
|
1379
|
+
parentComponent,
|
|
1380
|
+
primaryPackageJson,
|
|
1381
|
+
primaryPackagePath,
|
|
1382
|
+
summary: normalizedSummary,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function extractAsarArchive(archivePath, targetDir) {
|
|
1387
|
+
const parsedArchive = listAsarEntries(archivePath);
|
|
1388
|
+
const validatedSymlinkTargets = validateArchiveSymlinkEntries(
|
|
1389
|
+
parsedArchive.entries,
|
|
1390
|
+
);
|
|
1391
|
+
safeMkdirSync(targetDir, { recursive: true });
|
|
1392
|
+
let archiveFd;
|
|
1393
|
+
try {
|
|
1394
|
+
for (const entry of parsedArchive.entries) {
|
|
1395
|
+
const destinationPath = resolve(
|
|
1396
|
+
targetDir,
|
|
1397
|
+
...normalizeArchiveRelativePath(entry.path).split("/"),
|
|
1398
|
+
);
|
|
1399
|
+
if (!isPathWithin(targetDir, destinationPath)) {
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`Refusing to extract ASAR entry outside target dir: ${entry.path}`,
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
if (entry.type === "directory") {
|
|
1405
|
+
safeMkdirSync(destinationPath, { recursive: true });
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
safeMkdirSync(dirname(destinationPath), { recursive: true });
|
|
1409
|
+
if (entry.type === "link") {
|
|
1410
|
+
const validatedLinkTarget = validatedSymlinkTargets.get(
|
|
1411
|
+
normalizeArchiveRelativePath(entry.path),
|
|
1412
|
+
);
|
|
1413
|
+
const resolvedLinkTargetPath = resolve(
|
|
1414
|
+
targetDir,
|
|
1415
|
+
...validatedLinkTarget.split("/"),
|
|
1416
|
+
);
|
|
1417
|
+
if (!isPathWithin(targetDir, resolvedLinkTargetPath)) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
`ASAR symlink ${entry.path} target escapes extraction root: ${validatedLinkTarget}`,
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
const relativeLinkTarget = relative(
|
|
1423
|
+
dirname(destinationPath),
|
|
1424
|
+
resolvedLinkTargetPath,
|
|
1425
|
+
);
|
|
1426
|
+
try {
|
|
1427
|
+
symlinkSync(relativeLinkTarget, destinationPath);
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
if (process.platform === "win32") {
|
|
1430
|
+
thoughtLog(
|
|
1431
|
+
"Unable to recreate ASAR symlink on Windows; falling back",
|
|
1432
|
+
entry.path,
|
|
1433
|
+
error.message,
|
|
1434
|
+
);
|
|
1435
|
+
try {
|
|
1436
|
+
const linkTargetStats = statSync(resolvedLinkTargetPath);
|
|
1437
|
+
if (linkTargetStats.isDirectory()) {
|
|
1438
|
+
safeMkdirSync(destinationPath, { recursive: true });
|
|
1439
|
+
} else if (linkTargetStats.isFile()) {
|
|
1440
|
+
safeCopyFileSync(resolvedLinkTargetPath, destinationPath);
|
|
1441
|
+
}
|
|
1442
|
+
continue;
|
|
1443
|
+
} catch {
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
`Failed to recreate ASAR symlink ${entry.path} -> ${validatedLinkTarget} at ${destinationPath}: ${error.message}`,
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (entry.unpacked) {
|
|
1454
|
+
safeCopyFileSync(
|
|
1455
|
+
resolveUnpackedEntryPath(archivePath, entry.path),
|
|
1456
|
+
destinationPath,
|
|
1457
|
+
);
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (archiveFd === undefined) {
|
|
1461
|
+
archiveFd = openSync(archivePath, "r");
|
|
1462
|
+
}
|
|
1463
|
+
const fileBuffer = readPackedEntryBuffer(
|
|
1464
|
+
archivePath,
|
|
1465
|
+
parsedArchive.archiveDataOffset,
|
|
1466
|
+
entry,
|
|
1467
|
+
archiveFd,
|
|
1468
|
+
);
|
|
1469
|
+
safeWriteSync(destinationPath, fileBuffer);
|
|
1470
|
+
if (entry.executable && process.platform !== "win32") {
|
|
1471
|
+
try {
|
|
1472
|
+
chmodSync(destinationPath, 0o755);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
throw new Error(
|
|
1475
|
+
`Failed to mark extracted ASAR entry ${entry.path} executable at ${destinationPath}: ${error.message}`,
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
} finally {
|
|
1481
|
+
if (archiveFd !== undefined) {
|
|
1482
|
+
closeSync(archiveFd);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
export async function extractAsarToTempDir(archivePath) {
|
|
1488
|
+
let tempDir;
|
|
1489
|
+
try {
|
|
1490
|
+
tempDir = safeMkdtempSync(join(getTmpDir(), "asar-deps-"));
|
|
1491
|
+
const extracted = await safeExtractArchive(
|
|
1492
|
+
archivePath,
|
|
1493
|
+
tempDir,
|
|
1494
|
+
async () => {
|
|
1495
|
+
extractAsarArchive(archivePath, tempDir);
|
|
1496
|
+
},
|
|
1497
|
+
"asar",
|
|
1498
|
+
{
|
|
1499
|
+
metadata: { archivePath },
|
|
1500
|
+
},
|
|
1501
|
+
);
|
|
1502
|
+
if (!extracted) {
|
|
1503
|
+
return undefined;
|
|
1504
|
+
}
|
|
1505
|
+
return tempDir;
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
if (DEBUG_MODE) {
|
|
1508
|
+
console.log(
|
|
1509
|
+
`Error extracting ASAR archive ${archivePath}:`,
|
|
1510
|
+
error.message,
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
cleanupAsarTempDir(tempDir);
|
|
1514
|
+
return undefined;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
export function cleanupAsarTempDir(tempDir) {
|
|
1519
|
+
if (!tempDir) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const resolvedDir = resolve(tempDir);
|
|
1523
|
+
const expectedBase = resolve(getTmpDir());
|
|
1524
|
+
if (
|
|
1525
|
+
basename(resolvedDir).startsWith("asar-deps-") &&
|
|
1526
|
+
resolve(resolvedDir, "..") === expectedBase
|
|
1527
|
+
) {
|
|
1528
|
+
safeRmSync(resolvedDir, { force: true, recursive: true });
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
export function buildAsarExtractionSummary(
|
|
1533
|
+
archiveAnalysis,
|
|
1534
|
+
extractionPerformed,
|
|
1535
|
+
) {
|
|
1536
|
+
const properties = [];
|
|
1537
|
+
if (archiveAnalysis?.packageManifestPaths?.length) {
|
|
1538
|
+
addSanitizedProperty(
|
|
1539
|
+
properties,
|
|
1540
|
+
"cdx:asar:embeddedManifests",
|
|
1541
|
+
archiveAnalysis.packageManifestPaths
|
|
1542
|
+
.map((manifestPath) =>
|
|
1543
|
+
toArchiveOccurrence("", manifestPath).replace(/^#/, ""),
|
|
1544
|
+
)
|
|
1545
|
+
.join(", "),
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
if (archiveAnalysis?.summary?.packageJsonCount) {
|
|
1549
|
+
addSanitizedProperty(
|
|
1550
|
+
properties,
|
|
1551
|
+
"cdx:asar:manifestInventoryComplete",
|
|
1552
|
+
String(!isDryRun || extractionPerformed),
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
return properties;
|
|
1556
|
+
}
|