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:
1 parent
2dee400f93
commit
0c7e8a6e17
19 files changed
+594
-4
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
Vendored
+2
-1
@@ -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 } },
|
||||
|
||||
Reference in new issue
Block a user