Bind each lookup reply to its query, batch unique native callees, and fetch conservative literal string candidates while preserving Unicode, case, pagination, and raw source Evidence. Preserve explicit native search failures and describe unknown string representations. Verified with Windows IDA Pro 9.3 and the native MCP plugin built from c9d15d98e8a143fc74f6e14037c48f63889b6d6c with a loopback listener. Production CLI/MCP parity and two-function switching passed on the public fixture; equivalent tasks passed on Windows whoami.exe. Windows IDA Free was not used. No personal paths or license registration information are included. Validation: 182 focused tests, static checks, architecture guards, test discovery, and docs checks passed. Same-main measurements show warm task time 675 to 317 ms and estimated response tokens 16618 to 11509; cold-start, overall CPU, and memory improvement were not established. Detailed scope and measurements are in experiments/analysis-efficiency/ida-native-windows.md.
627 lines
22 KiB
TypeScript
627 lines
22 KiB
TypeScript
import { z } from "zod";
|
|
import {
|
|
nativeDecompileSchema,
|
|
nativeDisassemblySchema,
|
|
nativeCalleesSchema,
|
|
nativeXrefsSchema,
|
|
nativeLiteralSearchPattern,
|
|
nativeStringsSchema,
|
|
} from "./IdaNativeProtocol.js";
|
|
import { analysisSearchInput } from "../contracts/analysisSearchContract.js";
|
|
import {
|
|
AnalysisInputError,
|
|
AnalysisCancelledError,
|
|
AnalysisProtocolError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import {
|
|
functionDossierSchema,
|
|
functionInstructionWindowSchema,
|
|
} from "../domain/hopperValues.js";
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
import type { RelationshipCoverage } from "../domain/relationshipCoverage.js";
|
|
import { readCallGraphCallees, unavailableCallGraph } from "./IdaCallGraph.js";
|
|
import type { IdaMcpConnection } from "./IdaMcpConnection.js";
|
|
import type { IdaOperation } from "./IdaProviderCapabilities.js";
|
|
import {
|
|
IDA_LIMITATIONS,
|
|
IDA_NATIVE_STRING_LIMITATION,
|
|
} from "./IdaProviderCapabilities.js";
|
|
import type { EvidenceLocation } from "../domain/evidence.js";
|
|
import {
|
|
disassemblySchema,
|
|
idaAddressSchema,
|
|
idaFunctionSchema,
|
|
legacyCalleesSchema,
|
|
legacyPageSchema,
|
|
legacyStringSchema,
|
|
legacyXrefsSchema,
|
|
modernCalleesSchema,
|
|
modernDecompileSchema,
|
|
modernDisassemblySchema,
|
|
modernLookupSchema,
|
|
modernLookupBatchSchema,
|
|
modernPageSchema,
|
|
modernStringsSchema,
|
|
modernXrefsSchema,
|
|
nextOffset,
|
|
type IdaDisassembly,
|
|
type IdaFunction,
|
|
} from "./IdaProtocolValues.js";
|
|
|
|
/** One operation's raw source observations, retained alongside normalized results. */
|
|
export interface IdaRawObservation {
|
|
readonly tool: string;
|
|
readonly arguments: Readonly<Record<string, JsonValue>>;
|
|
readonly result: JsonValue;
|
|
}
|
|
|
|
const unknownBody = {
|
|
available: false,
|
|
reason: "IDA MCP does not report complete function body ranges.",
|
|
} as const;
|
|
const identity = (fn: IdaFunction) => ({
|
|
address: fn.address,
|
|
name: fn.name,
|
|
classification: null,
|
|
body: unknownBody,
|
|
});
|
|
/** The only lookup answers upstream reports for a name or address that is not a function. */
|
|
const MISSING_PROCEDURE_ANSWERS: ReadonlySet<string> = new Set([
|
|
"Not found",
|
|
"Not a function",
|
|
]);
|
|
const lines = (asm: IdaDisassembly) =>
|
|
asm.lines.map(({ address, instruction }) => `${address}: ${instruction}`);
|
|
const searchQuerySchema = z.object(analysisSearchInput);
|
|
|
|
/** Translate explicit analyst questions into a closed set of read-only upstream requests. */
|
|
export class IdaOperationRunner {
|
|
readonly raw: IdaRawObservation[] = [];
|
|
readonly locations: EvidenceLocation[] = [];
|
|
readonly limitations: string[] = [];
|
|
relationshipCoverage: RelationshipCoverage | undefined;
|
|
constructor(
|
|
private readonly connection: IdaMcpConnection,
|
|
private readonly protocol: "legacy" | "modern" | "native",
|
|
private readonly database?: string,
|
|
private readonly options: {
|
|
readonly signal?: AbortSignal | undefined;
|
|
readonly selectorOrigin?: "provider" | undefined;
|
|
/** False when the loaded processor may not be x86, so `callees` cannot be trusted alone. */
|
|
readonly x86Only?: boolean;
|
|
/** False when the upstream server does not advertise `callgraph`. */
|
|
readonly callGraphAvailable?: boolean;
|
|
} = {},
|
|
) {}
|
|
|
|
async call(
|
|
tool: string,
|
|
args: Readonly<Record<string, JsonValue>>,
|
|
): Promise<JsonValue> {
|
|
if (this.options.signal?.aborted) throw new AnalysisCancelledError(tool);
|
|
const scoped =
|
|
this.database === undefined ? args : { ...args, database: this.database };
|
|
const result = await this.connection.call(tool, scoped);
|
|
this.raw.push({ tool, arguments: scoped, result });
|
|
if (this.options.signal?.aborted) throw new AnalysisCancelledError(tool);
|
|
return result;
|
|
}
|
|
|
|
async functions(): Promise<IdaFunction[]> {
|
|
const result: IdaFunction[] = [];
|
|
let offset: number | null = 0;
|
|
while (offset !== null) {
|
|
const current: number = offset;
|
|
const value: z.infer<typeof legacyPageSchema> | undefined =
|
|
this.protocol === "legacy"
|
|
? legacyPageSchema.parse(
|
|
await this.call("list_functions", { offset, count: 100 }),
|
|
)
|
|
: modernPageSchema.parse(
|
|
await this.call("list_funcs", {
|
|
queries: { offset, count: 100 },
|
|
}),
|
|
)[0];
|
|
if (value === undefined)
|
|
throw new AnalysisProtocolError("IDA MCP omitted its function page.");
|
|
result.push(...value.data.map((fn) => idaFunctionSchema.parse(fn)));
|
|
offset = value.next_offset;
|
|
if (offset !== null && offset <= current)
|
|
throw new AnalysisProtocolError(
|
|
"IDA MCP function pagination did not advance.",
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Resolves a procedure. When `requestedBy` names the caller's operation, an
|
|
* upstream "not found" or "not a function" answer is reported as invalid
|
|
* input; every other failed lookup stays a producer fault. `run()` passes
|
|
* `requestedBy` only for caller-supplied selectors. Provider-derived
|
|
* selectors, such as the addresses composed workflows revisit, and the
|
|
* callee resolution inside `relationships()` pass none, because those
|
|
* addresses were reported by IDA itself.
|
|
*/
|
|
async resolve(query: string, requestedBy?: string): Promise<IdaFunction> {
|
|
if (this.protocol !== "legacy") {
|
|
const item = modernLookupSchema.parse(
|
|
await this.call("lookup_funcs", { queries: [query] }),
|
|
)[0];
|
|
return this.lookupFunction(query, item, requestedBy);
|
|
}
|
|
const address = idaAddressSchema.safeParse(
|
|
/^0x/iu.test(query) ? query : undefined,
|
|
);
|
|
const found = idaFunctionSchema.parse(
|
|
await this.call(
|
|
address.success ? "get_function_by_address" : "get_function_by_name",
|
|
address.success ? { address: address.data } : { name: query },
|
|
),
|
|
);
|
|
// Legacy producers may echo an interior query or call site in address.
|
|
// Resolve the reported symbol again to obtain its actual entry, never infer from size.
|
|
const entry = idaFunctionSchema.parse(
|
|
await this.call("get_function_by_name", { name: found.name }),
|
|
);
|
|
if (entry.name !== found.name)
|
|
throw new AnalysisProtocolError(
|
|
"IDA function identity changed during lookup.",
|
|
);
|
|
return entry;
|
|
}
|
|
|
|
private lookupFunction(
|
|
query: string,
|
|
item: z.infer<typeof modernLookupBatchSchema>[number] | undefined,
|
|
requestedBy?: string,
|
|
): IdaFunction {
|
|
if (item?.query !== query)
|
|
throw new AnalysisProtocolError(
|
|
`IDA function lookup omitted or contradicted query ${JSON.stringify(query)}.`,
|
|
);
|
|
if (
|
|
requestedBy !== undefined &&
|
|
item.fn === null &&
|
|
item.error !== null &&
|
|
MISSING_PROCEDURE_ANSWERS.has(item.error)
|
|
)
|
|
throw new AnalysisInputError(
|
|
requestedBy,
|
|
{
|
|
cause: new AnalysisProtocolError(
|
|
`IDA function lookup failed for ${query}: ${item.error}`,
|
|
),
|
|
},
|
|
[
|
|
{
|
|
path: [],
|
|
reason: "invalid_value",
|
|
message: `IDA could not resolve procedure ${JSON.stringify(query)}: ${item.error}.`,
|
|
},
|
|
],
|
|
);
|
|
if (item.fn === null || item.error !== null)
|
|
throw new AnalysisProtocolError(
|
|
`IDA function lookup failed for ${query}: ${item.error ?? "missing function"}`,
|
|
);
|
|
return item.fn;
|
|
}
|
|
|
|
/** True only for modern callee reads on targets not proven x86-only; x86 reads never reach the graph code. */
|
|
private readsCallGraph(direction: "callers" | "callees"): boolean {
|
|
return (
|
|
this.protocol === "modern" &&
|
|
direction === "callees" &&
|
|
this.options.x86Only === false
|
|
);
|
|
}
|
|
|
|
/** Add processor-neutral function callees on targets where upstream `callees` is unsound. */
|
|
private async addCallGraphCallees(
|
|
fn: IdaFunction,
|
|
result: Map<string, ReturnType<typeof identity>>,
|
|
): Promise<void> {
|
|
const graph =
|
|
this.options.callGraphAvailable === false
|
|
? unavailableCallGraph("the upstream server does not advertise it")
|
|
: await readCallGraphCallees(
|
|
(tool, args) => this.call(tool, args),
|
|
fn.address,
|
|
);
|
|
this.relationshipCoverage = graph.coverage;
|
|
this.limitations.push(...graph.limitations);
|
|
for (const callee of graph.callees)
|
|
if (!result.has(callee.address))
|
|
result.set(callee.address, identity({ ...callee, size: "unknown" }));
|
|
}
|
|
|
|
async strings(
|
|
nativePattern?: string,
|
|
): Promise<{ address: string; value: string }[]> {
|
|
if (this.protocol === "native")
|
|
this.limitations.push(IDA_NATIVE_STRING_LIMITATION);
|
|
const result: { address: string; value: string }[] = [];
|
|
let offset: number | null = 0;
|
|
while (offset !== null) {
|
|
const current: number = offset;
|
|
if (this.protocol === "legacy") {
|
|
const page = legacyPageSchema.parse(
|
|
await this.call("list_strings", { offset, count: 100 }),
|
|
);
|
|
result.push(
|
|
...page.data.map((item) => {
|
|
const s = legacyStringSchema.parse(item);
|
|
return { address: s.address, value: s.string };
|
|
}),
|
|
);
|
|
offset = page.next_offset;
|
|
} else {
|
|
const page = (
|
|
this.protocol === "native" ? nativeStringsSchema : modernStringsSchema
|
|
).parse(
|
|
await this.call("find_regex", {
|
|
pattern:
|
|
this.protocol === "native"
|
|
? (nativePattern ?? "[\\s\\S]*")
|
|
: "(?s).*",
|
|
offset,
|
|
limit: 100,
|
|
}),
|
|
);
|
|
result.push(
|
|
...page.matches.map((s) => ({ address: s.addr, value: s.string })),
|
|
);
|
|
offset = nextOffset(page.cursor, current);
|
|
}
|
|
if (offset !== null && offset <= current)
|
|
throw new AnalysisProtocolError(
|
|
"IDA MCP string pagination did not advance.",
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async assembly(fn: IdaFunction): Promise<IdaDisassembly> {
|
|
if (this.protocol === "legacy") {
|
|
const result = disassemblySchema.parse(
|
|
await this.call("disassemble_function", { start_address: fn.address }),
|
|
);
|
|
this.verifyAssembly(fn, result);
|
|
return result;
|
|
}
|
|
let result: IdaDisassembly | undefined;
|
|
let offset: number | null = 0;
|
|
while (offset !== null) {
|
|
const page = (
|
|
this.protocol === "native"
|
|
? nativeDisassemblySchema
|
|
: modernDisassemblySchema
|
|
).parse(
|
|
await this.call("disasm", {
|
|
addr: fn.address,
|
|
offset,
|
|
max_instructions: 100,
|
|
}),
|
|
);
|
|
if (page.asm === null || page.error !== undefined)
|
|
throw new AnalysisProtocolError(
|
|
`IDA disassembly failed: ${page.error ?? "missing assembly"}`,
|
|
);
|
|
this.verifyAssembly(fn, page.asm);
|
|
if (result === undefined) result = page.asm;
|
|
else result.lines.push(...page.asm.lines);
|
|
offset = nextOffset(page.cursor, offset);
|
|
}
|
|
if (result === undefined)
|
|
throw new AnalysisProtocolError("IDA omitted assembly.");
|
|
return result;
|
|
}
|
|
|
|
private verifyAssembly(fn: IdaFunction, asm: IdaDisassembly): void {
|
|
if (asm.start_ea !== fn.address || asm.name !== fn.name)
|
|
throw new AnalysisProtocolError(
|
|
"IDA disassembly identifies a different function than the resolved entry.",
|
|
);
|
|
}
|
|
|
|
async pseudocode(fn: IdaFunction): Promise<string | null> {
|
|
if (this.protocol === "legacy")
|
|
return z
|
|
.string()
|
|
.nullable()
|
|
.parse(await this.call("decompile_function", { address: fn.address }));
|
|
if (this.protocol === "native") {
|
|
const page = nativeDecompileSchema.parse(
|
|
await this.call("decompile", { addr: fn.address }),
|
|
);
|
|
if (page.addr !== fn.address)
|
|
throw new AnalysisProtocolError(
|
|
"Native IDA pseudocode identifies a different function than the resolved entry.",
|
|
);
|
|
if (!page.ok || page.error !== undefined || page.pseudocode === undefined)
|
|
throw new AnalysisProtocolError(
|
|
`Native IDA decompilation failed: ${page.error ?? "missing pseudocode"}`,
|
|
);
|
|
this.limitations.push(
|
|
"Native pseudocode is rendered GUI text; the upstream viewer traversal is bounded and does not establish complete source recovery.",
|
|
);
|
|
return page.pseudocode;
|
|
}
|
|
const result: string[] = [];
|
|
let offset: number | null = 0;
|
|
while (offset !== null) {
|
|
const page = modernDecompileSchema.parse(
|
|
await this.call("decompile", {
|
|
addr: fn.address,
|
|
offset,
|
|
max_lines: 100,
|
|
}),
|
|
);
|
|
if (page.code === null || page.error !== undefined)
|
|
throw new AnalysisProtocolError(
|
|
`IDA decompilation failed: ${page.error ?? "missing code"}`,
|
|
);
|
|
result.push(page.code);
|
|
offset = nextOffset(page.cursor, offset);
|
|
}
|
|
return result.join("\n");
|
|
}
|
|
|
|
async xrefs(
|
|
address: string,
|
|
): Promise<
|
|
{ address: string; type: string; function: IdaFunction | null }[]
|
|
> {
|
|
if (this.protocol === "legacy")
|
|
return legacyXrefsSchema.parse(
|
|
await this.call("get_xrefs_to", { address }),
|
|
);
|
|
const page = (
|
|
this.protocol === "native" ? nativeXrefsSchema : modernXrefsSchema
|
|
).parse(await this.call("xrefs_to", { addrs: [address], limit: 1000 }))[0];
|
|
if (page?.xrefs == null || page.error !== undefined)
|
|
throw new AnalysisProtocolError(
|
|
`IDA xrefs failed: ${page?.error ?? "missing references"}`,
|
|
);
|
|
if (page.more === true)
|
|
this.limitations.push(
|
|
"Upstream xrefs_to capped results at 1000 references; additional references remain unknown.",
|
|
);
|
|
return page.xrefs.map((xref) => ({
|
|
address: xref.addr,
|
|
type: xref.type,
|
|
function: xref.fn,
|
|
}));
|
|
}
|
|
|
|
async relationships(
|
|
fn: IdaFunction,
|
|
direction: "callers" | "callees",
|
|
): Promise<ReturnType<typeof identity>[]> {
|
|
const queries: string[] = [];
|
|
const result = new Map<string, ReturnType<typeof identity>>();
|
|
if (this.readsCallGraph(direction)) {
|
|
await this.addCallGraphCallees(fn, result);
|
|
return [...result.values()];
|
|
}
|
|
if (this.protocol === "legacy") {
|
|
const value = await this.call(
|
|
direction === "callers" ? "get_callers" : "get_callees",
|
|
{ function_address: fn.address },
|
|
);
|
|
const items =
|
|
direction === "callers"
|
|
? z.array(idaFunctionSchema).parse(value)
|
|
: legacyCalleesSchema.parse(value);
|
|
for (const item of items) {
|
|
if ("type" in item && item.type === "external")
|
|
result.set(item.address, identity({ ...item, size: "unknown" }));
|
|
else queries.push(item.name);
|
|
}
|
|
} else if (direction === "callers") {
|
|
this.limitations.push(
|
|
"Upstream xrefs classify references as code or data, without proving call edges. Direct callers are unavailable in this profile.",
|
|
);
|
|
this.relationshipCoverage = {
|
|
status: "unknown",
|
|
reason: "Upstream xrefs do not establish direct caller relationships.",
|
|
};
|
|
return [];
|
|
} else {
|
|
const page = (
|
|
this.protocol === "native" ? nativeCalleesSchema : modernCalleesSchema
|
|
).parse(
|
|
await this.call("callees", { addrs: [fn.address], limit: 500 }),
|
|
)[0];
|
|
if (page?.callees == null || page.error !== undefined)
|
|
throw new AnalysisProtocolError(
|
|
`IDA callees failed: ${page?.error ?? "missing callees"}`,
|
|
);
|
|
if (page.more === true)
|
|
this.limitations.push(
|
|
"Upstream callees capped results at 500; additional callees remain unknown.",
|
|
);
|
|
this.relationshipCoverage =
|
|
page.more === true
|
|
? {
|
|
status: "partial",
|
|
limitations: [
|
|
"Upstream callees capped results at 500; additional callees remain unknown.",
|
|
],
|
|
}
|
|
: { status: "complete" };
|
|
for (const item of page.callees) {
|
|
if ("type" in item && item.type === "external")
|
|
result.set(
|
|
item.addr,
|
|
identity({ address: item.addr, name: item.name, size: "unknown" }),
|
|
);
|
|
else queries.push(item.addr);
|
|
}
|
|
}
|
|
const querySet = new Set(queries);
|
|
if (this.protocol === "native" && querySet.size > 0) {
|
|
const rows = modernLookupBatchSchema.parse(
|
|
await this.call("lookup_funcs", { queries: [...querySet] }),
|
|
);
|
|
const byQuery = new Map(rows.map((row) => [row.query, row]));
|
|
if (
|
|
rows.length !== querySet.size ||
|
|
byQuery.size !== rows.length ||
|
|
rows.some((row) => !querySet.has(row.query))
|
|
)
|
|
throw new AnalysisProtocolError(
|
|
"Native IDA returned an inconsistent callee lookup batch.",
|
|
);
|
|
for (const query of querySet) {
|
|
const resolved = identity(
|
|
this.lookupFunction(query, byQuery.get(query)),
|
|
);
|
|
result.set(resolved.address, resolved);
|
|
}
|
|
} else {
|
|
for (const query of querySet) {
|
|
const resolved = identity(await this.resolve(query));
|
|
result.set(resolved.address, resolved);
|
|
}
|
|
}
|
|
if (this.relationshipCoverage === undefined)
|
|
this.relationshipCoverage = {
|
|
status: "unknown",
|
|
reason: "The legacy relationship response does not report coverage.",
|
|
};
|
|
return [...result.values()];
|
|
}
|
|
|
|
async run(
|
|
operation: IdaOperation,
|
|
parameters: Readonly<Record<string, JsonValue>>,
|
|
): Promise<JsonValue> {
|
|
const query =
|
|
operation === "search_procedures" || operation === "search_strings"
|
|
? searchQuerySchema.parse(parameters)
|
|
: undefined;
|
|
const matches =
|
|
query === undefined ? undefined : this.searchPredicate(query);
|
|
if (operation === "list_procedures" || operation === "search_procedures") {
|
|
const items = (await this.functions()).map((fn) => ({
|
|
address: fn.address,
|
|
value: fn.name,
|
|
}));
|
|
return matches === undefined
|
|
? items
|
|
: items.filter(({ value }) => matches(value));
|
|
}
|
|
if (operation === "list_strings" || operation === "search_strings") {
|
|
const nativePattern =
|
|
this.protocol === "native" && query?.mode === "literal"
|
|
? nativeLiteralSearchPattern(query.pattern, query.case_sensitive)
|
|
: undefined;
|
|
const items = await this.strings(nativePattern);
|
|
return matches !== undefined
|
|
? items.filter(({ value }) => matches(value))
|
|
: parameters.address === undefined
|
|
? items
|
|
: items.filter(
|
|
({ address }) =>
|
|
address === idaAddressSchema.parse(parameters.address),
|
|
);
|
|
}
|
|
if (operation === "xrefs") {
|
|
const inspectedAddress = idaAddressSchema.parse(parameters.address);
|
|
this.locations.push({ kind: "address", address: inspectedAddress });
|
|
return (await this.xrefs(inspectedAddress)).map(({ address }) => address);
|
|
}
|
|
const fn = await this.resolve(
|
|
z.string().min(1).parse(parameters.procedure),
|
|
this.options.selectorOrigin === "provider" ? undefined : operation,
|
|
);
|
|
this.locations.push({ kind: "address", address: fn.address });
|
|
switch (operation) {
|
|
case "procedure_address":
|
|
return fn.address;
|
|
case "procedure_pseudo_code":
|
|
return this.pseudocode(fn);
|
|
case "procedure_assembly":
|
|
return lines(await this.assembly(fn)).join("\n");
|
|
case "procedure_callers":
|
|
if (this.protocol !== "legacy")
|
|
throw new AnalysisProtocolError(
|
|
"This IDA MCP profile cannot distinguish direct callers from other code references.",
|
|
);
|
|
return (await this.relationships(fn, "callers")).map(
|
|
({ address }) => address,
|
|
);
|
|
case "procedure_callees":
|
|
return (await this.relationships(fn, "callees")).map(
|
|
({ address }) => address,
|
|
);
|
|
case "read_function_instructions":
|
|
return functionInstructionWindowSchema.parse({
|
|
procedure: identity(fn),
|
|
instructions: lines(await this.assembly(fn)),
|
|
limitations: IDA_LIMITATIONS,
|
|
});
|
|
case "analyze_function":
|
|
return this.dossier(fn);
|
|
}
|
|
}
|
|
|
|
private searchPredicate(
|
|
query: z.infer<typeof searchQuerySchema>,
|
|
): (value: string) => boolean {
|
|
let matches: (value: string) => boolean;
|
|
if (query.mode === "regex") {
|
|
try {
|
|
const regex = new RegExp(
|
|
query.pattern,
|
|
query.case_sensitive ? "u" : "iu",
|
|
);
|
|
matches = (value) => regex.test(value);
|
|
} catch (cause: unknown) {
|
|
throw new AnalysisInputError("search", { cause });
|
|
}
|
|
} else {
|
|
const needle = query.case_sensitive
|
|
? query.pattern
|
|
: query.pattern.toLowerCase();
|
|
matches = (value) =>
|
|
(query.case_sensitive ? value : value.toLowerCase()).includes(needle);
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
private async dossier(fn: IdaFunction): Promise<JsonValue> {
|
|
const asm = await this.assembly(fn);
|
|
const pseudocode = await this.pseudocode(fn);
|
|
const callers = await this.relationships(fn, "callers");
|
|
const callees = await this.relationships(fn, "callees");
|
|
const signature =
|
|
asm.return_type == null || asm.arguments == null
|
|
? null
|
|
: `${asm.return_type} ${fn.name}(${asm.arguments.map((arg) => `${arg.type} ${arg.name}`).join(", ")})`;
|
|
return functionDossierSchema.parse({
|
|
procedure: { ...identity(fn), signature, locals: [] },
|
|
pseudocode: pseudocode ?? "",
|
|
assembly: lines(asm),
|
|
comments: [],
|
|
callers,
|
|
callees,
|
|
incoming_references: [],
|
|
outgoing_references: [],
|
|
referenced_strings: [],
|
|
referenced_names: [],
|
|
basic_blocks: [],
|
|
native_api: null,
|
|
native_value_flow: null,
|
|
limitations: [
|
|
...IDA_LIMITATIONS,
|
|
...this.limitations,
|
|
"Stack frame records are not evidence of source locals. Rendered comments do not identify regular versus inline origin.",
|
|
...(pseudocode === null ? ["Pseudocode is unavailable."] : []),
|
|
],
|
|
});
|
|
}
|
|
}
|