Files
reverse-engineering-anything/src/contracts/toolEffects.test.ts
T
7aa1b51282 feat(diagnostics): report a byte entropy profile for one local file (#2331)
* feat(diagnostics): report a byte entropy profile for one local file

The diff adds the MCP tool inspect_byte_entropy and the CLI command
inspect-byte-entropy, both implemented by ByteEntropyService. One
absolute regular file is read offline into observed Evidence: file-wide
SHA-256 and Shannon entropy, then fixed blocks and adjacent runs for an
optional window, using a power-of-two block size from 256 to 65536
(default 4096), with high at 7.2 or above and low at 1.0 or below. A
window past EOF, a missing path, or a non-file is an input error; a size
or identity change during the read is an artifact-changed error; empty
input reports entropy 0; cancellation is honored before and during the
read. Summary detail retains the complete series and returns file
entropy, counts, and the ten longest runs for inspect_analysis_view,
while the registered effect remains read-only Evidence and the
diagnostics docs record the operation and its errors.

* fix(diagnostics): flag zero-size files as not shown empty

Generated files such as Linux /proc entries report size 0 while holding
content, so inspect_byte_entropy read nothing and returned an empty-looking
report. A file that reports size 0 now carries a limitation that the result
does not show the file is empty. The result stays a success because the
tool reads the declared size, which the file-stability checks depend on.

* fix(mcp): reject unsupported diagnostic summaries before inspection

* fix(ci): repair integrated checks and Hopper test baseline

* test(entropy): use shared MCP schema verification after merge

* fix(catalog): list inspect_byte_entropy under its own provider

Adding inspect_byte_entropy to BINARY_DIAGNOSTICS_TOOL_CONTRACTS put it
under pwntools-elf in the generated product catalog, because the
pwntools group took every binary diagnostics tool except
inspect_pe_resources. The service reports provider byte-entropy.

Add a BYTE_ENTROPY_PROVIDER identity beside PE_RESOURCES_PROVIDER, use
it in ByteEntropyService, give it its own catalog group, and name the
pwntools tools (inspect_binary_layout, inspect_recorded_crash)
explicitly so a new diagnostics tool is not attributed to pwntools by
default. The catalog test pins both groups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFTE4BHbGYzgf8Uhzcppdh

* fix(cli): resolve the byte-entropy path with absoluteCallerPath

main replaced the node:path resolve import in binaryDiagnosticsCommands.ts
with absoluteCallerPath (#4067), so the byte-entropy command's
resolve(args.path) no longer type-checks once the PR is merged with main.
Use absoluteCallerPath like the other commands, and add
src/filesystem/callerPath.ts matching main so the branch builds on its own.

* fix(diagnostics): retain entropy read cleanup ownership

* docs(diagnostics): explain entropy output capacity

* fix(diagnostics): preserve entropy cancellation outcomes

* fix(diagnostics): close the entropy input and warn on large reports

Public inspect_byte_entropy input now uses publicInputStrictObject before
detail is added, so unknown keys stay rejected on the advertised schema.
The tool description warns that complete output can become very large and
points callers to a shorter length or a larger block_size.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(diagnostics): keep byte-entropy registration mergeable with main

Register inspect_byte_entropy beside layout inspection and reject
unretained summaries before the file read, without editing workflows.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(diagnostics): register byte entropy with the shared tool options

Use toolRegistrationOptions so the handler typechecks on this branch and
on main, and still refuse unretained summaries before the file read.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 14:19:12 +08:00

223 lines
9.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { TOOL_CONTRACTS } from "./toolContracts.js";
import {
TOOL_EFFECTS,
annotationsFromEffects,
type ToolEffects,
} from "./toolEffects.js";
/**
* Descriptions that name running, launching, executing, starting, installing,
* or uninstalling something. A matching tool must declare foreign execution or
* device mutation unless it is listed below with the reason the wording does
* not describe that effect.
*/
const EXECUTION_WORDING =
/\b(?:run|runs|running|launch|launches|launching|execute|executes|executing|execution|start|starts|install|installs|uninstall|uninstalls)\b/iu;
const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
inspect_with_objdump:
"runs a caller-selected inspection tool, not target code",
start_gdb_session:
"starts a debugger session; execution is attributed to gdb_console",
start_frida_session:
"spawned targets remain suspended until resume_frida_session",
inspect_native_load_image: "states that it does not execute the target",
resolve_native_call_targets: "states that it does not execute code",
inspect_byte_entropy: "states that it profiles the file without execution",
inspect_pe_resources: "states that it does not execute the image",
inspect_desktop_application:
"states that directory triage performs no target execution",
inspect_go_binary: "reads embedded metadata without executing the target",
inspect_rust_binary:
"reads an embedded dependency list without executing the target",
inspect_sqlite_database:
"uses a trusted inspector worker without executing application code or caller SQL",
inspect_binary_layout:
"parses the binary offline with operator-configured pwntools",
inspect_recorded_crash: "inspects a core file without launching its target",
inspect_macho: "runs host Mach-O inspection tools, not the target",
inspect_evm_interface:
"decodes bytecode in REA's bundled worker without executing it",
compare_wasm_interfaces:
"compares retained inspection Evidence without executing modules",
inspect_wasm_artifact:
"WABT validates and decompiles the module without executing it",
identify_flutter_build:
"identifies the build through execution-free APK parsing",
inspect_dart_aot: "inspects snapshot bytes without executing them",
inspect_analysis_view:
"reports retained WASM execution/network states without performing execution",
inspect_managed_artifact: "reads metadata without executing target code",
inspect_managed_members: "reads metadata without executing target code",
inspect_managed_native_boundaries:
"reads metadata without executing target code",
verify_managed_native_boundaries: "states it does not execute managed code",
project_managed_application_graph: "states it does not execute managed code",
inspect_android_package: "states it does not execute the APK",
search_android_classes: "states it does not execute the APK",
inspect_apktool_client:
"inspects the operator-selected apktool launcher; never installs it",
inspect_pcap_client:
"probes the operator-selected tshark and capinfos; never installs them",
inspect_jeb_client: "talks to a caller-started JEB; never launches it",
open_jeb_project: "the engine analyzes the file; REA does not execute it",
inspect_adb_client:
"inspects the operator-selected adb client; never starts its server",
list_adb_devices: "may start the local adb server, which REA does not own",
inspect_firmware_regions: "states there is no target execution",
inspect_gpu_code_objects:
"reads static declarations without executing GPU code",
inspect_model_artifact:
"states that model code and inference are never executed",
extract_firmware: "states that extracted code is never executed",
analyze_javascript_application: "reconstructs the graph without executing",
reconcile_javascript_runtime: "never invents execution claims",
observe_javascript_runtime: "never evaluates or resumes the target",
trace_application_feature: "never executes the application",
trace_javascript_semantics: "static reachability never claims execution",
compare_javascript_export_shapes: "states it does not execute JavaScript",
project_android_application_graph: "execution-free inventory",
project_apple_application_graph: "execution-free inventory",
project_harmony_application_graph: "execution-free inventory",
trace_web_source_location: "states there is no application execution",
trace_web_module_imports:
"runs only REA's resolver stub in operator-configured Chromium",
export_web_scripts: "states there is no refetch or execution",
observe_web_execution:
"instruments an externally driven page; it does not run code itself",
recover_javascript_sources: "states there is no application code execution",
goto_address: "start names an object boundary, not a launch",
prev_address: "start names an object boundary, not a launch",
trace_call_path: "start names the traversal origin",
observe_native_ui: "observes an already-running app without launching it",
inspect_adb_package: "reports install times without changing the device",
list_adb_processes: "lists running processes without changing the device",
inspect_web_event_listeners: "states it does not evaluate JavaScript",
};
const declaresForeignAuthority = (name: string): boolean => {
const effects = TOOL_EFFECTS[name];
return (
effects !== undefined &&
(effects.executesForeignCode || effects.mutatesDevice)
);
};
describe("tool effects", () => {
it("audits every public tool exactly once", () => {
expect(Object.keys(TOOL_EFFECTS).sort()).toEqual(
TOOL_CONTRACTS.map(({ name }) => name).sort(),
);
});
it("keeps execution and device wording aligned with declared effects", () => {
const undeclared = TOOL_CONTRACTS.filter(
({ name, description }) =>
EXECUTION_WORDING.test(description) &&
!declaresForeignAuthority(name) &&
EXECUTION_WORDING_EXCEPTIONS[name] === undefined,
).map(({ name }) => name);
expect(undeclared).toEqual([]);
const staleExceptions = Object.keys(EXECUTION_WORDING_EXCEPTIONS).filter(
(name) => {
const contract = TOOL_CONTRACTS.find(
(candidate) => candidate.name === name,
);
return (
contract === undefined ||
!EXECUTION_WORDING.test(contract.description) ||
declaresForeignAuthority(name)
);
},
);
expect(staleExceptions).toEqual([]);
});
it("marks foreign execution and device mutation as destructive", () => {
const destructive = TOOL_CONTRACTS.filter(
({ effects }) => effects.executesForeignCode || effects.mutatesDevice,
);
expect(destructive.map(({ name }) => name).sort()).toEqual([
"capture_browser_scenario",
"capture_electron_scenario",
"capture_native_ui_scenario",
"capture_process_scenario",
"close_gdb_session",
"close_rizin_debug_session",
"cutter_command",
"execute_rizin_command",
"gdb_console",
"install_adb_package",
"instrument_with_frida",
"load_frida_script",
"observe_native_calls",
"push_adb_file",
"resume_frida_session",
"rizin_debug_command",
"start_adb_activity",
"start_adb_app",
"start_rizin_debug_session",
"stop_adb_app",
"uninstall_adb_package",
]);
for (const contract of destructive)
expect(contract.annotations, contract.name).toMatchObject({
readOnlyHint: false,
destructiveHint: true,
});
});
it("derives destructiveHint from each destructive effect alone", () => {
// Keep annotation algebra independent of any operation's audited effects.
const base: ToolEffects = {
mutatesTarget: false,
mutatesSession: false,
writesFilesystem: false,
launchesProcess: false,
accessesNetwork: false,
changesUiState: false,
mayDiscardData: false,
executesForeignCode: false,
mutatesDevice: false,
idempotent: true,
};
expect(annotationsFromEffects(base)).toEqual({
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
});
for (const flag of [
"mayDiscardData",
"executesForeignCode",
"mutatesDevice",
] as const)
expect(annotationsFromEffects({ ...base, [flag]: true }), flag).toEqual({
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
});
});
it("marks automatic GDB detach as destructive target execution", () => {
const effects = TOOL_EFFECTS["close_gdb_session"];
if (effects === undefined)
throw new Error("close_gdb_session is unaudited");
expect(effects).toMatchObject({
mutatesTarget: true,
executesForeignCode: true,
});
expect(annotationsFromEffects(effects).destructiveHint).toBe(true);
});
it("publishes the advertised annotations derived from canonical effects", () => {
for (const contract of TOOL_CONTRACTS)
expect(contract.annotations, contract.name).toEqual(
annotationsFromEffects(contract.effects),
);
});
});