mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
4d8d76b253
* fix(electron): resolve pages served from inside app.asar Packaged Electron applications load their renderer pages from file URLs inside app.asar. The host filesystem cannot stat those paths, so list_electron_targets excluded every such page as unsupported_url and inspect_electron_page could not select it. Resolve these URLs the way Electron's file protocol does: the first .asar path component that is a regular file is the archive, and the remainder must name a regular file in its header. Directories, links, and missing members stay rejected. Parsed headers are cached by archive identity. * fix(artifacts): extract and explain rewritten ASAR unpacked companions Packaging tools commonly sign or strip .asar.unpacked native binaries after writing the archive header, so their declared hashes no longer match. analyze_javascript_application already accepts integrity_policy, but extract_artifact did not, and inspect_artifact and extract_artifact still told callers to "get a fresh copy", which cannot help for a shipped app. - Accept integrity_policy on extract_artifact (MCP and CLI). The default still fails closed; record-and-continue writes the observed bytes and returns the extracted files' integrity_contradictions records. - Name the unpacked entry and the post-packaging cause in integrity failures, and point every operation that accepts integrity_policy at record-and-continue. - Report an active unpacked entry whose companion file is absent as unavailable with its logical path instead of a format failure. - Share one integrity_policy schema across the inventory, extraction, and JavaScript application inputs. * fix(electron): bound ASAR headers before allocation --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
396 lines
13 KiB
TypeScript
396 lines
13 KiB
TypeScript
import { mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import { createPackage } from "@electron/asar";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
import { CdpElectronProvider } from "../../../src/browser/CdpElectronProvider.js";
|
|
import {
|
|
inspectElectronPageInputSchema,
|
|
listElectronTargetsInputSchema,
|
|
} from "../../../src/domain/javascript/electronObservation.js";
|
|
import {
|
|
startFakeCdpBrowser,
|
|
type FakeCdpBrowser,
|
|
} from "../../fixtures/fakeCdpBrowser.js";
|
|
|
|
const browsers: FakeCdpBrowser[] = [];
|
|
const temporary: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(browsers.splice(0).map(async (browser) => browser.close()));
|
|
await Promise.all(
|
|
temporary
|
|
.splice(0)
|
|
.map(async (path) => rm(path, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
describe("CdpElectronProvider target access", () => {
|
|
it("returns every in-scope resource beyond the former collection ceiling", async () => {
|
|
const root = await electronFixture();
|
|
const index = join(root, "index.html");
|
|
const count = 2_001;
|
|
await Promise.all(
|
|
Array.from({ length: count }, (_value, item) =>
|
|
writeFile(join(root, `inventory-${String(item)}.js`), ""),
|
|
),
|
|
);
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(index).href,
|
|
electronInventoryCount: count,
|
|
});
|
|
browsers.push(browser);
|
|
const inspected = await new CdpElectronProvider().inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "electron-page",
|
|
observation_ms: 0,
|
|
}),
|
|
);
|
|
if (!inspected.ok) throw inspected.error;
|
|
expect(inspected.value.resources).toHaveLength(count + 1);
|
|
expect(inspected.value.completeness.truncated_sections).toEqual([]);
|
|
});
|
|
|
|
it("returns every Electron page target exposed by the endpoint inline", async () => {
|
|
const root = await electronFixture();
|
|
const index = join(root, "index.html");
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(index).href,
|
|
additionalElectronPageCount: 205,
|
|
});
|
|
browsers.push(browser);
|
|
const listed = await new CdpElectronProvider().listTargets(
|
|
listElectronTargetsInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
}),
|
|
);
|
|
if (!listed.ok) throw listed.error;
|
|
expect(listed.value.targets).toHaveLength(206);
|
|
expect(listed.value.targets[0]?.target_id).toBe("electron-page");
|
|
expect(listed.value.targets.at(-1)?.target_id).toBe("electron-page-0204");
|
|
});
|
|
});
|
|
|
|
describe("CdpElectronProvider target selection", () => {
|
|
it("lists and inspects every canonical local file target", async () => {
|
|
const root = await electronFixture();
|
|
const index = join(root, "index.html");
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(index).href,
|
|
duplicateElectronInventory: true,
|
|
});
|
|
browsers.push(browser);
|
|
const provider = new CdpElectronProvider();
|
|
const listed = await provider.listTargets(
|
|
listElectronTargetsInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
}),
|
|
);
|
|
if (!listed.ok) throw listed.error;
|
|
expect(listed.value.targets).toEqual([
|
|
expect.objectContaining({
|
|
target_id: "electron-page",
|
|
file_path: index,
|
|
}),
|
|
]);
|
|
expect(listed.value.excluded).toEqual({
|
|
unsupported_url: 3,
|
|
non_page: 2,
|
|
});
|
|
|
|
const inspected = await provider.inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "electron-page",
|
|
observation_ms: 0,
|
|
}),
|
|
);
|
|
if (!inspected.ok) throw inspected.error;
|
|
expect(inspected.value.target.file_path).toBe(index);
|
|
expect(inspected.value.frames).toEqual([
|
|
expect.objectContaining({ file_path: index }),
|
|
]);
|
|
expect(inspected.value.dom).toMatchObject({ total_nodes: 2 });
|
|
expect(inspected.value.scripts).toMatchObject({
|
|
total: 1,
|
|
items: [
|
|
expect.objectContaining({
|
|
file_path: join(root, "app.js"),
|
|
source: {
|
|
included: false,
|
|
reason: "source capture was not selected",
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
expect(inspected.value.resources).toEqual([
|
|
expect.objectContaining({ file_path: join(root, "app.js") }),
|
|
]);
|
|
const methods = browser.commands.map(({ method }) => method);
|
|
expect(methods).not.toContain("Runtime.evaluate");
|
|
expect(methods).not.toContain("Network.enable");
|
|
expect(methods).not.toContain("Debugger.getScriptSource");
|
|
expect(methods).toContain("Target.detachFromTarget");
|
|
|
|
const missing = await provider.inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "missing",
|
|
observation_ms: 0,
|
|
}),
|
|
);
|
|
expect(missing).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "BrowserObservationError",
|
|
operation: "inspect_electron_page",
|
|
reason: "target_not_found",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("inspects a page-scoped Electron endpoint without browser attachment", async () => {
|
|
const root = await electronFixture();
|
|
const index = join(root, "index.html");
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(index).href,
|
|
pageScopedVersionWebSocket: true,
|
|
omitTargetWebSocket: true,
|
|
});
|
|
browsers.push(browser);
|
|
const result = await new CdpElectronProvider().inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "electron-page",
|
|
observation_ms: 0,
|
|
}),
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.target.file_path).toBe(index);
|
|
expect(browser.commands.map(({ method }) => method)).not.toContain(
|
|
"Target.attachToTarget",
|
|
);
|
|
expect(
|
|
browser.commands.every(({ sessionId }) => sessionId === undefined),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("maps cancelled Electron discovery to the Electron operation", async () => {
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
const result = await new CdpElectronProvider().listTargets(
|
|
listElectronTargetsInputSchema.parse({
|
|
cdp_endpoint: "http://127.0.0.1:9222",
|
|
}),
|
|
{ signal: controller.signal },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisCancelledError",
|
|
operation: "list_electron_targets",
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("CdpElectronProvider packaged pages", () => {
|
|
it("lists and inspects packaged pages served from inside app.asar", async () => {
|
|
const root = await createTestTempDirectory("rea-electron-asar-provider-");
|
|
temporary.push(root);
|
|
const source = join(root, "source");
|
|
await mkdir(source);
|
|
await writeFile(
|
|
join(source, "index.html"),
|
|
"<script src='app.js'></script>",
|
|
);
|
|
await writeFile(join(source, "app.js"), "export const app = true;");
|
|
const archive = join(root, "app.asar");
|
|
await createPackage(source, archive);
|
|
const packaged = await realpath(archive);
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: `${pathToFileURL(join(archive, "index.html")).href}?isBookDoubleClicked=0#/library`,
|
|
});
|
|
browsers.push(browser);
|
|
const provider = new CdpElectronProvider();
|
|
const listed = await provider.listTargets(
|
|
listElectronTargetsInputSchema.parse({ cdp_endpoint: browser.endpoint }),
|
|
);
|
|
if (!listed.ok) throw listed.error;
|
|
expect(listed.value.targets).toEqual([
|
|
expect.objectContaining({
|
|
target_id: "electron-page",
|
|
file_path: join(packaged, "index.html"),
|
|
}),
|
|
]);
|
|
|
|
const inspected = await provider.inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "electron-page",
|
|
observation_ms: 0,
|
|
}),
|
|
);
|
|
if (!inspected.ok) throw inspected.error;
|
|
expect(inspected.value.target.file_path).toBe(join(packaged, "index.html"));
|
|
expect(inspected.value.scripts.items).toEqual([
|
|
expect.objectContaining({ file_path: join(packaged, "app.js") }),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("CdpElectronProvider capture", () => {
|
|
it.each([{}, { scriptSource: null }, { scriptSource: 123 }])(
|
|
"rejects malformed Electron source replies instead of inventing source: %j",
|
|
async (reply) => {
|
|
const root = await electronFixture();
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(join(root, "index.html")).href,
|
|
commandResult: ({ method }) =>
|
|
method === "Debugger.getScriptSource" ? reply : undefined,
|
|
});
|
|
browsers.push(browser);
|
|
const result = await new CdpElectronProvider().inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "electron-page",
|
|
observation_ms: 0,
|
|
include_script_sources: true,
|
|
}),
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
operation: "inspect_electron_page",
|
|
reason: "protocol_error",
|
|
userMessage: expect.stringContaining("scriptSource must be a string"),
|
|
},
|
|
});
|
|
expect(browser.commands.map(({ method }) => method)).toContain(
|
|
"Target.detachFromTarget",
|
|
);
|
|
},
|
|
);
|
|
|
|
it("retains an explicitly empty Electron producer script", async () => {
|
|
const root = await electronFixture();
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(join(root, "index.html")).href,
|
|
commandResult: ({ method }) =>
|
|
method === "Debugger.getScriptSource"
|
|
? { scriptSource: "" }
|
|
: undefined,
|
|
});
|
|
browsers.push(browser);
|
|
const result = await new CdpElectronProvider().inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "electron-page",
|
|
observation_ms: 0,
|
|
include_script_sources: true,
|
|
}),
|
|
);
|
|
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.scripts.items[0]?.source).toMatchObject({
|
|
included: true,
|
|
artifact: { text: "", bytes: 0 },
|
|
});
|
|
});
|
|
|
|
it("captures Electron script source only after separate approval", async () => {
|
|
const root = await electronFixture();
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(join(root, "index.html")).href,
|
|
});
|
|
browsers.push(browser);
|
|
const result = await new CdpElectronProvider().inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "electron-page",
|
|
observation_ms: 0,
|
|
include_script_sources: true,
|
|
}),
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.scripts.items[0]?.source).toMatchObject({
|
|
included: true,
|
|
artifact: { text: "export const observed = 'source-secret';" },
|
|
});
|
|
expect(browser.commands.map(({ method }) => method)).toContain(
|
|
"Debugger.getScriptSource",
|
|
);
|
|
});
|
|
|
|
it("rebases DOM parent indexes across allowed file documents", async () => {
|
|
const root = await electronFixture();
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(join(root, "index.html")).href,
|
|
extraCollections: true,
|
|
});
|
|
browsers.push(browser);
|
|
const result = await new CdpElectronProvider().inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "electron-page",
|
|
observation_ms: 0,
|
|
}),
|
|
);
|
|
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.dom).toMatchObject({
|
|
total_nodes: 4,
|
|
nodes: [
|
|
{ index: 0, parent_index: -1 },
|
|
{ index: 1, parent_index: 0 },
|
|
{ index: 2, parent_index: -1 },
|
|
{ index: 3, parent_index: 2 },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("rejects mixed Electron evidence when the main frame navigates", async () => {
|
|
const root = await electronFixture();
|
|
const page = join(root, "index.html");
|
|
const next = join(root, "next.html");
|
|
await writeFile(next, "<html>next</html>");
|
|
const browser = await startFakeCdpBrowser({
|
|
electronFileUrl: pathToFileURL(page).href,
|
|
navigateDuringCaptureUrl: pathToFileURL(next).href,
|
|
});
|
|
browsers.push(browser);
|
|
const result = await new CdpElectronProvider().inspectPage(
|
|
inspectElectronPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "electron-page",
|
|
observation_ms: 0,
|
|
}),
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "BrowserObservationError",
|
|
operation: "inspect_electron_page",
|
|
reason: "target_changed",
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
const electronFixture = async (): Promise<string> => {
|
|
const root = await createTestTempDirectory("rea-electron-provider-");
|
|
temporary.push(root);
|
|
await writeFile(join(root, "index.html"), "<script src='app.js'></script>");
|
|
await writeFile(join(root, "app.js"), "export const app = true;");
|
|
await writeFile(join(root, "child.html"), "<div>child</div>");
|
|
return root;
|
|
};
|