feat(native): inspect native export tables through CLI and MCP

Expose the reader as inspect_native_exports / inspect-native-exports:
one stable, digest-bound read of an explicit file with a caller-selected
input budget, typed failures for malformed, unsupported, oversized and
cancelled reads, and observed Evidence bound to the file path and
SHA-256. Mach-O inputs point to inspect_macho.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 committed 2026-10-22 07:22:06 +00:00
1 parent 2dee400f93
commit 0c7e8a6e17
19 files changed
+594 -4

No files matched your search

+7
View File
@@ -257,6 +257,13 @@ export const providerCatalog = (sources) => {
({ name }) => name === "inspect_byte_entropy",
),
},
{
identity: sources.artifactProviders.NATIVE_EXPORTS_PROVIDER,
contracts:
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
({ name }) => name === "inspect_native_exports",
),
},
{
identity: sources.captureProvider.TSHARK_PROVIDER_IDENTITY,
contracts: sources.captureContracts.CAPTURE_TOOL_CONTRACTS,
+1 -1
View File
@@ -498,7 +498,7 @@ const workflowAvailabilityFor = ({
remediation:
"Supply WABT 1.0.42 via absolute REA_WABT_BIN_DIRECTORY. No active binary target is required; tools are checked at execution.",
};
if (name === "inspect_pe_resources")
if (name === "inspect_pe_resources" || name === "inspect_native_exports")
return { reason: "available", remediation: null };
if (name === "inspect_evm_interface") {
if (policy.evmInterfaceEnabled !== true)
@@ -55,6 +55,13 @@ export const BYTE_ENTROPY_PROVIDER = Object.freeze({
version: "1",
} as const);
/** Provider identity for portable, execution-free PE and ELF export tables. */
export const NATIVE_EXPORTS_PROVIDER = Object.freeze({
id: "rea-native-exports",
name: "REA PE/ELF export table reader",
version: "1",
} as const);
/** Provider identity for deterministic managed-code comparison workflows. */
export const MANAGED_WORKFLOW_PROVIDER = {
id: "rea-dotnet-workflows",
@@ -0,0 +1,20 @@
import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
InspectNativeExportsInput,
NativeExports,
} from "../../domain/native/nativeExports.js";
import type { Result } from "../../domain/result.js";
/** Stable read and export-table decoding, independent of an active engine session. */
export interface NativeExportsPort {
readonly identity: ProviderIdentity;
close?(): Promise<void>;
inspect(
input: InspectNativeExportsInput,
options?: ExecutionOptions,
): Promise<Result<NativeExports, AnalysisError>>;
}
@@ -0,0 +1,79 @@
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { NativeExportsPort } from "./NativeExportsPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import {
inspectNativeExportsInputSchema,
nativeExportsSchema,
} from "../../domain/native/nativeExports.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
const OPERATION = "inspect_native_exports";
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
export class NativeExportsService {
constructor(readonly provider: NativeExportsPort) {}
close(): Promise<void> {
return this.provider.close?.() ?? Promise.resolve();
}
/** Read one explicitly selected PE or ELF image's export table. */
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const input = inspectNativeExportsInputSchema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
cause: input.error,
}),
);
const result = await this.provider.inspect(input.data, options);
if (!result.ok) return result;
const report = nativeExportsSchema.safeParse(result.value);
if (
!report.success ||
report.data.artifact.path !== input.data.path ||
report.data.artifact.bytes > input.data.max_file_bytes ||
report.data.coverage.exports !== report.data.exports.length
)
return err(
new AnalysisOutputError(
OPERATION,
"Native export reader returned malformed output or changed artifact identity.",
),
);
const value = report.data;
return completeProviderEvidence(
createEvidence(
{
path: value.artifact.path,
format: value.format,
sha256: value.artifact.sha256,
},
this.provider.identity,
{
operation: OPERATION,
parameters: input.data,
result: value,
confidence: "observed",
limitations: value.limitations,
locations: [{ kind: "artifact-path", path: value.artifact.path }],
},
),
options?.signal,
);
}
}
+33
View File
@@ -5,6 +5,7 @@ import {
createRecordedCrashService,
createBinaryLayoutService,
createPeResourcesService,
createNativeExportsService,
} from "../composition/binaryDiagnostics.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
@@ -14,6 +15,7 @@ import { withCommandCancellation } from "./commandCancellation.js";
import { inspectBinaryLayoutInputSchema } from "../domain/native/binaryLayout.js";
import { inspectRecordedCrashInputSchema } from "../domain/native/recordedCrash.js";
import { inspectPeResourcesInputSchema } from "../domain/native/peResources.js";
import { inspectNativeExportsInputSchema } from "../domain/native/nativeExports.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
@@ -95,6 +97,37 @@ export const registerBinaryDiagnosticsCommands = (
);
}),
});
cli.command(CLI_COMMANDS.inspectNativeExports, {
description:
"Inspect a PE or ELF image's export table without executing it",
args: z.object({ path: z.string().describe("Local PE or ELF image") }),
options: z.object({
maxFileBytes: inspectNativeExportsInputSchema.shape.max_file_bytes,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const exports = createNativeExportsService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectNativeExports,
async () =>
finishAnalysisCommand(
() =>
exports.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
},
{ signal },
),
exports,
"rea-native-exports",
"inspect_native_exports",
signal,
),
);
}),
});
const service = createBinaryLayoutService(environment);
cli.command(CLI_COMMANDS.inspectBinaryLayout, {
description:
+1
View File
@@ -66,6 +66,7 @@ export const CLI_COMMANDS = Object.freeze({
inspectGpuCodeObjects: "inspect-gpu-code-objects",
inspectRustBinary: "inspect-rust-binary",
inspectPeResources: "inspect-pe-resources",
inspectNativeExports: "inspect-native-exports",
inspectAnalysisView: "inspect-analysis-view",
listFridaDevices: "list-frida-devices",
listFridaProcesses: "list-frida-processes",
+6
View File
@@ -6,11 +6,17 @@ import { PwntoolsLayoutProvider } from "../native/pwntools/PwntoolsLayoutProvide
import { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
import { PeResourcesProvider } from "../native/pe/PeResourcesProvider.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js";
import { NativeExportsProvider } from "../native/exports/NativeExportsProvider.js";
/** Portable PE inspection with no engine, installation or target acquisition. */
export const createPeResourcesService = (): PeResourcesService =>
new PeResourcesService(new PeResourcesProvider());
/** Portable PE/ELF export tables with no engine, toolchain or target acquisition. */
export const createNativeExportsService = (): NativeExportsService =>
new NativeExportsService(new NativeExportsProvider());
/** Compose the offline adapter without import-time I/O or dependency acquisition. */
export const createBinaryLayoutService = (
environment: Readonly<NodeJS.ProcessEnv> = process.env,
@@ -14,6 +14,10 @@ import {
analysisDetailOutputSchemaOf,
analysisDetailSchema,
} from "../analysisDetail.js";
import {
inspectNativeExportsInputSchema,
nativeExportsSchema,
} from "../../domain/native/nativeExports.js";
import type { ToolContract } from "../toolContractTypes.js";
import { toolContractMetadata } from "../toolEffects.js";
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
@@ -76,6 +80,21 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
},
],
},
{
name: "inspect_native_exports",
...toolContractMetadata("inspect_native_exports"),
kind: "native-provider",
description:
"Inspect one explicit local PE or ELF image's export table without executing it or opening an engine: PE export-directory names, ordinals, addresses and forwarders, or ELF defined default/protected global, weak and unique .dynsym symbols with type, binding and symbol version. Ordinal-only, forwarded and hidden-version exports stay distinct. Portable on any supported host. Coverage is partial when table entries are unreadable and unsupported for an ELF without section headers; an empty complete inventory means the image declares no exports. Mach-O exports use inspect_macho.",
inputSchema: inspectNativeExportsInputSchema,
outputSchema: evidenceResultOf(nativeExportsSchema),
examples: [
{
title: "List a native library's exports",
input: { path: "/artifacts/native/libexample.so" },
},
],
},
{
name: "inspect_binary_layout",
...toolContractMetadata("inspect_binary_layout"),
+1
View File
@@ -27,6 +27,7 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
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_native_exports: "reads the export table without executing it",
inspect_desktop_application:
"states that directory triage performs no target execution",
inspect_go_binary: "reads embedded metadata without executing the target",
+1
View File
@@ -253,6 +253,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
}),
inspect_byte_entropy: evidence,
inspect_pe_resources: evidence,
inspect_native_exports: evidence,
inspect_analysis_view: evidence,
inspect_firmware_regions: effects({
mutatesSession: true,
+212
View File
@@ -0,0 +1,212 @@
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
import {
ArtifactFileCleanupFailure,
assertArtifactReadActive,
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../../artifacts/ArtifactFileOperation.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import type { NativeExportsPort } from "../../application/binaryDiagnostics/NativeExportsPort.js";
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
} from "../../domain/analysisErrorCore.js";
import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js";
import {
MAX_INSPECT_NATIVE_EXPORTS_FILE_BYTES,
type InspectNativeExportsInput,
} from "../../domain/native/nativeExports.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { err, ok } from "../../domain/result.js";
import { NATIVE_EXPORTS_PROVIDER } from "../../application/InvestigationProviders.js";
import {
BinaryFormatFailure,
BinaryResourceFailure,
} from "../metadata/BinaryContainer.js";
import { parseNativeExports } from "./NativeExportParser.js";
const OPERATION = "inspect_native_exports";
/** Portable export-table reader over one bounded, identity-checked regular file. */
export class NativeExportsProvider implements NativeExportsPort {
readonly identity = NATIVE_EXPORTS_PROVIDER;
readonly #resources = new ArtifactResourceScope();
async close(): Promise<void> {
try {
await this.#resources.close();
} catch (cause: unknown) {
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.cleanup === undefined
)
throw cause;
const error = new ProviderCleanupError(
NATIVE_EXPORTS_PROVIDER.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: OPERATION },
);
bindArtifactFileCleanup(cause, error);
throw error;
}
}
async inspect(input: InspectNativeExportsInput, options?: ExecutionOptions) {
try {
return await this.#resources.run(async () => {
const snapshot = await readStableArtifact(
input.path,
input.max_file_bytes,
options?.signal,
undefined,
this.#resources,
);
assertArtifactReadActive(options?.signal);
const report = parseNativeExports(snapshot.bytes, {
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
});
assertArtifactReadActive(options?.signal);
return ok(report);
});
} catch (cause) {
return err(nativeExportsFailure(cause, input, options?.signal));
}
}
}
const fileSizeRemediation = (input: InspectNativeExportsInput): string =>
input.max_file_bytes >= MAX_INSPECT_NATIVE_EXPORTS_FILE_BYTES
? `The image exceeds the ${String(MAX_INSPECT_NATIVE_EXPORTS_FILE_BYTES)}-byte input ceiling this operation accepts. This release has no runtime override. Select a smaller artifact.`
: `Raise max_file_bytes above ${String(input.max_file_bytes)} bytes or select a smaller artifact; the limit is the caller-selected input read budget.`;
const nativeExportsFailurePrimary = (
cause: unknown,
input: InspectNativeExportsInput,
): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (cause instanceof ArtifactReaderFailure && cause.reason === "integrity")
return new AnalysisArtifactChangedError(
OPERATION,
input.path,
cause.message,
{ cause },
);
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
return new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
if (cause instanceof BinaryResourceFailure)
return new AnalysisResourceConstraintError(
OPERATION,
"capacity",
`Selected artifact ${input.path}: ${cause.message}`,
{ boundary: cause.boundary, maximum: cause.maximum_bytes },
{
cause,
...fixedAllowanceOptions(
"The native export inventory allowance",
"count",
),
},
);
if (cause instanceof BinaryFormatFailure)
return cause.kind === "unsupported"
? new AnalysisUnsupportedTargetError(
OPERATION,
input.path,
cause.message,
{
cause,
},
)
: new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_format",
message: `Selected artifact ${input.path}: ${cause.message}`,
},
]);
if (cause instanceof ArtifactReaderFailure) {
if (cause.reason === "limit")
return new AnalysisResourceConstraintError(
OPERATION,
"file-size",
cause.message,
{ max_file_bytes: input.max_file_bytes },
{
cause,
retryable: false,
remediationAction: fileSizeRemediation(input),
},
);
if (cause.reason === "path" || cause.reason === "format")
return new AnalysisInputError(OPERATION, { cause }, [
{ path: ["path"], reason: "invalid_format", message: cause.message },
]);
}
if (cause instanceof Error && "code" in cause) {
if (cause.code === "EACCES" || cause.code === "EPERM")
return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, {
cause,
});
if (
["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes(
String(cause.code),
)
)
return new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_value",
message: `Selected image could not be read (${String(cause.code)}): ${input.path}.`,
},
]);
}
return new ProviderAdapterError(NATIVE_EXPORTS_PROVIDER.id, OPERATION, {
cause,
diagnostics: {
path: input.path,
reason: cause instanceof Error ? cause.message : String(cause),
},
});
};
const nativeExportsFailure = (
cause: unknown,
input: InspectNativeExportsInput,
signal: AbortSignal | undefined,
): AnalysisError => {
const primary =
cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "failed"
? nativeExportsFailurePrimary(
cause.fileCleanupFailure.outcome.cause,
input,
)
: cause instanceof ArtifactFileCleanupFailure && signal?.aborted
? new AnalysisCancelledError(OPERATION, { cause: signal.reason })
: nativeExportsFailurePrimary(cause, input);
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
NATIVE_EXPORTS_PROVIDER.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ operation: OPERATION, cause },
);
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
bindArtifactFileCleanup(cause, error);
return error;
};
+18 -1
View File
@@ -12,9 +12,14 @@ import { wasmArtifactModule } from "../composition/wasm.js";
import { registerWasmTools } from "./registerWasmTools.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { captureProviderEnvironment } from "./providerEnvironment.js";
import { createPeResourcesService } from "../composition/binaryDiagnostics.js";
import {
createNativeExportsService,
createPeResourcesService,
} from "../composition/binaryDiagnostics.js";
import type { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
import type { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js";
import { registerPeResourcesTool } from "./registerPeResourcesTool.js";
import { registerNativeExportsTool } from "./registerNativeExportsTool.js";
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import {
evmInterfaceModule,
@@ -350,6 +355,7 @@ export const createServer = (
const il2cppMetadata =
options.il2cppMetadata ?? createIl2cppMetadataService();
const peResources = createPeResourcesService();
const nativeExports = createNativeExportsService();
const electronFuses = options.electronFuses ?? createElectronFuseService();
const webNetworkCapture =
options.webNetworkCapture ?? createWebNetworkCaptureService(environment);
@@ -451,6 +457,7 @@ export const createServer = (
modelArtifact.close(),
gpuCodeObjects.close(),
peResources.close(),
nativeExports.close(),
electronFuses.close(),
webNetworkCapture.close(),
javascriptRecovery.close?.() ?? Promise.resolve(),
@@ -514,6 +521,7 @@ export const createServer = (
rustBinary,
il2cppMetadata,
peResources,
nativeExports,
electronFuses,
webNetworkCapture,
};
@@ -745,6 +753,7 @@ const registerConfiguredAnalysisTools = (
rustBinary,
il2cppMetadata,
peResources,
nativeExports,
webNetworkCapture,
artifactResources,
}: ServerToolContext,
@@ -794,6 +803,13 @@ const registerConfiguredAnalysisTools = (
recordEvidence,
withAdmittedAnalysis,
);
registerNativeExportsTool(
server,
nativeExports,
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerGoTools(
server,
goBinary,
@@ -927,6 +943,7 @@ interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
readonly rustBinary: RustBinaryService;
readonly il2cppMetadata: Il2cppMetadataService;
readonly peResources: PeResourcesService;
readonly nativeExports: NativeExportsService;
readonly electronFuses: ElectronFuseService;
readonly webNetworkCapture: WebNetworkCaptureService;
readonly artifactResources: ArtifactResourceScope;
+31
View File
@@ -0,0 +1,31 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Register the explicit-file export-table workflow and retain its complete Evidence. */
export const registerNativeExportsTool = (
server: EvidenceMcpServer,
service: NativeExportsService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_native_exports");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
applicationToolRegistrationOptions(contract),
handler(contract),
);
};
@@ -218,6 +218,7 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
{ capture_path: nul, output_directory: "/tmp/exported-scripts" },
],
["inspect_pe_resources", { path: nul }],
["inspect_native_exports", { path: nul }],
["inspect_binary_layout", { path: nul }],
["inspect_recorded_crash", { path: nul }],
["inspect_wasm_artifact", { path: nul }],
@@ -0,0 +1,104 @@
import { createHash } from "node:crypto";
import { symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createNativeExportsService } from "../../../src/composition/binaryDiagnostics.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { nativeExportsSchema } from "../../../src/domain/native/nativeExports.js";
import {
elfExportImage,
peExportImage,
} from "../../fixtures/binaryDiagnostics/nativeExports.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("native export stable filesystem boundary", () => {
it("binds PE and ELF export inventories to the selected file", async () => {
const root = await createTestTempDirectory("rea-native-exports-");
const service = createNativeExportsService();
for (const [name, bytes, format] of [
[
"fixture.dll",
peExportImage({
functions: [{ rva: 0x2000 }],
names: [{ name: "Exported", index: 0 }],
}),
"pe",
],
[
"libfixture.so",
elfExportImage({ symbols: [{ name: "exported" }] }),
"elf",
],
] as const) {
const path = join(root, name);
await writeFile(path, bytes);
const result = await service.inspect({ path });
if (!result.ok) throw new Error(JSON.stringify(result.error));
const evidence = parseEvidence(result.value);
const sha256 = createHash("sha256").update(bytes).digest("hex");
expect(evidence).toMatchObject({
operation: "inspect_native_exports",
provider: { id: "rea-native-exports" },
confidence: "observed",
subject: { local_path: path, format, digest: { sha256 } },
});
expect(
nativeExportsSchema.parse(evidence.normalized_result),
).toMatchObject({
artifact: { path, sha256, bytes: bytes.length },
format,
coverage: { status: "complete", exports: 1 },
});
}
await service.close();
});
it("returns typed input, unsupported, budget and cancellation failures", async () => {
const root = await createTestTempDirectory("rea-native-export-failures-");
const path = join(root, "fixture.dll");
const alias = join(root, "alias.dll");
const macho = join(root, "fixture.dylib");
const malformed = join(root, "malformed.dll");
await writeFile(path, peExportImage({ functions: [] }));
const machoBytes = Buffer.alloc(64);
machoBytes.writeUInt32LE(0xfeedfacf, 0);
await writeFile(macho, machoBytes);
await writeFile(malformed, Buffer.from("MZ not a PE image"));
await symlink(path, alias);
const service = createNativeExportsService();
const failure = async (
input: Record<string, unknown>,
signal?: AbortSignal,
) => {
const result = await service.inspect(
input,
signal === undefined ? undefined : { signal },
);
if (result.ok) throw new Error("Expected a failure");
return result.error;
};
for (const [candidate, reason] of [
[alias, "invalid_format"],
[malformed, "invalid_format"],
[join(root, "missing.dll"), "invalid_value"],
] as const)
expect(await failure({ path: candidate })).toMatchObject({
_tag: "AnalysisInputError",
operation: "inspect_native_exports",
issues: [{ path: ["path"], reason }],
});
expect(await failure({ path: macho })).toMatchObject({
_tag: "AnalysisUnsupportedTargetError",
reason: expect.stringContaining("inspect_macho"),
});
expect(await failure({ path, max_file_bytes: 64 })).toMatchObject({
_tag: "AnalysisResourceConstraintError",
});
expect(
await failure({ path }, AbortSignal.abort("caller stop")),
).toMatchObject({ _tag: "AnalysisCancelledError" });
await service.close();
});
});
@@ -78,6 +78,7 @@ const standaloneAnalysisCases = (
{ name: "inspect_evm_interface", input: { encoding: "hex" } },
{ name: "inspect_sqlite_database", input: {} },
{ name: "inspect_pe_resources", input: {} },
{ name: "inspect_native_exports", input: {} },
{ name: "inspect_electron_fuses", input: {} },
{ name: "inspect_packet_capture", input: { fields: ["ip.addr"] } },
{
@@ -1,4 +1,4 @@
import { randomBytes } from "node:crypto";
import { createHash, randomBytes } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
@@ -20,6 +20,8 @@ import {
binaryLayoutFixture,
BINARY_LAYOUT_TEST_PROVIDER,
} from "../../fixtures/binaryDiagnostics/layout.js";
import { elfExportImage } from "../../fixtures/binaryDiagnostics/nativeExports.js";
import { parseNativeExports } from "../../../src/native/exports/NativeExportParser.js";
it("advertises exact valid schemas and records inline layout without a binary target", async () => {
const value = binaryLayoutFixture();
@@ -181,3 +183,50 @@ it("profiles real bytes with exact offsets, valid advertised schemas and read-on
).isError,
).toBe(true);
});
it("records a portable export inventory through MCP without a binary target", async () => {
const root = await createTestTempDirectory("rea-native-exports-mcp-");
const path = join(root, "libfixture.so");
const bytes = elfExportImage({
symbols: [{ name: "exported_fn" }, { name: "hidden_fn", visibility: 2 }],
});
await writeFile(path, bytes);
const session = createTestBinarySession(() => {
throw new Error("deep provider must not start");
});
const server = createServer({ kind: "session", session });
const client = new Client({ name: "native-exports-contract", version: "1" });
onTestFinished(async () => {
await client.close();
await server.close();
await session.close();
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const schemas = await advertisedMcpToolSchemas(
client,
"inspect_native_exports",
);
expect(schemas.ajv.validate(schemas.inputSchema, { path })).toBe(true);
expect(
schemas.ajv.validate(schemas.inputSchema, { path: "relative.so" }),
).toBe(false);
const response = await client.callTool({
name: "inspect_native_exports",
arguments: { path },
});
assertRecordedMcpEvidence(
response,
"inspect_native_exports",
schemas,
session,
parseNativeExports(bytes, {
path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
}),
"observed",
);
});
+2 -1
View File
@@ -35,7 +35,8 @@ export const assertRecordedMcpEvidence = (
name:
| "inspect_wasm_artifact"
| "inspect_binary_layout"
| "inspect_evm_interface",
| "inspect_evm_interface"
| "inspect_native_exports",
schemas: Awaited<ReturnType<typeof advertisedMcpToolSchemas>>,
session: BinarySession,
expected: { readonly artifact: { readonly sha256: string } },