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>
109 lines
4.0 KiB
TypeScript
109 lines
4.0 KiB
TypeScript
import { symlink, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { createPeImportsService } from "../../../src/composition/binaryDiagnostics.js";
|
|
import { peImportImage } from "../../fixtures/binaryDiagnostics/peImports.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
describe("PE import stable filesystem boundary", () => {
|
|
it("reports non-file and malformed PE selections as caller input errors", async () => {
|
|
const root = await createTestTempDirectory("rea-pe-imports-files-");
|
|
const path = join(root, "app.exe");
|
|
const alias = join(root, "alias.exe");
|
|
const malformed = join(root, "malformed.exe");
|
|
await writeFile(path, peImportImage());
|
|
await writeFile(malformed, Buffer.from("not a pe"));
|
|
await symlink(path, alias);
|
|
const service = createPeImportsService();
|
|
for (const [candidate, reason] of [
|
|
[root, "invalid_format"],
|
|
[alias, "invalid_format"],
|
|
[malformed, "invalid_format"],
|
|
[join(root, "missing.exe"), "invalid_value"],
|
|
[join(path, "child.exe"), "invalid_value"],
|
|
] as const) {
|
|
const result = await service.inspect({ path: candidate });
|
|
if (result.ok) throw new Error("Expected file acquisition failure");
|
|
expect(result.error).toMatchObject({
|
|
_tag: "AnalysisInputError",
|
|
operation: "inspect_pe_imports",
|
|
issues: [
|
|
{
|
|
path: ["path"],
|
|
reason,
|
|
message: expect.any(String),
|
|
},
|
|
],
|
|
});
|
|
}
|
|
});
|
|
|
|
it("returns typed resource and cancellation failures", async () => {
|
|
const root = await createTestTempDirectory("rea-pe-imports-budget-");
|
|
const path = join(root, "app.exe");
|
|
await writeFile(path, peImportImage());
|
|
const service = createPeImportsService();
|
|
const tooSmall = await service.inspect({ path, max_file_bytes: 64 });
|
|
if (tooSmall.ok) throw new Error("Expected resource budget failure");
|
|
expect(tooSmall.error._tag).toBe("AnalysisResourceConstraintError");
|
|
const cancelled = await service.inspect(
|
|
{ path },
|
|
{ signal: AbortSignal.abort() },
|
|
);
|
|
if (cancelled.ok) throw new Error("Expected cancellation");
|
|
expect(cancelled.error._tag).toBe("AnalysisCancelledError");
|
|
});
|
|
|
|
it("derives limit recovery from the parser's structured limit tag", async () => {
|
|
const root = await createTestTempDirectory("rea-pe-imports-limits-");
|
|
const twoDlls = join(root, "two.exe");
|
|
const longNames = join(root, "names.exe");
|
|
await writeFile(
|
|
twoDlls,
|
|
peImportImage({
|
|
dlls: [
|
|
{ name: "A.dll", functions: [{ name: "a" }] },
|
|
{ name: "B.dll", functions: [{ name: "b" }] },
|
|
],
|
|
}),
|
|
);
|
|
const name = "A".repeat(4096);
|
|
await writeFile(
|
|
longNames,
|
|
peImportImage({
|
|
sectionBytes: 0x1200000,
|
|
dlls: [
|
|
{
|
|
name: "KERNEL32.dll",
|
|
functions: Array.from({ length: 4097 }, () => ({ name })),
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
const service = createPeImportsService();
|
|
const dlls = await service.inspect({ path: twoDlls, max_dlls: 1 });
|
|
if (dlls.ok) throw new Error("Expected DLL budget failure");
|
|
expect(dlls.error).toMatchObject({
|
|
_tag: "AnalysisResourceConstraintError",
|
|
reportedLimits: { limit: "dlls", maximum: 1 },
|
|
remediationAction: expect.stringContaining("Raise max_dlls above 1"),
|
|
retryable: false,
|
|
});
|
|
const text = await service.inspect({
|
|
path: longNames,
|
|
max_file_bytes: 536870912,
|
|
});
|
|
if (text.ok) throw new Error("Expected retained-text failure");
|
|
expect(text.error).toMatchObject({
|
|
_tag: "AnalysisResourceConstraintError",
|
|
reportedLimits: { limit: "retained_text", maximum: 16777216 },
|
|
remediationAction: undefined,
|
|
fixedAllowance: {
|
|
name: expect.stringContaining("16777216-byte PE import retained-text"),
|
|
kind: "bytes",
|
|
},
|
|
retryable: false,
|
|
});
|
|
});
|
|
});
|