mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
* feat(pe): inspect import and delay-load tables Add inspect_pe_imports / inspect-pe-imports to read PE32/PE32+ import and delay-load directories without executing the image. The report keeps DLL names, named and ordinal functions, ILT/IAT RVAs, and a pefile import hash of the standard import table. A missing directory is observed absence, not proof the image never imported APIs. * fix(pe): default PE import fixture options Allow peImportImage() without an argument so CLI and filesystem tests can use the KERNEL32 sample without repeating an empty options object. * test(pe): keep import path cases on the dedicated MCP contract inspect_pe_imports already rejects relative MCP paths in its own boundary test. Leave the shared absolute-path matrix to tools whose handler issue projection matches that suite's expected shape. * fix(native): index virtual-to-file mapping lookups mappedFileOffset scans every mapping twice per span, so a parser that translates one span per table entry costs sections x entries. Add fileMappingLookup, which builds an O(n log n) interval index once and answers each span in O(log n) with the same accept, reject and translation results. A differential test checks it against mappedFileOffset over randomized overlapping mappings and zero fills. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g * fix(pe): report partial import evidence and exact pefile hashes - Withhold imphash with an explicit reason whenever the standard import table is partial (unreadable names or thunks, missing tables), or when pefile 2024.8.26 would hash a different row sequence: invalid or truncated names, ordinal 0, descriptors without functions, tables its bounds or repeat/spread heuristics discard, and its 8192-thunk import symbol cap. Delay-load issues do not withhold it. - Hash ordinal imports with pefile's complete ordlookup tables and its ord<decimal> fallback instead of declining unmapped ordinals. - Report a nonterminal descriptor whose FirstThunk is zero, and a delay-load descriptor whose IAT or INT RVA is zero, as malformed_entry partial coverage instead of a complete empty inventory. - Directory evidence lists the file spans actually consumed for the descriptor array and terminator; discontiguous mappings produce separate spans and separate Evidence ranges. - Carry exhausted limits as PeImportLimitFailure.limit and derive recovery from it; the fixed 16 MiB retained-text ceiling reports a fixed allowance instead of max_file_bytes advice. - Translate RVAs through the indexed mapping lookup and yield with a cancellation check every 1024 table entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g * fix(pe): read only own ordinal tables for DLL names DLL names such as constructor and __proto__ pass pefile's name rule but resolved inherited object members, throwing before the ord<n> fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g * fix(native): preserve PE import facts and prove hash compatibility * test(native): verify PE import workflows and measured costs * refactor(native): narrow PE facts across the container union * docs(native): record integrated PE import validation * fix(native): bound every PE import RVA span * docs(native): measure RVA guard completed workflows * fix(pe): reconcile import coverage with retained rows * fix(pe): retain partial imports at descriptor mapping gaps --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
192 lines
6.7 KiB
TypeScript
192 lines
6.7 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
import { toolContract } from "../../src/contracts/toolContracts.js";
|
|
import {
|
|
peImportImage,
|
|
peImportFixtureOffset,
|
|
peImportLeadingSectionImage,
|
|
peImportManySectionsImage,
|
|
peImportDirectoryAtRva,
|
|
peImportDirectoryMappingStops,
|
|
peWithoutImportTableFixture,
|
|
} from "../fixtures/binaryDiagnostics/peImports.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../support/cli/cliFixture.js";
|
|
|
|
cliTest("reports PE import names through the public CLI", async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-pe-imports-cli-");
|
|
const path = join(root, "app.exe");
|
|
await writeFile(path, peImportImage());
|
|
const result = await cli.run({
|
|
arguments: ["inspect-pe-imports", path, "--json"],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const evidence = toolContract("inspect_pe_imports").outputSchema.parse(
|
|
result.json,
|
|
);
|
|
expect(evidence.normalized_result.imports[0]?.name).toBe("KERNEL32.dll");
|
|
expect(evidence.normalized_result.imphash.md5).toBe(
|
|
"92cef6cc91a85825f3c8a0516517c66f",
|
|
);
|
|
});
|
|
|
|
cliTest(
|
|
"resolves a relative PE path against the working directory",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-pe-imports-cli-rel-");
|
|
await writeFile(join(root, "unsigned.exe"), peWithoutImportTableFixture());
|
|
const result = await cli.run({
|
|
arguments: ["inspect-pe-imports", "unsigned.exe", "--json"],
|
|
cwd: root,
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const evidence = toolContract("inspect_pe_imports").outputSchema.parse(
|
|
result.json,
|
|
);
|
|
expect(evidence.normalized_result.import_directory).toBeNull();
|
|
expect(evidence.subject?.local_path).toBe(join(root, "unsigned.exe"));
|
|
},
|
|
);
|
|
|
|
cliTest("reports a truncated PE as invalid input", async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-pe-imports-cli-bad-");
|
|
const path = join(root, "broken.exe");
|
|
await writeFile(path, Buffer.from("MZ"));
|
|
const result = await cli.run({
|
|
arguments: ["inspect-pe-imports", path, "--json"],
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: { operation: "inspect_pe_imports" },
|
|
});
|
|
});
|
|
|
|
cliTest(
|
|
"preserves directory declarations and partial rows at RVA and mapping boundaries",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-pe-imports-cli-directory-rva-",
|
|
);
|
|
for (const [kind, rva, retained, mappingGap] of [
|
|
["standard", 0xffffffec, 1, false],
|
|
["standard", 0xfffffff0, 0, false],
|
|
["delay", 0xffffffe0, 1, false],
|
|
["delay", 0xfffffff0, 0, false],
|
|
["standard", 0x40000000, 0, true],
|
|
["standard", 0x40000000, 1, true],
|
|
["delay", 0x40000000, 0, true],
|
|
["delay", 0x40000000, 1, true],
|
|
] as const) {
|
|
const path = join(root, `${kind}-${String(rva)}-${String(retained)}.exe`);
|
|
await writeFile(
|
|
path,
|
|
mappingGap
|
|
? peImportDirectoryMappingStops(kind, retained)
|
|
: peImportDirectoryAtRva(kind, rva),
|
|
);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-pe-imports", path, "--json"],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const report = toolContract("inspect_pe_imports").outputSchema.parse(
|
|
result.json,
|
|
).normalized_result;
|
|
const directory =
|
|
kind === "standard"
|
|
? report.import_directory
|
|
: report.delay_import_directory;
|
|
const rows = kind === "standard" ? report.imports : report.delay_imports;
|
|
expect(directory).toMatchObject({ rva });
|
|
expect(directory?.locations).toHaveLength(retained);
|
|
expect(rows).toHaveLength(retained);
|
|
expect(report.coverage.status).toBe("partial");
|
|
expect(report.imphash.md5).toBe(
|
|
kind === "delay" ? "92cef6cc91a85825f3c8a0516517c66f" : null,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"retains a partial inventory when an IAT RVA overflows",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-pe-imports-cli-overflow-");
|
|
for (const bits of [32, 64] as const) {
|
|
const bytes = peImportImage({ bits });
|
|
bytes.writeUInt32LE(
|
|
0x100000000 - bits / 8,
|
|
peImportFixtureOffset(0x1000) + 16,
|
|
);
|
|
const path = join(root, `app-${String(bits)}.exe`);
|
|
await writeFile(path, bytes);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-pe-imports", path, "--json"],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const report = toolContract("inspect_pe_imports").outputSchema.parse(
|
|
result.json,
|
|
).normalized_result;
|
|
expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([
|
|
"GetProcAddress",
|
|
]);
|
|
expect(report.coverage.status).toBe("partial");
|
|
expect(report.issues).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
code: "malformed_entry",
|
|
location: "0x100000000",
|
|
}),
|
|
]),
|
|
);
|
|
expect(report.imphash.md5).toBeNull();
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"keeps complete facts when pefile cannot see the same import table",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-pe-imports-cli-compatibility-",
|
|
);
|
|
const functions = Array.from({ length: 140 }, (_, i) => ({
|
|
name: `Function${String(i).padStart(3, "0")}`,
|
|
}));
|
|
const cases = [
|
|
[
|
|
"cross-section",
|
|
peImportImage({
|
|
sectionBytes: 0x4000,
|
|
splitAt: 0x1000,
|
|
dlls: [{ name: "KERNEL32.dll", functions }],
|
|
}),
|
|
140,
|
|
],
|
|
["section-cap", peImportManySectionsImage(2049, 2), 2],
|
|
["null-section", peImportLeadingSectionImage(false), 2],
|
|
] as const;
|
|
for (const [name, bytes, count] of cases) {
|
|
const path = join(root, `${name}.exe`);
|
|
await writeFile(path, bytes);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-pe-imports", path, "--json"],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const report = toolContract("inspect_pe_imports").outputSchema.parse(
|
|
result.json,
|
|
).normalized_result;
|
|
expect(report.imports[0]?.name).toBe("KERNEL32.dll");
|
|
expect(report.imports[0]?.functions).toHaveLength(count);
|
|
expect(report.coverage.status).toBe("complete");
|
|
expect(report.imphash).toMatchObject({
|
|
md5: null,
|
|
reason: expect.stringMatching(/mapping|compatibility/u),
|
|
});
|
|
if (name === "cross-section")
|
|
expect(report.imports[0]?.functions[128]?.name).toBe("Function128");
|
|
}
|
|
},
|
|
);
|