Files
reverse-engineering-anything/tests/boundary/filesystem/peImports.test.ts
T
4ef916da03 feat(pe): inspect import and delay-load tables (#6509)
* 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>
2026-10-24 11:44:12 +08:00

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,
});
});
});