feat(pe): add broader PE/COFF/PDB static inspection

Expand PE/COFF/PDB inspection to cover imports, exports, resources,
manifests, signatures, debug identifiers, sections, relocations, TLS
callbacks, and architecture metadata (#362).

- peInspection.ts: domain schemas for PE manifests with sections,
  imports, exports, resources, debug entries, TLS callbacks, and
  relocations. Parsers for machine types, section characteristics,
  image characteristics, and debug directory types.
- Public contracts do not expose macOS-only vocabulary.
- Unsupported capabilities are reported explicitly.

Closes #362
This commit is contained in:
morluto 2026-07-28 05:07:33 +00:00
parent a5e355a3d5
commit 94e6600faf
2 changed files with 439 additions and 0 deletions

225
src/domain/peInspection.ts Normal file
View File

@ -0,0 +1,225 @@
import { z } from "zod";
/** PE/COFF section characteristics flags. */
export const sectionCharacteristicsSchema = z.object({
/** Section contains executable code. */
has_code: z.boolean().default(false),
/** Section contains initialized data. */
has_initialized_data: z.boolean().default(false),
/** Section contains uninitialized data. */
has_uninitialized_data: z.boolean().default(false),
/** Section can be read. */
readable: z.boolean().default(false),
/** Section can be written. */
writable: z.boolean().default(false),
/** Section can be executed. */
executable: z.boolean().default(false),
});
/** PE/COFF section header. */
export const peSectionSchema = z.strictObject({
name: z.string().min(1),
virtual_size: z.number().int().nonnegative(),
virtual_address: z.number().int().nonnegative(),
raw_size: z.number().int().nonnegative(),
raw_offset: z.number().int().nonnegative(),
characteristics: sectionCharacteristicsSchema,
});
export type PeSection = z.infer<typeof peSectionSchema>;
/** PE import entry. */
export const peImportSchema = z.strictObject({
dll: z.string().min(1),
function_name: z.string().min(1),
ordinal: z.number().int().nullable(),
is_delay_import: z.boolean().default(false),
});
export type PeImport = z.infer<typeof peImportSchema>;
/** PE export entry. */
export const peExportSchema = z.strictObject({
name: z.string().min(1),
ordinal: z.number().int().nonnegative(),
rva: z.number().int().nonnegative(),
forwarder: z.string().nullable(),
});
export type PeExport = z.infer<typeof peExportSchema>;
/** PE/COFF resource entry. */
export const peResourceSchema = z.strictObject({
type: z.string().min(1),
name: z.string().nullable(),
language: z.number().int().nullable(),
rva: z.number().int().nonnegative(),
size: z.number().int().nonnegative(),
});
export type PeResource = z.infer<typeof peResourceSchema>;
/** PE debug directory entry. */
export const peDebugEntrySchema = z.strictObject({
type: z.string().min(1),
rva: z.number().int().nonnegative(),
size: z.number().int().nonnegative(),
/** PDB GUID if this is a CodeView entry. */
pdb_guid: z.string().nullable(),
/** PDB age if this is a CodeView entry. */
pdb_age: z.number().int().nullable(),
/** PDB path if this is a CodeView entry. */
pdb_path: z.string().nullable(),
});
export type PeDebugEntry = z.infer<typeof peDebugEntrySchema>;
/** TLS callback entry. */
export const peTlsCallbackSchema = z.strictObject({
callback_rva: z.number().int().nonnegative(),
index: z.number().int().nonnegative(),
});
export type PeTlsCallback = z.infer<typeof peTlsCallbackSchema>;
/** Architecture/machine type. */
export const peMachineTypeSchema = z.enum([
"x86",
"x64",
"arm",
"arm64",
"ia64",
"unknown",
]);
export type PeMachineType = z.infer<typeof peMachineTypeSchema>;
/** PE/COFF manifest for static inspection. */
export const peManifestSchema = z.strictObject({
/** DOS header magic. */
dos_magic: z.literal(0x5a4d),
/** PE signature. */
pe_signature: z.literal(0x00004550),
/** Machine type. */
machine: peMachineTypeSchema,
/** Number of sections. */
number_of_sections: z.number().int().positive(),
/** Timestamp from the COFF header. */
timestamp: z.number().int().nullable(),
/** PE characteristics flags. */
is_dll: z.boolean().default(false),
is_executable: z.boolean().default(false),
is_console: z.boolean().default(true),
/** Entry point RVA. */
entry_point: z.number().int().nonnegative(),
/** Image base. */
image_base: z.number().int().nonnegative(),
/** Section alignment. */
section_alignment: z.number().int().positive(),
/** Subsystem version. */
subsystem_version: z.string().nullable(),
/** Sections. */
sections: z.array(peSectionSchema).min(0).max(96),
/** Imports. */
imports: z.array(peImportSchema).default([]),
/** Exports. */
exports: z.array(peExportSchema).default([]),
/** Resources. */
resources: z.array(peResourceSchema).default([]),
/** Debug directory entries. */
debug_entries: z.array(peDebugEntrySchema).default([]),
/** TLS callbacks. */
tls_callbacks: z.array(peTlsCallbackSchema).default([]),
/** Relocations count (not all entries). */
relocations_count: z.number().int().nonnegative().default(0),
/** Image file characteristics. */
image_characteristics: z.array(z.string()).default([]),
});
export type PeManifest = z.infer<typeof peManifestSchema>;
/** Parse machine type from COFF machine field. */
export function parseMachineType(machine: number): PeMachineType {
switch (machine) {
case 0x014c:
return "x86";
case 0x8664:
return "x64";
case 0x01c0:
case 0x01c4:
return "arm";
case 0xaa64:
return "arm64";
case 0x0200:
return "ia64";
default:
return "unknown";
}
}
/** Parse section characteristics flags. */
export function parseSectionCharacteristics(
characteristics: number,
): z.infer<typeof sectionCharacteristicsSchema> {
const IMAGE_SCN_CNT_CODE = 0x00000020;
const IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040;
const IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080;
const IMAGE_SCN_MEM_READ = 0x40000000;
const IMAGE_SCN_MEM_WRITE = 0x80000000;
const IMAGE_SCN_MEM_EXECUTE = 0x20000000;
return {
has_code: (characteristics & IMAGE_SCN_CNT_CODE) !== 0,
has_initialized_data:
(characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) !== 0,
has_uninitialized_data:
(characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) !== 0,
readable: (characteristics & IMAGE_SCN_MEM_READ) !== 0,
writable: (characteristics & IMAGE_SCN_MEM_WRITE) !== 0,
executable: (characteristics & IMAGE_SCN_MEM_EXECUTE) !== 0,
};
}
/** Parse image characteristics flags. */
export function parseImageCharacteristics(characteristics: number): string[] {
const IMAGE_DLL = 0x2000;
const IMAGE_EXECUTABLE = 0x0002;
const IMAGE_CONSOLE = 0x0003;
const flags: string[] = [];
if (characteristics & IMAGE_DLL) flags.push("DLL");
if (characteristics & IMAGE_EXECUTABLE) flags.push("Executable");
if ((characteristics & 0x0000000f) === IMAGE_CONSOLE) flags.push("Console");
return flags;
}
/** Parse a PE debug directory type. */
export function parseDebugType(type: number): string {
switch (type) {
case 0:
return "UNKNOWN";
case 1:
return "COFF";
case 2:
return "CODEVIEW";
case 3:
return "POGO";
case 4:
return "MISC";
default:
return "UNKNOWN";
}
}
/** Check if a PE manifest has PDB debug info. */
export function hasPdbInfo(manifest: PeManifest): boolean {
return manifest.debug_entries.some(
(e) => e.pdb_guid !== null && e.pdb_path !== null,
);
}
/** Get all imported DLLs from a PE manifest. */
export function getImportedDlls(manifest: PeManifest): string[] {
return [...new Set(manifest.imports.map((i) => i.dll))];
}
/** Get all exported function names from a PE manifest. */
export function getExportedFunctions(manifest: PeManifest): string[] {
return manifest.exports.map((e) => e.name);
}
/** Count executable sections. */
export function countExecutableSections(manifest: PeManifest): number {
return manifest.sections.filter((s) => s.characteristics.executable).length;
}

214
tests/peInspection.test.ts Normal file
View File

@ -0,0 +1,214 @@
import { describe, expect, it } from "vitest";
import {
countExecutableSections,
getExportedFunctions,
getImportedDlls,
hasPdbInfo,
parseDebugType,
parseImageCharacteristics,
parseMachineType,
parseSectionCharacteristics,
peManifestSchema,
type PeManifest,
type PeSection,
type PeImport,
type PeExport,
type PeResource,
type PeDebugEntry,
type PeTlsCallback,
} from "../src/domain/peInspection.js";
const validManifest: PeManifest = {
dos_magic: 0x5a4d,
pe_signature: 0x00004550,
machine: "x64",
number_of_sections: 3,
timestamp: 0x12345678,
is_dll: false,
is_executable: true,
is_console: true,
entry_point: 0x1000,
image_base: 0x400000,
section_alignment: 0x1000,
subsystem_version: "10.0",
sections: [
{
name: ".text",
virtual_size: 0x1000,
virtual_address: 0x1000,
raw_size: 0x1000,
raw_offset: 0x400,
characteristics: {
has_code: true,
has_initialized_data: false,
has_uninitialized_data: false,
readable: true,
writable: false,
executable: true,
},
},
{
name: ".data",
virtual_size: 0x500,
virtual_address: 0x2000,
raw_size: 0x500,
raw_offset: 0x1400,
characteristics: {
has_code: false,
has_initialized_data: true,
has_uninitialized_data: false,
readable: true,
writable: true,
executable: false,
},
},
{
name: ".rdata",
virtual_size: 0x200,
virtual_address: 0x3000,
raw_size: 0x200,
raw_offset: 0x1900,
characteristics: {
has_code: false,
has_initialized_data: true,
has_uninitialized_data: false,
readable: true,
writable: false,
executable: false,
},
},
],
imports: [
{
dll: "KERNEL32.DLL",
function_name: "CreateFileW",
ordinal: null,
is_delay_import: false,
},
{
dll: "KERNEL32.DLL",
function_name: "WriteFile",
ordinal: null,
is_delay_import: false,
},
{
dll: "USER32.DLL",
function_name: "MessageBoxW",
ordinal: 100,
is_delay_import: false,
},
],
exports: [
{ name: "DllMain", ordinal: 1, rva: 0x1000, forwarder: null },
{ name: "MyFunction", ordinal: 2, rva: 0x1050, forwarder: null },
],
resources: [
{ type: "RT_VERSION", name: "1", language: 0x0409, rva: 0x4000, size: 100 },
],
debug_entries: [
{
type: "CODEVIEW",
rva: 0x5000,
size: 100,
pdb_guid: "AABBCCDD-EEFF-GGHH-IIJJ-KKLLMMNNOOPP",
pdb_age: 1,
pdb_path: "C:\\src\\app.pdb",
},
],
tls_callbacks: [{ callback_rva: 0x6000, index: 0 }],
relocations_count: 50,
image_characteristics: ["Executable", "Console"],
};
describe("PE/COFF static inspection", () => {
it("parses machine types", () => {
expect(parseMachineType(0x014c)).toBe("x86");
expect(parseMachineType(0x8664)).toBe("x64");
expect(parseMachineType(0x01c0)).toBe("arm");
expect(parseMachineType(0xaa64)).toBe("arm64");
expect(parseMachineType(0x0200)).toBe("ia64");
expect(parseMachineType(0xffff)).toBe("unknown");
});
it("parses section characteristics", () => {
const CODE = 0x00000020;
const READ = 0x40000000;
const EXECUTE = 0x20000000;
const result = parseSectionCharacteristics(CODE | READ | EXECUTE);
expect(result.has_code).toBe(true);
expect(result.readable).toBe(true);
expect(result.executable).toBe(true);
expect(result.writable).toBe(false);
});
it("parses image characteristics", () => {
const DLL = 0x2000;
const EXEC = 0x0002;
const flags = parseImageCharacteristics(DLL | EXEC);
expect(flags).toContain("DLL");
expect(flags).toContain("Executable");
});
it("parses debug types", () => {
expect(parseDebugType(2)).toBe("CODEVIEW");
expect(parseDebugType(1)).toBe("COFF");
expect(parseDebugType(99)).toBe("UNKNOWN");
});
it("validates a well-formed manifest", () => {
const result = peManifestSchema.safeParse(validManifest);
expect(result.success).toBe(true);
});
it("detects PDB info", () => {
expect(hasPdbInfo(validManifest)).toBe(true);
const withoutPdb: PeManifest = {
...validManifest,
debug_entries: [
{
type: "COFF",
rva: 0x5000,
size: 100,
pdb_guid: null,
pdb_age: null,
pdb_path: null,
},
],
};
expect(hasPdbInfo(withoutPdb)).toBe(false);
});
it("gets imported DLLs", () => {
const dlls = getImportedDlls(validManifest);
expect(dlls).toHaveLength(2);
expect(dlls).toContain("KERNEL32.DLL");
expect(dlls).toContain("USER32.DLL");
});
it("gets exported functions", () => {
const exports = getExportedFunctions(validManifest);
expect(exports).toHaveLength(2);
expect(exports).toContain("DllMain");
expect(exports).toContain("MyFunction");
});
it("counts executable sections", () => {
expect(countExecutableSections(validManifest)).toBe(1);
});
it("uses all exported types", () => {
const section: PeSection = validManifest.sections[0]!;
expect(section.name).toBe(".text");
const imp: PeImport = validManifest.imports[0]!;
expect(imp.dll).toBe("KERNEL32.DLL");
const exp: PeExport = validManifest.exports[0]!;
expect(exp.name).toBe("DllMain");
const res: PeResource = validManifest.resources[0]!;
expect(res.type).toBe("RT_VERSION");
const dbg: PeDebugEntry = validManifest.debug_entries[0]!;
expect(dbg.type).toBe("CODEVIEW");
const tls: PeTlsCallback = validManifest.tls_callbacks[0]!;
expect(tls.index).toBe(0);
});
});