Files
reverse-engineering-anything/src/application/adb/AdbDeviceAnalysisService.test.ts
T
fef6f73064 feat(adb): complete ADB device support for Android reverse engineering (#1552)
* feat(adb): acquire installed APK sets from connected Android devices

Add a read-only ADB device-acquisition family over the caller's adb binary:
inspect_adb_client, list_adb_devices, inspect_adb_device, list_adb_packages,
and pull_adb_package, exposed through the CLI and MCP with shared admission,
Evidence provenance (binary, version, serial, transport), and fixed argv
vectors that never reach a shell.

pull_adb_package acquires the complete split set reported by pm path into a
package-named directory that must not exist, digesting every file with
SHA-256 and keeping per-file failure reasons and partial coverage. Device
kind is reported as an inference with its basis; getprop whitelisting keeps
build identity bounded. REA installs nothing and never manages devices or
emulators; the first device query may start the caller's local adb server,
recorded when observed.

Verified against adb 34.0.5 on Linux with an Android 14 (API 34) x86_64
emulator: full 224-package inventory, a real two-APK split set pulled with
byte-exact digests, and a 73.9 MB system-package pull. npm run verify:adb
records the lane; parser goldens come from the same device. READMEs carry
the new row in every supported language.

* feat(adb): complete the adb surface with observation, transfer, and lifecycle tools

Extend the ADB family from five to twenty-four operations, all through
fixed adb argument vectors with no shell exposure:

Observation: read_adb_logcat (bounded dump, count/buffer/pid), inspect_adb_package
(dumpsys package projection), list_adb_processes (ps -A), list_adb_directory
(ls -la with kinds and symlink targets), list_adb_features (hex GL versions),
list_adb_services (AIDL slash-suffixed names), inspect_adb_display (wm size and
density), inspect_adb_window (focus declarations), read_adb_setting (device-side
nulls reported as such), capture_adb_screen (exec-out screencap with PNG
dimensions), collect_adb_bugreport (digested device archive).

Transfer and search: pull_adb_file and push_adb_file (device-side sha256sum
verification, explicit overwrite authority), resolve_adb_packages (substring
package search with exact-match flag — the find step of an easy get).

Lifecycle for dynamic analysis, each declaring mutatesTarget:
install_adb_package and uninstall_adb_package preserving the device's
Success/Failure verdict, start_adb_app (resolve-activity then am start -W),
start_adb_activity (typed extras as --es/--ez/--ei/--el/--ef), and stop_adb_app
(am force-stop).

Verified end-to-end against adb 34.0.5 on Linux with an Android 14 (API 34)
x86_64 emulator: 287 services, 92 features, split-set pulls with byte-exact
digests, verified push/pull roundtrip, sized screen capture, and the full
install → resolve → start → observe-in-ps → force-stop → uninstall lifecycle.
Fixed a v-mode regex incompatibility in the settings key pattern and added
argument descriptions required by the CLI catalog audit.

* fix(adb): quote remote shell operands and protect captured output

* test(adb): cover bugreport destinations containing spaces

---------

Co-authored-by: REA Contributor <contributor@rea.tools>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-10 23:01:24 +08:00

97 lines
2.9 KiB
TypeScript

import { expect, it } from "vitest";
import { AdbDeviceAnalysisService } from "./AdbDeviceAnalysisService.js";
import type { AdbDeviceAnalysisPort } from "./AdbDeviceAnalysisPort.js";
import { createAnalysisExecution } from "../AnalysisProvider.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { parseEvidence } from "../../domain/evidence.js";
import { ok } from "../../domain/result.js";
const identity = { id: "adb", name: "Android Debug Bridge", version: null };
const execution = createAnalysisExecution(
{
client: {
binary_path: "/usr/bin/adb",
path_source: "path",
version: "34.0.5-debian",
installed_path: "/usr/bin/adb",
},
devices: [],
may_have_started_adb_server: false,
},
identity,
);
const refusedPort: AdbDeviceAnalysisPort = {
async inspectAvailability() {
throw new Error("must not probe");
},
async close() {},
async execute() {
throw new Error("must not execute");
},
};
it.each([
[{ serial: "" }],
[{ serial: "has space" }],
[{ serial: "emulator-5554", scope: "unknown" }],
[{ serial: "emulator-5554", extra: true }],
])("rejects invalid device selection before any effect: %j", async (input) => {
const service = new AdbDeviceAnalysisService(refusedPort);
expect(await service.execute("inspect_adb_device", input)).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
});
it("rejects an unknown operation before any effect", async () => {
const service = new AdbDeviceAnalysisService(refusedPort);
expect(
await service.execute("inspect_adb_device" as never, {}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
});
it("composes observed Evidence with the caller parameters inline", async () => {
const port: AdbDeviceAnalysisPort = {
async inspectAvailability() {
return { status: "available", code: null, reason: null, diagnostics: {} };
},
async close() {},
async execute(request) {
expect(request).toEqual({
operation: "list_adb_devices",
input: {},
});
return ok(execution);
},
};
const result = await new AdbDeviceAnalysisService(port).execute(
"list_adb_devices",
{},
);
if (!result.ok) throw new Error(result.error.message);
const evidence = parseEvidence(result.value);
expect(evidence.provider).toEqual(identity);
expect(evidence.operation).toBe("list_adb_devices");
expect(evidence.parameters).toEqual({});
expect(evidence.confidence).toBe("observed");
});
it("preserves cancellation before execution", async () => {
const controller = new AbortController();
controller.abort();
const service = new AdbDeviceAnalysisService(refusedPort);
const result = await service.execute(
"list_adb_devices",
{},
{
signal: controller.signal,
},
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});