diff --git a/CHANGELOG.md b/CHANGELOG.md
index d210f22..ee01e41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,127 @@
+# **v0.7.5 - Structural validator expansion**
+**Released: 2026‑07‑01**
+
+This release substantially expands IOCX's structural validator suite with four new parser/validator pairs (export tables, delay-load imports, VS_VERSIONINFO, and the resource directory's Type → Name → Language hierarchy), enriches public metadata with security-relevant Optional Header and per-resource fields, and adds 24 new structural reason codes across exports, resources, and delay-load. All new code lands with 100% line and branch coverage backed by real-binary verification.
+
+## Added
+
+### New structural validators
+
+- **Export table parser and validator** (pe_exports, exports). Decodes the 40-byte IMAGE_EXPORT_DIRECTORY, EAT, ENPT, and EOT purely from bytes; emits ten new reason codes covering directory, name-pointer, and function-entry anomalies. Forwarder detection follows the PE-spec rule (address RVA within export directory range). Absence of an export directory is not treated as a defect.
+- **Delay-load import parser and validator** (parser_delay_imports, validator_delay_imports). Decodes the 32-byte IMAGE_DELAY_IMPORT_DESCRIPTOR, INT, and IAT purely from bytes; emits eight new reason codes covering directory, descriptor, and entry anomalies. PE32+ vs PE32 thunk sizing determined once from OPTIONAL_HEADER.Magic; v1 vs v0 attribute mode captured explicitly rather than coerced; bound state detected by bound_iat_rva != 0. Absence of a delay-load directory is not treated as a defect.
+- **VS_VERSIONINFO parser and validator** (pe_version_info, validator_version_info). Decodes the version-info envelope, VS_FIXEDFILEINFO, StringFileInfo, and VarFileInfo purely from bytes; emits four new reason codes covering header, FixedInfo, StringFileInfo, and VarFileInfo malformations. Leaf selection across multiple RT_VERSION entries is deterministic, sorted by (name_id, language_id). Absence of RT_VERSION is not treated as a defect.
+- **Resource hierarchy enforcement.** Resource validator now tracks tree depth and enforces the PE spec's Type → Name → Language layering, emitting two new reason codes (RESOURCE_DIRECTORY_LANGUAGE_NOT_ID, RESOURCE_DATA_AT_INVALID_DEPTH).
+
+### Reason codes added (24 total)
+
+- **Resource hierarchy (2):** RESOURCE_DIRECTORY_LANGUAGE_NOT_ID, RESOURCE_DATA_AT_INVALID_DEPTH
+- **VS_VERSIONINFO (4):** RESOURCE_VERSIONINFO_INVALID_HEADER, _INVALID_FIXEDINFO, _INVALID_STRINGFILEINFO, _INVALID_VARFILEINFO
+- **Exports (10):** EXPORT_DIRECTORY_INVALID_HEADER, _OUT_OF_BOUNDS, EXPORT_TABLE_TRUNCATED, EXPORT_NAME_RVA_INVALID, _NOT_ASCII, _POINTER_TABLE_UNSORTED, _ORDINAL_INDEX_INVALID, EXPORT_ORDINAL_OUT_OF_RANGE, EXPORT_FUNCTION_RVA_INVALID, EXPORT_FORWARDER_MALFORMED
+- **Delay-load (8):** DELAY_IMPORT_DIRECTORY_INVALID_HEADER, _OUT_OF_BOUNDS, DELAY_IMPORT_TABLE_TRUNCATED, DELAY_IMPORT_DESCRIPTOR_INVALID, DELAY_IMPORT_DLL_NAME_INVALID, _INT_IAT_MISMATCH, _ATTRIBUTES_LEGACY_VA_MODE, DELAY_IMPORT_ENTRY_INVALID
+
+All new codes follow the established pattern: priority-resolved sub-reasons surfaced via details["reason"], with sub-table scoping via details["table"] where applicable.
+
+### Public metadata enrichment
+
+- **Optional Header fields.** New fields in the optional_header block: dll_characteristics (raw value), dll_characteristics_flags (decoded flag names sorted by bit position), dll_characteristics_unknown_bits (hex string for unrecognised bits), win32_version_value, loader_flags, stack_reserve_size, stack_commit_size, heap_reserve_size, heap_commit_size.
+- **Header decoding.** New subsystem_name field in the header block, decoded from IMAGE_SUBSYSTEM_* (e.g., "WINDOWS_CUI"); returns null for unknown values. Raw subsystem integer unchanged. New machine_name field decoded from IMAGE_FILE_MACHINE_* (e.g., "AMD64", "I386", "ARM64"); the supporting MACHINE_NAMES table covers all 29 documented machine types.
+- **Resource metadata.** The resources field now exposes a structured ResourceEntry per resource covering type, name, language, language_name, codepage, size, entropy, rva, raw_offset, and errors. Per-resource Shannon entropy is rounded to 4 decimal places. Output is sorted by (type, language, rva) for snapshot stability. Resources whose data bytes cannot be read are no longer silently dropped; they are emitted with errors populated (tags: size_invalid, rva_invalid, data_out_of_bounds, raw_offset_invalid).
+
+### Schema typing
+
+- New TypedDicts: ExportStruct, ExportDirectoryHeader, ExportFunctionEntry, ExportNamePointerEntry, DelayImportStruct, DelayImportDescriptor, DelayImportEntry, VersionInfoStruct and its sub-types (FixedFileInfo, StringFileInfo, StringTable, VarFileInfo, VarEntry, Translation).
+- InternalMetadata.resources_struct is now Optional[ResourcesStruct] with a fully-typed ResourceEntry shape replacing List[Any]. InternalMetadata.export_struct, delay_import_struct, and version_info_struct are typed as Optional of their respective structs.
+- New constants module iocx.parsers.pe_constants houses SUBSYSTEM_NAMES, MACHINE_NAMES, DLL_CHARACTERISTICS_FLAGS, and the derived DLL_CHARACTERISTICS_KNOWN_MASK.
+
+## Changed
+
+### Validator dispatcher
+
+Three new validators registered in the structural validator chain, in order:
+
+```
+validate_resources → validate_version_info → validate_exports → validate_delay_imports → validate_entropy
+```
+
+This completes the resource and import/export structural validator clusters ahead of the entropy/derived layer.
+
+### Parser robustness
+
+- **Guarded RVA→offset conversion.** pe.get_offset_from_rva calls are now guarded against pefile.PEFormatError and AttributeError. A corrupt RVA produces a -1 sentinel in raw_offset rather than propagating the exception. The validator's existing data_raw < 0 arm maps this to the existing RESOURCE_DATA_OUT_OF_BOUNDS reason code; no new code introduced.
+- **Resource entropy now computed over the correct byte range.** Previously sliced get_memory_mapped_image() with the raw file offset; now correctly uses the RVA. Caught before snapshot stamping; entropy values match the previous release's behaviour for all existing fixtures.
+
+### Schema and decoding semantics
+
+- **ResourceEntry schema expanded.** Fields are now total=False Optional to accommodate per-entry computation failures. New fields: codepage, errors. The name, rva, and raw_offset fields are now populated where they were previously declared but absent from output.
+- **_decode_langid returns None for undecodable LANGIDs** (previously returned the magic string "unknown"). The early-return guard if langid < 0x0400 was removed; it was rejecting valid neutral-sublang LANGIDs (LANGID 0x0001 now correctly decodes as "ar" for Arabic).
+- **Optional Header missing-field convention.** New Optional Header fields use None as the default value when pefile cannot extract the field, distinct from 0 which indicates the binary's actual value. This is a deliberate semantic split for security-relevant fields where "missing" and "zero" carry different meaning. Existing Optional Header fields retain their default-to-zero behaviour for backward compatibility.
+
+### Extended analyser refactor
+
+- **analyse_extended cleaned up.** Removed duplicated _SUBSYSTEM_MAP and _MACHINE_MAP lookup tables now that decoded names come from the parser layer. The legacy subsystem_human and machine_human fields are removed from extended metadata output. Resource entropy summary statistics (entropy_min, entropy_max, entropy_avg) now compute over only entries with computed entropy values, excluding entropy: None entries.
+
+## Fixed
+
+- **Resource validator no longer silently returns** when a directory's own RVA falls outside .rsrc. Previously suppressed any reporting for malformed directory placement.
+
+## Documentation
+
+- **Reason codes reference** extended with new top-level sections for Resource Hierarchy Anomalies, Resource Version-Info Anomalies, Export Anomalies, and Delay-Load Import Anomalies, each with dedicated sub-reason taxonomy sections documenting the details["reason"] and details["table"] contracts.
+- **Validator documentation** gained sections 2.5 (VS_VERSIONINFO), 2.6 (exports), and 2.7 (delay-load imports), each with explicit determinism rationale. Section 2.7 frames the three spec-interpretation questions (v0 vs v1 attribute mode, INT/IAT mirror vs bound assumption, declared-size vs walk-to-terminator) that produce cross-tool divergence.
+- **Schema reference** documents the new Optional Header fields (including the dll_characteristics_unknown_bits role in preserving complete information about non-decoded bits) and the mixed-default convention with rationale.
+- **analyse_extended module purpose** documented inline via module docstring clarifying that the module performs shape conversion and derived-statistics computation only, not new information extraction.
+- **_decode_langid semantics** documented inline (primary language and sublang decomposition, fallback to default region, fallback to primary-language-only, fallback to None).
+
+## Internal
+
+- **100% line and branch coverage** on all new modules: pe_version_info, validator_version_info, pe_exports, exports, parser_delay_imports, validator_delay_imports, _parse_optional_header, _parse_header, pe_constants, and resource validator additions.
+- **Defensive-path coverage** via monkeypatched struct.error injection across all four parsers. Narrow-except negative tests confirm parser exception handling does not silently swallow exceptions outside the documented catch list.
+- **One # pragma: no cover** applied to a defensive return in the exports validator's _first_unsorted_index helper, documented inline as unreachable from the caller.
+- **End-to-end binary verification.** Delay-load parser cross-checked against mspaint.exe via dumpbin /imports: 107 imports decoded from gdiplus.dll's delay-load directory with byte-exact agreement on DLL name, hint values, IAT addresses, ordering, and bound state across both tools.
+
+**Total: ~650 new tests bringing the suite to 1370 tests.**
+
+## Compatibility
+
+### No remapping of existing reason codes
+
+Existing fixture expected outputs are unchanged for binaries that don't exercise the new pathways.
+
+### Snapshot refresh required
+
+The following changes will produce diffs in fixture expected outputs and require a coordinated snapshot refresh:
+
+- Resource fixtures gain new ResourceEntry keys (codepage, errors, name, rva, raw_offset); resources with errors now appear where they were previously silently dropped.
+- language_name no longer returns "unknown"; consumers checking language_name == "unknown" must update to language_name is None.
+- Every fixture with an optional header gains new Optional Header keys (dll_characteristics, _flags, _unknown_bits, win32_version_value, loader_flags, stack/heap sizing fields).
+- header block gains subsystem_name and machine_name keys.
+- Extended metadata no longer emits subsystem_human and machine_human; consumers should use subsystem_name and machine_name (the parser layer is now the single source of truth for both).
+
+All refreshes are mechanical via the existing fixture regeneration tooling.
+
+### No public IOC schema changes in this release
+
+export_struct and delay_import_struct are exposed only in internal metadata by design - they feed validators, not consumer schema. Public IOC schema exposure for version-info is deferred to a coordinated future release with corresponding fixture corpus refresh.
+
+## Known scheduled work
+
+- **Single-anomaly fixtures** targeting the new reason codes across resources, version-info, exports, and delay-load (~25 fixtures planned). Includes negative-control fixtures (exp_forwarder_to_ordinal_valid, delay_well_formed_bound_modern, delay_well_formed_unbound_with_ordinal_import) demonstrating that validators do not false-positive on healthy spec-valid inputs.
+- **Public IOC schema promotion** of version_info_struct planned for a future release with corpus refresh. Contains consumer-facing metadata (CompanyName, ProductVersion, OriginalFilename, FileDescription) with established IOC value.
+- **Internal-only structural data.** export_struct, delay_import_struct, and Load Config metadata remain internal by design — they exist to feed validators and heuristics, not the public IOC schema. Consumers needing structural information about these directories should rely on the validators' reason codes and (for exports/delay-load) the existing consumer-facing metadata fields.
+- **SUBLANG table refinement.** The current implementation models sublang values as language-independent, which is incorrect for multilingual edge cases (sublang 0x02 means UK English with primary English, but Swiss German with primary German). A flat LCID → BCP-47 mapping is the structural fix; deferred since current behaviour is correct for the common case.
+- **TLS Directory parser and validator.** Originally deferred from this release; planned for the next release.
+- **Cross-tool divergence study** using the new fixtures. Delay-load is a particularly strong divergence candidate because the three identified spec-interpretation questions are known to produce inconsistent output across pefile, LIEF, Ghidra, and IDA. Tracked separately as a methodology contribution opportunity.
+- **Filed work items**:
+ - pefile_usage_policy.md documenting the deterministic-subset usage pattern.
+ - PE_CFG_DECLARATION_INCONSISTENT heuristic (DllCharacteristics ↔ Load Config GuardFlags cross-validator consistency).
+ - PE_FIELDS_IMPLAUSIBLE heuristic for "cannot exist in wild" binaries.
+ - PE_DLL_CHARACTERISTICS_INCONSISTENT heuristic for intra-field flag dependency violations (e.g., HIGH_ENTROPY_VA without DYNAMIC_BASE).
+ - Existing-field default migration (the 0 vs None inconsistency in OptionalHeaderInfo).
+
+---
+
# **v0.7.4.1 — Windows‑Compatible PE Detection Hotfix**
+**Released: 2026‑05‑28**
IOCX v0.7.4.1 removes the `python-magic` dependency, improves PE detection accuracy, and reduces IOCX’s attack surface.
@@ -19,6 +142,7 @@ IOCX v0.7.4.1 removes the `python-magic` dependency, improves PE detection accur
---
# **v0.7.4 — Advanced Directory Parsing & Metadata Expansion**
+**Released: 2026‑05‑26**
IOCX v0.7.4 significantly expands static PE coverage with advanced directory parsing, extended metadata extraction, and deterministic structural validation. This release improves correctness across modern compiler outputs while preserving IOCX’s static‑only, zero execution design.
diff --git a/README-pypi.md b/README-pypi.md
index 15fb53b..dac56be 100644
--- a/README-pypi.md
+++ b/README-pypi.md
@@ -40,21 +40,19 @@ If you need predictable, automatable IOC extraction — IOCX is built for you.
---
+## Version highlights (v0.7.5)
+
+- Added detection for malformed exports, delay-load tables, resources, VS_VERSIONINFO, and Optional Header fields via 24 structural reason codes
+- Surfaces security metadata — DLL characteristics flags, subsystem/machine decoding, per-resource Shannon entropy
+- Never crashes on malformed input — byte-level parsing with structured error tombstones
+- 1370 tests at 100% coverage — deterministic output, snapshot-stable, cross-verified against `dumpbin`
+
## Version highlights (v0.7.4.1)
- Removed the `python-magic` dependency, which caused import failures on Windows systems
- Added a pure‑Python file‑type detector for full cross‑platform portability
- No behavioural changes to IOC extraction
-- The `--min-length` consistency fix is planned for **v0.7.5**
-
-## Version highlights (v0.7.4)
-
-- Full **Load Config Directory** parsing and validation
-- Extended Optional Header metadata for downstream heuristics
-- Structural anomaly heuristics (GuardCF, unmapped cookie, SEH issues)
-- Faster, more resilient PE Analysis
-- Raw IOC extraction remains world-class
-- Zero regressions across all workloads
+- The `--min-length` consistency fix is planned for **v0.7.6**
---
diff --git a/README.md b/README.md
index fdf80dc..82e73f6 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -202,6 +202,15 @@ Fast path — no PE parsing.
Show Version History
+### **v0.7.5 — Structural Validator Expansion**
+- Four new PE structural validators — exports, delay-load imports, VS_VERSIONINFO, and resource hierarchy
+- 24 new reason codes with priority-resolved sub-reason taxonomies
+- Deterministic byte-level parsing — no reliance on pefile's lazy interpretation
+- Security-relevant metadata — DLL characteristics, subsystem/machine name decoding, per-resource entropy
+- 1370 tests at 100% coverage — end-to-end verified against `dumpbin` on real binaries
+
+---
+
### **v0.7.4.1 — Windows Compatibility Hotfix**
- Removed the `python-magic` dependency, which caused import failures on Windows systems
- Added a pure‑Python file‑type detector for full cross‑platform portability
diff --git a/docs/specs/reason-codes.md b/docs/specs/reason-codes.md
index 0f8980e..b082488 100644
--- a/docs/specs/reason-codes.md
+++ b/docs/specs/reason-codes.md
@@ -113,6 +113,12 @@
| **RESOURCE_DIRECTORY_LOOP** | Recursive directory traversal detects a cycle (malformed or malicious resource tree) | Directory A → B → A | Per‑file |
| **RESOURCE_DIRECTORY_ZERO_LENGTH** | A resource directory exists but has zero length or no valid entries | RVA = `0x3000`, size = `0` | Per‑file |
+### Resource Hierarchy Anomalies
+| Reason Code | What Triggers It | Example Pattern | Scope |
+|-------------|------------------|-----------------|-------|
+| **RESOURCE_DIRECTORY_LANGUAGE_NOT_ID** | A depth‑2 directory (Language layer) contains a named entry instead of an integer LCID, violating the Type → Name → Language hierarchy | Language entry keyed by "EN-US" instead of LCID 0x0409 | Per‑file
+| **RESOURCE_DATA_AT_INVALID_DEPTH** | A data leaf appears at depth 0 (Type) or depth 1 (Name) instead of depth 2 (Language), skipping required hierarchy layers | Root Type directory contains a direct data leaf with no Name/Language subdirectories | Per‑file
+
### **Resource Entry / Data Anomalies**
| Reason Code | What Triggers It | Example Pattern | Scope |
@@ -121,6 +127,17 @@
| **RESOURCE_DATA_OUT_OF_BOUNDS** | Resource data block lies outside the file or outside the `.rsrc` section | Data offset = `0x1F0000`, file size = `0x1E0000` | Per‑file |
| **RESOURCE_DATA_OVERLAPS_OTHER_DATA** | Two resource data blobs overlap in raw or virtual space | Data A: `0x2000–0x2400`, Data B: `0x2300–0x2500` | Per‑file |
+### Resource Version‑Info Anomalies
+
+| Reason Code | What Triggers It | Example Pattern | Scope |
+|-------------|------------------|-----------------|-------|
+| **RESOURCE_VERSIONINFO_INVALID_HEADER** | The VS_VERSIONINFO envelope is malformed: placement outside `.rsrc`, `szKey` not equal to "VS_VERSION_INFO", or `wLength` inconsistent with the buffer size | szKey = "VS_VERSION_BAD" instead of "VS_VERSION_INFO" | Per‑file
+| **RESOURCE_VERSIONINFO_INVALID_FIXEDINFO** | The embedded VS_FIXEDFILEINFO has an incorrect `dwSignature` (expected `0xFEEF04BD`) or `dwStrucVersion` (expected `0x00010000`), or fails to parse | dwSignature = `0xDEADBEEF` instead of `0xFEEF04BD` | Per‑file
+| **RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO** | A StringFileInfo, StringTable, or String child is malformed: invalid length field, non‑hex lang_codepage key, or truncated string entry StringTable | key = "ENGLISHX" instead of 8‑hex‑char form | Per‑file
+| **RESOURCE_VERSIONINFO_INVALID_VARFILEINFO** | A VarFileInfo or Var child is malformed, or the Translation array's length is not a DWORD multiple Var. | wValueLength = 6 (not divisible by 4) for a Translation array | Per‑file
+
+*Note: absence of an RT_VERSION resource is not treated as a structural anomaly — many legitimate binary types (kernel drivers, MSI helpers, cross‑compiled artefacts) omit version‑info entirely.*
+
### **Resource String‑Table Anomalies**
| Reason Code | What Triggers It | Example Pattern | Scope |
@@ -158,6 +175,204 @@
---
+## EXPORT ANOMALIES
+
+### Export Directory Anomalies
+| Reason Code | What Triggers It | Example Pattern | Scope |
+|-------------|------------------|-----------------|-------|
+| **EXPORT_DIRECTORY_INVALID_HEADER** | The 40‑byte IMAGE_EXPORT_DIRECTORY header could not be decoded, or its declared counts and array RVAs are mutually inconsistent (e.g., NumberOfFunctions > 0 but AddressOfFunctions == 0, or NumberOfNames > NumberOfFunctions) | NumberOfNames = 50, NumberOfFunctions = 20 | Per‑file |
+| **EXPORT_DIRECTORY_OUT_OF_BOUNDS** | The export directory's declared (rva, size) extends past SizeOfImage | Directory RVA = 0x1F0000, size = 0x1000, SizeOfImage = 0x1F0500 | Per‑file |
+| **EXPORT_TABLE_TRUNCATED** | One of the export sub‑tables (EAT, ENPT, EOT) declares more entries than the file physically contains, or pe.get_data failed to read the declared extent | NumberOfFunctions = 1000, EAT physical extent only covers 50 entries | Per‑file |
+
+### Export Name Pointer Anomalies
+
+| Reason Code | What Triggers It | Example Pattern | Scope |
+|-------------|------------------|-----------------|-------|
+| **EXPORT_NAME_RVA_INVALID** | A name pointer entry's RVA is zero, missing, or points to a string that could not be read or was unterminated within the maximum scan length | Name RVA = 0x0, or RVA points to bytes with no NUL terminator within 1024 bytes | Per‑entry |
+| **EXPORT_NAME_NOT_ASCII** | A name string decoded successfully but contains non‑printable bytes or characters outside the printable ASCII range (0x20–0x7E) | Name = "Foo\x01Bar", or name decoded with Unicode replacement characters | Per‑entry |
+| **EXPORT_NAME_POINTER_TABLE_UNSORTED** | The Export Name Pointer Table is not sorted lexicographically by name, violating the PE spec requirement that enables binary search by GetProcAddress | Names in order: ["Zeta", "Alpha", "Mu"] | Per‑file |
+| **EXPORT_NAME_ORDINAL_INDEX_INVALID** | An EOT entry is missing, or its value is greater than or equal to NumberOfFunctions (i.e., it points outside the EAT) | EOT entry = 500, NumberOfFunctions = 100 | Per‑entry |
+
+### Export Function Entry Anomalies
+
+| Reason Code | What Triggers It | Example Pattern | Scope |
+|-------------|------------------|-----------------|-------|
+| **EXPORT_ORDINAL_OUT_OF_RANGE** | The maximum computed ordinal (Base + NumberOfFunctions - 1) exceeds the 16‑bit range. Per PE spec, ordinals must fit in a WORD | Base = 0xFFF0, NumberOfFunctions = 32, max ordinal = 0x1000F | Per‑file |
+| **EXPORT_FUNCTION_RVA_INVALID** | A function entry's address RVA is non‑zero, is not a forwarder, and points outside the PE image (>= SizeOfImage) | Address RVA = 0x2000000, SizeOfImage = 0x400000 | Per‑entry |
+| **EXPORT_FORWARDER_MALFORMED** | A function entry's RVA points within the export directory (indicating a forwarder) but the resulting string is unreadable, contains non‑printable bytes, or does not match the spec format DllName.SymbolName or DllName.#Ordinal | Forwarder string = "KERNEL32\x01LoadLibraryA", or "InvalidForwarderNoDot" | Per‑entry |
+
+## EXPORT SUB‑REASONS
+
+Several export reason codes carry a reason field in their details payload that narrows the pathology. The full taxonomy:
+
+### EXPORT_DIRECTORY_INVALID_HEADER
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| top_level_decode | The 40‑byte header could not be unpacked from the file bytes |
+| eat_rva_zero_with_nonzero_count | NumberOfFunctions > 0 but AddressOfFunctions == 0 |
+| enpt_rva_zero_with_nonzero_count | NumberOfNames > 0 but AddressOfNames == 0 |
+| eot_rva_zero_with_nonzero_count | NumberOfNames > 0 but AddressOfNameOrdinals == 0 |
+| num_names_exceeds_num_functions | NumberOfNames > NumberOfFunctions (impossible in a well‑formed table) |
+
+### EXPORT_TABLE_TRUNCATED
+
+The table field (not reason) identifies the affected sub‑table:
+
+| table value | Meaning |
+|-------------|---------|
+| export_directory_header | The 40‑byte header itself was short |
+| eat_truncated, enpt_truncated, eot_truncated | A sub‑table's declared size exceeded available file bytes |
+| eat_read_failed, enpt_read_failed, eot_read_failed | pe.get_data raised when reading the sub‑table |
+| eat_rva_zero, enpt_rva_zero, eot_rva_zero | RVA was zero despite a non‑zero declared count |
+
+### EXPORT_NAME_RVA_INVALID
+
+Priority‑resolved; the first matching tag wins:
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| name_rva_missing | Parser did not capture the entry's name RVA |
+| name_rva_zero | RVA was explicitly zero |
+| read_failed | pe.get_data raised when reading the name string |
+| unterminated | No NUL terminator found within the maximum scan length |
+
+### EXPORT_NAME_NOT_ASCII
+
+Priority‑resolved:
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| non_ascii | Decode produced Unicode replacement characters |
+| name_not_printable_ascii | Decoded successfully but contains bytes outside 0x20–0x7E |
+
+### EXPORT_NAME_ORDINAL_INDEX_INVALID
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| missing | Parser could not read the EOT entry |
+| out_of_range | Ordinal index >= NumberOfFunctions |
+
+### EXPORT_ORDINAL_OUT_OF_RANGE
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| max_exceeds_u16 | Base + NumberOfFunctions − 1 > 0xFFFF |
+
+### EXPORT_FORWARDER_MALFORMED
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| unreadable | RVA points into export directory but string could not be decoded |
+| format | Decoded fine but does not match DllName.SymbolName or DllName.#Ordinal |
+
+### EXPORT_FUNCTION_RVA_INVALID
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| exceeds_image | Address RVA >= SizeOfImage |
+
+---
+
+## DELAY‑LOAD IMPORT ANOMALIES
+
+### Delay‑Load Directory Anomalies
+
+| Reason Code | What Triggers It | Example Pattern | Scope |
+|-------------|------------------|-----------------|-------|
+| **DELAY_IMPORT_DIRECTORY_INVALID_HEADER** | The delay‑load directory's parser could not complete top‑level decoding (e.g., the directory placement could not be read, or initial structures were unrecoverable) | Directory at RVA that pefile cannot resolve to a section | Per‑file |
+| **DELAY_IMPORT_DIRECTORY_OUT_OF_BOUNDS** | The delay‑load directory's declared (rva, size) extends past SizeOfImage | Directory RVA = 0xFF000, size = 0x4000, SizeOfImage = 0x100000 | Per‑file |
+| **DELAY_IMPORT_TABLE_TRUNCATED** | One of the delay‑load sub‑tables (descriptor array, INT, IAT, bound IAT, unload IAT) declares more entries than the file physically contains, or pe.get_data failed to read the declared extent, or no zero‑descriptor terminator was found before reaching the directory's declared end | Descriptor count of 50 but only 12 fit in declared size; or INT walk hits max imports without NULL thunk | Per‑file |
+
+### Delay‑Load Descriptor Anomalies
+
+| Reason Code | What Triggers It | Example Pattern | Scope |
+|-------------|------------------|-----------------|-------|
+| **DELAY_IMPORT_DESCRIPTOR_INVALID** | A per‑descriptor structural error: the INT or IAT RVA is zero despite the descriptor being non‑terminating, the sub‑table read failed, or the sub‑table was unparseable. Affects one descriptor at a time | Descriptor for "gdiplus.dll" has int_rva = 0 | Per‑descriptor |
+| **DELAY_IMPORT_DLL_NAME_INVALID** | A descriptor's DLL name RVA points to a string that is zero, unreadable, unterminated within the maximum scan length, or contains non‑printable bytes | Name RVA = 0x0, or name = "kernel32\x01dll" | Per‑descriptor |
+| **DELAY_IMPORT_INT_IAT_MISMATCH** | A descriptor's Import Name Table and Import Address Table have different lengths. Per spec, they must be parallel arrays of identical length terminated by a NULL thunk | INT has 14 entries, IAT has 12 entries — strong malformation signal | Per‑descriptor |
+| **DELAY_IMPORT_ATTRIBUTES_LEGACY_VA_MODE** | A descriptor's Attributes field has the low bit clear, indicating v0 (pre‑Windows 2000) mode where table fields are raw VAs rather than RVAs. Vanishingly rare in modern binaries | Attributes = 0x00000000 instead of 0x00000001 | Per‑descriptor |
+
+### Delay‑Load Entry Anomalies
+
+| Reason Code | What Triggers It | Example Pattern | Scope |
+|-------------|------------------|-----------------|-------|
+| **DELAY_IMPORT_ENTRY_INVALID** | A per‑import entry has structural malformation: the INT thunk is missing or zero, an ordinal value is zero (invalid), or the IMAGE_IMPORT_BY_NAME structure is malformed (unreadable, too short, unterminated, or non‑printable name) | INT entry with high bit set but ordinal = 0; or hint+name structure with NUL‑less buffer | Per‑entry |
+
+## DELAY‑LOAD IMPORT SUB‑REASONS
+
+Most delay‑load reason codes carry a reason field in their details payload that narrows the pathology. The full taxonomy:
+
+### DELAY_IMPORT_DIRECTORY_INVALID_HEADER
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| top_level_decode | The parser could not complete top‑level decoding of the delay‑load directory |
+
+### DELAY_IMPORT_TABLE_TRUNCATED
+
+The table field (not reason) identifies the affected sub‑table:
+
+| table value | Meaning |
+|-------------|---------|
+| delay_import_descriptor_truncated | A descriptor's 32‑byte structure was short |
+| delay_import_descriptor_read_failed | pe.get_data raised when reading a descriptor |
+| delay_import_descriptor_unterminated | No zero descriptor found before the directory's declared end |
+| delay_import_descriptor_max_exceeded | Hit the hard descriptor limit (4096) without finding a NULL terminator |
+| int_truncated, iat_truncated | A sub‑table's declared extent exceeded available file bytes |
+| int_read_failed, iat_read_failed | pe.get_data raised when reading a thunk |
+| int_max_exceeded, iat_max_exceeded | Hit the imports‑per‑descriptor limit (16384) without finding a NULL terminator |
+| int_unpack_failed, iat_unpack_failed | struct.unpack failed on a thunk value |
+
+### DELAY_IMPORT_DESCRIPTOR_INVALID
+
+Carries both table and reason in details:
+
+| table reason (priority order) | Meaning |
+|-------------------------------|---------|
+| int int_rva_zero | INT RVA is zero despite the descriptor being non‑terminating |
+| int int_truncated, int_read_failed, int_max_exceeded, int_unpack_failed | Sub‑table read or parse failure (also surfaces via DELAY_IMPORT_TABLE_TRUNCATED) |
+| iat iat_rva_zero | IAT RVA is zero |
+| iat iat_truncated, iat_read_failed, iat_max_exceeded, iat_unpack_failed | Same as INT |
+
+### DELAY_IMPORT_DLL_NAME_INVALID
+
+Priority‑resolved; the first matching tag wins:
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| dll_name_rva_zero | DLL name RVA was explicitly zero |
+| read_failed | pe.get_data raised when reading the DLL name string |
+| unterminated | No NUL terminator found within the maximum scan length (512 bytes) |
+| dll_name_not_printable | Decoded successfully but contains bytes outside 0x20–0x7E |
+| non_ascii | Decode produced Unicode replacement characters |
+
+### DELAY_IMPORT_INT_IAT_MISMATCH
+
+No sub‑reasons; the code itself names the pathology. Cross‑table length disagreement.
+
+### DELAY_IMPORT_ATTRIBUTES_LEGACY_VA_MODE
+
+No sub‑reasons. Fires when the Attributes field's low bit is zero.
+
+### DELAY_IMPORT_ENTRY_INVALID
+
+Priority‑resolved:
+
+| Sub‑reason | Meaning |
+|------------|---------|
+| int_entry_missing | Parser could not read this entry's INT thunk |
+| int_entry_zero | INT entry is zero at an unexpected position (terminator before INT length matches IAT) |
+| ordinal_zero | High bit set on INT entry but ordinal value is zero |
+| name_read_failed | pe.get_data raised when reading the IMAGE_IMPORT_BY_NAME structure |
+| name_too_short | IMAGE_IMPORT_BY_NAME buffer was less than 3 bytes |
+| hint_unpack_failed | Could not unpack the WORD hint |
+| name_unterminated | Name string had no NUL terminator within the maximum scan length |
+| name_non_ascii | Name decode produced Unicode replacement characters |
+| name_not_printable | Name decoded successfully but contains non‑printable bytes |
+
+---
+
## **PACKER HEURISTICS (Interpretation Layer)**
| Reason Code | What Triggers It | Example Pattern | Scope |
diff --git a/docs/specs/structural-validation-deterministic-heuristics.md b/docs/specs/structural-validation-deterministic-heuristics.md
index f395392..2fa68cd 100644
--- a/docs/specs/structural-validation-deterministic-heuristics.md
+++ b/docs/specs/structural-validation-deterministic-heuristics.md
@@ -33,6 +33,8 @@ This is **structural verification**.
Each validator inspects a distinct subsystem of the PE format.
Together, they form a complete, deterministic structural model of the binary.
+Some structural metadata extracted by parsers is **producer-facing**: it exists to enable validators and heuristics, not to be exposed via the public IOC schema. Examples include the export and delay-load structural details. Other structural metadata is **consumer-facing** and intended for public exposure: version-info string fields are an example of this, planned for promotion in a future release.
+
---
# **2.1 Entropy Validator**
@@ -201,6 +203,94 @@ This closes one of the most subtle structural attack surfaces in the PE format.
---
+# 2.10 Version‑Info Validator
+## Validates the structural integrity of the VS_VERSIONINFO blob extracted from the RT_VERSION resource.
+
+This validator performs:
+
+- Top‑level envelope validation: placement within .rsrc, szKey conformance to "VS_VERSION_INFO", and wLength consistency with the buffer.
+- VS_FIXEDFILEINFO signature and struct‑version validation.
+- StringFileInfo / StringTable / String hierarchy traversal, with per‑substructure length and key‑format checks.
+- VarFileInfo / Var validation, including DWORD‑alignment of the Translation array.
+- Deterministic leaf selection: when multiple RT_VERSION leaves exist, the parser sorts by (name_id, language_id) so the chosen blob is stable across runs.
+
+VS_VERSIONINFO is a recursive, length‑prefixed nested‑structure format with multiple optional children, variable‑length UTF‑16 keys, and DWORD‑alignment rules between every field. It is one of the most failure‑prone surfaces in the PE format for general‑purpose parsers — small differences in how a parser handles truncated wLength fields, malformed szKey strings, or misaligned Translation arrays produce divergent output across tools and across versions of the same tool.
+
+The version‑info parser is implemented as a pure struct‑level decoder with no reliance on external library interpretation:
+
+- Length and alignment arithmetic is performed against the raw buffer using fixed PE‑spec formulas.
+- All loops are bounded by wLength, child_end, and body_end so no walk can exceed the input.
+- Sub‑structure failures emit deterministic tombstone tags in an errors list rather than raising exceptions or being silently swallowed.
+- Leaf selection across multiple RT_VERSION entries uses a stable sort key, not parser iteration order.
+
+This ensures that for any given input blob, the parser produces the same decoded dict on every run, on every platform, regardless of library version. Malformed inputs produce predictable structural errors rather than partial parses or library‑specific exceptions.
+
+The validator then maps these structural states to a small, well‑defined set of reason codes (`RESOURCE_VERSIONINFO_INVALID_HEADER`, `_INVALID_FIXEDINFO`, `_INVALID_STRINGFILEINFO`, `_INVALID_VARFILEINFO`), which downstream heuristics and IOC consumers can rely on as a stable contract.
+
+Version‑info is a high‑signal forensic surface: CompanyName, OriginalFilename, and ProductVersion are routinely impersonated in adversarial samples. Deterministic extraction is a prerequisite for treating these fields as reliable triage signals.
+
+---
+
+# 2.11 Exports Validator
+## Validates the structural integrity of the PE export table extracted by parser_exports.
+
+This validator performs:
+
+- Top‑level decode failure detection and short‑circuit
+- Export directory placement within SizeOfImage
+- Truncation reporting across EAT, ENPT, and EOT sub‑tables
+- Header consistency checks (declared counts vs declared RVAs, name count vs function count)
+- Per‑entry name pointer validation: RVA, encoding, ordinal index bounds
+- Export Name Pointer Table sort order (PE spec requires lexicographic ordering for binary search)
+- Per‑entry function validation: ordinal range, address RVA bounds, forwarder string format
+
+Absence of an export directory is not treated as a structural defect — most EXE files legitimately have no exports.
+
+The export table is the second‑most parser‑sensitive surface in the PE format after VS_VERSIONINFO. Three properties make general‑purpose export parsers prone to divergent output: the EAT can contain a mix of function RVAs and forwarder string pointers distinguished only by whether the RVA falls inside the export directory; the ENPT is required to be sorted but malformed binaries routinely violate this; and the EOT cross‑references the EAT by index, creating a join that breaks silently if either side is truncated.
+
+The exports parser reads all four critical tables (header, EAT, ENPT, EOT) directly from raw bytes via struct.unpack_from, with bounded array reads and per‑position fallback to None when bytes are missing. The validator's per‑entry checks treat the parser's tombstone tags as a stable contract — each tag maps to a deterministic reason code and sub‑reason. Priority lists govern which sub‑reason wins when an entry carries multiple malformations.
+
+This ensures that for any given malformed export table, the validator produces the same set of reason codes on every run, regardless of platform or pefile version. Forwarder strings are validated against the PE spec's DllName.SymbolName and DllName.#Ordinal grammar via a single conservative regex, not by attempting runtime resolution.
+
+---
+
+## 2.12 Delay-Load Imports Validator
+
+### Validates the structural integrity of the PE delay-load import directory and its descriptor array.
+
+This validator performs:
+
+- Top-level decode failure detection and short-circuit for unrecoverable directory placement.
+- Delay-load directory placement within `SizeOfImage`.
+- Truncation reporting across the descriptor array and per-descriptor INT and IAT sub-tables.
+- Per-descriptor structural validation: zero-RVA sub-tables, sub-table read failures, length-budget exhaustion.
+- DLL name string validation: RVA presence, readability, NUL termination, printable ASCII compliance.
+- INT/IAT parallel-array length consistency check (the strongest single signal of malformation).
+- v0 (legacy VA-mode) attribute detection for pre-Windows 2000 binaries.
+- Per-entry validation of INT thunks and IMAGE_IMPORT_BY_NAME structures, including ordinal validity, hint readability, and name structural correctness.
+
+Absence of a delay-load directory is not treated as a structural defect — most binaries do not use delay-loading. Bound state (`bound_iat_rva != 0`) is captured by the parser but not flagged as anomalous; bound delay-load is the normal pattern for Microsoft-shipped binaries.
+
+The delay-load directory is one of the most divergence-prone surfaces in the PE format. Three properties make general-purpose delay-load parsers prone to inconsistent output:
+
+The directory is a chain of variable-content structures whose interpretation depends on a single bit in the Attributes field — v0 binaries (Attributes bit 0 clear) use raw virtual addresses requiring ImageBase subtraction, while v1 binaries use RVAs directly. Many parsers do not implement v0 support and silently coerce the values, producing output that differs across tool versions and across binaries depending on which mode is detected.
+
+The INT and IAT are parallel arrays whose elements must agree on length, but whose interpretations diverge: an INT entry is a thunk describing an import (ordinal or hint+name pointer); an IAT entry is initially a mirror of the INT thunk and later becomes a runtime-resolved address. Bound binaries have the IAT pre-populated with bound addresses, breaking the mirror property. Parsers that assume the mirror property unconditionally produce wrong results on bound binaries; parsers that assume the bound property unconditionally produce wrong results on unbound binaries.
+
+The descriptor array is terminated by a zero-filled IMAGE_DELAY_IMPORT_DESCRIPTOR rather than by a count field. Parsers that trust the directory's declared size and parsers that walk until terminator both work on well-formed binaries but disagree on truncated ones — the former stops at declared end and reports a clean truncation; the latter reads past declared end if a terminator is absent, producing data that the former considers out-of-bounds.
+
+The delay-load parser is implemented as a pure `struct`-level decoder over `pe.get_data`-acquired byte buffers:
+
+- The 32-byte IMAGE_DELAY_IMPORT_DESCRIPTOR structure is unpacked via a single `struct.unpack_from` call. No reliance on pefile's `DIRECTORY_ENTRY_DELAY_IMPORT` interpretation.
+- The Attributes v1 flag is captured from the raw value; v0 binaries are reported via a dedicated reason code rather than silently coerced. The PE32+ vs PE32 distinction is captured from `OPTIONAL_HEADER.Magic` once at parse-start and used for the entire walk to determine thunk size (DWORD vs QWORD).
+- The descriptor array walk has both an explicit zero-descriptor terminator check and a hard count limit (4096), with distinct truncation tags for each termination cause.
+- INT and IAT thunk arrays are walked with the same dual-bounded strategy: zero terminator detection plus hard limit (16384 per descriptor). Each thunk's struct.unpack failure is reported deterministically.
+- Bound state is detected by `bound_iat_rva != 0` (a single byte-level field comparison), not by inference from IAT value patterns. The detection is bit-exact across runs.
+- INT/IAT length mismatch is detected by a single integer comparison after both arrays are walked. The validator emits a dedicated reason code rather than letting the inconsistency propagate as per-entry errors.
+- The IMAGE_IMPORT_BY_NAME structure is read with a bounded scan (1024 bytes) and validated against printable ASCII rules. Substructure failures emit deterministic tombstone tags in per-entry `errors` lists.
+
+---
+
# **3. Deterministic Heuristics Layer**
### *Heuristics interpret structural truth — they never override it.*
diff --git a/iocx/analysis/extended.py b/iocx/analysis/extended.py
index e5e127e..ef66053 100644
--- a/iocx/analysis/extended.py
+++ b/iocx/analysis/extended.py
@@ -1,36 +1,31 @@
# Copyright (c) 2026 MalX Labs and contributors
# SPDX-License-Identifier: MPL-2.0
+"""
+Project public metadata into Detection-shaped output for downstream consumers.
+
+This module does NOT compute new information about the PE; it restructures
+the already-extracted public metadata into the Detection format expected by
+the CLI and detection consumer API.
+
+The primary additions made here are:
+- Derived statistics (counts, entropy ranges)
+- Sorted/grouped views (DLLs grouped by name)
+- Shape conversion from metadata blocks to Detection records
+
+Anything that decodes raw PE structure (e.g., subsystem names, machine
+types, DLL characteristics) belongs in the parser layer (iocx.parsers),
+not here. If you find yourself adding decoding logic here, consider whether
+it should be in pe_parser / pe_constants instead.
+"""
+
from dataclasses import asdict
from iocx.models import Detection
-# Optional: translate machine + subsystem for readability
-_MACHINE_MAP = {
- 0x014c: "x86",
- 0x8664: "AMD64",
- 0x0200: "IA64",
-}
-
-_SUBSYSTEM_MAP = {
- 1: "Native",
- 2: "Windows GUI",
- 3: "Windows CUI",
- 5: "OS/2 CUI",
- 7: "POSIX CUI",
- 9: "Windows CE GUI",
- 10: "EFI Application",
- 11: "EFI Boot Service Driver",
- 12: "EFI Runtime Driver",
- 14: "EFI ROM",
- 16: "Xbox",
-}
def analyse_extended(pe, metadata, strings):
detections = []
- #
- # Summary block
- #
import_details = metadata.get("import_details", [])
delayed_imports = metadata.get("delayed_imports", [])
bound_imports = metadata.get("bound_imports", [])
@@ -39,6 +34,9 @@ def analyse_extended(pe, metadata, strings):
tls = metadata.get("tls")
signatures = metadata.get("signatures", [])
+ #
+ # Summary block — derived statistics on the metadata lists
+ #
detections.append(
Detection(
category="pe_metadata",
@@ -59,21 +57,17 @@ def analyse_extended(pe, metadata, strings):
)
#
- # Grouped imports
+ # Grouped imports — group by DLL with sorted function lists
#
grouped = {}
for imp in import_details:
dll = imp["dll"]
func = imp["function"]
ordinal = imp["ordinal"]
-
- # Represent ordinal-only imports as "#123"
if func is None and ordinal is not None:
func = f"#{ordinal}"
-
grouped.setdefault(dll, []).append(func)
- # Sort DLLs and functions for stable output
for dll in sorted(grouped.keys(), key=str.lower):
funcs = sorted(grouped[dll], key=lambda x: (x.startswith("#"), x.lower()))
detections.append(
@@ -87,7 +81,9 @@ def analyse_extended(pe, metadata, strings):
)
#
- # Delayed imports
+ # Delayed imports — same grouping pattern as imports
+ # Note: full structural validation of delay-load tables is deferred
+ # to a future requirement.
#
if delayed_imports:
grouped_delayed = {}
@@ -112,7 +108,7 @@ def analyse_extended(pe, metadata, strings):
)
#
- # Bound imports
+ # Bound imports — sorted by DLL name
#
if bound_imports:
detections.append(
@@ -122,17 +118,22 @@ def analyse_extended(pe, metadata, strings):
start=0,
end=0,
metadata={
- "entries": sorted(bound_imports, key=lambda x: x["dll"].lower() if x["dll"] else "")
+ "entries": sorted(
+ bound_imports,
+ key=lambda x: x["dll"].lower() if x["dll"] else "",
+ )
},
)
)
#
# Exports summary
+ # Note: this is a metadata view. Structural validity of the export
+ # table is reported separately via the validator's reason codes
+ # (EXPORT_DIRECTORY_INVALID_HEADER, EXPORT_NAME_RVA_INVALID, etc.).
#
export_names = [e["name"] for e in exports if e.get("name")]
forwarded = [e for e in exports if e.get("forwarder")]
-
detections.append(
Detection(
category="pe_metadata",
@@ -149,6 +150,8 @@ def analyse_extended(pe, metadata, strings):
#
# TLS directory
+ # Note: depends on the TLS parsing work currently deferred. Current
+ # output reflects whatever the existing TLS extraction produces.
#
if tls:
detections.append(
@@ -162,28 +165,24 @@ def analyse_extended(pe, metadata, strings):
)
#
- # Header (with human-friendly translations)
+ # Header — verbatim pass-through. The parser layer now provides
+ # subsystem_name and (after the machine decoding move) machine_name.
#
header = metadata.get("header", {})
- machine = header.get("machine") or 0
- subsystem = header.get("subsystem") or 0
-
- header_pretty = dict(header)
- header_pretty["machine_human"] = _MACHINE_MAP.get(machine, f"0x{machine:04x}")
- header_pretty["subsystem_human"] = _SUBSYSTEM_MAP.get(subsystem, subsystem)
-
- detections.append(
- Detection(
- category="pe_metadata",
- value="header",
- start=0,
- end=0,
- metadata=header_pretty,
+ if header:
+ detections.append(
+ Detection(
+ category="pe_metadata",
+ value="header",
+ start=0,
+ end=0,
+ metadata=header,
+ )
)
- )
#
- # Optional Header
+ # Optional Header — verbatim pass-through. The parser layer provides
+ # decoded DLL characteristics flags and sizing data.
#
optional_header = metadata.get("optional_header")
if optional_header:
@@ -198,7 +197,7 @@ def analyse_extended(pe, metadata, strings):
)
#
- # Rich Header
+ # Rich Header — verbatim pass-through.
#
rich_header = metadata.get("rich_header")
if rich_header:
@@ -213,7 +212,7 @@ def analyse_extended(pe, metadata, strings):
)
#
- # Digital Signature
+ # Digital Signature — verbatim pass-through with a presence flag.
#
if signatures:
detections.append(
@@ -230,11 +229,26 @@ def analyse_extended(pe, metadata, strings):
)
#
- # Resource summary
+ # Resource summary — derived statistics on the resources list
#
if resources:
types = sorted({r["type"] for r in resources})
- entropies = [r["entropy"] for r in resources]
+ # Resources with computation errors have entropy = None;
+ # exclude them from statistics so a single failure doesn't
+ # poison the aggregate values.
+ valid_entropies = [r["entropy"] for r in resources if r["entropy"] is not None]
+ if valid_entropies:
+ entropy_stats = {
+ "entropy_min": min(valid_entropies),
+ "entropy_max": max(valid_entropies),
+ "entropy_avg": sum(valid_entropies) / len(valid_entropies),
+ }
+ else:
+ entropy_stats = {
+ "entropy_min": None,
+ "entropy_max": None,
+ "entropy_avg": None,
+ }
detections.append(
Detection(
category="pe_metadata",
@@ -244,9 +258,7 @@ def analyse_extended(pe, metadata, strings):
metadata={
"count": len(resources),
"types": types,
- "entropy_min": min(entropies),
- "entropy_max": max(entropies),
- "entropy_avg": sum(entropies) / len(entropies),
+ **entropy_stats,
},
)
)
diff --git a/iocx/engine.py b/iocx/engine.py
index 7c77308..fd63364 100644
--- a/iocx/engine.py
+++ b/iocx/engine.py
@@ -11,8 +11,11 @@
from .parsers.pe_parser import parse_pe, analyse_pe_sections, analyse_data_directories, sanitize_sections, analyse_data_directories_raw
from .parsers.string_extractor import extract_strings
from .parsers.pe_resources import build_resource_structure
+from .parsers.pe_version_info import build_version_info
from .parsers.pe_load_config import analyse_load_config
from .parsers.pe_optional_header import extract_optional_header_metadata
+from .parsers.pe_exports import build_export_structure
+from .parsers.pe_delay_imports import build_delay_import_structure
from .detectors import all_detectors
from .models import Detection, PluginContext
from .plugins.loader import PluginLoader
@@ -162,6 +165,9 @@ def _pipeline_pe(self, path: str) -> Dict[str, Any]:
}
self._internal_metadata["resources_struct"] = build_resource_structure(pe)
+ self._internal_metadata["version_info_struct"] = build_version_info(pe)
+ self._internal_metadata["export_struct"] = build_export_structure(pe)
+ self._internal_metadata["delay_import_struct"] = build_delay_import_structure(pe)
self._internal_metadata["data_directories_raw"] = analyse_data_directories_raw(pe)
self._internal_metadata.update(extract_optional_header_metadata(pe))
internal: InternalMetadata = self._internal_metadata
diff --git a/iocx/parsers/pe_constants.py b/iocx/parsers/pe_constants.py
new file mode 100644
index 0000000..0972f44
--- /dev/null
+++ b/iocx/parsers/pe_constants.py
@@ -0,0 +1,79 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Constant lookup tables for Optional Header field decoding.
+Sources: Microsoft PE format specification, winnt.h.
+"""
+
+# IMAGE_SUBSYSTEM_* values per PE spec
+SUBSYSTEM_NAMES = {
+ 0: "UNKNOWN",
+ 1: "NATIVE",
+ 2: "WINDOWS_GUI",
+ 3: "WINDOWS_CUI",
+ 5: "OS2_CUI",
+ 7: "POSIX_CUI",
+ 8: "NATIVE_WINDOWS",
+ 9: "WINDOWS_CE_GUI",
+ 10: "EFI_APPLICATION",
+ 11: "EFI_BOOT_SERVICE_DRIVER",
+ 12: "EFI_RUNTIME_DRIVER",
+ 13: "EFI_ROM",
+ 14: "XBOX",
+ 16: "WINDOWS_BOOT_APPLICATION",
+}
+
+# IMAGE_DLLCHARACTERISTICS_* bit flags
+DLL_CHARACTERISTICS_FLAGS = {
+ 0x0020: "HIGH_ENTROPY_VA",
+ 0x0040: "DYNAMIC_BASE",
+ 0x0080: "FORCE_INTEGRITY",
+ 0x0100: "NX_COMPAT",
+ 0x0200: "NO_ISOLATION",
+ 0x0400: "NO_SEH",
+ 0x0800: "NO_BIND",
+ 0x1000: "APPCONTAINER",
+ 0x2000: "WDM_DRIVER",
+ 0x4000: "GUARD_CF",
+ 0x8000: "TERMINAL_SERVER_AWARE",
+}
+
+# Moved from extended layer - expanded to cover the full PE spec
+MACHINE_NAMES = {
+ 0x0000: "UNKNOWN",
+ 0x014C: "I386",
+ 0x0162: "R3000",
+ 0x0166: "R4000",
+ 0x0168: "R10000",
+ 0x0169: "WCEMIPSV2",
+ 0x0184: "ALPHA",
+ 0x01A2: "SH3",
+ 0x01A3: "SH3DSP",
+ 0x01A6: "SH4",
+ 0x01A8: "SH5",
+ 0x01C0: "ARM",
+ 0x01C2: "THUMB",
+ 0x01C4: "ARMNT",
+ 0x01D3: "AM33",
+ 0x01F0: "POWERPC",
+ 0x01F1: "POWERPCFP",
+ 0x0200: "IA64",
+ 0x0266: "MIPS16",
+ 0x0366: "MIPSFPU",
+ 0x0466: "MIPSFPU16",
+ 0x0EBC: "EBC",
+ 0x5032: "RISCV32",
+ 0x5064: "RISCV64",
+ 0x5128: "RISCV128",
+ 0x6232: "LOONGARCH32",
+ 0x6264: "LOONGARCH64",
+ 0x8664: "AMD64",
+ 0xAA64: "ARM64",
+ 0xC0EE: "CEE",
+}
+
+# Mask covering all known DLL characteristics bits
+DLL_CHARACTERISTICS_KNOWN_MASK = 0
+for _bit in DLL_CHARACTERISTICS_FLAGS:
+ DLL_CHARACTERISTICS_KNOWN_MASK |= _bit
diff --git a/iocx/parsers/pe_delay_imports.py b/iocx/parsers/pe_delay_imports.py
new file mode 100644
index 0000000..0b982ed
--- /dev/null
+++ b/iocx/parsers/pe_delay_imports.py
@@ -0,0 +1,472 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Deterministic structural extraction of the PE delay-load import table.
+
+Independent of pefile's DIRECTORY_ENTRY_DELAY_IMPORT interpretation.
+Pefile is used only to:
+ - Locate the delay-load directory (RVA, size)
+ - Determine PE32 vs PE32+ via OPTIONAL_HEADER.Magic
+ - Resolve RVAs to file offsets via pe.get_data
+
+All structural fields are decoded from the raw 32-byte
+IMAGE_DELAY_IMPORT_DESCRIPTOR structure. The INT, IAT, and DLL name
+strings are read raw.
+
+Output contract:
+ None - no delay-load directory present (not an error)
+ dict per the documented contract (see DelayImportStruct in
+ iocx.schemas.internal_schema).
+"""
+
+from __future__ import annotations
+
+import re
+import struct
+from typing import Any, Dict, List, Optional, Tuple
+
+# IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT = 13
+_DELAY_IMPORT_DIRECTORY_INDEX = 13
+_DESCRIPTOR_SIZE = 32 # IMAGE_DELAY_IMPORT_DESCRIPTOR is 32 bytes
+
+# OPTIONAL_HEADER.Magic values
+_MAGIC_PE32 = 0x10B
+_MAGIC_PE32_PLUS = 0x20B
+
+# DLL name length cap to defend against unterminated reads
+_DLL_NAME_MAX_LEN = 512
+# Import-by-name length cap
+_IMPORT_NAME_MAX_LEN = 1024
+
+# Hard limit on descriptors to defend against pathological inputs claiming
+# arbitrarily many. Real binaries rarely have more than a few hundred.
+_MAX_DESCRIPTORS = 4096
+
+# Hard limit on imports per descriptor for the same reason
+_MAX_IMPORTS_PER_DESCRIPTOR = 16384
+
+# DLL name structural check: ASCII printable, typical filename charset.
+# Conservative — accepts the common cases without trying to validate
+# filesystem semantics.
+_DLL_NAME_RE = re.compile(r"^[\x20-\x7E]{1,255}$")
+
+
+def build_delay_import_structure(pe) -> Optional[Dict[str, Any]]:
+ """
+ Locate and structurally decode the PE delay-load import table.
+
+ Returns None if no delay-load directory is present. Otherwise returns
+ a dict per the module docstring contract. Never raises; decode
+ failures produce tombstone entries in `errors` and `truncations`.
+ """
+ placement = _locate_delay_import_directory(pe)
+ if placement is None:
+ return None
+
+ rva, size = placement
+ is_64bit = _is_pe32_plus(pe)
+ thunk_size = 8 if is_64bit else 4
+ truncations: List[str] = []
+ errors: List[str] = []
+
+ descriptors = _read_descriptors(
+ pe, rva, size, thunk_size, truncations, errors,
+ )
+
+ return {
+ "rva": rva,
+ "size": size,
+ "is_64bit": is_64bit,
+ "descriptors": descriptors,
+ "truncations": truncations,
+ "errors": errors,
+ }
+
+
+# =================================================================
+# Locator
+# =================================================================
+
+def _locate_delay_import_directory(pe) -> Optional[Tuple[int, int]]:
+ """Return (rva, size) of the delay-load directory, or None if absent."""
+ try:
+ data_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[_DELAY_IMPORT_DIRECTORY_INDEX]
+ rva = int(data_dir.VirtualAddress)
+ size = int(data_dir.Size)
+ except (AttributeError, IndexError, ValueError, TypeError):
+ return None
+
+ if rva == 0 or size == 0:
+ return None
+
+ return (rva, size)
+
+
+def _is_pe32_plus(pe) -> bool:
+ """Determine PE32+ (64-bit) by reading OPTIONAL_HEADER.Magic."""
+ try:
+ magic = int(pe.OPTIONAL_HEADER.Magic)
+ return magic == _MAGIC_PE32_PLUS
+ except (AttributeError, ValueError, TypeError):
+ # Default to 32-bit if we can't tell. Conservative — produces
+ # smaller thunks, lower risk of over-reading buffers.
+ return False
+
+
+# =================================================================
+# Descriptor array
+# =================================================================
+
+def _read_descriptors(
+ pe,
+ base_rva: int,
+ declared_size: int,
+ thunk_size: int,
+ truncations: List[str],
+ errors: List[str],
+) -> List[Dict[str, Any]]:
+ """
+ Walk the array of IMAGE_DELAY_IMPORT_DESCRIPTOR structures.
+
+ The array is terminated by a zero descriptor (all 32 bytes zero).
+ We also enforce a max descriptor count and stop at the declared
+ directory size to defend against pathological inputs.
+ """
+ descriptors: List[Dict[str, Any]] = []
+ pos = base_rva
+ end = base_rva + declared_size
+
+ # Read up to _MAX_DESCRIPTORS or until zero terminator or end
+ for index in range(_MAX_DESCRIPTORS):
+ if pos + _DESCRIPTOR_SIZE > end:
+ # Declared size exceeded — could be truncation or just end
+ # of the array without a zero terminator. We tag it only if
+ # we haven't yet seen a zero terminator.
+ if descriptors and not _is_zero_terminator(descriptors[-1]):
+ truncations.append("delay_import_descriptor_unterminated")
+ break
+
+ try:
+ raw = bytes(pe.get_data(pos, _DESCRIPTOR_SIZE))
+ except Exception:
+ truncations.append("delay_import_descriptor_read_failed")
+ break
+
+ if len(raw) < _DESCRIPTOR_SIZE:
+ truncations.append("delay_import_descriptor_truncated")
+ break
+
+ decoded = _decode_descriptor(raw, index)
+ if decoded is None:
+ errors.append(f"descriptor_unpack_failed_at_{index}")
+ break
+
+ # Check for zero terminator BEFORE adding to results
+ if _is_zero_descriptor(decoded):
+ break
+
+ # Read INT, IAT, and DLL name for this descriptor
+ _enrich_descriptor(pe, decoded, thunk_size, truncations)
+ descriptors.append(decoded)
+ pos += _DESCRIPTOR_SIZE
+ else:
+ # Hit max without finding terminator
+ truncations.append("delay_import_descriptor_max_exceeded")
+
+ return descriptors
+
+
+def _decode_descriptor(buf: bytes, index: int) -> Optional[Dict[str, Any]]:
+ """Unpack a 32-byte IMAGE_DELAY_IMPORT_DESCRIPTOR."""
+ try:
+ unpacked = struct.unpack_from(" bool:
+ """A descriptor with all-zero fields signals end of array."""
+ return (
+ d["attributes"] == 0
+ and d["dll_name_rva"] == 0
+ and d["module_handle_rva"] == 0
+ and d["iat_rva"] == 0
+ and d["int_rva"] == 0
+ and d["bound_iat_rva"] == 0
+ and d["unload_iat_rva"] == 0
+ and d["timestamp"] == 0
+ )
+
+
+def _is_zero_terminator(d: Dict[str, Any]) -> bool:
+ """Convenience for the unterminated-array check."""
+ return _is_zero_descriptor(d)
+
+
+# =================================================================
+# Per-descriptor enrichment
+# =================================================================
+
+def _enrich_descriptor(
+ pe,
+ descriptor: Dict[str, Any],
+ thunk_size: int,
+ truncations: List[str],
+) -> None:
+ """Read DLL name, INT/IAT thunks, and per-import name strings."""
+ # ---- DLL name ----
+ dll_name_rva = descriptor["dll_name_rva"]
+ if dll_name_rva == 0:
+ descriptor["errors"].append("dll_name_rva_zero")
+ else:
+ name, err = _read_asciiz(pe, dll_name_rva, _DLL_NAME_MAX_LEN)
+ if err is not None:
+ descriptor["errors"].append(err)
+ else:
+ descriptor["dll_name"] = name
+ descriptor["dll_name_valid"] = bool(_DLL_NAME_RE.match(name))
+ if not descriptor["dll_name_valid"]:
+ descriptor["errors"].append("dll_name_not_printable")
+
+ # ---- INT (Import Name Table) ----
+ int_rva = descriptor["int_rva"]
+ int_thunks = _read_thunk_array(
+ pe, int_rva, thunk_size, "int",
+ descriptor["errors"], truncations,
+ )
+
+ # ---- IAT (Import Address Table) ----
+ iat_rva = descriptor["iat_rva"]
+ iat_thunks = _read_thunk_array(
+ pe, iat_rva, thunk_size, "iat",
+ descriptor["errors"], truncations,
+ )
+
+ # ---- Cross-validate INT/IAT lengths ----
+ if len(int_thunks) != len(iat_thunks):
+ descriptor["errors"].append("int_iat_length_mismatch")
+
+ # ---- Build per-import entries by joining INT and IAT ----
+ max_len = max(len(int_thunks), len(iat_thunks))
+ high_bit = 1 << (thunk_size * 8 - 1)
+
+ for i in range(max_len):
+ int_value = int_thunks[i] if i < len(int_thunks) else None
+ iat_value = iat_thunks[i] if i < len(iat_thunks) else None
+ entry = _decode_import_entry(
+ pe, i, int_value, iat_value, high_bit, thunk_size,
+ )
+ descriptor["imports"].append(entry)
+
+
+def _decode_import_entry(
+ pe,
+ index: int,
+ int_value: Optional[int],
+ iat_value: Optional[int],
+ high_bit: int,
+ thunk_size: int,
+) -> Dict[str, Any]:
+ """
+ Build one DelayImportEntry from an INT/IAT pair.
+
+ INT entry semantics:
+ - High bit set: low bits are an ordinal value
+ - High bit clear: value is an RVA to IMAGE_IMPORT_BY_NAME
+ """
+ errors: List[str] = []
+ is_ordinal = False
+ ordinal: Optional[int] = None
+ hint: Optional[int] = None
+ name: Optional[str] = None
+ name_rva: Optional[int] = None
+ name_valid = False
+
+ if int_value is None:
+ errors.append("int_entry_missing")
+ elif int_value == 0:
+ errors.append("int_entry_zero")
+ elif int_value & high_bit:
+ is_ordinal = True
+ # Ordinal is the low 16 bits per PE spec
+ ordinal = int_value & 0xFFFF
+ if ordinal == 0:
+ errors.append("ordinal_zero")
+ else:
+ name_rva = int_value
+ # Read IMAGE_IMPORT_BY_NAME: WORD hint + ASCIIZ name
+ hint, name, read_err = _read_import_by_name(pe, name_rva)
+ if read_err is not None:
+ errors.append(read_err)
+ elif name is None:
+ errors.append("name_read_failed")
+ else:
+ name_valid = bool(re.match(r"^[\x20-\x7E]{1,512}$", name))
+ if not name_valid:
+ errors.append("name_not_printable")
+
+ return {
+ "index": index,
+ "is_ordinal": is_ordinal,
+ "ordinal": ordinal,
+ "hint": hint,
+ "name": name,
+ "name_rva": name_rva,
+ "name_valid": name_valid,
+ "iat_value": iat_value,
+ "errors": errors,
+ }
+
+
+# =================================================================
+# Thunk array reader
+# =================================================================
+
+def _read_thunk_array(
+ pe,
+ rva: int,
+ thunk_size: int,
+ tag: str,
+ descriptor_errors: List[str],
+ truncations: List[str],
+) -> List[int]:
+ """
+ Read a NULL-terminated array of thunks (INT or IAT).
+
+ Walks one thunk at a time until either a zero terminator is found,
+ the max-imports limit is hit, or the read fails.
+ """
+ if rva == 0:
+ descriptor_errors.append(f"{tag}_rva_zero")
+ return []
+
+ thunks: List[int] = []
+ pos = rva
+ fmt = " Tuple[Optional[str], Optional[str]]:
+ """
+ Read a NUL-terminated ASCII string at the given RVA.
+ Returns (string, error_tag). On success, error_tag is None.
+ """
+ try:
+ raw = bytes(pe.get_data(rva, max_len))
+ except Exception:
+ return None, "read_failed"
+
+ if not raw:
+ return None, "empty_read"
+
+ nul_pos = raw.find(b"\x00")
+ if nul_pos == -1:
+ return None, "unterminated"
+
+ try:
+ s = raw[:nul_pos].decode("ascii")
+ except UnicodeDecodeError:
+ s = raw[:nul_pos].decode("ascii", errors="replace")
+ return s, "non_ascii"
+
+ return s, None
+
+
+def _read_import_by_name(
+ pe,
+ rva: int,
+) -> Tuple[Optional[int], Optional[str], Optional[str]]:
+ """
+ Read IMAGE_IMPORT_BY_NAME structure:
+ WORD Hint
+ BYTE Name[] (NUL-terminated ASCII)
+
+ Returns (hint, name, error_tag).
+ """
+ try:
+ raw = bytes(pe.get_data(rva, _IMPORT_NAME_MAX_LEN))
+ except Exception:
+ return None, None, "name_read_failed"
+
+ if len(raw) < 3:
+ return None, None, "name_too_short"
+
+ try:
+ (hint,) = struct.unpack_from(" Optional[Dict[str, Any]]:
+ """
+ Locate and structurally decode the PE export table.
+
+ Returns None if no export directory is present. Otherwise returns a
+ dict per the module docstring contract. Never raises; decode failures
+ produce tombstone entries in the `errors` and `truncations` lists.
+ """
+ placement = _locate_export_directory(pe)
+ if placement is None:
+ return None
+
+ rva, size = placement
+ errors: List[str] = []
+ truncations: List[str] = []
+
+ # Read the 40-byte export directory header from raw bytes
+ try:
+ header_bytes = bytes(pe.get_data(rva, _EXPORT_DIRECTORY_SIZE))
+ except Exception:
+ return _empty_result(rva, size, errors=["header_read_failed"])
+
+ if len(header_bytes) < _EXPORT_DIRECTORY_SIZE:
+ truncations.append("export_directory_header")
+ return _empty_result(rva, size,
+ errors=errors,
+ truncations=truncations)
+
+ header = _decode_export_directory(header_bytes)
+ if header is None:
+ return _empty_result(rva, size, errors=["header_unpack_failed"])
+
+ # Compute the directory's extent for forwarder detection (PE spec:
+ # if an EAT entry's RVA points within the export directory, it's a
+ # forwarder).
+ dir_start = rva
+ dir_end = rva + size
+
+ # ---- Read EAT (Export Address Table) ----
+ eat = _read_dword_array(
+ pe,
+ header["AddressOfFunctions"],
+ header["NumberOfFunctions"],
+ truncations,
+ tag="eat",
+ )
+
+ # ---- Read ENPT (Export Name Pointer Table) ----
+ enpt = _read_dword_array(
+ pe,
+ header["AddressOfNames"],
+ header["NumberOfNames"],
+ truncations,
+ tag="enpt",
+ )
+
+ # ---- Read EOT (Export Ordinal Table) ----
+ eot = _read_word_array(
+ pe,
+ header["AddressOfNameOrdinals"],
+ header["NumberOfNames"],
+ truncations,
+ tag="eot",
+ )
+
+ # Build name pointer entries with resolved names
+ name_pointers, name_by_index = _build_name_pointers(pe, enpt, eot, header)
+
+ # Build function entries, joining EAT with name resolution
+ functions = _build_functions(
+ pe, eat, header, dir_start, dir_end, name_by_index
+ )
+
+ return {
+ "rva": rva,
+ "size": size,
+ "header": header,
+ "functions": functions,
+ "name_pointers": name_pointers,
+ "truncations": truncations,
+ "errors": errors,
+ }
+
+
+# =================================================================
+# Locator
+# =================================================================
+
+def _locate_export_directory(pe) -> Optional[tuple]:
+ """
+ Return (rva, size) of the export data directory, or None if absent.
+ """
+ try:
+ data_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[_EXPORT_DIRECTORY_INDEX]
+ rva = int(data_dir.VirtualAddress)
+ size = int(data_dir.Size)
+ except (AttributeError, IndexError, ValueError, TypeError):
+ return None
+
+ if rva == 0 or size == 0:
+ return None
+
+ return (rva, size)
+
+
+def _empty_result(
+ rva: int,
+ size: int,
+ *,
+ errors: Optional[List[str]] = None,
+ truncations: Optional[List[str]] = None,
+) -> Dict[str, Any]:
+ """Build a minimal result dict for early-return cases."""
+ return {
+ "rva": rva,
+ "size": size,
+ "header": None,
+ "functions": [],
+ "name_pointers": [],
+ "truncations": truncations or [],
+ "errors": errors or [],
+ }
+
+
+# =================================================================
+# Header decoder
+# =================================================================
+
+def _decode_export_directory(buf: bytes) -> Optional[Dict[str, int]]:
+ """
+ Unpack the 40-byte IMAGE_EXPORT_DIRECTORY structure.
+
+ Field order per PE spec:
+ DWORD Characteristics
+ DWORD TimeDateStamp
+ WORD MajorVersion
+ WORD MinorVersion
+ DWORD Name
+ DWORD Base
+ DWORD NumberOfFunctions
+ DWORD NumberOfNames
+ DWORD AddressOfFunctions
+ DWORD AddressOfNames
+ DWORD AddressOfNameOrdinals
+ """
+ try:
+ unpacked = struct.unpack_from(" List[Optional[int]]:
+ """
+ Read `count` little-endian DWORDs starting at `rva`. Returns a list of
+ exactly `count` elements; positions where the read failed contain None.
+ Appends a truncation tag if fewer bytes were available than declared.
+ """
+ if count == 0:
+ return []
+ if rva == 0:
+ truncations.append(f"{tag}_rva_zero")
+ return [None] * count
+
+ byte_count = count * 4
+ try:
+ raw = bytes(pe.get_data(rva, byte_count))
+ except Exception:
+ truncations.append(f"{tag}_read_failed")
+ return [None] * count
+
+ if len(raw) < byte_count:
+ truncations.append(f"{tag}_truncated")
+
+ result: List[Optional[int]] = []
+ for i in range(count):
+ offset = i * 4
+ if offset + 4 > len(raw):
+ result.append(None)
+ else:
+ result.append(struct.unpack_from(" List[Optional[int]]:
+ """
+ Read `count` little-endian WORDs starting at `rva`. Same semantics as
+ _read_dword_array.
+ """
+ if count == 0:
+ return []
+ if rva == 0:
+ truncations.append(f"{tag}_rva_zero")
+ return [None] * count
+
+ byte_count = count * 2
+ try:
+ raw = bytes(pe.get_data(rva, byte_count))
+ except Exception:
+ truncations.append(f"{tag}_read_failed")
+ return [None] * count
+
+ if len(raw) < byte_count:
+ truncations.append(f"{tag}_truncated")
+
+ result: List[Optional[int]] = []
+ for i in range(count):
+ offset = i * 2
+ if offset + 2 > len(raw):
+ result.append(None)
+ else:
+ result.append(struct.unpack_from(" tuple:
+ """
+ Build the name pointer entries and a side index mapping
+ EAT-index -> name, used by _build_functions to enrich function entries.
+ """
+ entries: List[Dict[str, Any]] = []
+ name_by_index: Dict[int, str] = {}
+
+ num_names = header["NumberOfNames"]
+ num_funcs = header["NumberOfFunctions"]
+
+ for i in range(num_names):
+ name_rva = enpt[i] if i < len(enpt) else None
+ ordinal_index = eot[i] if i < len(eot) else None
+ entry_errors: List[str] = []
+ name: Optional[str] = None
+ name_valid = False
+
+ if name_rva is None:
+ entry_errors.append("name_rva_missing")
+ elif name_rva == 0:
+ entry_errors.append("name_rva_zero")
+ else:
+ name, decode_error = _read_asciiz(pe, name_rva, _NAME_MAX_LEN)
+ if decode_error:
+ entry_errors.append(decode_error)
+ else:
+ name_valid = _is_valid_export_name(name)
+ if not name_valid:
+ entry_errors.append("name_not_printable_ascii")
+
+ # Cross-check the ordinal index against EAT bounds
+ if ordinal_index is None:
+ entry_errors.append("ordinal_index_missing")
+ elif ordinal_index >= num_funcs:
+ entry_errors.append("ordinal_index_out_of_range")
+ elif name is not None and name_valid:
+ # Record the resolved name against its EAT index for use by
+ # _build_functions
+ name_by_index[ordinal_index] = name
+
+ entries.append({
+ "index": i,
+ "name_rva": name_rva,
+ "ordinal_index": ordinal_index,
+ "name": name,
+ "name_valid": name_valid,
+ "errors": entry_errors,
+ })
+
+ return entries, name_by_index
+
+
+# =================================================================
+# Function entries
+# =================================================================
+
+def _build_functions(
+ pe,
+ eat: List[Optional[int]],
+ header: Dict[str, int],
+ dir_start: int,
+ dir_end: int,
+ name_by_index: Dict[int, str],
+) -> List[Dict[str, Any]]:
+ """
+ Build the function entry list from the EAT, joining with name resolution
+ and decoding forwarder strings where applicable.
+ """
+ entries: List[Dict[str, Any]] = []
+ base = header["Base"]
+ num_funcs = header["NumberOfFunctions"]
+
+ for i in range(num_funcs):
+ address_rva = eat[i] if i < len(eat) else None
+ ordinal = base + i
+
+ is_forwarder = False
+ forwarder: Optional[str] = None
+ forwarder_valid = False
+ name_rva: Optional[int] = None
+
+ if address_rva is not None and address_rva != 0:
+ # PE spec: an EAT entry RVA that points within the export
+ # directory itself is a forwarder string pointer.
+ if dir_start <= address_rva < dir_end:
+ is_forwarder = True
+ forwarder, _ = _read_asciiz(pe, address_rva, _FORWARDER_MAX_LEN)
+ if forwarder is not None:
+ forwarder_valid = bool(_FORWARDER_RE.match(forwarder))
+
+ # Look up resolved name from name pointer table cross-reference
+ name = name_by_index.get(i)
+ if name is not None:
+ # The name's RVA is recorded in name_pointers, not duplicated
+ # here — but we expose name_rva as None to keep the shape
+ # consistent.
+ name_rva = None
+
+ entries.append({
+ "index": i,
+ "ordinal": ordinal,
+ "address_rva": address_rva,
+ "is_forwarder": is_forwarder,
+ "forwarder": forwarder,
+ "forwarder_valid": forwarder_valid,
+ "name": name,
+ "name_rva": name_rva,
+ })
+
+ return entries
+
+
+# =================================================================
+# String reading
+# =================================================================
+
+def _read_asciiz(
+ pe,
+ rva: int,
+ max_len: int,
+) -> tuple:
+ """
+ Read a NUL-terminated ASCII string starting at `rva`. Returns
+ (string, error_tag). On success, error_tag is None. On failure, string
+ is None and error_tag describes the failure.
+
+ Reads opportunistically: tries to read up to max_len bytes, accepts a
+ short read as the available extent, and scans for the NUL terminator
+ within whatever bytes came back.
+ """
+ if rva == 0:
+ return None, "rva_zero"
+
+ try:
+ raw = bytes(pe.get_data(rva, max_len))
+ except Exception:
+ return None, "read_failed"
+
+ if not raw:
+ return None, "empty_read"
+
+ nul_pos = raw.find(b"\x00")
+ if nul_pos == -1:
+ # No terminator found within max_len — treat as truncated
+ return None, "unterminated"
+
+ try:
+ s = raw[:nul_pos].decode("ascii")
+ except UnicodeDecodeError:
+ # Fallback: decode with replacement, but flag the structural defect
+ s = raw[:nul_pos].decode("ascii", errors="replace")
+ return s, "non_ascii"
+
+ return s, None
+
+
+def _is_valid_export_name(s: str) -> bool:
+ """
+ PE export names should be printable ASCII identifiers. Conservative
+ check: printable ASCII range only, no control chars, at least one char.
+ """
+ if not s:
+ return False
+ return all(0x20 <= ord(c) <= 0x7E for c in s)
diff --git a/iocx/parsers/pe_parser.py b/iocx/parsers/pe_parser.py
index c1e8c5d..f7387a4 100644
--- a/iocx/parsers/pe_parser.py
+++ b/iocx/parsers/pe_parser.py
@@ -7,8 +7,14 @@
import struct
from .string_extractor import extract_strings_from_bytes
from ..analysis.obfuscation import _shannon_entropy
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Optional
from .language_map import PRIMARY_LANG, SUBLANG, DEFAULT_REGION
+from .pe_constants import (
+ DLL_CHARACTERISTICS_FLAGS,
+ DLL_CHARACTERISTICS_KNOWN_MASK,
+ SUBSYSTEM_NAMES,
+ MACHINE_NAMES
+)
# ---------------------------------------------------------------------------
# Low-level helpers
@@ -120,20 +126,17 @@ def _entropy(data: bytes | None) -> float:
return ent
-def _decode_langid(langid: int) -> str:
- """Return a human-readable locale string from a Windows LANGID."""
+def _decode_langid(langid) -> Optional[str]:
+ """Return a BCP-47-like locale string from a Windows LANGID, or None if undecodable."""
if not isinstance(langid, int):
- return "unknown"
-
- if langid < 0x0400:
- return "unknown"
+ return None
- primary = langid & 0x3FF # low 10 bits
- sublang = (langid >> 10) & 0x3F # high bits
+ primary = langid & 0x3FF # low 10 bits
+ sublang = (langid >> 10) & 0x3F # high 6 bits
lang = PRIMARY_LANG.get(primary)
if not lang:
- return "unknown"
+ return None
region = SUBLANG.get(sublang)
if region:
@@ -143,13 +146,20 @@ def _decode_langid(langid: int) -> str:
if default_region:
return f"{lang}-{default_region}"
- # If no region known, return just the language
+ # Primary language known but no region info — return language only
return lang
# ---------------------------------------------------------------------------
# Parsing helpers
# ---------------------------------------------------------------------------
+def _safe_attr(obj, *names, default=None):
+ for name in names:
+ value = getattr(obj, name, None)
+ if value is not None:
+ return value
+ return default
+
def _parse_imports(pe):
imports: list[str] = []
@@ -324,6 +334,20 @@ def _parse_optional_header(pe):
if not opt:
return opt, {}
+ # ---- DLL characteristics decoding ----
+ dll_chars = getattr(opt, "DllCharacteristics", None)
+ if dll_chars is None:
+ dll_chars_flags = None
+ dll_chars_unknown = None
+ else:
+ dll_chars_flags = [
+ DLL_CHARACTERISTICS_FLAGS[bit]
+ for bit in sorted(DLL_CHARACTERISTICS_FLAGS.keys())
+ if dll_chars & bit
+ ]
+ unknown = dll_chars & ~DLL_CHARACTERISTICS_KNOWN_MASK
+ dll_chars_unknown = f"0x{unknown:04X}" if unknown else None
+
optional_header = {
"section_alignment": getattr(opt, "SectionAlignment", 0),
"file_alignment": getattr(opt, "FileAlignment", 0),
@@ -335,6 +359,15 @@ def _parse_optional_header(pe):
f"{getattr(opt, 'MinorOperatingSystemVersion', 0)}",
"subsystem_version": f"{getattr(opt, 'MajorSubsystemVersion', 0)}."
f"{getattr(opt, 'MinorSubsystemVersion', 0)}",
+ "dll_characteristics": dll_chars,
+ "dll_characteristics_flags": dll_chars_flags,
+ "dll_characteristics_unknown_bits": dll_chars_unknown,
+ "win32_version_value": _safe_attr(opt, "Win32VersionValue", "Reserved1"),
+ "loader_flags": getattr(opt, "LoaderFlags", None),
+ "stack_reserve_size": getattr(opt, "SizeOfStackReserve", None),
+ "stack_commit_size": getattr(opt, "SizeOfStackCommit", None),
+ "heap_reserve_size": getattr(opt, "SizeOfHeapReserve", None),
+ "heap_commit_size": getattr(opt, "SizeOfHeapCommit", None),
}
return opt, optional_header
@@ -343,16 +376,22 @@ def _parse_optional_header(pe):
def _parse_header(pe, opt):
fh = getattr(pe, "FILE_HEADER", None)
+ subsystem = getattr(opt, "Subsystem", 0) if opt else 0
+ subsystem_name = SUBSYSTEM_NAMES.get(subsystem)
+ machine = getattr(fh, "Machine", 0) if fh else 0
+ machine_name = MACHINE_NAMES.get(machine)
+
return {
"entry_point": getattr(opt, "AddressOfEntryPoint", 0) if opt else 0,
"image_base": getattr(opt, "ImageBase", 0) if opt else 0,
- "subsystem": getattr(opt, "Subsystem", 0) if opt else 0,
+ "subsystem": subsystem,
+ "subsystem_name": subsystem_name,
"timestamp": getattr(fh, "TimeDateStamp", 0) if fh else 0,
- "machine": getattr(fh, "Machine", 0) if fh else 0,
+ "machine": machine,
+ "machine_name": machine_name,
"characteristics": getattr(fh, "Characteristics", 0) if fh else 0,
}
-
def _parse_resources(pe):
resources: list[dict[str, Any]] = []
resource_strings: list[str] = []
@@ -372,40 +411,69 @@ def _parse_resources(pe):
for entry in getattr(pe.DIRECTORY_ENTRY_RESOURCE, "entries", []):
type_id = getattr(entry, "id", None)
- type_name = pefile.RESOURCE_TYPE.get(type_id, str(type_id))
+ type_name = pefile.RESOURCE_TYPE.get(type_id, f"RT_UNKNOWN_{type_id}")
if not hasattr(entry, "directory"):
continue
for res in getattr(entry.directory, "entries", []):
+ # Capture the resource's named identifier if present
+ res_name = str(res.name) if getattr(res, "name", None) is not None else None
lang = getattr(res, "id", None)
+
if not hasattr(res, "directory"):
continue
if not getattr(res.directory, "entries", []):
continue
data_entry = res.directory.entries[0].data
- size = data_entry.struct.Size
- if size <= 0:
- continue
+ ds = data_entry.struct
+ size = ds.Size
+ rva = ds.OffsetToData
+ codepage = getattr(ds, "CodePage", 0) or None
- offset = data_entry.struct.OffsetToData
- if offset < 0 or offset + size > len(mm):
- continue
+ # Guarded RVA→offset
+ try:
+ raw_offset = pe.get_offset_from_rva(rva)
+ except (pefile.PEFormatError, AttributeError):
+ raw_offset = None
- blob = mm[offset:offset + size]
- ent = _entropy(blob)
+ # Per-entry error tombstones; emit the entry either way so
+ # invalid resources remain visible to consumers.
+ errors: list[str] = []
+ entropy_val: float | None = None
+
+ if size <= 0:
+ errors.append("size_invalid")
+ elif rva < 0:
+ errors.append("rva_invalid")
+ elif rva + size > len(mm):
+ errors.append("data_out_of_bounds")
+ else:
+ blob = mm[rva:rva + size] # slice with RVA, not raw_offset
+ entropy_val = round(_entropy(blob), 4)
resources.append({
"type": type_name,
+ "name": res_name,
"language": lang,
"language_name": _decode_langid(lang),
- "size": size,
- "entropy": ent,
+ "codepage": codepage,
+ "size": size if size > 0 else None,
+ "entropy": entropy_val,
+ "rva": rva,
+ "raw_offset": raw_offset,
+ "errors": errors or None,
})
- return resources, resource_strings
+ # Deterministic ordering for snapshot stability
+ resources.sort(key=lambda r: (
+ r["type"],
+ r["language"] if r["language"] is not None else -1,
+ r["rva"] if r["rva"] is not None else -1,
+ ))
+ return resources, resource_strings
def _parse_data_directories(pe):
dirs: list[dict[str, Any]] = []
diff --git a/iocx/parsers/pe_resources.py b/iocx/parsers/pe_resources.py
index 198c1c5..3dc014b 100644
--- a/iocx/parsers/pe_resources.py
+++ b/iocx/parsers/pe_resources.py
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: MPL-2.0
from typing import Dict, Any
-
+import pefile
def build_resource_structure(pe) -> Dict[str, Any]:
"""
@@ -60,7 +60,14 @@ def build_directory(node, entry_struct=None) -> Dict[str, Any]:
d = data.struct
data_rva = d.OffsetToData
data_size = d.Size
- raw_offset = pe.get_offset_from_rva(data_rva)
+
+ # Guarded RVA→offset: a corrupt RVA must not crash the
+ # parser. -1 is the sentinel the validator already treats
+ # as out-of-bounds via its data_raw < 0 arm.
+ try:
+ raw_offset = pe.get_offset_from_rva(data_rva)
+ except (pefile.PEFormatError, AttributeError):
+ raw_offset = -1
entries.append(
{
diff --git a/iocx/parsers/pe_version_info.py b/iocx/parsers/pe_version_info.py
new file mode 100644
index 0000000..7f93219
--- /dev/null
+++ b/iocx/parsers/pe_version_info.py
@@ -0,0 +1,322 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Deterministic VS_VERSIONINFO extraction.
+
+This module is intentionally independent of the resource-tree parser so that
+version-info concerns live in their own layer with their own fixtures and
+reason codes.
+
+Output contract:
+ None - no RT_VERSION resource present (not an error)
+ dict with keys:
+ rva, size - placement of the chosen VS_VERSIONINFO blob
+ decoded - True if the top-level header was unpackable
+ header_ok - szKey == "VS_VERSION_INFO"
+ length_consistent - wLength fits within the blob
+ fixed_file_info - dict or None
+ string_file_info - list of StringFileInfo dicts (possibly empty)
+ var_file_info - list of VarFileInfo dicts (possibly empty)
+ errors - list of per-substructure tombstone tags
+"""
+
+from typing import Dict, Any, List, Optional, Tuple
+import struct
+
+RT_VERSION = 16
+_VS_VERSION_INFO_KEY = "VS_VERSION_INFO"
+_VS_FFI_SIGNATURE = 0xFEEF04BD
+_VS_FFI_STRUCT_VERSION = 0x00010000
+
+
+def build_version_info(pe) -> Optional[Dict[str, Any]]:
+ """
+ Locate and decode the first RT_VERSION leaf in the resource tree.
+
+ Deterministic ordering: leaves are sorted by (name_id, language_id)
+ with non-integer keys pushed to the end, so the choice of "first"
+ leaf is stable across runs.
+ """
+ if not hasattr(pe, "DIRECTORY_ENTRY_RESOURCE"):
+ return None
+
+ leaf = _find_first_version_leaf(pe.DIRECTORY_ENTRY_RESOURCE)
+ if leaf is None:
+ return None
+
+ try:
+ rva = int(leaf.data.struct.OffsetToData)
+ size = int(leaf.data.struct.Size)
+ except (AttributeError, struct.error):
+ return {
+ "rva": None, "size": None,
+ "decoded": False, "header_ok": False, "length_consistent": False,
+ "fixed_file_info": None,
+ "string_file_info": [], "var_file_info": [],
+ "errors": ["leaf_struct_unpack"],
+ }
+
+ try:
+ raw = bytes(pe.get_data(rva, size))
+ except Exception:
+ return {
+ "rva": rva, "size": size,
+ "decoded": False, "header_ok": False, "length_consistent": False,
+ "fixed_file_info": None,
+ "string_file_info": [], "var_file_info": [],
+ "errors": ["read_failed"],
+ }
+
+ decoded = _decode_vs_versioninfo(raw)
+ decoded["rva"] = rva
+ decoded["size"] = size
+ return decoded
+
+
+# =================================================================
+# Locator
+# =================================================================
+
+def _find_first_version_leaf(root_dir):
+ leaves: List[Tuple[Any, Any, Any]] = []
+ try:
+ for type_entry in root_dir.entries:
+ if getattr(type_entry, "id", None) != RT_VERSION:
+ continue
+ if not hasattr(type_entry, "directory"):
+ continue
+ for name_entry in type_entry.directory.entries:
+ name_key = getattr(name_entry, "id", None)
+ if not hasattr(name_entry, "directory"):
+ continue
+ for lang_entry in name_entry.directory.entries:
+ if not hasattr(lang_entry, "data"):
+ continue
+ lang_key = getattr(lang_entry, "id", None)
+ leaves.append((name_key, lang_key, lang_entry))
+ except (AttributeError, IndexError):
+ return None
+
+ if not leaves:
+ return None
+
+ _SENTINEL = 1 << 31
+
+ def _key(t):
+ n, l, _ = t
+ return (n if isinstance(n, int) else _SENTINEL,
+ l if isinstance(l, int) else _SENTINEL)
+
+ leaves.sort(key=_key)
+ return leaves[0][2]
+
+
+# =================================================================
+# Decoder
+# =================================================================
+
+def _align4(n: int) -> int:
+ return (n + 3) & ~3
+
+
+def _u16(buf: bytes, off: int) -> int:
+ return struct.unpack_from(" Tuple[str, int]:
+ """Read a NUL-terminated UTF-16LE string. Returns (text, bytes_consumed_inc_NUL)."""
+ end = off
+ while end + 1 < len(buf):
+ if buf[end] == 0 and buf[end + 1] == 0:
+ break
+ end += 2
+ s = buf[off:end].decode("utf-16-le", errors="replace")
+ return s, (end - off) + 2
+
+
+def _decode_vs_versioninfo(buf: bytes) -> Dict[str, Any]:
+ out: Dict[str, Any] = {
+ "decoded": False,
+ "header_ok": False,
+ "length_consistent": False,
+ "w_type": None,
+ "fixed_file_info": None,
+ "string_file_info": [],
+ "var_file_info": [],
+ "errors": [],
+ }
+
+ if len(buf) < 6:
+ out["errors"].append("too_short")
+ return out
+
+ try:
+ w_length = _u16(buf, 0)
+ w_value_length = _u16(buf, 2)
+ w_type = _u16(buf, 4)
+ except struct.error:
+ out["errors"].append("header_unpack")
+ return out
+
+ out["decoded"] = True
+ out["w_type"] = w_type
+ out["length_consistent"] = 6 <= w_length <= len(buf)
+
+ key_str, key_consumed = _read_utf16_sz(buf, 6)
+ out["header_ok"] = (key_str == _VS_VERSION_INFO_KEY)
+
+ pos = _align4(6 + key_consumed)
+
+ # ---- VS_FIXEDFILEINFO (52 bytes when present) ----
+ if w_value_length >= 52 and pos + w_value_length <= len(buf):
+ try:
+ ffi = struct.unpack_from("<13I", buf, pos)
+ (sig, sver, fv_ms, fv_ls, pv_ms, pv_ls,
+ flags_mask, flags, file_os, ftype, fsubtype,
+ dt_ms, dt_ls) = ffi
+ out["fixed_file_info"] = {
+ "signature": sig,
+ "signature_ok": sig == _VS_FFI_SIGNATURE,
+ "struct_version": sver,
+ "struct_version_ok": sver == _VS_FFI_STRUCT_VERSION,
+ "file_version": (fv_ms, fv_ls),
+ "product_version": (pv_ms, pv_ls),
+ "file_flags_mask": flags_mask,
+ "file_flags": flags,
+ "file_os": file_os,
+ "file_type": ftype,
+ "file_subtype": fsubtype,
+ "file_date": (dt_ms, dt_ls),
+ }
+ except struct.error:
+ out["errors"].append("fixed_file_info_unpack")
+ elif w_value_length != 0:
+ out["errors"].append("fixed_file_info_truncated")
+
+ pos = _align4(pos + w_value_length)
+
+ end = min(w_length, len(buf)) if out["length_consistent"] else len(buf)
+
+ # ---- Children: StringFileInfo / VarFileInfo ----
+ while pos + 6 <= end:
+ try:
+ c_len = _u16(buf, pos)
+ _c_vlen = _u16(buf, pos + 2)
+ _c_type = _u16(buf, pos + 4)
+ except struct.error:
+ out["errors"].append("child_header_unpack")
+ break
+
+ if c_len < 6 or pos + c_len > end:
+ out["errors"].append("child_length_invalid")
+ break
+
+ key_str, key_consumed = _read_utf16_sz(buf, pos + 6)
+ body_start = _align4(pos + 6 + key_consumed)
+ child_end = pos + c_len
+
+ if key_str == "StringFileInfo":
+ out["string_file_info"].append(
+ _decode_string_file_info(buf, body_start, child_end)
+ )
+ elif key_str == "VarFileInfo":
+ out["var_file_info"].append(
+ _decode_var_file_info(buf, body_start, child_end)
+ )
+ else:
+ out["errors"].append("unknown_child")
+
+ pos = _align4(child_end)
+
+ return out
+
+
+def _decode_string_file_info(buf: bytes, start: int, end: int) -> Dict[str, Any]:
+ result: Dict[str, Any] = {"tables": [], "errors": []}
+ pos = start
+ while pos + 6 <= end:
+ try:
+ t_len = _u16(buf, pos)
+ except struct.error:
+ result["errors"].append("string_table_header")
+ return result
+ if t_len < 6 or pos + t_len > end:
+ result["errors"].append("string_table_length")
+ return result
+
+ key_str, key_consumed = _read_utf16_sz(buf, pos + 6)
+ body_start = _align4(pos + 6 + key_consumed)
+ body_end = pos + t_len
+
+ table: Dict[str, Any] = {
+ "lang_codepage": key_str,
+ "strings": {},
+ "errors": [],
+ }
+ # StringTable key must be 8 hex chars:
+ if len(key_str) != 8 or any(c not in "0123456789ABCDEFabcdef" for c in key_str):
+ table["errors"].append("lang_codepage_key")
+
+ sp = body_start
+ while sp + 6 <= body_end:
+ try:
+ s_len = _u16(buf, sp)
+ except struct.error:
+ table["errors"].append("string_header")
+ break
+ if s_len < 6 or sp + s_len > body_end:
+ table["errors"].append("string_length")
+ break
+ sk, sk_used = _read_utf16_sz(buf, sp + 6)
+ val_start = _align4(sp + 6 + sk_used)
+ val_end = sp + s_len
+ if val_end > val_start:
+ sv, _ = _read_utf16_sz(buf, val_start)
+ else:
+ sv = ""
+ # Bound stored strings to keep the dict deterministic in size
+ table["strings"][sk[:128]] = sv[:512]
+ sp = _align4(val_end)
+
+ result["tables"].append(table)
+ pos = _align4(body_end)
+
+ return result
+
+
+def _decode_var_file_info(buf: bytes, start: int, end: int) -> Dict[str, Any]:
+ result: Dict[str, Any] = {"vars": [], "errors": []}
+ pos = start
+ while pos + 6 <= end:
+ try:
+ v_len = _u16(buf, pos)
+ v_vlen = _u16(buf, pos + 2)
+ except struct.error:
+ result["errors"].append("var_header")
+ return result
+ if v_len < 6 or pos + v_len > end:
+ result["errors"].append("var_length")
+ return result
+
+ key_str, key_consumed = _read_utf16_sz(buf, pos + 6)
+ val_start = _align4(pos + 6 + key_consumed)
+ val_end = pos + v_len
+
+ if v_vlen % 4 != 0:
+ result["errors"].append("translation_not_dword_aligned")
+
+ translations = []
+ nd = (val_end - val_start) // 4
+ for i in range(nd):
+ try:
+ lang, cp = struct.unpack_from(" List[StructuralIssue]:
+ issues: List[StructuralIssue] = []
+
+ di = metadata.get("delay_import_struct")
+ if di is None:
+ return issues # no delay-load directory — not a defect
+
+ size_of_image = analysis.get("size_of_image")
+
+ # ---- Top-level decode failures short-circuit ----
+ if di.get("errors"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.DELAY_IMPORT_DIRECTORY_INVALID_HEADER,
+ details={"reason": "top_level_decode",
+ "errors": list(di["errors"])},
+ ))
+ return issues
+
+ _validate_placement(di, size_of_image, issues)
+ _validate_truncations(di, issues)
+ _validate_descriptors(di, size_of_image, issues)
+
+ return issues
+
+
+# =================================================================
+# Placement
+# =================================================================
+
+def _validate_placement(di: Dict[str, Any],
+ size_of_image: Optional[int],
+ issues: List[StructuralIssue]) -> None:
+ """
+ The delay-load directory must lie within the PE image (SizeOfImage).
+ """
+ rva = di.get("rva")
+ size = di.get("size") or 0
+
+ # Skip placement check if the analysis layer didn't populate
+ # size_of_image. This shouldn't happen in normal operation — if it
+ # does, an upstream bug needs investigating, not a placement issue.
+ if rva is None or size_of_image is None:
+ return
+
+ if rva + size > size_of_image:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.DELAY_IMPORT_DIRECTORY_OUT_OF_BOUNDS,
+ details={"rva": rva, "size": size,
+ "size_of_image": size_of_image},
+ ))
+
+
+# =================================================================
+# Truncations
+# =================================================================
+
+def _validate_truncations(di: Dict[str, Any],
+ issues: List[StructuralIssue]) -> None:
+ """
+ Map parser truncation tags to a single reason code with structured
+ details. Each tag becomes one issue so the consumer sees one issue
+ per truncated table.
+ """
+ for tag in di.get("truncations", []) or []:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.DELAY_IMPORT_TABLE_TRUNCATED,
+ details={"table": tag},
+ ))
+
+
+# =================================================================
+# Descriptor-level validation
+# =================================================================
+
+def _validate_descriptors(di: Dict[str, Any],
+ size_of_image: Optional[int],
+ issues: List[StructuralIssue]) -> None:
+ """
+ Walk each IMAGE_DELAY_IMPORT_DESCRIPTOR and emit per-descriptor
+ structural issues.
+ """
+ descriptors = di.get("descriptors", []) or []
+ is_64bit = di.get("is_64bit", False)
+
+ for descriptor in descriptors:
+ index = descriptor.get("index")
+ descriptor_errors = descriptor.get("errors", []) or []
+
+ # ---- v0 (legacy VA mode) attribute check ----
+ # Pre-Windows 2000 binaries use raw VAs in delay-load tables
+ # rather than RVAs. Vanishingly rare in modern binaries.
+ if not descriptor.get("attributes_v1", True):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.DELAY_IMPORT_ATTRIBUTES_LEGACY_VA_MODE,
+ details={"index": index,
+ "attributes": descriptor.get("attributes")},
+ ))
+
+ # ---- DLL name validation ----
+ dll_name_reason = _first_matching(
+ descriptor_errors, _DLL_NAME_ERROR_PRIORITY
+ )
+ if dll_name_reason != "unknown":
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.DELAY_IMPORT_DLL_NAME_INVALID,
+ details={"index": index,
+ "dll_name_rva": descriptor.get("dll_name_rva"),
+ "dll_name": descriptor.get("dll_name"),
+ "reason": dll_name_reason},
+ ))
+
+ # ---- INT/IAT table-level errors ----
+ # INT and IAT each produce their own descriptor-level errors
+ # via the parser. Emit one issue per affected table.
+ int_reason = _first_matching(
+ descriptor_errors, _INT_RVA_ERROR_PRIORITY
+ )
+ if int_reason != "unknown":
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.DELAY_IMPORT_DESCRIPTOR_INVALID,
+ details={"index": index,
+ "table": "int",
+ "reason": int_reason,
+ "int_rva": descriptor.get("int_rva")},
+ ))
+
+ iat_reason = _first_matching(
+ descriptor_errors, _IAT_RVA_ERROR_PRIORITY
+ )
+ if iat_reason != "unknown":
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.DELAY_IMPORT_DESCRIPTOR_INVALID,
+ details={"index": index,
+ "table": "iat",
+ "reason": iat_reason,
+ "iat_rva": descriptor.get("iat_rva")},
+ ))
+
+ # ---- INT/IAT length mismatch ----
+ # This is a strong signal of malformation. Emitted as its own
+ # reason code rather than folded into DESCRIPTOR_INVALID because
+ # it's a cross-table consistency check, not a per-table issue.
+ if "int_iat_length_mismatch" in descriptor_errors:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.DELAY_IMPORT_INT_IAT_MISMATCH,
+ details={"index": index,
+ "dll_name": descriptor.get("dll_name")},
+ ))
+
+ # ---- Per-import-entry validation ----
+ _validate_import_entries(descriptor, issues)
+
+
+def _validate_import_entries(descriptor: Dict[str, Any],
+ issues: List[StructuralIssue]) -> None:
+ """
+ Emit per-import-entry issues. Each malformed entry produces at most
+ one issue with priority-resolved sub-reason.
+ """
+ descriptor_index = descriptor.get("index")
+ imports = descriptor.get("imports", []) or []
+
+ for entry in imports:
+ entry_errors = entry.get("errors", []) or []
+ if not entry_errors:
+ continue
+
+ reason = _first_matching(entry_errors, _ENTRY_ERROR_PRIORITY)
+ if reason == "unknown":
+ continue
+
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.DELAY_IMPORT_ENTRY_INVALID,
+ details={
+ "descriptor_index": descriptor_index,
+ "entry_index": entry.get("index"),
+ "is_ordinal": entry.get("is_ordinal"),
+ "ordinal": entry.get("ordinal"),
+ "name": entry.get("name"),
+ "name_rva": entry.get("name_rva"),
+ "reason": reason,
+ },
+ ))
+
+
+# =================================================================
+# Helpers
+# =================================================================
+
+def _first_matching(errors: List[str], candidates: List[str]) -> str:
+ """Return the first error tag from `candidates` that appears in `errors`."""
+ for c in candidates:
+ if c in errors:
+ return c
+ return "unknown"
diff --git a/iocx/validators/exports.py b/iocx/validators/exports.py
new file mode 100644
index 0000000..fb5b8eb
--- /dev/null
+++ b/iocx/validators/exports.py
@@ -0,0 +1,361 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Validate the export-table structure produced by parser_exports.
+
+Absence of an export directory is NOT a structural defect — most EXEs
+(as opposed to DLLs) legitimately have no exports. We only emit codes
+when an export directory is present and structurally malformed.
+
+Reason codes emitted:
+ EXPORT_DIRECTORY_INVALID_HEADER
+ EXPORT_DIRECTORY_OUT_OF_BOUNDS
+ EXPORT_TABLE_TRUNCATED
+ EXPORT_NAME_RVA_INVALID
+ EXPORT_NAME_NOT_ASCII
+ EXPORT_NAME_POINTER_TABLE_UNSORTED
+ EXPORT_ORDINAL_OUT_OF_RANGE
+ EXPORT_FORWARDER_MALFORMED
+ EXPORT_FUNCTION_RVA_INVALID
+ EXPORT_NAME_ORDINAL_INDEX_INVALID
+"""
+
+from typing import Any, Dict, List
+
+from iocx.reason_codes import ReasonCodes
+from iocx.validators.schema import StructuralIssue
+from iocx.schemas.internal_schema import InternalMetadata
+from iocx.schemas.analysis import AnalysisDict
+from .decorators import depends_on
+
+
+# Deterministic priority orders for mapping parser error tags to a single
+# reason. The validator emits at most one issue per malformed entry per
+# pathology class; the first tag in priority order wins.
+_NAME_RVA_ERROR_PRIORITY = [
+ "name_rva_missing",
+ "name_rva_zero",
+ "read_failed",
+ "unterminated",
+]
+
+_NAME_ENCODING_ERROR_PRIORITY = [
+ "non_ascii",
+ "name_not_printable_ascii",
+]
+
+
+@depends_on("internal", "analysis")
+def validate_exports(metadata: InternalMetadata, analysis: AnalysisDict) -> List[StructuralIssue]:
+ issues: List[StructuralIssue] = []
+
+ exp = metadata.get("export_struct")
+ if exp is None:
+ return issues
+
+ size_of_image = analysis.get("size_of_image")
+
+ # If the parser couldn't decode the header at all, the rest of the
+ # validation can't run meaningfully.
+ if exp.get("errors"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER,
+ details={"reason": "top_level_decode",
+ "errors": list(exp["errors"])},
+ ))
+ return issues
+
+ _validate_placement(exp, size_of_image, issues)
+ _validate_truncations(exp, issues)
+
+ header = exp.get("header")
+ if header is None:
+ return issues
+
+ _validate_header_consistency(exp, header, issues)
+ _validate_name_pointers(exp, header, issues)
+ _validate_functions(exp, header, size_of_image, issues)
+ _validate_name_pointer_ordering(exp, issues)
+
+ return issues
+
+
+# =================================================================
+# Placement
+# =================================================================
+
+def _validate_placement(exp, size_of_image, issues):
+ """
+ The export directory must lie within the PE image (SizeOfImage). We
+ don't require it to lie in a specific section (exports can live
+ anywhere in the image), but it must be within the image bounds.
+ """
+ rva = exp.get("rva")
+ size = exp.get("size") or 0
+
+ # Skip placement check if the analysis layer didn't populate
+ # size_of_image. This shouldn't happen in normal operation. If it
+ # does, an upstream bug needs investigating, not a placement issue.
+ if rva is None or size_of_image is None:
+ return
+
+ if rva + size > size_of_image:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_DIRECTORY_OUT_OF_BOUNDS,
+ details={"rva": rva, "size": size,
+ "size_of_image": size_of_image},
+ ))
+
+
+# =================================================================
+# Truncations
+# =================================================================
+
+def _validate_truncations(exp: Dict[str, Any],
+ issues: List[StructuralIssue]) -> None:
+ """
+ Map parser truncation tags to a single reason code with structured
+ details. Each tag becomes one issue so the consumer sees one issue
+ per truncated table rather than a single bundled report.
+ """
+ for tag in exp.get("truncations", []) or []:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_TABLE_TRUNCATED,
+ details={"table": tag},
+ ))
+
+
+# =================================================================
+# Header consistency
+# =================================================================
+
+def _validate_header_consistency(exp: Dict[str, Any],
+ header: Dict[str, int],
+ issues: List[StructuralIssue]) -> None:
+ """
+ Sanity-check declared counts against declared array RVAs.
+
+ A non-zero NumberOfFunctions with AddressOfFunctions == 0 is
+ structurally inconsistent: the EAT is declared to exist but has no
+ location. Same logic for NumberOfNames / AddressOfNames /
+ AddressOfNameOrdinals.
+
+ Also flag NumberOfNames > NumberOfFunctions, which is impossible
+ in a well-formed export table.
+ """
+ num_funcs = header.get("NumberOfFunctions", 0)
+ num_names = header.get("NumberOfNames", 0)
+ addr_funcs = header.get("AddressOfFunctions", 0)
+ addr_names = header.get("AddressOfNames", 0)
+ addr_name_ord = header.get("AddressOfNameOrdinals", 0)
+
+ if num_funcs > 0 and addr_funcs == 0:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER,
+ details={"reason": "eat_rva_zero_with_nonzero_count",
+ "NumberOfFunctions": num_funcs},
+ ))
+
+ if num_names > 0 and addr_names == 0:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER,
+ details={"reason": "enpt_rva_zero_with_nonzero_count",
+ "NumberOfNames": num_names},
+ ))
+
+ if num_names > 0 and addr_name_ord == 0:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER,
+ details={"reason": "eot_rva_zero_with_nonzero_count",
+ "NumberOfNames": num_names},
+ ))
+
+ if num_names > num_funcs:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER,
+ details={"reason": "num_names_exceeds_num_functions",
+ "NumberOfNames": num_names,
+ "NumberOfFunctions": num_funcs},
+ ))
+
+
+# =================================================================
+# Name pointer table
+# =================================================================
+
+def _validate_name_pointers(exp: Dict[str, Any],
+ header: Dict[str, int],
+ issues: List[StructuralIssue]) -> None:
+ """
+ For each name pointer entry, emit at most one issue per pathology
+ class:
+ - one EXPORT_NAME_RVA_INVALID if the name RVA is unusable
+ - one EXPORT_NAME_NOT_ASCII if the name decoded but is non-ASCII
+ - one EXPORT_NAME_ORDINAL_INDEX_INVALID if the ordinal index is bad
+
+ Priority orders in module-level constants determine which sub-reason
+ wins when an entry carries multiple tags in the same class.
+ """
+ num_funcs = header.get("NumberOfFunctions", 0)
+
+ for entry in exp.get("name_pointers", []) or []:
+ index = entry.get("index")
+ entry_errors = entry.get("errors", []) or []
+
+ # ---- Name RVA validation: one issue per entry, priority-resolved ----
+ rva_reason = _first_matching(entry_errors, _NAME_RVA_ERROR_PRIORITY)
+ if rva_reason != "unknown":
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_NAME_RVA_INVALID,
+ details={"index": index,
+ "name_rva": entry.get("name_rva"),
+ "reason": rva_reason},
+ ))
+
+ # ---- Name encoding: one issue per entry, priority-resolved ----
+ encoding_reason = _first_matching(
+ entry_errors, _NAME_ENCODING_ERROR_PRIORITY
+ )
+ if encoding_reason != "unknown":
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_NAME_NOT_ASCII,
+ details={"index": index,
+ "name": entry.get("name"),
+ "reason": encoding_reason},
+ ))
+
+ # ---- Ordinal index bounds ----
+ if "ordinal_index_missing" in entry_errors:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_NAME_ORDINAL_INDEX_INVALID,
+ details={"index": index,
+ "reason": "missing"},
+ ))
+ elif "ordinal_index_out_of_range" in entry_errors:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_NAME_ORDINAL_INDEX_INVALID,
+ details={"index": index,
+ "ordinal_index": entry.get("ordinal_index"),
+ "num_functions": num_funcs,
+ "reason": "out_of_range"},
+ ))
+
+
+def _validate_name_pointer_ordering(exp: Dict[str, Any],
+ issues: List[StructuralIssue]) -> None:
+ """
+ The PE spec requires the Export Name Pointer Table to be sorted
+ lexicographically by name so that GetProcAddress can use binary search.
+ Unsorted tables are a real malware pattern (some packers use them to
+ confuse static analysers that assume sorted entries).
+ """
+ names: List[str] = []
+ for entry in exp.get("name_pointers", []) or []:
+ name = entry.get("name")
+ # Skip ordering check if any name in the table is unreadable.
+ # Reporting "unsorted" against a partial view is misleading; the
+ # disorder might just be a downstream effect of the decode failures.
+ # The per-entry EXPORT_NAME_RVA_INVALID / EXPORT_NAME_NOT_ASCII
+ # codes already carry the signal that the table is malformed.
+ if name is None or not entry.get("name_valid"):
+ return
+ names.append(name)
+
+ if not names:
+ return
+
+ sorted_names = sorted(names)
+ if names != sorted_names:
+ # Report once per table, not per pair
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_NAME_POINTER_TABLE_UNSORTED,
+ details={"name_count": len(names),
+ "first_violation_index": _first_unsorted_index(names)},
+ ))
+
+
+def _first_unsorted_index(names: List[str]) -> int:
+ """Return the index of the first name that violates ascending order."""
+ for i in range(1, len(names)):
+ if names[i] < names[i - 1]:
+ return i
+ return -1 # pragma: no cover - defensive; caller guarantees unsorted input
+
+
+# =================================================================
+# Function entries
+# =================================================================
+
+def _validate_functions(exp, header, size_of_image, issues):
+ """
+ For each function entry, check:
+ - ordinal range fits in u16 (single max-ordinal check covers all entries)
+ - address_rva, when non-zero and non-forwarder, points within image
+ - forwarder strings, when present, conform to the spec format
+ """
+ base = header.get("Base", 0)
+ num_funcs = header.get("NumberOfFunctions", 0)
+
+ # Ordinal range sanity: Base + (NumberOfFunctions - 1) must fit in u16.
+ # Some malware sets Base near 0xFFFF to push ordinals into an invalid
+ # range while keeping the count plausible. The per-entry ordinal check
+ # is redundant here. entry.ordinal is computed as Base + index by the
+ # parser, so any entry-level overflow is implied by the max check.
+ if num_funcs > 0:
+ max_ordinal = base + num_funcs - 1
+ if max_ordinal > 0xFFFF:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_ORDINAL_OUT_OF_RANGE,
+ details={"reason": "max_exceeds_u16",
+ "base": base,
+ "num_functions": num_funcs,
+ "max_ordinal": max_ordinal},
+ ))
+
+ for entry in exp.get("functions", []) or []:
+ index = entry.get("index")
+ ordinal = entry.get("ordinal")
+ address_rva = entry.get("address_rva")
+
+ if entry.get("is_forwarder"):
+ forwarder = entry.get("forwarder")
+ if forwarder is None:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_FORWARDER_MALFORMED,
+ details={"index": index, "ordinal": ordinal,
+ "reason": "unreadable"},
+ ))
+ elif not entry.get("forwarder_valid"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_FORWARDER_MALFORMED,
+ details={"index": index, "ordinal": ordinal,
+ "forwarder": forwarder, "reason": "format"},
+ ))
+ continue
+
+ # An EAT entry of 0 means "this ordinal slot is unused" -> legal per spec
+ if address_rva is None or address_rva == 0:
+ continue
+
+ # Otherwise the RVA must point within the image
+ if size_of_image is not None and address_rva >= size_of_image:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.EXPORT_FUNCTION_RVA_INVALID,
+ details={"index": index, "ordinal": ordinal,
+ "address_rva": address_rva,
+ "size_of_image": size_of_image,
+ "reason": "exceeds_image"},
+ ))
+
+
+# =================================================================
+# Helpers
+# =================================================================
+
+def _first_matching(errors: List[str], candidates: List[str]) -> str:
+ """Return the first error tag from `candidates` that appears in `errors`."""
+ for c in candidates:
+ if c in errors:
+ return c
+ return "unknown"
diff --git a/iocx/validators/resources.py b/iocx/validators/resources.py
index 55d2678..bac625e 100644
--- a/iocx/validators/resources.py
+++ b/iocx/validators/resources.py
@@ -29,8 +29,8 @@ def validate_resources(metadata: InternalMetadata, analysis: AnalysisDict) -> Li
rsrc_va = rsrc_section["virtual_address"]
rsrc_vs = rsrc_section["virtual_size"]
- rsrc_raw = rsrc_section["raw_address"]
- rsrc_raw_size = rsrc_section["raw_size"]
+ rsrc_raw = rsrc_section["raw_address"] # reserved
+ rsrc_raw_size = rsrc_section["raw_size"] # reserved
def rva_in_rsrc(rva: int, size: int = 0) -> bool:
return rsrc_va <= rva and (rva + size) <= (rsrc_va + rsrc_vs)
@@ -52,7 +52,7 @@ def raw_overlaps_section(raw_start: int, size: int, sec: Dict[str, Any]) -> bool
# ---------------------------------------------------------
# Recursive directory validation
# ---------------------------------------------------------
- def validate_directory(dir_node: Dict[str, Any]) -> None:
+ def validate_directory(dir_node: Dict[str, Any], depth: int = 0) -> None:
rva = dir_node["rva"]
size = dir_node["size"]
@@ -62,6 +62,8 @@ def validate_directory(dir_node: Dict[str, Any]) -> None:
entries = dir_node["entries"]
+ # Reserved: only reachable if size is sourced from the on-disk
+ # IMAGE_RESOURCE_DIRECTORY header rather than derived from len(entries)
# Zero-length directory
if size == 0:
issues.append(StructuralIssue(
@@ -79,6 +81,17 @@ def validate_directory(dir_node: Dict[str, Any]) -> None:
return
visited_dirs.add(rva)
+ # --- language layer (depth 2) must use integer LCIDs ---
+ # Per PE spec, the Type → Name → Language tree's deepest directory
+ # layer is keyed by language ID. Named entries here are malformed.
+ if depth == 2:
+ for e in entries:
+ if e["name"] is not None and e["id"] is None:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_DIRECTORY_LANGUAGE_NOT_ID,
+ details={"rva": rva, "name": e["name"]},
+ ))
+
# Entries
for entry in entries:
if entry["is_directory"]:
@@ -92,9 +105,19 @@ def validate_directory(dir_node: Dict[str, Any]) -> None:
))
continue
- validate_directory(target)
+ validate_directory(target, depth + 1) # <-- depth bumped
continue
+ # --- data entries should only appear at depth 2 (Language layer) ---
+ # A data leaf at depth 0 or 1 means the tree shape violates the
+ # Type → Name → Language hierarchy.
+ if depth != 2:
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_DATA_AT_INVALID_DEPTH,
+ details={"rva": rva, "depth": depth,
+ "data_rva": entry["data_rva"]},
+ ))
+
# ------------------------------
# Data entry
# ------------------------------
@@ -118,7 +141,8 @@ def validate_directory(dir_node: Dict[str, Any]) -> None:
))
continue
- # Raw bounds
+ # Raw bounds (data_raw == -1 sentinel from a guarded RVA→offset
+ # lookup also lands here, preserving the existing reason code).
if data_raw < 0 or data_raw + data_size > file_size:
issues.append(StructuralIssue(
issue=ReasonCodes.RESOURCE_DATA_OUT_OF_BOUNDS,
diff --git a/iocx/validators/version_info.py b/iocx/validators/version_info.py
new file mode 100644
index 0000000..995607f
--- /dev/null
+++ b/iocx/validators/version_info.py
@@ -0,0 +1,119 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Validate the version-info structure produced by parser_version_info.
+
+Absence of RT_VERSION is NOT a structural defect — kernel drivers, MSI
+custom-action DLLs and many cross-compiled binaries legitimately omit it.
+We only emit structural codes when an RT_VERSION resource is present and
+malformed.
+"""
+
+from typing import List
+
+from iocx.reason_codes import ReasonCodes
+from iocx.validators.schema import StructuralIssue
+from iocx.schemas.internal_schema import InternalMetadata
+from iocx.schemas.analysis import AnalysisDict
+from .decorators import depends_on
+
+
+@depends_on("internal", "analysis")
+def validate_version_info(metadata: InternalMetadata, analysis: AnalysisDict) -> List[StructuralIssue]:
+ issues: List[StructuralIssue] = []
+
+ vi = metadata.get("version_info_struct")
+ if vi is None:
+ return issues # no RT_VERSION present — not a defect
+
+ sections = analysis["sections"]
+ rsrc_section = next(
+ (s for s in sections if s["name"].lower() == ".rsrc"),
+ None,
+ )
+
+ # ---- Placement within .rsrc ----
+ if rsrc_section is not None and vi.get("rva") is not None:
+ rsrc_va = rsrc_section["virtual_address"]
+ rsrc_vs = rsrc_section["virtual_size"]
+ rva = vi["rva"]
+ size = vi["size"] or 0
+ if not (rsrc_va <= rva and rva + size <= rsrc_va + rsrc_vs):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER,
+ details={"reason": "placement", "rva": rva, "size": size},
+ ))
+
+ # ---- Top-level header ----
+ if not vi.get("decoded"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER,
+ details={"reason": "undecoded", "errors": vi.get("errors", [])},
+ ))
+ return issues # nothing further to validate
+
+ if not vi.get("header_ok"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER,
+ details={"reason": "szkey_mismatch"},
+ ))
+ if not vi.get("length_consistent"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER,
+ details={"reason": "length_inconsistent"},
+ ))
+
+ # ---- VS_FIXEDFILEINFO ----
+ ffi = vi.get("fixed_file_info")
+ if ffi is None:
+ # Only flag if there were parse errors; some binaries legitimately
+ # omit VS_FIXEDFILEINFO with wValueLength == 0.
+ if any(e.startswith("fixed_file_info") for e in vi.get("errors", [])):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO,
+ details={"reason": "parse_failed",
+ "errors": [e for e in vi["errors"]
+ if e.startswith("fixed_file_info")]},
+ ))
+ else:
+ if not ffi.get("signature_ok"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO,
+ details={"reason": "signature",
+ "signature": ffi.get("signature")},
+ ))
+ if not ffi.get("struct_version_ok"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO,
+ details={"reason": "struct_version",
+ "struct_version": ffi.get("struct_version")},
+ ))
+
+ # ---- StringFileInfo ----
+ for sfi in vi.get("string_file_info", []):
+ if sfi.get("errors"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO,
+ details={"errors": sfi["errors"],
+ "tables": len(sfi.get("tables", []))},
+ ))
+ continue
+ for tbl in sfi.get("tables", []):
+ if tbl.get("errors"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO,
+ details={"errors": tbl["errors"],
+ "lang_codepage": tbl.get("lang_codepage")},
+ ))
+
+ # ---- VarFileInfo ----
+ for vfi in vi.get("var_file_info", []):
+ if vfi.get("errors"):
+ issues.append(StructuralIssue(
+ issue=ReasonCodes.RESOURCE_VERSIONINFO_INVALID_VARFILEINFO,
+ details={"errors": vfi["errors"],
+ "vars": len(vfi.get("vars", []))},
+ ))
+
+ return issues
diff --git a/pyproject.toml b/pyproject.toml
index 78395ac..11e5217 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "iocx"
-version = "0.7.4.1"
+version = "0.7.5"
description = "A deterministic, high‑performance static‑analysis engine that extracts high‑signal IOCs from PE binaries, text, and logs — built for SOC automation and modern threat‑analysis pipelines."
authors = [
{ name = "MalX Labs" }
diff --git a/tests/contract/snapshots/layer1_core/clean_iocx_demo.core.json b/tests/contract/snapshots/layer1_core/clean_iocx_demo.core.json
index e0217fc..b626dab 100644
--- a/tests/contract/snapshots/layer1_core/clean_iocx_demo.core.json
+++ b/tests/contract/snapshots/layer1_core/clean_iocx_demo.core.json
@@ -1,308 +1,323 @@
{
- "file": "tests/contract/fixtures/layer1_core/clean_iocx_demo.core.exe",
- "type": "PE",
- "iocs": {
- "urls": [],
- "domains": [],
- "ips": [],
- "hashes": [],
- "emails": [],
- "filepaths": [
- "C:\\Users\\Public\\Documents\\iocx_demo.exe"
- ],
- "base64": [],
- "crypto.btc": [],
- "crypto.eth": []
- },
- "metadata": {
- "file_type": "PE",
- "imports": [
- "KERNEL32.dll",
- "msvcrt.dll"
- ],
- "sections": [
- ".text",
- ".data",
- ".rdata",
- ".pdata",
- ".xdata",
- ".bss",
- ".idata",
- ".CRT",
- ".tls",
- ".reloc"
- ],
- "resources": [],
- "resource_strings": [],
- "import_details": [
- {
- "dll": "KERNEL32.dll",
- "function": "DeleteCriticalSection",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "EnterCriticalSection",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "GetLastError",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "GetSystemTime",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "InitializeCriticalSection",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "IsDBCSLeadByteEx",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "LeaveCriticalSection",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "MultiByteToWideChar",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "SetUnhandledExceptionFilter",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "Sleep",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "TlsGetValue",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "VirtualProtect",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "VirtualQuery",
- "ordinal": null
- },
- {
- "dll": "KERNEL32.dll",
- "function": "WideCharToMultiByte",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "__C_specific_handler",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "___lc_codepage_func",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "___mb_cur_max_func",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "__getmainargs",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "__initenv",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "__iob_func",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "__set_app_type",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "__setusermatherr",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "_amsg_exit",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "_cexit",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "_commode",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "_errno",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "_fmode",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "_initterm",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "_lock",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "_onexit",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "_unlock",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "abort",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "calloc",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "exit",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "fprintf",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "fputc",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "free",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "fwrite",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "localeconv",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "malloc",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "memcpy",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "memset",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "signal",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "strerror",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "strlen",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "strncmp",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "vfprintf",
- "ordinal": null
- },
- {
- "dll": "msvcrt.dll",
- "function": "wcslen",
- "ordinal": null
- }
- ],
- "delayed_imports": [],
- "bound_imports": [],
- "exports": [],
- "tls": {
- "start_address": 5368770560,
- "end_address": 5368770568,
- "callbacks": 5368766520
- },
- "header": {
- "entry_point": 5136,
- "image_base": 5368709120,
- "subsystem": 3,
- "timestamp": 1776246181,
- "machine": 34404,
- "characteristics": 558
- },
- "optional_header": {
- "section_alignment": 4096,
- "file_alignment": 512,
- "size_of_image": 69632,
- "size_of_headers": 1024,
- "linker_version": "2.41",
- "os_version": "4.0",
- "subsystem_version": "5.2"
- },
- "rich_header": null,
- "signatures": [],
- "has_signature": false
- }
+ "file": "tests/contract/fixtures/layer1_core/clean_iocx_demo.core.exe",
+ "type": "PE",
+ "iocs": {
+ "urls": [],
+ "domains": [],
+ "ips": [],
+ "hashes": [],
+ "emails": [],
+ "filepaths": [
+ "C:\\Users\\Public\\Documents\\iocx_demo.exe"
+ ],
+ "base64": [],
+ "crypto.btc": [],
+ "crypto.eth": []
+ },
+ "metadata": {
+ "file_type": "PE",
+ "imports": [
+ "KERNEL32.dll",
+ "msvcrt.dll"
+ ],
+ "sections": [
+ ".text",
+ ".data",
+ ".rdata",
+ ".pdata",
+ ".xdata",
+ ".bss",
+ ".idata",
+ ".CRT",
+ ".tls",
+ ".reloc"
+ ],
+ "resources": [],
+ "resource_strings": [],
+ "import_details": [
+ {
+ "dll": "KERNEL32.dll",
+ "function": "DeleteCriticalSection",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "EnterCriticalSection",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "GetLastError",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "GetSystemTime",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "InitializeCriticalSection",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "IsDBCSLeadByteEx",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "LeaveCriticalSection",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "MultiByteToWideChar",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "SetUnhandledExceptionFilter",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "Sleep",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "TlsGetValue",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "VirtualProtect",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "VirtualQuery",
+ "ordinal": null
+ },
+ {
+ "dll": "KERNEL32.dll",
+ "function": "WideCharToMultiByte",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "__C_specific_handler",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "___lc_codepage_func",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "___mb_cur_max_func",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "__getmainargs",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "__initenv",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "__iob_func",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "__set_app_type",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "__setusermatherr",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "_amsg_exit",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "_cexit",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "_commode",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "_errno",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "_fmode",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "_initterm",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "_lock",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "_onexit",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "_unlock",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "abort",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "calloc",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "exit",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "fprintf",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "fputc",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "free",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "fwrite",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "localeconv",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "malloc",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "memcpy",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "memset",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "signal",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "strerror",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "strlen",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "strncmp",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "vfprintf",
+ "ordinal": null
+ },
+ {
+ "dll": "msvcrt.dll",
+ "function": "wcslen",
+ "ordinal": null
+ }
+ ],
+ "delayed_imports": [],
+ "bound_imports": [],
+ "exports": [],
+ "tls": {
+ "start_address": 5368770560,
+ "end_address": 5368770568,
+ "callbacks": 5368766520
+ },
+ "header": {
+ "entry_point": 5136,
+ "image_base": 5368709120,
+ "subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
+ "timestamp": 1776246181,
+ "machine": 34404,
+ "machine_name": "AMD64",
+ "characteristics": 558
+ },
+ "optional_header": {
+ "section_alignment": 4096,
+ "file_alignment": 512,
+ "size_of_image": 69632,
+ "size_of_headers": 1024,
+ "linker_version": "2.41",
+ "os_version": "4.0",
+ "subsystem_version": "5.2",
+ "dll_characteristics": 352,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "DYNAMIC_BASE",
+ "NX_COMPAT"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 2097152,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
+ },
+ "rich_header": null,
+ "signatures": [],
+ "has_signature": false
+ }
}
diff --git a/tests/contract/snapshots/layer2_edge/load_config_clang.full.json b/tests/contract/snapshots/layer2_edge/load_config_clang.full.json
index d1b0b92..b0ca6b8 100644
--- a/tests/contract/snapshots/layer2_edge/load_config_clang.full.json
+++ b/tests/contract/snapshots/layer2_edge/load_config_clang.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer2_edge/load_config_cookie_valid.full.json b/tests/contract/snapshots/layer2_edge/load_config_cookie_valid.full.json
index cc30e57..725727e 100644
--- a/tests/contract/snapshots/layer2_edge/load_config_cookie_valid.full.json
+++ b/tests/contract/snapshots/layer2_edge/load_config_cookie_valid.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer2_edge/load_config_full_msvc.full.json b/tests/contract/snapshots/layer2_edge/load_config_full_msvc.full.json
index 0977fec..5bbd33c 100644
--- a/tests/contract/snapshots/layer2_edge/load_config_full_msvc.full.json
+++ b/tests/contract/snapshots/layer2_edge/load_config_full_msvc.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer2_edge/load_config_large_padded.full.json b/tests/contract/snapshots/layer2_edge/load_config_large_padded.full.json
index a1dc1d9..3b25e88 100644
--- a/tests/contract/snapshots/layer2_edge/load_config_large_padded.full.json
+++ b/tests/contract/snapshots/layer2_edge/load_config_large_padded.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer2_edge/load_config_minimal_mingw.full.json b/tests/contract/snapshots/layer2_edge/load_config_minimal_mingw.full.json
index 89f2630..e5b2cda 100644
--- a/tests/contract/snapshots/layer2_edge/load_config_minimal_mingw.full.json
+++ b/tests/contract/snapshots/layer2_edge/load_config_minimal_mingw.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer2_edge/load_config_seh_table.full.json b/tests/contract/snapshots/layer2_edge/load_config_seh_table.full.json
index 8f46d2f..adbc2eb 100644
--- a/tests/contract/snapshots/layer2_edge/load_config_seh_table.full.json
+++ b/tests/contract/snapshots/layer2_edge/load_config_seh_table.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/broken_rva_addresses.full.json b/tests/contract/snapshots/layer3_adversarial/broken_rva_addresses.full.json
index 9e93e75..fab3a1c 100644
--- a/tests/contract/snapshots/layer3_adversarial/broken_rva_addresses.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/broken_rva_addresses.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/corrupted_data_directories.full.json b/tests/contract/snapshots/layer3_adversarial/corrupted_data_directories.full.json
index 3ee05c1..603309f 100644
--- a/tests/contract/snapshots/layer3_adversarial/corrupted_data_directories.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/corrupted_data_directories.full.json
@@ -29,8 +29,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -40,7 +42,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -94,11 +105,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -113,7 +124,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/crypto_entropy_payload.full.json b/tests/contract/snapshots/layer3_adversarial/crypto_entropy_payload.full.json
index fd3b73c..204ca37 100644
--- a/tests/contract/snapshots/layer3_adversarial/crypto_entropy_payload.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/crypto_entropy_payload.full.json
@@ -35,10 +35,15 @@
"resources": [
{
"type": "RT_MANIFEST",
+ "name": null,
"language": 1,
- "language_name": "unknown",
+ "language_name": "ar",
+ "codepage": null,
"size": 381,
- "entropy": 4.9116145157351045
+ "entropy": 4.9116,
+ "rva": 24672,
+ "raw_offset": 10336,
+ "errors": null
}
],
"resource_strings": [
@@ -268,8 +273,10 @@
"entry_point": 4732,
"image_base": 5368709120,
"subsystem": 2,
+ "subsystem_name": "WINDOWS_GUI",
"timestamp": 1776422599,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 35
},
"optional_header": {
@@ -279,7 +286,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
},
"rich_header": {
"key": "291fb053",
@@ -524,11 +543,11 @@
"entry_point": 4732,
"image_base": 5368709120,
"subsystem": 2,
+ "subsystem_name": "WINDOWS_GUI",
"timestamp": 1776422599,
"machine": 34404,
- "characteristics": 35,
- "machine_human": "AMD64",
- "subsystem_human": "Windows GUI"
+ "machine_name": "AMD64",
+ "characteristics": 35
}
},
{
@@ -543,7 +562,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
}
},
{
@@ -590,9 +621,9 @@
"types": [
"RT_MANIFEST"
],
- "entropy_min": 4.9116145157351045,
- "entropy_max": 4.9116145157351045,
- "entropy_avg": 4.9116145157351045
+ "entropy_min": 4.9116,
+ "entropy_max": 4.9116,
+ "entropy_avg": 4.9116
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/directory_raw_mismatch.full.json b/tests/contract/snapshots/layer3_adversarial/directory_raw_mismatch.full.json
index fc9257b..74b2bc7 100644
--- a/tests/contract/snapshots/layer3_adversarial/directory_raw_mismatch.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/directory_raw_mismatch.full.json
@@ -29,8 +29,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -40,7 +42,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -94,11 +105,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -113,7 +124,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/directory_zero_size_nonzero_rva.full.json b/tests/contract/snapshots/layer3_adversarial/directory_zero_size_nonzero_rva.full.json
index c8d54e7..745952a 100644
--- a/tests/contract/snapshots/layer3_adversarial/directory_zero_size_nonzero_rva.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/directory_zero_size_nonzero_rva.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_000_entrypoint_zero.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_000_entrypoint_zero.full.json
index b5651d9..3d17ebf 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_000_entrypoint_zero.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_000_entrypoint_zero.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_001_entrypoint_negative.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_001_entrypoint_negative.full.json
index d45bfb9..7e6447e 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_001_entrypoint_negative.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_001_entrypoint_negative.full.json
@@ -31,8 +31,10 @@
"entry_point": 4294967295,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 4294967295,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_002_entrypoint_in_headers.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_002_entrypoint_in_headers.full.json
index 21f0032..a5a4d8a 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_002_entrypoint_in_headers.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_002_entrypoint_in_headers.full.json
@@ -31,8 +31,10 @@
"entry_point": 512,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 512,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_003_entrypoint_gap_between_sections.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_003_entrypoint_gap_between_sections.full.json
index 8368964..e92bf5a 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_003_entrypoint_gap_between_sections.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_003_entrypoint_gap_between_sections.full.json
@@ -31,8 +31,10 @@
"entry_point": 7936,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 7936,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_004_entrypoint_non_exec_section.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_004_entrypoint_non_exec_section.full.json
index bfab447..579fed2 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_004_entrypoint_non_exec_section.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_004_entrypoint_non_exec_section.full.json
@@ -31,8 +31,10 @@
"entry_point": 8208,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 8208,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_005_entrypoint_rsrc.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_005_entrypoint_rsrc.full.json
index 300390d..8071d3a 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_005_entrypoint_rsrc.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_005_entrypoint_rsrc.full.json
@@ -31,8 +31,10 @@
"entry_point": 12320,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 12320,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_006_entrypoint_discardable.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_006_entrypoint_discardable.full.json
index e3e8470..8e20fe3 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_006_entrypoint_discardable.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_006_entrypoint_discardable.full.json
@@ -31,8 +31,10 @@
"entry_point": 4112,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 4112,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_007_entrypoint_zero_length_section.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_007_entrypoint_zero_length_section.full.json
index d6e6cc4..082e5a4 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_007_entrypoint_zero_length_section.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_007_entrypoint_zero_length_section.full.json
@@ -31,8 +31,10 @@
"entry_point": 4096,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 4096,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_008_entrypoint_beyond_virtual_size.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_008_entrypoint_beyond_virtual_size.full.json
index 79ca3c6..ab38a91 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_008_entrypoint_beyond_virtual_size.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_008_entrypoint_beyond_virtual_size.full.json
@@ -31,8 +31,10 @@
"entry_point": 6144,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 6144,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_009_entrypoint_in_overlay.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_009_entrypoint_in_overlay.full.json
index 648199f..fdd57b6 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_009_entrypoint_in_overlay.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_009_entrypoint_in_overlay.full.json
@@ -31,8 +31,10 @@
"entry_point": 20480,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 20480,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_010_sections_rwx.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_010_sections_rwx.full.json
index aa79e7a..f79d137 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_010_sections_rwx.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_010_sections_rwx.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -121,11 +132,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -140,7 +151,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_011_sections_code_not_exec.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_011_sections_code_not_exec.full.json
index 23f80c5..668cf6a 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_011_sections_code_not_exec.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_011_sections_code_not_exec.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_012_sections_codelike_not_exec.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_012_sections_codelike_not_exec.full.json
index 4622dee..1dd4f49 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_012_sections_codelike_not_exec.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_012_sections_codelike_not_exec.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_013_sections_non_ascii_name.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_013_sections_non_ascii_name.full.json
index 98fb971..fb8cb1b 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_013_sections_non_ascii_name.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_013_sections_non_ascii_name.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_014_sections_empty_name.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_014_sections_empty_name.full.json
index a56cd50..08fbf90 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_014_sections_empty_name.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_014_sections_empty_name.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_015_sections_impossible_flags.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_015_sections_impossible_flags.full.json
index ce9b45f..77355f3 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_015_sections_impossible_flags.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_015_sections_impossible_flags.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -121,11 +132,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -140,7 +151,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_016_sections_raw_misaligned.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_016_sections_raw_misaligned.full.json
index 4c3d0d3..c98efee 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_016_sections_raw_misaligned.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_016_sections_raw_misaligned.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_017_sections_overlap_headers.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_017_sections_overlap_headers.full.json
index a757fd0..d959f00 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_017_sections_overlap_headers.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_017_sections_overlap_headers.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_018_sections_zero_length.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_018_sections_zero_length.full.json
index 9a3a933..2b655bc 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_018_sections_zero_length.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_018_sections_zero_length.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_019_sections_raw_overlap.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_019_sections_raw_overlap.full.json
index ba9d367..7b5c66d 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_019_sections_raw_overlap.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_019_sections_raw_overlap.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_020_sections_virtual_overlap.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_020_sections_virtual_overlap.full.json
index 0c22266..209fd57 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_020_sections_virtual_overlap.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_020_sections_virtual_overlap.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -129,11 +140,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -148,7 +159,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_021_sections_out_of_order_raw.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_021_sections_out_of_order_raw.full.json
index 71ad112..690da5b 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_021_sections_out_of_order_raw.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_021_sections_out_of_order_raw.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_022_sections_out_of_order_virtual.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_022_sections_out_of_order_virtual.full.json
index d8547cf..c93ec7c 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_022_sections_out_of_order_virtual.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_022_sections_out_of_order_virtual.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_023_sections_negative_fields.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_023_sections_negative_fields.full.json
index ff74596..4d94344 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_023_sections_negative_fields.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_023_sections_negative_fields.full.json
@@ -27,8 +27,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -38,7 +40,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -84,11 +95,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -103,7 +114,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_024_opt_size_of_image_too_small.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_024_opt_size_of_image_too_small.full.json
index 6ca33b4..6eea2ca 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_024_opt_size_of_image_too_small.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_024_opt_size_of_image_too_small.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_025_opt_size_of_headers_misaligned.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_025_opt_size_of_headers_misaligned.full.json
index abe9cc2..bd48465 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_025_opt_size_of_headers_misaligned.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_025_opt_size_of_headers_misaligned.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_026_opt_size_of_headers_too_small.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_026_opt_size_of_headers_too_small.full.json
index 5d36dd4..595f1fb 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_026_opt_size_of_headers_too_small.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_026_opt_size_of_headers_too_small.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_027_opt_section_alignment_invalid.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_027_opt_section_alignment_invalid.full.json
index abf03a8..a41d54e 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_027_opt_section_alignment_invalid.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_027_opt_section_alignment_invalid.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_028_opt_file_alignment_invalid.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_028_opt_file_alignment_invalid.full.json
index dd80696..d9cb6f4 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_028_opt_file_alignment_invalid.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_028_opt_file_alignment_invalid.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1536,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1536,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_029_opt_size_fields_too_small.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_029_opt_size_fields_too_small.full.json
index 91c4de0..22b3fc8 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_029_opt_size_fields_too_small.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_029_opt_size_fields_too_small.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_030_opt_image_base_misaligned.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_030_opt_image_base_misaligned.full.json
index 91a28bb..fc3cdb0 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_030_opt_image_base_misaligned.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_030_opt_image_base_misaligned.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4198964,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4198964,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_031_opt_num_dirs_invalid.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_031_opt_num_dirs_invalid.full.json
index 88e8d88..ba2eccd 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_031_opt_num_dirs_invalid.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_031_opt_num_dirs_invalid.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_032_opt_num_dirs_too_small.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_032_opt_num_dirs_too_small.full.json
index d9271d2..068e3a3 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_032_opt_num_dirs_too_small.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_032_opt_num_dirs_too_small.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_033_opt_size_of_image_misaligned.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_033_opt_size_of_image_misaligned.full.json
index 35c6bd1..6240b9e 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_033_opt_size_of_image_misaligned.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_033_opt_size_of_image_misaligned.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_034_ddir_negative_rva.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_034_ddir_negative_rva.full.json
index 7fcb1ba..ecf8645 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_034_ddir_negative_rva.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_034_ddir_negative_rva.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_035_ddir_negative_size.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_035_ddir_negative_size.full.json
index ccfc603..0dafe30 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_035_ddir_negative_size.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_035_ddir_negative_size.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_036_ddir_zero_zero.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_036_ddir_zero_zero.full.json
index 3490e58..959304c 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_036_ddir_zero_zero.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_036_ddir_zero_zero.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_037_ddir_zero_rva_nonzero_size.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_037_ddir_zero_rva_nonzero_size.full.json
index 6b2fa63..02e8f93 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_037_ddir_zero_rva_nonzero_size.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_037_ddir_zero_rva_nonzero_size.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_038_ddir_zero_size_nonzero_rva.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_038_ddir_zero_size_nonzero_rva.full.json
index 004a6cb..4f2fc11 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_038_ddir_zero_size_nonzero_rva.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_038_ddir_zero_size_nonzero_rva.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_039_ddir_in_headers.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_039_ddir_in_headers.full.json
index 4d7fc6c..d2202fe 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_039_ddir_in_headers.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_039_ddir_in_headers.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_040_ddir_out_of_range.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_040_ddir_out_of_range.full.json
index 4a46e73..3a3086e 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_040_ddir_out_of_range.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_040_ddir_out_of_range.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_041_ddir_raw_mismatch.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_041_ddir_raw_mismatch.full.json
index ce5083f..a0d769a 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_041_ddir_raw_mismatch.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_041_ddir_raw_mismatch.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_042_ddir_in_overlay.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_042_ddir_in_overlay.full.json
index b1cbca2..a0b304b 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_042_ddir_in_overlay.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_042_ddir_in_overlay.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_043_ddir_not_mapped.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_043_ddir_not_mapped.full.json
index d44eb49..399ed1c 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_043_ddir_not_mapped.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_043_ddir_not_mapped.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_044_ddir_spans_sections.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_044_ddir_spans_sections.full.json
index da68eb0..9281212 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_044_ddir_spans_sections.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_044_ddir_spans_sections.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/fixture_045_ddir_overlap.full.json b/tests/contract/snapshots/layer3_adversarial/fixture_045_ddir_overlap.full.json
index 89a2b7d..dce5414 100644
--- a/tests/contract/snapshots/layer3_adversarial/fixture_045_ddir_overlap.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/fixture_045_ddir_overlap.full.json
@@ -31,8 +31,10 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 258
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -110,11 +121,11 @@
"entry_point": 0,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 258,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 258
}
},
{
@@ -129,7 +140,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/franken_malformed_pe.full.json b/tests/contract/snapshots/layer3_adversarial/franken_malformed_pe.full.json
index a3e6522..ea0ba88 100644
--- a/tests/contract/snapshots/layer3_adversarial/franken_malformed_pe.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/franken_malformed_pe.full.json
@@ -32,8 +32,10 @@
"entry_point": 12288,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -43,7 +45,16 @@
"size_of_headers": 512,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -137,11 +148,11 @@
"entry_point": 12288,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -156,7 +167,16 @@
"size_of_headers": 512,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
@@ -232,7 +252,6 @@
"section_b": ".rdata"
}
},
-
{
"value": "pe_structure_anomaly",
"start": 0,
@@ -244,7 +263,6 @@
"max_section_end": 11776
}
},
-
{
"value": "pe_structure_anomaly",
"start": 0,
diff --git a/tests/contract/snapshots/layer3_adversarial/franken_malformed_pe.pe32.full.json b/tests/contract/snapshots/layer3_adversarial/franken_malformed_pe.pe32.full.json
index 40ea7bc..176c9ef 100644
--- a/tests/contract/snapshots/layer3_adversarial/franken_malformed_pe.pe32.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/franken_malformed_pe.pe32.full.json
@@ -32,8 +32,10 @@
"entry_point": 12288,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 2
},
"optional_header": {
@@ -43,7 +45,16 @@
"size_of_headers": 512,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -137,11 +148,11 @@
"entry_point": 12288,
"image_base": 4194304,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 2,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 2
}
},
{
@@ -156,7 +167,16 @@
"size_of_headers": 512,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/franken_url_domain_ip.full.json b/tests/contract/snapshots/layer3_adversarial/franken_url_domain_ip.full.json
index 0ae7387..0116f28 100644
--- a/tests/contract/snapshots/layer3_adversarial/franken_url_domain_ip.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/franken_url_domain_ip.full.json
@@ -66,10 +66,15 @@
"resources": [
{
"type": "RT_MANIFEST",
+ "name": null,
"language": 1,
- "language_name": "unknown",
+ "language_name": "ar",
+ "codepage": null,
"size": 381,
- "entropy": 4.9116145157351045
+ "entropy": 4.9116,
+ "rva": 28768,
+ "raw_offset": 11360,
+ "errors": null
}
],
"resource_strings": [
@@ -304,8 +309,10 @@
"entry_point": 5404,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 1777288054,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 35
},
"optional_header": {
@@ -315,7 +322,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
},
"rich_header": {
"key": "291fb073",
@@ -561,11 +580,11 @@
"entry_point": 5404,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 1777288054,
"machine": 34404,
- "characteristics": 35,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 35
}
},
{
@@ -580,7 +599,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
}
},
{
@@ -627,9 +658,9 @@
"types": [
"RT_MANIFEST"
],
- "entropy_min": 4.9116145157351045,
- "entropy_max": 4.9116145157351045,
- "entropy_avg": 4.9116145157351045
+ "entropy_min": 4.9116,
+ "entropy_max": 4.9116,
+ "entropy_avg": 4.9116
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/heuristic_rich.full.json b/tests/contract/snapshots/layer3_adversarial/heuristic_rich.full.json
index 5096817..b5f0997 100644
--- a/tests/contract/snapshots/layer3_adversarial/heuristic_rich.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/heuristic_rich.full.json
@@ -327,8 +327,10 @@
"entry_point": 5088,
"image_base": 5368709120,
"subsystem": 2,
+ "subsystem_name": "WINDOWS_GUI",
"timestamp": 1776351178,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 38
},
"optional_header": {
@@ -338,7 +340,20 @@
"size_of_headers": 1536,
"linker_version": "2.41",
"os_version": "4.0",
- "subsystem_version": "5.2"
+ "subsystem_version": "5.2",
+ "dll_characteristics": 352,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "DYNAMIC_BASE",
+ "NX_COMPAT"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 2097152,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
},
"rich_header": null,
"signatures": [],
@@ -633,11 +648,11 @@
"entry_point": 5088,
"image_base": 5368709120,
"subsystem": 2,
+ "subsystem_name": "WINDOWS_GUI",
"timestamp": 1776351178,
"machine": 34404,
- "characteristics": 38,
- "machine_human": "AMD64",
- "subsystem_human": "Windows GUI"
+ "machine_name": "AMD64",
+ "characteristics": 38
}
},
{
@@ -652,7 +667,20 @@
"size_of_headers": 1536,
"linker_version": "2.41",
"os_version": "4.0",
- "subsystem_version": "5.2"
+ "subsystem_version": "5.2",
+ "dll_characteristics": 352,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "DYNAMIC_BASE",
+ "NX_COMPAT"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 2097152,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/invalid_optional_header.full.json b/tests/contract/snapshots/layer3_adversarial/invalid_optional_header.full.json
index c91b032..0e4caa2 100644
--- a/tests/contract/snapshots/layer3_adversarial/invalid_optional_header.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/invalid_optional_header.full.json
@@ -27,8 +27,10 @@
"entry_point": 2415919104,
"image_base": 74565,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -38,7 +40,16 @@
"size_of_headers": 2048,
"linker_version": "0.0",
"os_version": "10.0",
- "subsystem_version": "99.99"
+ "subsystem_version": "99.99",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -84,11 +95,11 @@
"entry_point": 2415919104,
"image_base": 74565,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -103,7 +114,16 @@
"size_of_headers": 2048,
"linker_version": "0.0",
"os_version": "10.0",
- "subsystem_version": "99.99"
+ "subsystem_version": "99.99",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
@@ -164,6 +184,18 @@
"size": 512,
"size_of_image": 512
}
+ },
+ {
+ "value": "pe_structure_anomaly",
+ "start": 0,
+ "end": 0,
+ "category": "pe_heuristic",
+ "metadata": {
+ "reason": "top_level_decode",
+ "errors": [
+ "header_read_failed"
+ ]
+ }
}
]
}
diff --git a/tests/contract/snapshots/layer3_adversarial/invalid_optional_header.pe32.full.json b/tests/contract/snapshots/layer3_adversarial/invalid_optional_header.pe32.full.json
index 894b77d..99f66c6 100644
--- a/tests/contract/snapshots/layer3_adversarial/invalid_optional_header.pe32.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/invalid_optional_header.pe32.full.json
@@ -29,8 +29,10 @@
"entry_point": 2415919104,
"image_base": 74565,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
+ "machine_name": "I386",
"characteristics": 2
},
"optional_header": {
@@ -40,7 +42,16 @@
"size_of_headers": 2048,
"linker_version": "0.0",
"os_version": "10.0",
- "subsystem_version": "99.99"
+ "subsystem_version": "99.99",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -94,11 +105,11 @@
"entry_point": 2415919104,
"image_base": 74565,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 332,
- "characteristics": 2,
- "machine_human": "x86",
- "subsystem_human": "Windows CUI"
+ "machine_name": "I386",
+ "characteristics": 2
}
},
{
@@ -113,7 +124,16 @@
"size_of_headers": 2048,
"linker_version": "0.0",
"os_version": "10.0",
- "subsystem_version": "99.99"
+ "subsystem_version": "99.99",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
@@ -222,6 +242,16 @@
"size": 512,
"size_of_image": 512
}
+ },
+ {
+ "value": "pe_structure_anomaly",
+ "start": 0,
+ "end": 0,
+ "category": "pe_heuristic",
+ "metadata": {
+ "reason": "export_table_truncated",
+ "table": "export_directory_header"
+ }
}
]
}
diff --git a/tests/contract/snapshots/layer3_adversarial/invalid_section_alignment.full.json b/tests/contract/snapshots/layer3_adversarial/invalid_section_alignment.full.json
index fb4ac34..d2997a3 100644
--- a/tests/contract/snapshots/layer3_adversarial/invalid_section_alignment.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/invalid_section_alignment.full.json
@@ -29,8 +29,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -40,7 +42,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -94,11 +105,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -113,7 +124,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_cookie_too_small.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_cookie_too_small.full.json
index 420220d..9597a2c 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_cookie_too_small.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_cookie_too_small.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_cookie_in_overlay.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_cookie_in_overlay.full.json
index e315af8..7333839 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_cookie_in_overlay.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_cookie_in_overlay.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_cookie_invalid.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_cookie_invalid.full.json
index 52a1b47..0cbad97 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_cookie_invalid.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_cookie_invalid.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_guard_cf_inconsistent.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_guard_cf_inconsistent.full.json
index b60b325..91e0a97 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_guard_cf_inconsistent.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_guard_cf_inconsistent.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_seh_invalid.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_seh_invalid.full.json
index 650bfad..f41c478 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_seh_invalid.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_seh_invalid.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_size_exceeds_section.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_size_exceeds_section.full.json
index 92e6523..e843293 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_size_exceeds_section.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_size_exceeds_section.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_size_too_small.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_size_too_small.full.json
index 522eac2..e3093c4 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_size_too_small.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_size_too_small.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_truncated.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_truncated.full.json
index 3cec05d..0b1d2ca 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_malformed_truncated.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_malformed_truncated.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_rva_negative.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_rva_negative.full.json
index c8899a4..fb776cf 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_rva_negative.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_rva_negative.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_rva_zero.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_rva_zero.full.json
index 86b4cf2..b2b7cd3 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_rva_zero.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_rva_zero.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_but_fields_present.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_but_fields_present.full.json
index f2c781c..9b46425 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_but_fields_present.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_but_fields_present.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_invalid_rva.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_invalid_rva.full.json
index 9039922..09f061e 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_invalid_rva.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_invalid_rva.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_valid_rva.full.json b/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_valid_rva.full.json
index bfd6533..f1ebb87 100644
--- a/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_valid_rva.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/load_config_zero_size_valid_rva.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 34
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -102,11 +113,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 34,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 34
}
},
{
@@ -121,7 +132,16 @@
"size_of_headers": 1024,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/malformed_domain.full.json b/tests/contract/snapshots/layer3_adversarial/malformed_domain.full.json
index 0e915d4..4f175ff 100644
--- a/tests/contract/snapshots/layer3_adversarial/malformed_domain.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/malformed_domain.full.json
@@ -44,10 +44,15 @@
"resources": [
{
"type": "RT_MANIFEST",
+ "name": null,
"language": 1,
- "language_name": "unknown",
+ "language_name": "ar",
+ "codepage": null,
"size": 381,
- "entropy": 4.9116145157351045
+ "entropy": 4.9116,
+ "rva": 24672,
+ "raw_offset": 10336,
+ "errors": null
}
],
"resource_strings": [
@@ -282,8 +287,10 @@
"entry_point": 4932,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 1777298904,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 35
},
"optional_header": {
@@ -293,7 +300,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
},
"rich_header": {
"key": "291fb073",
@@ -539,11 +558,11 @@
"entry_point": 4932,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 1777298904,
"machine": 34404,
- "characteristics": 35,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 35
}
},
{
@@ -558,7 +577,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
}
},
{
@@ -605,9 +636,9 @@
"types": [
"RT_MANIFEST"
],
- "entropy_min": 4.9116145157351045,
- "entropy_max": 4.9116145157351045,
- "entropy_avg": 4.9116145157351045
+ "entropy_min": 4.9116,
+ "entropy_max": 4.9116,
+ "entropy_avg": 4.9116
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/malformed_import_table.full.json b/tests/contract/snapshots/layer3_adversarial/malformed_import_table.full.json
index f393720..b00f5e2 100644
--- a/tests/contract/snapshots/layer3_adversarial/malformed_import_table.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/malformed_import_table.full.json
@@ -29,8 +29,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -40,7 +42,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -94,11 +105,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -113,7 +124,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/malformed_ip.full.json b/tests/contract/snapshots/layer3_adversarial/malformed_ip.full.json
index ed1d5d2..8376f85 100644
--- a/tests/contract/snapshots/layer3_adversarial/malformed_ip.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/malformed_ip.full.json
@@ -50,10 +50,15 @@
"resources": [
{
"type": "RT_MANIFEST",
+ "name": null,
"language": 1,
- "language_name": "unknown",
+ "language_name": "ar",
+ "codepage": null,
"size": 381,
- "entropy": 4.9116145157351045
+ "entropy": 4.9116,
+ "rva": 24672,
+ "raw_offset": 10336,
+ "errors": null
}
],
"resource_strings": [
@@ -288,8 +293,10 @@
"entry_point": 5032,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 1777299340,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 35
},
"optional_header": {
@@ -299,7 +306,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
},
"rich_header": {
"key": "291fb073",
@@ -545,11 +564,11 @@
"entry_point": 5032,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 1777299340,
"machine": 34404,
- "characteristics": 35,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 35
}
},
{
@@ -564,7 +583,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
}
},
{
@@ -611,9 +642,9 @@
"types": [
"RT_MANIFEST"
],
- "entropy_min": 4.9116145157351045,
- "entropy_max": 4.9116145157351045,
- "entropy_avg": 4.9116145157351045
+ "entropy_min": 4.9116,
+ "entropy_max": 4.9116,
+ "entropy_avg": 4.9116
}
}
],
@@ -651,7 +682,7 @@
"function": "QueryPerformanceCounter"
}
},
- {
+ {
"value": "pe_structure_anomaly",
"start": 0,
"end": 0,
diff --git a/tests/contract/snapshots/layer3_adversarial/malformed_url.full.json b/tests/contract/snapshots/layer3_adversarial/malformed_url.full.json
index ebd267e..1fb0a08 100644
--- a/tests/contract/snapshots/layer3_adversarial/malformed_url.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/malformed_url.full.json
@@ -48,10 +48,15 @@
"resources": [
{
"type": "RT_MANIFEST",
+ "name": null,
"language": 1,
- "language_name": "unknown",
+ "language_name": "ar",
+ "codepage": null,
"size": 381,
- "entropy": 4.9116145157351045
+ "entropy": 4.9116,
+ "rva": 24672,
+ "raw_offset": 10336,
+ "errors": null
}
],
"resource_strings": [
@@ -286,8 +291,10 @@
"entry_point": 4904,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 1777300501,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 35
},
"optional_header": {
@@ -297,7 +304,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
},
"rich_header": {
"key": "291fb073",
@@ -543,11 +562,11 @@
"entry_point": 4904,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 1777300501,
"machine": 34404,
- "characteristics": 35,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 35
}
},
{
@@ -562,7 +581,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
}
},
{
@@ -609,9 +640,9 @@
"types": [
"RT_MANIFEST"
],
- "entropy_min": 4.9116145157351045,
- "entropy_max": 4.9116145157351045,
- "entropy_avg": 4.9116145157351045
+ "entropy_min": 4.9116,
+ "entropy_max": 4.9116,
+ "entropy_avg": 4.9116
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/overlapping_sections.full.json b/tests/contract/snapshots/layer3_adversarial/overlapping_sections.full.json
index c4fdf90..fea2f64 100644
--- a/tests/contract/snapshots/layer3_adversarial/overlapping_sections.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/overlapping_sections.full.json
@@ -30,8 +30,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -41,7 +43,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -121,11 +132,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -140,7 +151,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/packed_lookalike.full.json b/tests/contract/snapshots/layer3_adversarial/packed_lookalike.full.json
index 4652727..6d07915 100644
--- a/tests/contract/snapshots/layer3_adversarial/packed_lookalike.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/packed_lookalike.full.json
@@ -33,8 +33,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -44,7 +46,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -142,11 +153,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -161,7 +172,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/string_obfuscation_tricks.full.json b/tests/contract/snapshots/layer3_adversarial/string_obfuscation_tricks.full.json
index 2de5188..fb53e75 100644
--- a/tests/contract/snapshots/layer3_adversarial/string_obfuscation_tricks.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/string_obfuscation_tricks.full.json
@@ -41,10 +41,15 @@
"resources": [
{
"type": "RT_MANIFEST",
+ "name": null,
"language": 1,
- "language_name": "unknown",
+ "language_name": "ar",
+ "codepage": null,
"size": 381,
- "entropy": 4.9116145157351045
+ "entropy": 4.9116,
+ "rva": 24672,
+ "raw_offset": 10336,
+ "errors": null
}
],
"resource_strings": [
@@ -279,8 +284,10 @@
"entry_point": 4804,
"image_base": 5368709120,
"subsystem": 2,
+ "subsystem_name": "WINDOWS_GUI",
"timestamp": 1776422601,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 35
},
"optional_header": {
@@ -290,7 +297,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
},
"rich_header": {
"key": "291fb073",
@@ -536,11 +555,11 @@
"entry_point": 4804,
"image_base": 5368709120,
"subsystem": 2,
+ "subsystem_name": "WINDOWS_GUI",
"timestamp": 1776422601,
"machine": 34404,
- "characteristics": 35,
- "machine_human": "AMD64",
- "subsystem_human": "Windows GUI"
+ "machine_name": "AMD64",
+ "characteristics": 35
}
},
{
@@ -555,7 +574,19 @@
"size_of_headers": 1024,
"linker_version": "14.44",
"os_version": "6.0",
- "subsystem_version": "6.0"
+ "subsystem_version": "6.0",
+ "dll_characteristics": 32800,
+ "dll_characteristics_flags": [
+ "HIGH_ENTROPY_VA",
+ "TERMINAL_SERVER_AWARE"
+ ],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 1048576,
+ "stack_commit_size": 4096,
+ "heap_reserve_size": 1048576,
+ "heap_commit_size": 4096
}
},
{
@@ -602,9 +633,9 @@
"types": [
"RT_MANIFEST"
],
- "entropy_min": 4.9116145157351045,
- "entropy_max": 4.9116145157351045,
- "entropy_avg": 4.9116145157351045
+ "entropy_min": 4.9116,
+ "entropy_max": 4.9116,
+ "entropy_avg": 4.9116
}
}
],
diff --git a/tests/contract/snapshots/layer3_adversarial/truncated_rich_header.full.json b/tests/contract/snapshots/layer3_adversarial/truncated_rich_header.full.json
index d32a714..e7d3f86 100644
--- a/tests/contract/snapshots/layer3_adversarial/truncated_rich_header.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/truncated_rich_header.full.json
@@ -29,8 +29,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -40,7 +42,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -94,11 +105,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -113,12 +124,21 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
"heuristics": [
- {
+ {
"value": "pe_structure_anomaly",
"start": 0,
"end": 0,
diff --git a/tests/contract/snapshots/layer3_adversarial/upx_name_only.full.json b/tests/contract/snapshots/layer3_adversarial/upx_name_only.full.json
index d96ebf1..821c000 100644
--- a/tests/contract/snapshots/layer3_adversarial/upx_name_only.full.json
+++ b/tests/contract/snapshots/layer3_adversarial/upx_name_only.full.json
@@ -31,8 +31,10 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
+ "machine_name": "AMD64",
"characteristics": 2
},
"optional_header": {
@@ -42,7 +44,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
},
"rich_header": null,
"signatures": [],
@@ -129,11 +140,11 @@
"entry_point": 4096,
"image_base": 5368709120,
"subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
"machine": 34404,
- "characteristics": 2,
- "machine_human": "AMD64",
- "subsystem_human": "Windows CUI"
+ "machine_name": "AMD64",
+ "characteristics": 2
}
},
{
@@ -148,7 +159,16 @@
"size_of_headers": 512,
"linker_version": "0.0",
"os_version": "0.0",
- "subsystem_version": "0.0"
+ "subsystem_version": "0.0",
+ "dll_characteristics": 0,
+ "dll_characteristics_flags": [],
+ "dll_characteristics_unknown_bits": null,
+ "win32_version_value": 0,
+ "loader_flags": 0,
+ "stack_reserve_size": 0,
+ "stack_commit_size": 0,
+ "heap_reserve_size": 0,
+ "heap_commit_size": 0
}
}
],
diff --git a/tests/unit/analysis/test_extended.py b/tests/unit/analysis/test_extended.py
index d0a1f37..e544e58 100644
--- a/tests/unit/analysis/test_extended.py
+++ b/tests/unit/analysis/test_extended.py
@@ -118,7 +118,9 @@ def test_header_human_fields():
metadata = {
"header": {
"machine": 0x8664, # AMD64
- "subsystem": 3, # Windows CUI
+ "machine_name": "AMD64",
+ "subsystem": 3, # WINDOWS_CUI
+ "subsystem_name": "WINDOWS_CUI",
"timestamp": 0,
}
}
@@ -126,8 +128,8 @@ def test_header_human_fields():
result = analyse_extended(None, metadata, [])
header = extract(result, "header")["metadata"]
- assert header["machine_human"] == "AMD64"
- assert header["subsystem_human"] == "Windows CUI"
+ assert header["machine_name"] == "AMD64"
+ assert header["subsystem_name"] == "WINDOWS_CUI"
def test_optional_header_included():
@@ -182,3 +184,229 @@ def test_empty_metadata_produces_minimal_output():
assert summary["resource_count"] == 0
assert summary["has_tls"] is False
assert summary["has_signature"] is False
+
+
+class TestAnalyseExtendedHeaderPassthrough:
+
+ def test_header_detection_matches_input_header_verbatim(self):
+ """Post-refactor, the extended header block is a verbatim pass-through
+ of the parser's header dict. No decoding, no enrichment."""
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [], "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {
+ "entry_point": 0x1000,
+ "image_base": 0x140000000,
+ "subsystem": 3,
+ "subsystem_name": "WINDOWS_CUI",
+ "timestamp": 1700000000,
+ "machine": 0x8664,
+ "machine_name": "AMD64",
+ "characteristics": 0x22,
+ },
+ "optional_header": None,
+ "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ header_detection = next(d for d in detections if d["value"] == "header")
+ # The detection's metadata should equal the input header exactly
+ assert header_detection["metadata"] == metadata["header"]
+
+ def test_no_subsystem_human_field_added(self):
+ """Post-refactor, the legacy subsystem_human field is removed."""
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [], "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {"subsystem": 3, "subsystem_name": "WINDOWS_CUI"},
+ "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ header_detection = next(d for d in detections if d["value"] == "header")
+ assert "subsystem_human" not in header_detection["metadata"]
+
+ def test_no_machine_human_field_added(self):
+ """Post-refactor, the legacy machine_human field is removed."""
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [], "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {"machine": 0x8664, "machine_name": "AMD64"},
+ "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ header_detection = next(d for d in detections if d["value"] == "header")
+ assert "machine_human" not in header_detection["metadata"]
+
+class TestAnalyseExtendedSummary:
+
+ def test_summary_counts_correct(self):
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [
+ {"dll": "a.dll", "function": "foo", "ordinal": None},
+ {"dll": "a.dll", "function": "bar", "ordinal": None},
+ {"dll": "b.dll", "function": "baz", "ordinal": None},
+ ],
+ "delayed_imports": [
+ {"dll": "c.dll", "function": "qux", "ordinal": None},
+ ],
+ "bound_imports": [],
+ "exports": [{"name": "foo", "ordinal": 1, "forwarder": None},
+ {"name": "bar", "ordinal": 2, "forwarder": None}],
+ "resources": [{
+ "type": "RT_ICON",
+ "name": None,
+ "language": 1033,
+ "language_name": "en-US",
+ "codepage": None,
+ "size": 100,
+ "entropy": 4.5,
+ "rva": 0x1000,
+ "raw_offset": 0x400,
+ "errors": None,
+ }],
+ "tls": {"start_address": 1, "end_address": 2, "callbacks": None},
+ "signatures": [{"signer": "test"}],
+ "header": {}, "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ summary = next(d for d in detections if d["value"] == "summary")["metadata"]
+
+ assert summary["dll_count"] == 2
+ assert summary["import_count"] == 3
+ assert summary["delayed_import_count"] == 1
+ assert summary["bound_import_count"] == 0
+ assert summary["export_count"] == 2
+ assert summary["resource_count"] == 1
+ assert summary["has_tls"] is True
+ assert summary["has_signature"] is True
+
+ def test_summary_with_no_data(self):
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [], "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {}, "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ summary = next(d for d in detections if d["value"] == "summary")["metadata"]
+
+ assert summary["dll_count"] == 0
+ assert summary["import_count"] == 0
+ assert summary["has_tls"] is False
+ assert summary["has_signature"] is False
+
+class TestAnalyseExtendedImports:
+
+ def test_imports_grouped_by_dll(self):
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [
+ {"dll": "a.dll", "function": "foo", "ordinal": None},
+ {"dll": "b.dll", "function": "bar", "ordinal": None},
+ {"dll": "a.dll", "function": "baz", "ordinal": None},
+ ],
+ "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {}, "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ imports = [d for d in detections if d["value"] == "imports"]
+
+ # Two DLLs in alphabetical order
+ assert len(imports) == 2
+ assert imports[0]["metadata"]["dll"] == "a.dll"
+ assert imports[1]["metadata"]["dll"] == "b.dll"
+ # Functions sorted alphabetically within each DLL
+ assert imports[0]["metadata"]["functions"] == ["baz", "foo"]
+
+ def test_ordinal_only_imports_displayed_as_hash(self):
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [
+ {"dll": "a.dll", "function": None, "ordinal": 42},
+ ],
+ "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {}, "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ imports = next(d for d in detections if d["value"] == "imports")
+ assert imports["metadata"]["functions"] == ["#42"]
+
+ def test_imports_with_mixed_name_and_ordinal_sorted(self):
+ """Named functions first, then ordinals."""
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [
+ {"dll": "a.dll", "function": None, "ordinal": 5},
+ {"dll": "a.dll", "function": "Bar", "ordinal": None},
+ {"dll": "a.dll", "function": "Alpha", "ordinal": None},
+ ],
+ "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {}, "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ imports = next(d for d in detections if d["value"] == "imports")
+ assert imports["metadata"]["functions"] == ["Alpha", "Bar", "#5"]
+
+class TestAnalyseExtendedConditionalSections:
+
+ def test_no_delayed_imports_section_when_empty(self):
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [], "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {}, "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ delayed = [d for d in detections if d["value"] == "delayed_imports"]
+ assert delayed == []
+
+ def test_no_tls_section_when_none(self):
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [], "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {}, "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ tls = [d for d in detections if d["value"] == "tls_directory"]
+ assert tls == []
+
+ def test_no_signature_section_when_empty(self):
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [], "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {}, "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ sig = [d for d in detections if d["value"] == "signature"]
+ assert sig == []
+
+ def test_no_rich_header_section_when_none(self):
+ from iocx.analysis.extended import analyse_extended
+
+ metadata = {
+ "import_details": [], "delayed_imports": [], "bound_imports": [],
+ "exports": [], "resources": [], "tls": None, "signatures": [],
+ "header": {}, "optional_header": None, "rich_header": None,
+ }
+ detections = analyse_extended(None, metadata, [])
+ rich = [d for d in detections if d["value"] == "rich_header"]
+ assert rich == []
diff --git a/tests/unit/analysis/test_extended_resource_entropy.py b/tests/unit/analysis/test_extended_resource_entropy.py
new file mode 100644
index 0000000..9e74fe6
--- /dev/null
+++ b/tests/unit/analysis/test_extended_resource_entropy.py
@@ -0,0 +1,289 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Covers the entropy_min / entropy_max / entropy_avg computation introduced
+during the extended.py refactor, which handles per-resource error tombstones
+(entropy: None) without raising on min() / max() of an empty sequence.
+"""
+
+from __future__ import annotations
+from typing import Any, Dict, List, Optional
+import pytest
+from iocx.analysis.extended import analyse_extended
+
+
+# =================================================================
+# Test helpers
+# =================================================================
+
+def _resource(
+ type_: str = "RT_RCDATA",
+ name: Optional[str] = None,
+ language: Optional[int] = 1033,
+ language_name: Optional[str] = "en-US",
+ codepage: Optional[int] = None,
+ size: Optional[int] = 100,
+ entropy: Optional[float] = 4.5,
+ rva: int = 0x1000,
+ raw_offset: Optional[int] = 0x400,
+ errors: Optional[List[str]] = None,
+) -> Dict[str, Any]:
+ """Build a resource entry dict matching the public ResourceEntry shape."""
+ return {
+ "type": type_,
+ "name": name,
+ "language": language,
+ "language_name": language_name,
+ "codepage": codepage,
+ "size": size,
+ "entropy": entropy,
+ "rva": rva,
+ "raw_offset": raw_offset,
+ "errors": errors,
+ }
+
+
+def _metadata_with_resources(resources: List[Dict[str, Any]]) -> Dict[str, Any]:
+ """Build a minimal metadata dict containing only the resources field."""
+ return {
+ "import_details": [],
+ "delayed_imports": [],
+ "bound_imports": [],
+ "exports": [],
+ "resources": resources,
+ "tls": None,
+ "signatures": [],
+ "header": {},
+ "optional_header": None,
+ "rich_header": None,
+ }
+
+
+def _resource_summary(detections: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
+ """Extract the resource-summary detection from the analyser output."""
+ for d in detections:
+ if d["value"] == "resources":
+ return d["metadata"]
+ return None
+
+
+# =================================================================
+# Happy path: all entropies present
+# =================================================================
+
+class TestAllEntropiesPresent:
+
+ def test_single_resource_min_max_avg_equal(self):
+ resources = [_resource(entropy=4.5)]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ assert summary is not None
+ assert summary["count"] == 1
+ assert summary["entropy_min"] == 4.5
+ assert summary["entropy_max"] == 4.5
+ assert summary["entropy_avg"] == 4.5
+
+ def test_multiple_resources_computes_stats(self):
+ resources = [
+ _resource(type_="RT_ICON", entropy=2.0, rva=0x1000),
+ _resource(type_="RT_STRING", entropy=4.0, rva=0x2000),
+ _resource(type_="RT_VERSION", entropy=6.0, rva=0x3000),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ assert summary["count"] == 3
+ assert summary["entropy_min"] == 2.0
+ assert summary["entropy_max"] == 6.0
+ assert summary["entropy_avg"] == 4.0
+
+ def test_entropy_avg_handles_floating_point_precision(self):
+ # Three values whose average is a repeating decimal
+ resources = [
+ _resource(entropy=1.0, rva=0x1000),
+ _resource(entropy=2.0, rva=0x2000),
+ _resource(entropy=2.0, rva=0x3000),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ # 5.0 / 3 == 1.6666...
+ assert summary["entropy_avg"] == pytest.approx(5.0 / 3)
+
+
+# =================================================================
+# Mixed: some entries have entropy None due to per-entry errors
+# =================================================================
+
+class TestMixedValidAndNoneEntropies:
+
+ def test_excludes_none_entries_from_aggregates(self):
+ resources = [
+ _resource(type_="RT_ICON", entropy=3.0, rva=0x1000),
+ _resource(
+ type_="RT_STRING",
+ entropy=None,
+ size=None,
+ rva=0x2000,
+ errors=["size_invalid"],
+ ),
+ _resource(type_="RT_VERSION", entropy=5.0, rva=0x3000),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+
+ # count reflects total resources (including the error one)
+ assert summary["count"] == 3
+
+ # entropy stats compute over only the valid two
+ assert summary["entropy_min"] == 3.0
+ assert summary["entropy_max"] == 5.0
+ assert summary["entropy_avg"] == 4.0
+
+ def test_single_valid_entropy_among_errors(self):
+ resources = [
+ _resource(entropy=None, rva=0x1000, errors=["data_out_of_bounds"]),
+ _resource(entropy=7.5, rva=0x2000),
+ _resource(entropy=None, rva=0x3000, errors=["rva_invalid"]),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ assert summary["count"] == 3
+ assert summary["entropy_min"] == 7.5
+ assert summary["entropy_max"] == 7.5
+ assert summary["entropy_avg"] == 7.5
+
+ def test_types_field_includes_error_entries(self):
+ """The types list is built over all resources, not just valid-entropy ones."""
+ resources = [
+ _resource(type_="RT_ICON", entropy=3.0, rva=0x1000),
+ _resource(type_="RT_CUSTOM", entropy=None, rva=0x2000, errors=["size_invalid"]),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ assert "RT_ICON" in summary["types"]
+ assert "RT_CUSTOM" in summary["types"]
+
+
+# =================================================================
+# All entries have entropy None: aggregates must be None
+# =================================================================
+
+class TestAllEntropiesNone:
+
+ def test_all_errored_resources_produce_none_stats(self):
+ resources = [
+ _resource(entropy=None, rva=0x1000, errors=["size_invalid"]),
+ _resource(entropy=None, rva=0x2000, errors=["data_out_of_bounds"]),
+ _resource(entropy=None, rva=0x3000, errors=["rva_invalid"]),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ assert summary["count"] == 3
+ assert summary["entropy_min"] is None
+ assert summary["entropy_max"] is None
+ assert summary["entropy_avg"] is None
+
+ def test_single_errored_resource_produces_none_stats(self):
+ resources = [
+ _resource(entropy=None, rva=0x1000, errors=["size_invalid"]),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ assert summary["count"] == 1
+ assert summary["entropy_min"] is None
+ assert summary["entropy_max"] is None
+ assert summary["entropy_avg"] is None
+
+ def test_no_raise_on_all_none_entropies(self):
+ """
+ Regression guard: previous implementation would have raised
+ ValueError on min() / max() of an empty list.
+ """
+ resources = [_resource(entropy=None, errors=["size_invalid"])]
+ # Must not raise
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ assert _resource_summary(detections) is not None
+
+
+# =================================================================
+# Empty resources list: summary block is omitted
+# =================================================================
+
+class TestEmptyResources:
+
+ def test_empty_resources_produces_no_summary(self):
+ """
+ When the resources list is empty, the resource summary detection
+ is not emitted at all (per the `if resources:` guard).
+ """
+ detections = analyse_extended(None, _metadata_with_resources([]), [])
+ assert _resource_summary(detections) is None
+
+
+# =================================================================
+# Edge cases: zero and extreme entropy values
+# =================================================================
+
+class TestEntropyEdgeValues:
+
+ def test_zero_entropy_included_in_stats(self):
+ """
+ Entropy of exactly 0.0 (all-zero resource bytes) is a valid value,
+ not a missing-value sentinel. Must be included in aggregates.
+ """
+ resources = [
+ _resource(entropy=0.0, rva=0x1000),
+ _resource(entropy=4.0, rva=0x2000),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ assert summary["entropy_min"] == 0.0
+ assert summary["entropy_max"] == 4.0
+ assert summary["entropy_avg"] == 2.0
+
+ def test_max_entropy_included_in_stats(self):
+ """
+ Entropy at the Shannon upper bound (8.0 for byte data) is valid.
+ """
+ resources = [
+ _resource(entropy=8.0, rva=0x1000),
+ _resource(entropy=4.0, rva=0x2000),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ assert summary["entropy_min"] == 4.0
+ assert summary["entropy_max"] == 8.0
+ assert summary["entropy_avg"] == 6.0
+
+ def test_all_zero_entropies(self):
+ """All-zero entropies still produce sensible stats."""
+ resources = [
+ _resource(entropy=0.0, rva=0x1000),
+ _resource(entropy=0.0, rva=0x2000),
+ ]
+ detections = analyse_extended(None, _metadata_with_resources(resources), [])
+ summary = _resource_summary(detections)
+ assert summary["entropy_min"] == 0.0
+ assert summary["entropy_max"] == 0.0
+ assert summary["entropy_avg"] == 0.0
+
+
+# =================================================================
+# Determinism
+# =================================================================
+
+class TestDeterminism:
+
+ def test_repeated_invocation_produces_identical_stats(self):
+ resources = [
+ _resource(entropy=3.1, rva=0x1000),
+ _resource(entropy=None, rva=0x2000, errors=["size_invalid"]),
+ _resource(entropy=5.7, rva=0x3000),
+ _resource(entropy=2.4, rva=0x4000),
+ ]
+ metadata = _metadata_with_resources(resources)
+
+ results = [analyse_extended(None, metadata, []) for _ in range(20)]
+ summaries = [_resource_summary(r) for r in results]
+ for s in summaries[1:]:
+ assert s == summaries[0]
diff --git a/tests/unit/parsers/test_pe_delay_imports.py b/tests/unit/parsers/test_pe_delay_imports.py
new file mode 100644
index 0000000..cae4df6
--- /dev/null
+++ b/tests/unit/parsers/test_pe_delay_imports.py
@@ -0,0 +1,1099 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Unit tests for iocx.parsers.pe_delay_imports.
+
+Strategy:
+- Byte-level fixture builders construct IMAGE_DELAY_IMPORT_DESCRIPTOR
+ structures, INT/IAT thunk arrays, and IMAGE_IMPORT_BY_NAME blobs
+ directly via struct.pack.
+- Fake PE objects with controlled OPTIONAL_HEADER, DATA_DIRECTORY[13],
+ and get_data() responses isolate parser logic from pefile.
+- Determinism tests assert byte-for-byte stable output across runs.
+"""
+
+from __future__ import annotations
+
+import struct
+from typing import Any, Dict, List, Optional
+
+import pytest
+
+from iocx.parsers.pe_delay_imports import (
+ build_delay_import_structure,
+ _decode_descriptor,
+ _decode_import_entry,
+ _is_pe32_plus,
+ _is_zero_descriptor,
+ _locate_delay_import_directory,
+ _read_asciiz,
+ _read_import_by_name,
+ _read_thunk_array,
+ _DESCRIPTOR_SIZE,
+ _DELAY_IMPORT_DIRECTORY_INDEX,
+ _MAGIC_PE32,
+ _MAGIC_PE32_PLUS,
+)
+
+
+# =================================================================
+# Byte-level builders
+# =================================================================
+
+def _build_descriptor(
+ attributes: int = 0x01,
+ dll_name_rva: int = 0x2000,
+ module_handle_rva: int = 0x3000,
+ iat_rva: int = 0x4000,
+ int_rva: int = 0x5000,
+ bound_iat_rva: int = 0,
+ unload_iat_rva: int = 0,
+ timestamp: int = 0,
+) -> bytes:
+ """Build a 32-byte IMAGE_DELAY_IMPORT_DESCRIPTOR."""
+ return struct.pack(
+ " bytes:
+ """Build a zero descriptor (array terminator)."""
+ return b"\x00" * _DESCRIPTOR_SIZE
+
+
+def _pack_dword(value: int) -> bytes:
+ return struct.pack(" bytes:
+ return struct.pack(" bytes:
+ """Build a DWORD-sized thunk array terminated by a NULL DWORD."""
+ return b"".join(_pack_dword(t) for t in thunks) + _pack_dword(0)
+
+
+def _build_int_iat_array_64(thunks: List[int]) -> bytes:
+ """Build a QWORD-sized thunk array terminated by a NULL QWORD."""
+ return b"".join(_pack_qword(t) for t in thunks) + _pack_qword(0)
+
+
+def _build_import_by_name(hint: int, name: str) -> bytes:
+ """Build an IMAGE_IMPORT_BY_NAME structure: WORD hint + ASCIIZ name."""
+ return struct.pack(" bytes:
+ return s.encode("ascii") + b"\x00"
+
+
+# Bit patterns for ordinal vs name thunks
+def _ordinal_thunk_32(ordinal: int) -> int:
+ return 0x80000000 | ordinal
+
+
+def _ordinal_thunk_64(ordinal: int) -> int:
+ return 0x8000000000000000 | ordinal
+
+
+# =================================================================
+# Fake PE object
+# =================================================================
+
+class _FakeDataDir:
+ def __init__(self, rva: int, size: int):
+ self.VirtualAddress = rva
+ self.Size = size
+
+
+class _FakeOptHdr:
+ def __init__(
+ self,
+ delay_dir: Optional[_FakeDataDir],
+ magic: int = _MAGIC_PE32_PLUS,
+ ):
+ self.Magic = magic
+ # DATA_DIRECTORY needs at least 14 entries (index 13 is delay-load)
+ self.DATA_DIRECTORY = [None] * 14
+ if delay_dir is not None:
+ self.DATA_DIRECTORY[_DELAY_IMPORT_DIRECTORY_INDEX] = delay_dir
+
+
+class _FakePE:
+ """
+ Minimal duck-typed pe object exposing OPTIONAL_HEADER, DATA_DIRECTORY,
+ and get_data(rva, size).
+
+ get_data handles offset reads: if a requested (rva, size) range falls
+ inside a stored buffer (looked up by the buffer's base RVA), the
+ appropriate slice is returned. This matches real pefile behaviour
+ where the parser walks thunk arrays one element at a time with
+ incrementing RVAs.
+ """
+ def __init__(
+ self,
+ delay_rva: int = 0,
+ delay_size: int = 0,
+ is_64bit: bool = True,
+ data_by_rva: Optional[Dict[int, bytes]] = None,
+ raise_on_get_data: Optional[Exception] = None,
+ raise_for_rva: Optional[int] = None,
+ ):
+ magic = _MAGIC_PE32_PLUS if is_64bit else _MAGIC_PE32
+ if delay_rva == 0 and delay_size == 0:
+ self.OPTIONAL_HEADER = _FakeOptHdr(None, magic=magic)
+ else:
+ self.OPTIONAL_HEADER = _FakeOptHdr(
+ _FakeDataDir(delay_rva, delay_size), magic=magic,
+ )
+ self._data = data_by_rva or {}
+ self._raise = raise_on_get_data
+ self._raise_for_rva = raise_for_rva
+
+ def get_data(self, rva: int, size: int) -> bytes:
+ if self._raise is not None:
+ if self._raise_for_rva is None or self._raise_for_rva == rva:
+ raise self._raise
+
+ candidates = sorted(
+ (base for base in self._data if base <= rva),
+ reverse=True,
+ )
+ for base in candidates:
+ buf = self._data[base]
+ offset = rva - base
+ if offset <= len(buf):
+ # Returns the available slice; may be empty if offset == len(buf)
+ # or if buf itself is empty. Parser short-read detection handles this.
+ return buf[offset:offset + size]
+
+ raise ValueError(f"no fixture data covers rva {rva:#x} size {size}")
+
+
+# =================================================================
+# _locate_delay_import_directory
+# =================================================================
+
+class TestLocator:
+
+ def test_no_data_directory_returns_none(self):
+ pe = type("FakePE", (), {})()
+ assert _locate_delay_import_directory(pe) is None
+
+ def test_index_error_returns_none(self):
+ class _PE:
+ class OPTIONAL_HEADER:
+ DATA_DIRECTORY = []
+ assert _locate_delay_import_directory(_PE) is None
+
+ def test_attribute_error_returns_none(self):
+ class _PE:
+ class OPTIONAL_HEADER:
+ pass
+ assert _locate_delay_import_directory(_PE) is None
+
+ def test_zero_rva_returns_none(self):
+ pe = _FakePE(delay_rva=0, delay_size=100)
+ assert _locate_delay_import_directory(pe) is None
+
+ def test_zero_size_returns_none(self):
+ pe = _FakePE(delay_rva=0x1000, delay_size=0)
+ assert _locate_delay_import_directory(pe) is None
+
+ def test_valid_directory_returns_rva_size(self):
+ pe = _FakePE(delay_rva=0x1000, delay_size=64)
+ assert _locate_delay_import_directory(pe) == (0x1000, 64)
+
+ def test_type_error_in_int_cast_returns_none(self):
+ class _BadDir:
+ VirtualAddress = "not an int"
+ Size = 64
+
+ class _PE:
+ class OPTIONAL_HEADER:
+ DATA_DIRECTORY = [None] * 14
+ _PE.OPTIONAL_HEADER.DATA_DIRECTORY[13] = _BadDir()
+ assert _locate_delay_import_directory(_PE) is None
+
+
+# =================================================================
+# _is_pe32_plus
+# =================================================================
+
+class TestIsPe32Plus:
+
+ def test_pe32_plus_magic_returns_true(self):
+ pe = _FakePE(is_64bit=True)
+ assert _is_pe32_plus(pe) is True
+
+ def test_pe32_magic_returns_false(self):
+ pe = _FakePE(is_64bit=False)
+ assert _is_pe32_plus(pe) is False
+
+ def test_missing_optional_header_returns_false(self):
+ pe = type("FakePE", (), {})()
+ assert _is_pe32_plus(pe) is False
+
+ def test_invalid_magic_returns_false(self):
+ class _BadOH:
+ Magic = "not an int"
+
+ class _PE:
+ OPTIONAL_HEADER = _BadOH()
+ assert _is_pe32_plus(_PE) is False
+
+
+# =================================================================
+# _decode_descriptor and _is_zero_descriptor
+# =================================================================
+
+class TestDecodeDescriptor:
+
+ def test_valid_descriptor_decoded(self):
+ buf = _build_descriptor(
+ attributes=0x01,
+ dll_name_rva=0x2000,
+ module_handle_rva=0x3000,
+ iat_rva=0x4000,
+ int_rva=0x5000,
+ bound_iat_rva=0x6000,
+ unload_iat_rva=0,
+ timestamp=0xDEADBEEF,
+ )
+ d = _decode_descriptor(buf, index=0)
+ assert d is not None
+ assert d["attributes"] == 0x01
+ assert d["attributes_v1"] is True
+ assert d["dll_name_rva"] == 0x2000
+ assert d["module_handle_rva"] == 0x3000
+ assert d["iat_rva"] == 0x4000
+ assert d["int_rva"] == 0x5000
+ assert d["bound_iat_rva"] == 0x6000
+ assert d["unload_iat_rva"] == 0
+ assert d["timestamp"] == 0xDEADBEEF
+ assert d["is_bound"] is True
+ assert d["imports"] == []
+ assert d["errors"] == []
+
+ def test_v0_descriptor_attributes_v1_false(self):
+ buf = _build_descriptor(attributes=0x00)
+ d = _decode_descriptor(buf, index=0)
+ assert d["attributes"] == 0
+ assert d["attributes_v1"] is False
+
+ def test_unbound_descriptor(self):
+ buf = _build_descriptor(bound_iat_rva=0)
+ d = _decode_descriptor(buf, index=0)
+ assert d["is_bound"] is False
+
+ def test_too_short_buffer_returns_none(self):
+ assert _decode_descriptor(b"\x00" * 16, index=0) is None
+
+ def test_zero_descriptor_recognized(self):
+ d = _decode_descriptor(_zero_descriptor(), index=0)
+ assert _is_zero_descriptor(d) is True
+
+ def test_non_zero_descriptor_not_recognized_as_terminator(self):
+ buf = _build_descriptor(dll_name_rva=0x2000)
+ d = _decode_descriptor(buf, index=0)
+ assert _is_zero_descriptor(d) is False
+
+ def test_descriptor_unpack_failure_appends_error_and_breaks(self, monkeypatch):
+ """
+ Cover the defensive struct.error path in _decode_descriptor.
+
+ Force struct.unpack_from to raise on a buffer that has passed all
+ length checks, simulating a hypothetical struct module failure.
+ The parser must append an indexed error tag and break the walk.
+ """
+ import iocx.parsers.pe_delay_imports as pdi
+
+ real_unpack_from = struct.unpack_from
+
+ def fake_unpack_from(fmt, buf, offset=0):
+ if fmt == " bytes:
+ """Build the 40-byte IMAGE_EXPORT_DIRECTORY structure."""
+ return struct.pack(
+ " bytes:
+ return b"".join(struct.pack(" bytes:
+ return b"".join(struct.pack(" bytes:
+ return s.encode("ascii") + b"\x00"
+
+
+# =================================================================
+# Fake pe object
+# =================================================================
+
+class _FakeDataDir:
+ def __init__(self, rva: int, size: int):
+ self.VirtualAddress = rva
+ self.Size = size
+
+
+class _FakeOptHdr:
+ def __init__(self, export_dir: Optional[_FakeDataDir]):
+ # Index 0 is IMAGE_DIRECTORY_ENTRY_EXPORT
+ self.DATA_DIRECTORY = [export_dir]
+
+
+class _FakePE:
+ """
+ Minimal duck-typed pe object exposing only what the parser uses:
+ OPTIONAL_HEADER.DATA_DIRECTORY[0] and get_data(rva, size).
+ """
+ def __init__(
+ self,
+ export_rva: int = 0,
+ export_size: int = 0,
+ data_by_rva: Optional[Dict[int, bytes]] = None,
+ raise_on_get_data: Optional[Exception] = None,
+ raise_for_rva: Optional[int] = None,
+ ):
+ if export_rva == 0 and export_size == 0:
+ self.OPTIONAL_HEADER = _FakeOptHdr(None)
+ else:
+ self.OPTIONAL_HEADER = _FakeOptHdr(
+ _FakeDataDir(export_rva, export_size)
+ )
+ self._data = data_by_rva or {}
+ self._raise = raise_on_get_data
+ self._raise_for_rva = raise_for_rva
+
+ def get_data(self, rva: int, size: int) -> bytes:
+ if self._raise is not None:
+ if self._raise_for_rva is None or self._raise_for_rva == rva:
+ raise self._raise
+ if rva not in self._data:
+ raise ValueError(f"no fixture data at rva {rva}")
+ return self._data[rva][:size]
+
+
+# =================================================================
+# _locate_export_directory
+# =================================================================
+
+class TestLocator:
+
+ def test_no_data_directory_returns_none(self):
+ pe = type("FakePE", (), {})()
+ assert _locate_export_directory(pe) is None
+
+ def test_index_error_returns_none(self):
+ class _PE:
+ class OPTIONAL_HEADER:
+ DATA_DIRECTORY = []
+ assert _locate_export_directory(_PE) is None
+
+ def test_attribute_error_returns_none(self):
+ class _PE:
+ class OPTIONAL_HEADER:
+ pass
+ assert _locate_export_directory(_PE) is None
+
+ def test_zero_rva_returns_none(self):
+ pe = _FakePE(export_rva=0, export_size=100)
+ assert _locate_export_directory(pe) is None
+
+ def test_zero_size_returns_none(self):
+ pe = _FakePE(export_rva=0x1000, export_size=0)
+ assert _locate_export_directory(pe) is None
+
+ def test_valid_directory_returns_rva_size(self):
+ pe = _FakePE(export_rva=0x1000, export_size=200)
+ assert _locate_export_directory(pe) == (0x1000, 200)
+
+ def test_type_error_in_int_cast_returns_none(self):
+ class _BadDir:
+ VirtualAddress = "not an int"
+ Size = 100
+ class _PE:
+ class OPTIONAL_HEADER:
+ DATA_DIRECTORY = [_BadDir()]
+ assert _locate_export_directory(_PE) is None
+
+
+# =================================================================
+# _decode_export_directory
+# =================================================================
+
+class TestDecodeExportDirectory:
+
+ def test_valid_header_decoded(self):
+ header = _build_export_directory_header(
+ characteristics=0,
+ timedatestamp=0xDEADBEEF,
+ major_version=1,
+ minor_version=2,
+ name_rva=0x1000,
+ base=1,
+ num_functions=5,
+ num_names=3,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ out = _decode_export_directory(header)
+ assert out is not None
+ assert out["TimeDateStamp"] == 0xDEADBEEF
+ assert out["MajorVersion"] == 1
+ assert out["MinorVersion"] == 2
+ assert out["Base"] == 1
+ assert out["NumberOfFunctions"] == 5
+ assert out["NumberOfNames"] == 3
+ assert out["AddressOfFunctions"] == 0x1100
+ assert out["AddressOfNames"] == 0x1200
+ assert out["AddressOfNameOrdinals"] == 0x1300
+
+ def test_too_short_returns_none(self):
+ assert _decode_export_directory(b"\x00" * 39) is None
+
+ def test_empty_returns_none(self):
+ assert _decode_export_directory(b"") is None
+
+ def test_all_zero_header_decoded(self):
+ header = b"\x00" * _EXPORT_DIRECTORY_SIZE
+ out = _decode_export_directory(header)
+ assert out is not None
+ assert out["NumberOfFunctions"] == 0
+ assert out["Base"] == 0
+
+
+# =================================================================
+# Array readers
+# =================================================================
+
+class TestReadDwordArray:
+
+ def test_zero_count_returns_empty_list(self):
+ truncations = []
+ result = _read_dword_array(_FakePE(), 0x1000, 0, truncations, "test")
+ assert result == []
+ assert truncations == []
+
+ def test_zero_rva_flags_truncation_returns_none_list(self):
+ truncations = []
+ result = _read_dword_array(_FakePE(), 0, 3, truncations, "test")
+ assert result == [None, None, None]
+ assert truncations == ["test_rva_zero"]
+
+ def test_read_failure_flags_truncation_returns_none_list(self):
+ truncations = []
+ pe = _FakePE(raise_on_get_data=RuntimeError("read failed"))
+ result = _read_dword_array(pe, 0x1000, 3, truncations, "test")
+ assert result == [None, None, None]
+ assert truncations == ["test_read_failed"]
+
+ def test_short_read_flags_truncation_with_partial_data(self):
+ truncations = []
+ # Caller wants 3 DWORDs (12 bytes) but only 6 bytes available
+ pe = _FakePE(data_by_rva={0x1000: b"\x01\x00\x00\x00\x02\x00"})
+ result = _read_dword_array(pe, 0x1000, 3, truncations, "test")
+ assert result[0] == 1
+ assert result[1] is None
+ assert result[2] is None
+ assert truncations == ["test_truncated"]
+
+ def test_full_read_no_truncation(self):
+ truncations = []
+ pe = _FakePE(data_by_rva={
+ 0x1000: _pack_dwords([0x11, 0x22, 0x33]),
+ })
+ result = _read_dword_array(pe, 0x1000, 3, truncations, "test")
+ assert result == [0x11, 0x22, 0x33]
+ assert truncations == []
+
+
+class TestReadWordArray:
+
+ def test_zero_count_returns_empty(self):
+ truncations = []
+ result = _read_word_array(_FakePE(), 0x1000, 0, truncations, "test")
+ assert result == []
+
+ def test_zero_rva_flags_truncation(self):
+ truncations = []
+ result = _read_word_array(_FakePE(), 0, 2, truncations, "test")
+ assert result == [None, None]
+ assert truncations == ["test_rva_zero"]
+
+ def test_full_read(self):
+ truncations = []
+ pe = _FakePE(data_by_rva={0x1000: _pack_words([1, 2, 3])})
+ result = _read_word_array(pe, 0x1000, 3, truncations, "test")
+ assert result == [1, 2, 3]
+
+ def test_short_read_partial_data(self):
+ truncations = []
+ pe = _FakePE(data_by_rva={0x1000: b"\x01\x00"}) # 1 WORD only
+ result = _read_word_array(pe, 0x1000, 3, truncations, "test")
+ assert result[0] == 1
+ assert result[1] is None
+ assert result[2] is None
+ assert truncations == ["test_truncated"]
+
+ def test_read_failure(self):
+ truncations = []
+ pe = _FakePE(raise_on_get_data=RuntimeError("nope"))
+ result = _read_word_array(pe, 0x1000, 2, truncations, "test")
+ assert result == [None, None]
+ assert truncations == ["test_read_failed"]
+
+
+# =================================================================
+# _read_asciiz
+# =================================================================
+
+class TestReadAsciiz:
+
+ def test_zero_rva_returns_error(self):
+ s, err = _read_asciiz(_FakePE(), 0, 100)
+ assert s is None
+ assert err == "rva_zero"
+
+ def test_read_failure_returns_error(self):
+ pe = _FakePE(raise_on_get_data=RuntimeError("nope"))
+ s, err = _read_asciiz(pe, 0x1000, 100)
+ assert s is None
+ assert err == "read_failed"
+
+ def test_empty_read_returns_error(self):
+ pe = _FakePE(data_by_rva={0x1000: b""})
+ s, err = _read_asciiz(pe, 0x1000, 100)
+ assert s is None
+ assert err == "empty_read"
+
+ def test_unterminated_returns_error(self):
+ pe = _FakePE(data_by_rva={0x1000: b"ABCDEF"}) # no NUL
+ s, err = _read_asciiz(pe, 0x1000, 6)
+ assert s is None
+ assert err == "unterminated"
+
+ def test_valid_ascii(self):
+ pe = _FakePE(data_by_rva={0x1000: _asciiz("hello")})
+ s, err = _read_asciiz(pe, 0x1000, 100)
+ assert s == "hello"
+ assert err is None
+
+ def test_empty_string_with_terminator(self):
+ pe = _FakePE(data_by_rva={0x1000: b"\x00"})
+ s, err = _read_asciiz(pe, 0x1000, 100)
+ assert s == ""
+ assert err is None
+
+ def test_non_ascii_returns_flag_with_replacement(self):
+ pe = _FakePE(data_by_rva={0x1000: b"caf\xc3\xa9\x00"}) # UTF-8 café
+ s, err = _read_asciiz(pe, 0x1000, 100)
+ assert err == "non_ascii"
+ assert s is not None
+ assert "?" in s or "\ufffd" in s.encode("utf-8").decode("utf-8", errors="replace")
+
+ def test_string_within_oversized_buffer(self):
+ pe = _FakePE(data_by_rva={0x1000: _asciiz("X") + b"\xff" * 100})
+ s, err = _read_asciiz(pe, 0x1000, 200)
+ assert s == "X"
+ assert err is None
+
+
+# =================================================================
+# _is_valid_export_name
+# =================================================================
+
+class TestIsValidExportName:
+
+ @pytest.mark.parametrize("name,expected", [
+ ("CreateFileW", True),
+ ("_imp__foo", True),
+ ("@ordinal_2", True),
+ ("Foo Bar", True), # space is 0x20, allowed
+ ("!@#$%^&*()", True), # all printable
+ ("", False),
+ ("Foo\x01Bar", False),
+ ("Foo\x7fBar", False),
+ ("Foo\x00Bar", False),
+ ("café", False), # non-ASCII
+ ])
+ def test_validation(self, name, expected):
+ assert _is_valid_export_name(name) is expected
+
+
+# =================================================================
+# build_export_structure — full roundtrips
+# =================================================================
+
+class TestBuildExportStructure:
+
+ def test_no_export_directory_returns_none(self):
+ pe = _FakePE(export_rva=0, export_size=0)
+ assert build_export_structure(pe) is None
+
+ def test_header_read_failure_returns_empty_result(self):
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=100,
+ raise_on_get_data=RuntimeError("simulated"),
+ )
+ result = build_export_structure(pe)
+ assert result is not None
+ assert result["header"] is None
+ assert "header_read_failed" in result["errors"]
+
+ def test_short_header_flags_truncation(self):
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=100,
+ data_by_rva={0x1000: b"\x00" * 20}, # less than 40 bytes
+ )
+ result = build_export_structure(pe)
+ assert result["header"] is None
+ assert "export_directory_header" in result["truncations"]
+
+ def test_unparseable_header(self, monkeypatch):
+ """Force _decode_export_directory to return None even with sufficient bytes."""
+ import iocx.parsers.pe_exports as pep
+
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=100,
+ data_by_rva={0x1000: b"\x00" * _EXPORT_DIRECTORY_SIZE},
+ )
+ monkeypatch.setattr(pep, "_decode_export_directory", lambda buf: None)
+ result = build_export_structure(pe)
+ assert result["header"] is None
+ assert "header_unpack_failed" in result["errors"]
+
+ def test_minimal_valid_export_table_with_one_named_function(self):
+ # Layout:
+ # Header at 0x1000 (40 bytes)
+ # EAT at 0x1100 (1 dword, points to function at 0x2000)
+ # ENPT at 0x1200 (1 dword, points to name string)
+ # EOT at 0x1300 (1 word, EAT index 0)
+ # Name string at 0x1400 ("Foo\0")
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=1,
+ num_names=1,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000]),
+ 0x1200: _pack_dwords([0x1400]),
+ 0x1300: _pack_words([0]),
+ 0x1400: _asciiz("Foo"),
+ },
+ )
+ result = build_export_structure(pe)
+
+ assert result is not None
+ assert result["rva"] == 0x1000
+ assert result["size"] == 200
+ assert result["truncations"] == []
+ assert result["errors"] == []
+ assert len(result["functions"]) == 1
+ assert len(result["name_pointers"]) == 1
+
+ fn = result["functions"][0]
+ assert fn["index"] == 0
+ assert fn["ordinal"] == 1
+ assert fn["address_rva"] == 0x2000
+ assert fn["is_forwarder"] is False
+ assert fn["name"] == "Foo"
+
+ np = result["name_pointers"][0]
+ assert np["index"] == 0
+ assert np["name_rva"] == 0x1400
+ assert np["ordinal_index"] == 0
+ assert np["name"] == "Foo"
+ assert np["name_valid"] is True
+ assert np["errors"] == []
+
+ def test_forwarder_detected_when_address_points_within_directory(self):
+ # Address RVA falls within the export directory range,
+ # indicating a forwarder per PE spec.
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=1,
+ num_names=0,
+ addr_functions=0x1100,
+ )
+ forwarder_rva = 0x1050 # within [0x1000, 0x1000 + 200)
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([forwarder_rva]),
+ forwarder_rva: _asciiz("KERNEL32.LoadLibraryA"),
+ },
+ )
+ result = build_export_structure(pe)
+ fn = result["functions"][0]
+ assert fn["is_forwarder"] is True
+ assert fn["forwarder"] == "KERNEL32.LoadLibraryA"
+ assert fn["forwarder_valid"] is True
+ assert fn["address_rva"] == forwarder_rva
+
+ def test_invalid_forwarder_format_flagged(self):
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=1,
+ num_names=0,
+ addr_functions=0x1100,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x1050]),
+ 0x1050: _asciiz("NoDotInThisString"),
+ },
+ )
+ result = build_export_structure(pe)
+ fn = result["functions"][0]
+ assert fn["is_forwarder"] is True
+ assert fn["forwarder"] == "NoDotInThisString"
+ assert fn["forwarder_valid"] is False
+
+ def test_forwarder_with_ordinal_syntax_valid(self):
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=1,
+ num_names=0,
+ addr_functions=0x1100,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x1050]),
+ 0x1050: _asciiz("KERNEL32.#42"),
+ },
+ )
+ result = build_export_structure(pe)
+ assert result["functions"][0]["forwarder_valid"] is True
+
+ def test_name_pointer_with_zero_rva_flagged(self):
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=1,
+ num_names=1,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000]),
+ 0x1200: _pack_dwords([0]), # name_rva = 0
+ 0x1300: _pack_words([0]),
+ },
+ )
+ result = build_export_structure(pe)
+ np = result["name_pointers"][0]
+ assert "name_rva_zero" in np["errors"]
+ assert np["name"] is None
+
+ def test_name_pointer_with_unterminated_string_flagged(self):
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=1,
+ num_names=1,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000]),
+ 0x1200: _pack_dwords([0x1400]),
+ 0x1300: _pack_words([0]),
+ 0x1400: b"NoTerminator" * 100, # no NUL within max scan
+ },
+ )
+ result = build_export_structure(pe)
+ np = result["name_pointers"][0]
+ assert "unterminated" in np["errors"]
+
+ def test_name_pointer_with_non_printable_name_flagged(self):
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=1,
+ num_names=1,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000]),
+ 0x1200: _pack_dwords([0x1400]),
+ 0x1300: _pack_words([0]),
+ 0x1400: b"Foo\x01Bar\x00", # contains control char
+ },
+ )
+ result = build_export_structure(pe)
+ np = result["name_pointers"][0]
+ assert "name_not_printable_ascii" in np["errors"]
+ assert np["name_valid"] is False
+
+ def test_ordinal_index_out_of_range_flagged(self):
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=1, # only 1 function
+ num_names=1,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000]),
+ 0x1200: _pack_dwords([0x1400]),
+ 0x1300: _pack_words([5]), # ordinal_index=5, but only 1 function
+ 0x1400: _asciiz("Foo"),
+ },
+ )
+ result = build_export_structure(pe)
+ np = result["name_pointers"][0]
+ assert "ordinal_index_out_of_range" in np["errors"]
+
+ def test_eat_truncation_propagates(self):
+ # Declare 5 functions but only provide bytes for 2
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=5,
+ num_names=0,
+ addr_functions=0x1100,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000, 0x2100]), # only 2 of 5
+ },
+ )
+ result = build_export_structure(pe)
+ assert "eat_truncated" in result["truncations"]
+ assert len(result["functions"]) == 5
+ assert result["functions"][0]["address_rva"] == 0x2000
+ assert result["functions"][2]["address_rva"] is None
+
+ def test_unused_eat_slots_legitimate(self):
+ # EAT entry of 0 is "this ordinal slot unused" per PE spec
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=3,
+ num_names=0,
+ addr_functions=0x1100,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000, 0, 0x2200]),
+ },
+ )
+ result = build_export_structure(pe)
+ assert result["functions"][0]["address_rva"] == 0x2000
+ assert result["functions"][1]["address_rva"] == 0
+ assert result["functions"][2]["address_rva"] == 0x2200
+
+ def test_name_resolution_joins_eat_with_enpt(self):
+ # Function at EAT index 2 has a name; functions 0, 1 don't.
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=3,
+ num_names=1,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000, 0x2100, 0x2200]),
+ 0x1200: _pack_dwords([0x1400]),
+ 0x1300: _pack_words([2]), # name points to EAT index 2
+ 0x1400: _asciiz("NamedFunc"),
+ },
+ )
+ result = build_export_structure(pe)
+ assert result["functions"][0]["name"] is None
+ assert result["functions"][1]["name"] is None
+ assert result["functions"][2]["name"] == "NamedFunc"
+
+ def test_empty_export_table(self):
+ # Header declares zero functions and zero names
+ header = _build_export_directory_header(base=1)
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=100,
+ data_by_rva={0x1000: header},
+ )
+ result = build_export_structure(pe)
+ assert result["functions"] == []
+ assert result["name_pointers"] == []
+ assert result["truncations"] == []
+
+ def test_name_pointer_with_truncated_enpt_flags_name_rva_missing(self):
+ """
+ Cover line 350: when ENPT is truncated, _build_name_pointers consumes
+ a None from enpt and tags name_rva_missing.
+ """
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=1,
+ num_names=3, # declared 3 names
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000]),
+ 0x1200: _pack_dwords([0x1400]), # only 1 of 3 DWORDs
+ 0x1300: _pack_words([0, 0, 0]), # EOT is full
+ 0x1400: _asciiz("Foo"),
+ },
+ )
+ result = build_export_structure(pe)
+
+ # Truncation should be flagged on the ENPT
+ assert "enpt_truncated" in result["truncations"]
+
+ # Entries 1 and 2 should carry name_rva_missing (their enpt[i] is None)
+ assert "name_rva_missing" in result["name_pointers"][1]["errors"]
+ assert "name_rva_missing" in result["name_pointers"][2]["errors"]
+
+ # Entry 0 is well-formed
+ assert result["name_pointers"][0]["name"] == "Foo"
+
+ def test_name_pointer_with_truncated_eot_flags_ordinal_index_missing(self):
+ """
+ Cover line 364: when EOT is truncated, _build_name_pointers consumes
+ a None from eot and tags ordinal_index_missing.
+ """
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=3,
+ num_names=3,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva={
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000, 0x2100, 0x2200]),
+ 0x1200: _pack_dwords([0x1400, 0x1404, 0x1408]),
+ 0x1300: _pack_words([0]), # only 1 of 3 WORDs
+ 0x1400: _asciiz("Foo"),
+ 0x1404: _asciiz("Bar"),
+ 0x1408: _asciiz("Baz"),
+ },
+ )
+ result = build_export_structure(pe)
+
+ assert "eot_truncated" in result["truncations"]
+ assert "ordinal_index_missing" in result["name_pointers"][1]["errors"]
+ assert "ordinal_index_missing" in result["name_pointers"][2]["errors"]
+ assert result["name_pointers"][0]["ordinal_index"] == 0
+
+
+# =================================================================
+# Output contract
+# =================================================================
+
+class TestOutputContract:
+
+ REQUIRED_KEYS = {
+ "rva", "size", "header", "functions", "name_pointers",
+ "truncations", "errors",
+ }
+
+ def test_successful_decode_has_all_required_keys(self):
+ header = _build_export_directory_header(base=1)
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=100,
+ data_by_rva={0x1000: header},
+ )
+ result = build_export_structure(pe)
+ assert self.REQUIRED_KEYS.issubset(result.keys())
+
+ def test_lists_are_lists_even_when_empty(self):
+ header = _build_export_directory_header(base=1)
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=100,
+ data_by_rva={0x1000: header},
+ )
+ result = build_export_structure(pe)
+ assert isinstance(result["functions"], list)
+ assert isinstance(result["name_pointers"], list)
+ assert isinstance(result["truncations"], list)
+ assert isinstance(result["errors"], list)
+
+
+# =================================================================
+# Determinism
+# =================================================================
+
+class TestDeterminism:
+
+ def test_repeated_parse_produces_identical_output(self):
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=2,
+ num_names=1,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ data = {
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000, 0x2100]),
+ 0x1200: _pack_dwords([0x1400]),
+ 0x1300: _pack_words([0]),
+ 0x1400: _asciiz("Foo"),
+ }
+
+ results = []
+ for _ in range(20):
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva=dict(data),
+ )
+ results.append(build_export_structure(pe))
+
+ for r in results[1:]:
+ assert r == results[0]
+
+ def test_malformed_input_deterministic(self):
+ # Truncated EAT, non-ASCII name, out-of-range ordinal
+ header = _build_export_directory_header(
+ base=1,
+ num_functions=5,
+ num_names=1,
+ addr_functions=0x1100,
+ addr_names=0x1200,
+ addr_name_ordinals=0x1300,
+ )
+ data = {
+ 0x1000: header,
+ 0x1100: _pack_dwords([0x2000]), # only 1 of 5
+ 0x1200: _pack_dwords([0x1400]),
+ 0x1300: _pack_words([99]),
+ 0x1400: b"caf\xc3\xa9\x00",
+ }
+
+ results = []
+ for _ in range(20):
+ pe = _FakePE(
+ export_rva=0x1000,
+ export_size=200,
+ data_by_rva=dict(data),
+ )
+ results.append(build_export_structure(pe))
+
+ for r in results[1:]:
+ assert r == results[0]
diff --git a/tests/unit/parsers/test_pe_optional_header_extended.py b/tests/unit/parsers/test_pe_optional_header_extended.py
new file mode 100644
index 0000000..8b490a9
--- /dev/null
+++ b/tests/unit/parsers/test_pe_optional_header_extended.py
@@ -0,0 +1,640 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Tests the _parse_optional_header and _parse_header routines plus the
+constants module. Uses minimal duck-typed pe / OPTIONAL_HEADER / FILE_HEADER
+objects to isolate parser logic from pefile's actual struct behaviour.
+
+Coverage targets:
+- Constants module: tables and derived mask
+- DLL characteristics decoding (empty, single, multi, unknown bits)
+- Subsystem name resolution (known, unknown, edge values)
+- Field extraction (existing fields preserved, new fields populated)
+- Missing-field handling (None for new fields, 0 for existing)
+- Determinism (repeated parse produces identical output)
+- JSON-safety (output round-trips through json.dumps)
+- Output contract (key set matches schema)
+"""
+
+from __future__ import annotations
+import json
+import pytest
+from typing import Any, Dict, List, Optional
+from iocx.parsers.pe_constants import (
+ DLL_CHARACTERISTICS_FLAGS,
+ DLL_CHARACTERISTICS_KNOWN_MASK,
+ SUBSYSTEM_NAMES,
+)
+from iocx.parsers.pe_parser import _parse_header, _parse_optional_header
+
+
+# =================================================================
+# Test doubles
+# =================================================================
+
+class _FakeOH:
+ """
+ Minimal duck-typed OPTIONAL_HEADER. Any field that's set on the instance
+ will be returned by getattr; any field that's not set returns the
+ getattr default (which is what _parse_optional_header relies on).
+ """
+ def __init__(self, **fields):
+ for k, v in fields.items():
+ setattr(self, k, v)
+
+
+class _FakeFH:
+ """Minimal duck-typed FILE_HEADER."""
+ def __init__(self, **fields):
+ for k, v in fields.items():
+ setattr(self, k, v)
+
+
+class _FakePE:
+ """Minimal duck-typed pe object."""
+ def __init__(self, optional_header=None, file_header=None):
+ if optional_header is not None:
+ self.OPTIONAL_HEADER = optional_header
+ if file_header is not None:
+ self.FILE_HEADER = file_header
+
+
+# Common builder for a fully-populated OH covering all extracted fields
+def _full_oh(**overrides) -> _FakeOH:
+ defaults = dict(
+ SectionAlignment=4096,
+ FileAlignment=512,
+ SizeOfImage=24576,
+ SizeOfHeaders=1024,
+ MajorLinkerVersion=14,
+ MinorLinkerVersion=42,
+ MajorOperatingSystemVersion=6,
+ MinorOperatingSystemVersion=0,
+ MajorSubsystemVersion=6,
+ MinorSubsystemVersion=0,
+ Subsystem=3,
+ DllCharacteristics=0x4160,
+ Win32VersionValue=0,
+ LoaderFlags=0,
+ SizeOfStackReserve=0x100000,
+ SizeOfStackCommit=0x1000,
+ SizeOfHeapReserve=0x100000,
+ SizeOfHeapCommit=0x1000,
+ AddressOfEntryPoint=0x1000,
+ ImageBase=0x140000000,
+ )
+ defaults.update(overrides)
+ return _FakeOH(**defaults)
+
+
+def _full_fh(**overrides) -> _FakeFH:
+ defaults = dict(
+ TimeDateStamp=1700000000,
+ Machine=0x8664,
+ Characteristics=0x22,
+ )
+ defaults.update(overrides)
+ return _FakeFH(**defaults)
+
+
+# =================================================================
+# Constants module
+# =================================================================
+
+class TestConstants:
+
+ def test_subsystem_names_contains_well_known_values(self):
+ assert SUBSYSTEM_NAMES[3] == "WINDOWS_CUI"
+ assert SUBSYSTEM_NAMES[2] == "WINDOWS_GUI"
+ assert SUBSYSTEM_NAMES[1] == "NATIVE"
+ assert SUBSYSTEM_NAMES[10] == "EFI_APPLICATION"
+
+ def test_dll_characteristics_flags_contains_well_known_bits(self):
+ assert DLL_CHARACTERISTICS_FLAGS[0x0040] == "DYNAMIC_BASE"
+ assert DLL_CHARACTERISTICS_FLAGS[0x0100] == "NX_COMPAT"
+ assert DLL_CHARACTERISTICS_FLAGS[0x4000] == "GUARD_CF"
+ assert DLL_CHARACTERISTICS_FLAGS[0x8000] == "TERMINAL_SERVER_AWARE"
+
+ def test_subsystem_table_covers_pe_spec(self):
+ """The SUBSYSTEM_NAMES table covers the well-known IMAGE_SUBSYSTEM_* values."""
+ required_subsystems = {0, 1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 13, 14, 16}
+ assert required_subsystems.issubset(set(SUBSYSTEM_NAMES.keys()))
+
+ def test_known_mask_covers_all_listed_bits(self):
+ expected_mask = 0
+ for bit in DLL_CHARACTERISTICS_FLAGS:
+ expected_mask |= bit
+ assert DLL_CHARACTERISTICS_KNOWN_MASK == expected_mask
+
+ def test_known_mask_is_subset_of_u16(self):
+ # All defined DLL characteristics fit in a u16
+ assert DLL_CHARACTERISTICS_KNOWN_MASK <= 0xFFFF
+
+ def test_subsystem_names_has_no_duplicate_values(self):
+ values = list(SUBSYSTEM_NAMES.values())
+ assert len(values) == len(set(values))
+
+ def test_dll_characteristics_flags_has_no_duplicate_names(self):
+ names = list(DLL_CHARACTERISTICS_FLAGS.values())
+ assert len(names) == len(set(names))
+
+
+# =================================================================
+# _parse_optional_header — top-level behaviour
+# =================================================================
+
+class TestParseOptionalHeaderTopLevel:
+
+ def test_returns_empty_dict_when_optional_header_missing(self):
+ pe = _FakePE()
+ opt, out = _parse_optional_header(pe)
+ assert opt is None
+ assert out == {}
+
+ def test_returns_optional_header_object_unchanged(self):
+ oh = _full_oh()
+ pe = _FakePE(optional_header=oh)
+ opt, out = _parse_optional_header(pe)
+ assert opt is oh
+
+ def test_returns_dict_with_expected_keys(self):
+ oh = _full_oh()
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ expected_keys = {
+ # Existing fields
+ "section_alignment", "file_alignment", "size_of_image",
+ "size_of_headers", "linker_version", "os_version",
+ "subsystem_version",
+ # New: DLL characteristics
+ "dll_characteristics", "dll_characteristics_flags",
+ "dll_characteristics_unknown_bits",
+ # New: deprecated/reserved DWORDs
+ "win32_version_value", "loader_flags",
+ # New: stack/heap sizing
+ "stack_reserve_size", "stack_commit_size",
+ "heap_reserve_size", "heap_commit_size",
+ }
+ assert set(out.keys()) == expected_keys
+
+
+# =================================================================
+# Existing fields — backward compatibility
+# =================================================================
+
+class TestExistingFieldsBackwardCompatible:
+
+ def test_existing_fields_match_full_oh(self):
+ oh = _full_oh()
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["section_alignment"] == 4096
+ assert out["file_alignment"] == 512
+ assert out["size_of_image"] == 24576
+ assert out["size_of_headers"] == 1024
+ assert out["linker_version"] == "14.42"
+ assert out["os_version"] == "6.0"
+ assert out["subsystem_version"] == "6.0"
+
+ def test_existing_fields_default_to_zero_when_missing(self):
+ # The existing parser uses 0 as the getattr default; preserved.
+ oh = _FakeOH() # no fields set
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["section_alignment"] == 0
+ assert out["file_alignment"] == 0
+ assert out["size_of_image"] == 0
+ assert out["size_of_headers"] == 0
+ assert out["linker_version"] == "0.0"
+ assert out["os_version"] == "0.0"
+ assert out["subsystem_version"] == "0.0"
+
+
+# =================================================================
+# New fields — extraction
+# =================================================================
+
+class TestNewFieldsExtraction:
+
+ def test_dll_characteristics_raw_value(self):
+ oh = _full_oh(DllCharacteristics=0x4140)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["dll_characteristics"] == 0x4140
+
+ def test_dll_characteristics_decoded_flags(self):
+ # 0x4140 = DYNAMIC_BASE (0x0040) | NX_COMPAT (0x0100) | GUARD_CF (0x4000)
+ oh = _full_oh(DllCharacteristics=0x4140)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["dll_characteristics_flags"] == [
+ "DYNAMIC_BASE", "NX_COMPAT", "GUARD_CF"
+ ]
+
+ def test_dll_characteristics_empty_when_zero(self):
+ oh = _full_oh(DllCharacteristics=0)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["dll_characteristics"] == 0
+ assert out["dll_characteristics_flags"] == []
+ assert out["dll_characteristics_unknown_bits"] is None
+
+ def test_dll_characteristics_unknown_bits(self):
+ # 0x10000 is outside the known mask
+ oh = _full_oh(DllCharacteristics=0x10040)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["dll_characteristics_flags"] == ["DYNAMIC_BASE"]
+ assert out["dll_characteristics_unknown_bits"] == "0x10000"
+
+ def test_dll_characteristics_all_known_bits_no_unknown(self):
+ # Every known bit set should produce no unknown_bits report
+ oh = _full_oh(DllCharacteristics=DLL_CHARACTERISTICS_KNOWN_MASK)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert len(out["dll_characteristics_flags"]) == len(DLL_CHARACTERISTICS_FLAGS)
+ assert out["dll_characteristics_unknown_bits"] is None
+
+ def test_dll_characteristics_only_unknown_bits(self):
+ oh = _full_oh(DllCharacteristics=0x10000)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["dll_characteristics_flags"] == []
+ assert out["dll_characteristics_unknown_bits"] == "0x10000"
+
+ def test_dll_characteristics_missing_returns_none(self):
+ oh = _FakeOH() # DllCharacteristics not set
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["dll_characteristics"] is None
+ assert out["dll_characteristics_flags"] is None
+ assert out["dll_characteristics_unknown_bits"] is None
+
+ def test_win32_version_value_extracted(self):
+ oh = _full_oh(Win32VersionValue=0x1234)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["win32_version_value"] == 0x1234
+
+ def test_win32_version_value_missing_returns_none(self):
+ oh = _FakeOH()
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["win32_version_value"] is None
+
+ def test_loader_flags_extracted(self):
+ oh = _full_oh(LoaderFlags=0xCAFE)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["loader_flags"] == 0xCAFE
+
+ def test_loader_flags_missing_returns_none(self):
+ oh = _FakeOH()
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["loader_flags"] is None
+
+ def test_stack_reserve_size_extracted(self):
+ oh = _full_oh(SizeOfStackReserve=0x200000)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["stack_reserve_size"] == 0x200000
+
+ def test_stack_commit_size_extracted(self):
+ oh = _full_oh(SizeOfStackCommit=0x2000)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["stack_commit_size"] == 0x2000
+
+ def test_heap_reserve_size_extracted(self):
+ oh = _full_oh(SizeOfHeapReserve=0x300000)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["heap_reserve_size"] == 0x300000
+
+ def test_heap_commit_size_extracted(self):
+ oh = _full_oh(SizeOfHeapCommit=0x3000)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["heap_commit_size"] == 0x3000
+
+ def test_stack_heap_sizes_missing_return_none(self):
+ oh = _FakeOH()
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["stack_reserve_size"] is None
+ assert out["stack_commit_size"] is None
+ assert out["heap_reserve_size"] is None
+ assert out["heap_commit_size"] is None
+
+ def test_pe32_plus_64bit_sizes(self):
+ # PE32+ binaries use 64-bit values; ensure they pass through
+ oh = _full_oh(SizeOfStackReserve=0x100000000)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["stack_reserve_size"] == 0x100000000
+
+
+# =================================================================
+# DLL characteristics flag ordering
+# =================================================================
+
+class TestDllCharacteristicsOrdering:
+
+ def test_flags_returned_in_bit_position_order(self):
+ # 0xC1A0 = HIGH_ENTROPY_VA (0x0020) | DYNAMIC_BASE (0x0040)
+ # | NX_COMPAT (0x0100) | GUARD_CF (0x4000)
+ # | TERMINAL_SERVER_AWARE (0x8000)
+ oh = _full_oh(DllCharacteristics=0xC160)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["dll_characteristics_flags"] == [
+ "HIGH_ENTROPY_VA",
+ "DYNAMIC_BASE",
+ "NX_COMPAT",
+ "GUARD_CF",
+ "TERMINAL_SERVER_AWARE",
+ ]
+
+ def test_ordering_deterministic_across_runs(self):
+ oh = _full_oh(DllCharacteristics=0x4160)
+ pe = _FakePE(optional_header=oh)
+ results = []
+ for _ in range(20):
+ _, out = _parse_optional_header(pe)
+ results.append(out["dll_characteristics_flags"])
+ assert all(r == results[0] for r in results)
+
+
+# =================================================================
+# _parse_header
+# =================================================================
+
+class TestParseHeader:
+
+ def test_full_header_extraction(self):
+ oh = _full_oh()
+ fh = _full_fh()
+ pe = _FakePE(optional_header=oh, file_header=fh)
+ out = _parse_header(pe, oh)
+ assert out["entry_point"] == 0x1000
+ assert out["image_base"] == 0x140000000
+ assert out["subsystem"] == 3
+ assert out["subsystem_name"] == "WINDOWS_CUI"
+ assert out["timestamp"] == 1700000000
+ assert out["machine"] == 0x8664
+ assert out["characteristics"] == 0x22
+
+ def test_subsystem_name_for_all_known_values(self):
+ for subsystem_id, expected_name in SUBSYSTEM_NAMES.items():
+ oh = _full_oh(Subsystem=subsystem_id)
+ fh = _full_fh()
+ pe = _FakePE(optional_header=oh, file_header=fh)
+ out = _parse_header(pe, oh)
+ assert out["subsystem"] == subsystem_id
+ assert out["subsystem_name"] == expected_name
+
+ def test_subsystem_name_none_for_unknown_value(self):
+ oh = _full_oh(Subsystem=99)
+ fh = _full_fh()
+ pe = _FakePE(optional_header=oh, file_header=fh)
+ out = _parse_header(pe, oh)
+ assert out["subsystem"] == 99
+ assert out["subsystem_name"] is None
+
+ def test_missing_optional_header_uses_zeros(self):
+ fh = _full_fh()
+ pe = _FakePE(file_header=fh)
+ out = _parse_header(pe, None)
+ assert out["entry_point"] == 0
+ assert out["image_base"] == 0
+ assert out["subsystem"] == 0
+ # Subsystem 0 is UNKNOWN per the table
+ assert out["subsystem_name"] == "UNKNOWN"
+ assert out["timestamp"] == 1700000000
+
+ def test_missing_file_header_uses_zeros(self):
+ oh = _full_oh()
+ pe = _FakePE(optional_header=oh)
+ out = _parse_header(pe, oh)
+ assert out["entry_point"] == 0x1000
+ assert out["timestamp"] == 0
+ assert out["machine"] == 0
+ assert out["characteristics"] == 0
+
+ def test_both_headers_missing(self):
+ pe = _FakePE()
+ out = _parse_header(pe, None)
+ assert out == {
+ "entry_point": 0,
+ "image_base": 0,
+ "subsystem": 0,
+ "subsystem_name": "UNKNOWN",
+ "timestamp": 0,
+ "machine": 0,
+ "machine_name": "UNKNOWN",
+ "characteristics": 0,
+ }
+
+ def test_returns_expected_key_set(self):
+ oh = _full_oh()
+ fh = _full_fh()
+ pe = _FakePE(optional_header=oh, file_header=fh)
+ out = _parse_header(pe, oh)
+ assert set(out.keys()) == {
+ "entry_point", "image_base", "subsystem", "subsystem_name",
+ "timestamp", "machine", "machine_name", "characteristics",
+ }
+
+# =================================================================
+# Win32VersionValue / Reserved1 fallback
+# =================================================================
+class TestWin32VersionExtraction:
+
+ def test_win32_version_value_falls_back_to_reserved1(self):
+ """Pefile uses Reserved1 for the deprecated Win32VersionValue field."""
+ oh = _FakeOH(Reserved1=42) # only Reserved1 is set
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["win32_version_value"] == 42
+
+
+ def test_win32_version_value_prefers_explicit_field_when_present(self):
+ """If both names are present, the explicit Win32VersionValue wins."""
+ oh = _FakeOH(Win32VersionValue=99, Reserved1=42)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["win32_version_value"] == 99
+
+
+ def test_win32_version_value_none_when_neither_present(self):
+ oh = _FakeOH() # neither Win32VersionValue nor Reserved1 set
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["win32_version_value"] is None
+
+# =================================================================
+# Machine names
+# =================================================================
+class TestMachineNames:
+
+ def test_machine_names_contains_well_known_values(self):
+ from iocx.parsers.pe_constants import MACHINE_NAMES
+ assert MACHINE_NAMES[0x014C] == "I386"
+ assert MACHINE_NAMES[0x8664] == "AMD64"
+ assert MACHINE_NAMES[0xAA64] == "ARM64"
+ assert MACHINE_NAMES[0x0200] == "IA64"
+
+ def test_machine_name_for_unknown_returns_none(self):
+ from iocx.parsers.pe_constants import MACHINE_NAMES
+ assert MACHINE_NAMES.get(0x9999) is None
+
+ def test_machine_zero_is_unknown(self):
+ """0x0000 is IMAGE_FILE_MACHINE_UNKNOWN per spec."""
+ from iocx.parsers.pe_constants import MACHINE_NAMES
+ assert MACHINE_NAMES[0x0000] == "UNKNOWN"
+
+ def test_machine_names_no_duplicate_values(self):
+ from iocx.parsers.pe_constants import MACHINE_NAMES
+ values = list(MACHINE_NAMES.values())
+ assert len(values) == len(set(values))
+
+ def test_machine_name_decoded_in_header(self):
+ oh = _full_oh()
+ fh = _full_fh(Machine=0x8664)
+ pe = _FakePE(optional_header=oh, file_header=fh)
+ out = _parse_header(pe, oh)
+ assert out["machine"] == 0x8664
+ assert out["machine_name"] == "AMD64"
+
+ def test_machine_name_none_for_unknown_machine(self):
+ oh = _full_oh()
+ fh = _full_fh(Machine=0x9999)
+ pe = _FakePE(optional_header=oh, file_header=fh)
+ out = _parse_header(pe, oh)
+ assert out["machine"] == 0x9999
+ assert out["machine_name"] is None
+
+ def test_machine_name_zero_is_unknown(self):
+ """machine=0 explicitly decodes to 'UNKNOWN' per spec."""
+ oh = _full_oh()
+ fh = _full_fh(Machine=0)
+ pe = _FakePE(optional_header=oh, file_header=fh)
+ out = _parse_header(pe, oh)
+ assert out["machine"] == 0
+ assert out["machine_name"] == "UNKNOWN"
+
+# =================================================================
+# JSON safety
+# =================================================================
+
+class TestJsonSafety:
+
+ def test_optional_header_output_serializes(self):
+ oh = _full_oh()
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ json.dumps(out) # must not raise
+
+ def test_optional_header_with_missing_fields_serializes(self):
+ oh = _FakeOH()
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ json.dumps(out)
+
+ def test_header_output_serializes(self):
+ oh = _full_oh()
+ fh = _full_fh()
+ pe = _FakePE(optional_header=oh, file_header=fh)
+ out = _parse_header(pe, oh)
+ json.dumps(out)
+
+ def test_header_with_missing_fields_serializes(self):
+ pe = _FakePE()
+ out = _parse_header(pe, None)
+ json.dumps(out)
+
+ def test_unknown_bits_as_string_serializes_cleanly(self):
+ oh = _full_oh(DllCharacteristics=0x10040)
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ result = json.dumps(out)
+ assert "0x10000" in result
+
+
+# =================================================================
+# Determinism
+# =================================================================
+
+class TestDeterminism:
+
+ def test_repeated_optional_header_parse_identical(self):
+ oh = _full_oh(DllCharacteristics=0x4160)
+ pe = _FakePE(optional_header=oh)
+ results = []
+ for _ in range(20):
+ _, out = _parse_optional_header(pe)
+ results.append(out)
+ for r in results[1:]:
+ assert r == results[0]
+
+ def test_repeated_header_parse_identical(self):
+ oh = _full_oh()
+ fh = _full_fh()
+ pe = _FakePE(optional_header=oh, file_header=fh)
+ results = [_parse_header(pe, oh) for _ in range(20)]
+ for r in results[1:]:
+ assert r == results[0]
+
+ def test_optional_header_with_missing_fields_deterministic(self):
+ oh = _FakeOH()
+ pe = _FakePE(optional_header=oh)
+ results = []
+ for _ in range(20):
+ _, out = _parse_optional_header(pe)
+ results.append(out)
+ for r in results[1:]:
+ assert r == results[0]
+
+
+# =================================================================
+# Conservative field handling
+# =================================================================
+
+class TestConservativeFieldHandling:
+
+ def test_individual_field_missing_does_not_break_others(self):
+ # Construct an OH missing only SizeOfStackReserve; other fields
+ # should populate normally
+ oh = _FakeOH(
+ SectionAlignment=4096,
+ Subsystem=3,
+ DllCharacteristics=0x4140,
+ # SizeOfStackReserve deliberately not set
+ SizeOfStackCommit=0x1000,
+ SizeOfHeapReserve=0x100000,
+ SizeOfHeapCommit=0x1000,
+ )
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["section_alignment"] == 4096
+ assert out["dll_characteristics"] == 0x4140
+ assert out["stack_commit_size"] == 0x1000
+ assert out["heap_reserve_size"] == 0x100000
+ assert out["stack_reserve_size"] is None # missing → None
+
+ def test_partial_field_failures_do_not_propagate(self):
+ # All security-related new fields missing, but stack/heap present
+ oh = _FakeOH(
+ SectionAlignment=4096,
+ SizeOfStackReserve=0x100000,
+ SizeOfStackCommit=0x1000,
+ )
+ pe = _FakePE(optional_header=oh)
+ _, out = _parse_optional_header(pe)
+ assert out["dll_characteristics"] is None
+ assert out["dll_characteristics_flags"] is None
+ assert out["dll_characteristics_unknown_bits"] is None
+ assert out["loader_flags"] is None
+ assert out["stack_reserve_size"] == 0x100000 # populated
diff --git a/tests/unit/parsers/test_pe_parser.py b/tests/unit/parsers/test_pe_parser.py
index 1a2101f..b6ab634 100644
--- a/tests/unit/parsers/test_pe_parser.py
+++ b/tests/unit/parsers/test_pe_parser.py
@@ -361,3 +361,128 @@ class FakePE:
result = _parse_data_directories_raw(FakePE())
assert result == [] # early return path
+
+
+# =================================================================
+# Defensive: guarded get_offset_from_rva
+# =================================================================
+
+class TestGuardedRvaToOffset:
+ """
+ Cover the try/except around pe.get_offset_from_rva in
+ build_resource_structure. A corrupt RVA must produce a -1 sentinel
+ in raw_offset rather than propagating the exception.
+ """
+
+ def _make_pe_with_data_leaf_raising(self, exception_to_raise: Exception):
+ """
+ Build a minimal fake pe whose resource tree contains a single
+ RT_VERSION leaf, where pe.get_offset_from_rva raises the given
+ exception.
+ """
+
+ def _struct_with(offset: int):
+ return type("S", (), {"OffsetToData": offset})()
+
+ # Leaf data entry — points to the corrupt RVA
+ class _FakeStruct:
+ OffsetToData = 0x1100
+ Size = 100
+
+ class _FakeData:
+ struct = _FakeStruct()
+
+ class _FakeLangEntry:
+ id = 0x0409
+ data = _FakeData()
+ # No `directory` attribute — this is a leaf, not a subdirectory
+
+ class _FakeLangDir:
+ entries = [_FakeLangEntry()]
+
+ # Name entry — points to the language directory
+ class _FakeNameEntry:
+ id = 1
+ directory = _FakeLangDir()
+ struct = _struct_with(0x80000020) # high bit set = "is directory"
+
+ class _FakeNameDir:
+ entries = [_FakeNameEntry()]
+
+ # Type entry — points to the name directory
+ class _FakeTypeEntry:
+ id = 16 # RT_VERSION
+ directory = _FakeNameDir()
+ struct = _struct_with(0x80000010)
+
+ class _FakeRootDir:
+ entries = [_FakeTypeEntry()]
+
+ class _FakeDataDir:
+ VirtualAddress = 0x1000
+
+ class _FakeOptHdr:
+ DATA_DIRECTORY = [None, None, _FakeDataDir()]
+
+ class _FakePE:
+ OPTIONAL_HEADER = _FakeOptHdr()
+ DIRECTORY_ENTRY_RESOURCE = _FakeRootDir()
+
+ def get_offset_from_rva(self, rva):
+ raise exception_to_raise
+
+ return _FakePE()
+
+ def test_pefile_format_error_yields_minus_one(self):
+ import pefile
+ from iocx.parsers.pe_resources import build_resource_structure
+
+ pe = self._make_pe_with_data_leaf_raising(
+ pefile.PEFormatError("simulated corrupt RVA")
+ )
+ result = build_resource_structure(pe)
+
+ assert result is not None
+ # Walk down to the leaf data entry
+ root = result["root"]
+ type_dir = root["entries"][0]["directory"]
+ name_dir = type_dir["entries"][0]["directory"]
+ leaf = name_dir["entries"][0]
+
+ assert leaf["is_directory"] is False
+ assert leaf["raw_offset"] == -1
+ # The other fields should still be populated normally
+ assert leaf["data_rva"] == 0x1100
+ assert leaf["data_size"] == 100
+
+ def test_attribute_error_yields_minus_one(self):
+ from iocx.parsers.pe_resources import build_resource_structure
+
+ pe = self._make_pe_with_data_leaf_raising(
+ AttributeError("simulated missing attribute")
+ )
+ result = build_resource_structure(pe)
+
+ root = result["root"]
+ type_dir = root["entries"][0]["directory"]
+ name_dir = type_dir["entries"][0]["directory"]
+ leaf = name_dir["entries"][0]
+
+ assert leaf["raw_offset"] == -1
+ assert leaf["data_rva"] == 0x1100
+ assert leaf["data_size"] == 100
+
+ def test_non_caught_exception_propagates(self):
+ """
+ Sanity check that the except clause is narrow — a RuntimeError
+ (not in the tuple) should still propagate. This protects against
+ someone widening the except to `Exception` without thinking.
+ """
+ import pytest as _pytest
+ from iocx.parsers.pe_resources import build_resource_structure
+
+ pe = self._make_pe_with_data_leaf_raising(
+ RuntimeError("not a caught exception type")
+ )
+ with _pytest.raises(RuntimeError):
+ build_resource_structure(pe)
diff --git a/tests/unit/parsers/test_pe_parser_extended.py b/tests/unit/parsers/test_pe_parser_extended.py
index 7dab37a..c7190d3 100644
--- a/tests/unit/parsers/test_pe_parser_extended.py
+++ b/tests/unit/parsers/test_pe_parser_extended.py
@@ -214,7 +214,13 @@ def test_resource_zero_size(monkeypatch):
monkeypatch.setattr("iocx.parsers.pe_parser.pefile.PE", lambda *a, **k: pe)
_, metadata = parse_pe("dummy.exe")
- assert metadata["resources"] == []
+ # Invalid resources are no longer dropped; they're emitted with errors populated.
+ assert len(metadata["resources"]) == 1
+ entry = metadata["resources"][0]
+ assert entry["errors"] == ["size_invalid"]
+ assert entry["size"] is None
+ assert entry["entropy"] is None
+ assert entry["language"] == 1033
def test_resource_out_of_bounds(monkeypatch):
@@ -224,7 +230,10 @@ def test_resource_out_of_bounds(monkeypatch):
monkeypatch.setattr("iocx.parsers.pe_parser.pefile.PE", lambda *a, **k: pe)
_, metadata = parse_pe("dummy.exe")
- assert metadata["resources"] == []
+ assert len(metadata["resources"]) == 1
+ entry = metadata["resources"][0]
+ assert entry["errors"] == ["data_out_of_bounds"]
+ assert entry["entropy"] is None
def test_resource_missing_directory_on_type(monkeypatch):
@@ -266,7 +275,10 @@ def test_resource_negative_offset(monkeypatch):
monkeypatch.setattr("iocx.parsers.pe_parser.pefile.PE", lambda *a, **k: pe)
_, metadata = parse_pe("dummy.exe")
- assert metadata["resources"] == []
+ assert len(metadata["resources"]) == 1
+ entry = metadata["resources"][0]
+ assert entry["errors"] == ["rva_invalid"]
+ assert entry["entropy"] is None
def test_resource_mixed_valid_and_invalid(monkeypatch):
@@ -981,14 +993,13 @@ def get_memory_mapped_image(self):
from iocx.parsers.pe_parser import _decode_langid
def test_decode_langid_non_int():
- assert _decode_langid("409") == "unknown"
- assert _decode_langid(None) == "unknown"
+ # Non-integer input cannot be decoded; returns None.
+ assert _decode_langid("409") is None
def test_decode_langid_too_small():
- # < 0x0400 should always be unknown
- assert _decode_langid(0x0000) == "unknown"
- assert _decode_langid(0x003F) == "unknown"
+ # LANGID 0 is LANG_NEUTRAL with no primary mapping; returns None.
+ assert _decode_langid(0x0000) is None
def test_decode_langid_valid_with_default_region():
@@ -1002,10 +1013,24 @@ def test_decode_langid_valid_without_region():
def test_decode_langid_unknown_primary():
- # Primary language 0x999 is not in PRIMARY_LANG
- assert _decode_langid(0x0999) == "unknown"
+ # Primary language 0x199 is not in PRIMARY_LANG; returns None.
+ # Note: previous test used 0x0999 which decomposed to primary=0x199.
+ # The new decoder reaches this state cleanly without the < 0x0400 guard.
+ assert _decode_langid(0x0999) is None
def test_decode_langid_region_branch():
# 0x0809 = English (United Kingdom) → explicit SUBLANG region
assert _decode_langid(0x0809) == "en-GB"
+
+
+def test_decode_langid_arabic_neutral():
+ # LANGID 1 (Arabic, sublang neutral) was previously rejected by the
+ # < 0x0400 guard. The new decoder correctly returns the primary
+ # language name.
+ assert _decode_langid(0x0001) == "ar"
+
+
+def test_decode_langid_en_us():
+ # Sanity: en-US still decodes correctly (regression guard).
+ assert _decode_langid(0x0409) == "en-US"
diff --git a/tests/unit/parsers/test_pe_version_info.py b/tests/unit/parsers/test_pe_version_info.py
new file mode 100644
index 0000000..6745f9a
--- /dev/null
+++ b/tests/unit/parsers/test_pe_version_info.py
@@ -0,0 +1,1357 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Unit tests for iocx.parsers.parser_version_info.
+
+Strategy:
+- Decoder tests build VS_VERSIONINFO byte buffers directly via helpers.
+ This isolates the decoder from pefile and gives precise control over
+ every malformation we want to exercise.
+- Locator tests use a minimal duck-typed fake-pe object so we can construct
+ resource trees with arbitrary RT_VERSION leaf populations without needing
+ real PE binaries on disk.
+- Determinism tests assert byte-for-byte stable output across repeated runs
+ on the same input, which is the headline property of the module.
+"""
+
+from __future__ import annotations
+
+import struct
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional, Tuple
+
+import pytest
+
+from iocx.parsers.pe_version_info import (
+ build_version_info,
+ _align4,
+ _decode_string_file_info,
+ _decode_var_file_info,
+ _decode_vs_versioninfo,
+ _find_first_version_leaf,
+ _read_utf16_sz,
+ _u16,
+ _VS_FFI_SIGNATURE,
+ _VS_FFI_STRUCT_VERSION,
+ _VS_VERSION_INFO_KEY,
+ RT_VERSION,
+)
+
+
+# =================================================================
+# Byte-level builders for VS_VERSIONINFO test fixtures
+# =================================================================
+
+def _utf16_sz(s: str) -> bytes:
+ """Encode a string as NUL-terminated UTF-16LE."""
+ return s.encode("utf-16-le") + b"\x00\x00"
+
+
+def _pad4(buf: bytes) -> bytes:
+ """Pad a buffer to a 4-byte boundary."""
+ pad = (-len(buf)) & 3
+ return buf + b"\x00" * pad
+
+
+def _build_ffi(
+ signature: int = _VS_FFI_SIGNATURE,
+ struct_version: int = _VS_FFI_STRUCT_VERSION,
+ file_version_ms: int = 0x00010000,
+ file_version_ls: int = 0x00020003,
+ product_version_ms: int = 0x00010000,
+ product_version_ls: int = 0x00020003,
+ file_flags_mask: int = 0,
+ file_flags: int = 0,
+ file_os: int = 0x00040004,
+ file_type: int = 1,
+ file_subtype: int = 0,
+ file_date_ms: int = 0,
+ file_date_ls: int = 0,
+) -> bytes:
+ """Build a 52-byte VS_FIXEDFILEINFO blob."""
+ return struct.pack(
+ "<13I",
+ signature, struct_version,
+ file_version_ms, file_version_ls,
+ product_version_ms, product_version_ls,
+ file_flags_mask, file_flags,
+ file_os, file_type, file_subtype,
+ file_date_ms, file_date_ls,
+ )
+
+
+def _build_string(key: str, value: str) -> bytes:
+ """
+ Build a single String entry:
+ wLength, wValueLength (chars including NUL), wType=1, szKey, [pad], Value
+ """
+ key_bytes = _utf16_sz(key)
+ value_bytes = _utf16_sz(value)
+ value_chars = len(value_bytes) // 2 # wValueLength is in WORDs for text
+ header_and_key = struct.pack(" bytes:
+ """Build a StringTable containing the given String entries."""
+ key_bytes = _utf16_sz(lang_codepage)
+ header_and_key = struct.pack(" bytes:
+ """Build a StringFileInfo child containing the given StringTables."""
+ key_bytes = _utf16_sz("StringFileInfo")
+ header_and_key = struct.pack(" bytes:
+ """Build a single Var entry containing the given (lang, codepage) pairs."""
+ key_bytes = _utf16_sz(key)
+ payload = b"".join(struct.pack(" bytes:
+ """Build a VarFileInfo child containing the given Vars."""
+ key_bytes = _utf16_sz("VarFileInfo")
+ header_and_key = struct.pack(" bytes:
+ """Build a complete VS_VERSIONINFO blob with optional FFI and children."""
+ if ffi is None:
+ ffi = b""
+ sz_key_bytes = _utf16_sz(sz_key)
+ value_length = w_value_length_override if w_value_length_override is not None else len(ffi)
+ header_and_key = struct.pack(" bytes:
+ """
+ Build a VarFileInfo child containing the given Vars.
+
+ Each entry in vars_ is (var_key, [(lang, codepage), ...]).
+ """
+ key_bytes = _utf16_sz("VarFileInfo")
+ header_and_key = struct.pack(" "_FakeDataEntry":
+ return cls(struct=_FakeDataStruct(offset_to_data, size))
+
+
+@dataclass
+class _FakeLeafEntry:
+ """A language-level entry that wraps a data leaf."""
+ id: Optional[int]
+ data: _FakeDataEntry
+
+ def __init__(self, lang_id: Optional[int], offset_to_data: int, size: int):
+ self.id = lang_id
+ self.data = _FakeDataEntry.make(offset_to_data, size)
+
+
+@dataclass
+class _FakeDirectory:
+ entries: list = field(default_factory=list)
+
+ def __init__(self, entries):
+ self.entries = entries
+
+
+@dataclass
+class _FakeNameEntry:
+ """A name-level entry whose .directory holds language entries."""
+ id: Optional[int]
+ directory: _FakeDirectory
+
+ def __init__(self, name_id: Optional[int], lang_entries):
+ self.id = name_id
+ self.directory = _FakeDirectory(lang_entries)
+
+
+@dataclass
+class _FakeTypeEntry:
+ """A type-level entry whose .directory holds name entries."""
+ id: int
+ directory: _FakeDirectory
+
+ def __init__(self, type_id: int, name_entries):
+ self.id = type_id
+ self.directory = _FakeDirectory(name_entries)
+
+
+class _FakePE:
+ """Minimal duck-typed pe object that exposes only what the parser uses."""
+ def __init__(self, root_entries, raw_data_by_rva: Dict[int, bytes],
+ raise_on_get_data: bool = False):
+ self.DIRECTORY_ENTRY_RESOURCE = _FakeDirectory(root_entries)
+ self._raw = raw_data_by_rva
+ self._raise = raise_on_get_data
+
+ def get_data(self, rva: int, size: int) -> bytes:
+ if self._raise:
+ raise RuntimeError("simulated read failure")
+ if rva not in self._raw:
+ raise ValueError(f"no fixture data at rva {rva}")
+ return self._raw[rva][:size]
+
+
+# =================================================================
+# Low-level helper tests
+# =================================================================
+
+class TestAlign4:
+ @pytest.mark.parametrize("n,expected", [
+ (0, 0), (1, 4), (2, 4), (3, 4), (4, 4),
+ (5, 8), (7, 8), (8, 8), (9, 12),
+ (100, 100), (101, 104),
+ ])
+ def test_aligns_up_to_multiple_of_4(self, n, expected):
+ assert _align4(n) == expected
+
+
+class TestU16:
+ def test_little_endian_decode(self):
+ assert _u16(b"\x01\x00", 0) == 1
+ assert _u16(b"\x00\x01", 0) == 256
+ assert _u16(b"\xFF\xFF", 0) == 0xFFFF
+
+ def test_offset_into_buffer(self):
+ buf = b"\xAA\xBB\xCC\xDD"
+ assert _u16(buf, 0) == 0xBBAA
+ assert _u16(buf, 2) == 0xDDCC
+
+ def test_raises_struct_error_on_overrun(self):
+ with pytest.raises(struct.error):
+ _u16(b"\x01", 0)
+
+
+class TestReadUtf16Sz:
+ def test_reads_simple_ascii_string(self):
+ buf = _utf16_sz("Hello")
+ s, consumed = _read_utf16_sz(buf, 0)
+ assert s == "Hello"
+ assert consumed == len(buf)
+
+ def test_reads_empty_string(self):
+ buf = b"\x00\x00"
+ s, consumed = _read_utf16_sz(buf, 0)
+ assert s == ""
+ assert consumed == 2
+
+ def test_reads_at_offset(self):
+ prefix = b"\xFF\xFF\xFF\xFF"
+ buf = prefix + _utf16_sz("World")
+ s, consumed = _read_utf16_sz(buf, len(prefix))
+ assert s == "World"
+ assert consumed == len(buf) - len(prefix)
+
+ def test_reads_unicode(self):
+ buf = _utf16_sz("café")
+ s, consumed = _read_utf16_sz(buf, 0)
+ assert s == "café"
+ assert consumed == len(buf)
+
+ def test_handles_unterminated_string_at_eob(self):
+ # No NUL terminator — should consume to end of buffer
+ buf = "VS_VERSION_INFO".encode("utf-16-le")
+ s, consumed = _read_utf16_sz(buf, 0)
+ # The function increments by 2 for the assumed NUL even if absent
+ assert s == "VS_VERSION_INFO"
+ assert consumed == len(buf) + 2
+
+ def test_handles_malformed_utf16_with_replacement(self):
+ # Odd-byte sequence that decodes with replacement chars
+ buf = b"\x41\x00\xFF\xD8\x00\x00" # 'A' + lone high surrogate + NUL
+ s, consumed = _read_utf16_sz(buf, 0)
+ assert "A" in s
+ assert "\uFFFD" in s
+ assert consumed == 6
+
+
+# =================================================================
+# Decoder tests — top-level header
+# =================================================================
+
+class TestDecodeHeader:
+ def test_empty_buffer_too_short(self):
+ out = _decode_vs_versioninfo(b"")
+ assert out["decoded"] is False
+ assert "too_short" in out["errors"]
+
+ def test_5_byte_buffer_too_short(self):
+ out = _decode_vs_versioninfo(b"\x00" * 5)
+ assert out["decoded"] is False
+ assert "too_short" in out["errors"]
+
+ def test_minimal_valid_header_decodes(self):
+ buf = _build_vs_versioninfo()
+ out = _decode_vs_versioninfo(buf)
+ assert out["decoded"] is True
+ assert out["header_ok"] is True
+ assert out["length_consistent"] is True
+
+ def test_szkey_mismatch_flags_header_not_ok(self):
+ buf = _build_vs_versioninfo(sz_key="VS_VERSION_BAD")
+ out = _decode_vs_versioninfo(buf)
+ assert out["decoded"] is True
+ assert out["header_ok"] is False
+ assert out["length_consistent"] is True
+
+ def test_wlength_too_large_flags_inconsistent(self):
+ buf = _build_vs_versioninfo(w_length_override=0xFFFF)
+ out = _decode_vs_versioninfo(buf)
+ assert out["length_consistent"] is False
+
+ def test_wlength_too_small_flags_inconsistent(self):
+ buf = _build_vs_versioninfo(w_length_override=4)
+ out = _decode_vs_versioninfo(buf)
+ assert out["length_consistent"] is False
+
+ def test_w_type_recorded(self):
+ buf = _build_vs_versioninfo(w_type=0)
+ out = _decode_vs_versioninfo(buf)
+ assert out["w_type"] == 0
+
+ buf = _build_vs_versioninfo(w_type=1)
+ out = _decode_vs_versioninfo(buf)
+ assert out["w_type"] == 1
+
+
+# =================================================================
+# Decoder tests — VS_FIXEDFILEINFO
+# =================================================================
+
+class TestDecodeFixedFileInfo:
+ def test_valid_ffi_decoded(self):
+ ffi = _build_ffi()
+ buf = _build_vs_versioninfo(ffi=ffi)
+ out = _decode_vs_versioninfo(buf)
+
+ assert out["fixed_file_info"] is not None
+ ffi_out = out["fixed_file_info"]
+ assert ffi_out["signature"] == _VS_FFI_SIGNATURE
+ assert ffi_out["signature_ok"] is True
+ assert ffi_out["struct_version"] == _VS_FFI_STRUCT_VERSION
+ assert ffi_out["struct_version_ok"] is True
+ assert ffi_out["file_version"] == (0x00010000, 0x00020003)
+ assert ffi_out["product_version"] == (0x00010000, 0x00020003)
+
+ def test_bad_signature_flagged(self):
+ ffi = _build_ffi(signature=0xDEADBEEF)
+ buf = _build_vs_versioninfo(ffi=ffi)
+ out = _decode_vs_versioninfo(buf)
+ assert out["fixed_file_info"]["signature"] == 0xDEADBEEF
+ assert out["fixed_file_info"]["signature_ok"] is False
+ assert out["fixed_file_info"]["struct_version_ok"] is True
+
+ def test_bad_struct_version_flagged(self):
+ ffi = _build_ffi(struct_version=0x00020000)
+ buf = _build_vs_versioninfo(ffi=ffi)
+ out = _decode_vs_versioninfo(buf)
+ assert out["fixed_file_info"]["struct_version_ok"] is False
+ assert out["fixed_file_info"]["signature_ok"] is True
+
+ def test_absent_ffi_returns_none(self):
+ buf = _build_vs_versioninfo(ffi=b"", w_value_length_override=0)
+ out = _decode_vs_versioninfo(buf)
+ assert out["fixed_file_info"] is None
+ assert "fixed_file_info_truncated" not in out["errors"]
+
+ def test_truncated_ffi_flagged(self):
+ # wValueLength claims 30 (< 52) but non-zero
+ buf = _build_vs_versioninfo(
+ ffi=b"\x00" * 30,
+ w_value_length_override=30,
+ )
+ out = _decode_vs_versioninfo(buf)
+ assert "fixed_file_info_truncated" in out["errors"]
+ assert out["fixed_file_info"] is None
+
+ def test_ffi_extending_past_buffer_flagged(self):
+ # wValueLength claims 100 but buffer only has space for ~52
+ ffi = _build_ffi()
+ buf = _build_vs_versioninfo(ffi=ffi, w_value_length_override=0xFF00)
+ out = _decode_vs_versioninfo(buf)
+ # Either truncated or unpack error — but FFI should not be decoded
+ assert out["fixed_file_info"] is None
+
+ def test_all_ffi_fields_decoded(self):
+ ffi = _build_ffi(
+ file_flags_mask=0xFF,
+ file_flags=0x0F,
+ file_os=0x00040004,
+ file_type=2,
+ file_subtype=5,
+ file_date_ms=0xCAFEBABE,
+ file_date_ls=0xDEADBEEF,
+ )
+ buf = _build_vs_versioninfo(ffi=ffi)
+ out = _decode_vs_versioninfo(buf)
+ ffi_out = out["fixed_file_info"]
+ assert ffi_out["file_flags_mask"] == 0xFF
+ assert ffi_out["file_flags"] == 0x0F
+ assert ffi_out["file_os"] == 0x00040004
+ assert ffi_out["file_type"] == 2
+ assert ffi_out["file_subtype"] == 5
+ assert ffi_out["file_date"] == (0xCAFEBABE, 0xDEADBEEF)
+
+
+# =================================================================
+# Decoder tests — StringFileInfo
+# =================================================================
+
+class TestDecodeStringFileInfo:
+ def test_single_string_table_decoded(self):
+ sfi = _build_string_file_info([
+ ("040904B0", {"CompanyName": "MalX Labs", "ProductName": "iocx"}),
+ ])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), string_file_info=sfi)
+ out = _decode_vs_versioninfo(buf)
+
+ assert len(out["string_file_info"]) == 1
+ sfi_out = out["string_file_info"][0]
+ assert len(sfi_out["tables"]) == 1
+ table = sfi_out["tables"][0]
+ assert table["lang_codepage"] == "040904B0"
+ assert table["errors"] == []
+ assert table["strings"]["CompanyName"] == "MalX Labs"
+ assert table["strings"]["ProductName"] == "iocx"
+
+ def test_bad_lang_codepage_key_flagged(self):
+ sfi = _build_string_file_info([
+ ("ENGLISHX", {"CompanyName": "MalX Labs"}),
+ ])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), string_file_info=sfi)
+ out = _decode_vs_versioninfo(buf)
+
+ table = out["string_file_info"][0]["tables"][0]
+ assert "lang_codepage_key" in table["errors"]
+ # But strings should still decode
+ assert table["strings"]["CompanyName"] == "MalX Labs"
+
+ def test_lang_codepage_wrong_length_flagged(self):
+ sfi = _build_string_file_info([
+ ("0409", {"CompanyName": "X"}), # only 4 hex chars
+ ])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), string_file_info=sfi)
+ out = _decode_vs_versioninfo(buf)
+ table = out["string_file_info"][0]["tables"][0]
+ assert "lang_codepage_key" in table["errors"]
+
+ def test_lang_codepage_lowercase_hex_accepted(self):
+ sfi = _build_string_file_info([
+ ("040904b0", {"CompanyName": "X"}),
+ ])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), string_file_info=sfi)
+ out = _decode_vs_versioninfo(buf)
+ table = out["string_file_info"][0]["tables"][0]
+ assert "lang_codepage_key" not in table["errors"]
+
+ def test_multiple_string_tables_decoded(self):
+ sfi = _build_string_file_info([
+ ("040904B0", {"CompanyName": "A"}),
+ ("080904B0", {"CompanyName": "B"}),
+ ])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), string_file_info=sfi)
+ out = _decode_vs_versioninfo(buf)
+ tables = out["string_file_info"][0]["tables"]
+ assert len(tables) == 2
+ assert tables[0]["strings"]["CompanyName"] == "A"
+ assert tables[1]["strings"]["CompanyName"] == "B"
+
+ def test_string_values_bounded_at_512_chars(self):
+ long_value = "X" * 1000
+ sfi = _build_string_file_info([
+ ("040904B0", {"Long": long_value}),
+ ])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), string_file_info=sfi)
+ out = _decode_vs_versioninfo(buf)
+ table = out["string_file_info"][0]["tables"][0]
+ assert len(table["strings"]["Long"]) == 512
+
+ def test_string_keys_bounded_at_128_chars(self):
+ long_key = "K" * 200
+ sfi = _build_string_file_info([
+ ("040904B0", {long_key: "value"}),
+ ])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), string_file_info=sfi)
+ out = _decode_vs_versioninfo(buf)
+ table = out["string_file_info"][0]["tables"][0]
+ stored_key = next(iter(table["strings"].keys()))
+ assert len(stored_key) == 128
+
+ def test_empty_string_value(self):
+ sfi = _build_string_file_info([
+ ("040904B0", {"EmptyValue": ""}),
+ ])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), string_file_info=sfi)
+ out = _decode_vs_versioninfo(buf)
+ table = out["string_file_info"][0]["tables"][0]
+ assert table["strings"]["EmptyValue"] == ""
+
+
+# =================================================================
+# Decoder tests — VarFileInfo
+# =================================================================
+
+class TestDecodeVarFileInfo:
+ def test_single_translation_decoded(self):
+ vfi = _build_var_file_info([("Translation", [(0x0409, 0x04B0)])])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), var_file_info=vfi)
+ out = _decode_vs_versioninfo(buf)
+
+ assert len(out["var_file_info"]) == 1
+ vfi_out = out["var_file_info"][0]
+ assert vfi_out["errors"] == []
+ assert len(vfi_out["vars"]) == 1
+ var = vfi_out["vars"][0]
+ assert var["key"] == "Translation"
+ assert var["translations"] == [{"lang": 0x0409, "codepage": 0x04B0}]
+
+ def test_multiple_translations_decoded(self):
+ translations = [(0x0409, 0x04B0), (0x0809, 0x04B0), (0x0407, 0x04E4)]
+ vfi = _build_var_file_info([("Translation", translations)])
+ buf = _build_vs_versioninfo(ffi=_build_ffi(), var_file_info=vfi)
+ out = _decode_vs_versioninfo(buf)
+
+ var = out["var_file_info"][0]["vars"][0]
+ assert len(var["translations"]) == 3
+ assert var["translations"][0] == {"lang": 0x0409, "codepage": 0x04B0}
+ assert var["translations"][2] == {"lang": 0x0407, "codepage": 0x04E4}
+
+ def test_misaligned_translation_flagged(self):
+ # Build a Var manually with wValueLength=6 (not DWORD-aligned)
+ key_bytes = _utf16_sz("Translation")
+ payload = b"\x09\x04\xB0\x04\xCC\xCC" # 6 bytes
+ header_and_key = struct.pack("= 2:
+ raise struct.error("simulated string header failure")
+ return real_u16(buf_, off)
+
+ monkeypatch.setattr(pvi, "_u16", fake_u16)
+
+ result = pvi._decode_string_file_info(inner, 0, len(inner))
+
+ # The StringTable should have been entered (call 1 succeeded) but the
+ # String entry header read failed (call 2 raised).
+ assert len(result["tables"]) == 1
+ assert "string_header" in result["tables"][0]["errors"]
+
+ # ---- Line 269-270: string_length malformation ----
+
+ def test_decode_string_file_info_flags_string_length_too_small(self):
+ """
+ Cover: s_len < 6 branch in String entry parsing.
+
+ Build a StringTable whose body contains a String header claiming
+ wLength = 2, which is less than the 6-byte minimum.
+ """
+ # StringTable header: wLength placeholder, wValueLength=0, wType=1
+ # Followed by key "040904B0" NUL-terminated UTF-16LE, then a malformed
+ # String header with wLength = 2.
+ st_key = _utf16_sz("040904B0")
+ st_header = struct.pack(" Dict[str, Any]:
+ return {"size_of_image": size_of_image}
+
+
+def _make_entry(
+ index: int = 0,
+ is_ordinal: bool = False,
+ ordinal: Optional[int] = None,
+ hint: Optional[int] = 0x10,
+ name: Optional[str] = "Foo",
+ name_rva: Optional[int] = 0x6000,
+ name_valid: bool = True,
+ iat_value: Optional[int] = None,
+ errors: Optional[List[str]] = None,
+) -> Dict[str, Any]:
+ return {
+ "index": index,
+ "is_ordinal": is_ordinal,
+ "ordinal": ordinal,
+ "hint": hint,
+ "name": name,
+ "name_rva": name_rva,
+ "name_valid": name_valid,
+ "iat_value": iat_value,
+ "errors": errors or [],
+ }
+
+
+def _make_descriptor(
+ index: int = 0,
+ attributes: int = 0x01,
+ attributes_v1: bool = True,
+ dll_name_rva: int = 0x2000,
+ dll_name: Optional[str] = "test.dll",
+ dll_name_valid: bool = True,
+ module_handle_rva: int = 0x3000,
+ iat_rva: int = 0x4000,
+ int_rva: int = 0x5000,
+ bound_iat_rva: int = 0,
+ unload_iat_rva: int = 0,
+ timestamp: int = 0,
+ is_bound: bool = False,
+ imports: Optional[List[Dict[str, Any]]] = None,
+ errors: Optional[List[str]] = None,
+) -> Dict[str, Any]:
+ return {
+ "index": index,
+ "attributes": attributes,
+ "attributes_v1": attributes_v1,
+ "dll_name_rva": dll_name_rva,
+ "dll_name": dll_name,
+ "dll_name_valid": dll_name_valid,
+ "module_handle_rva": module_handle_rva,
+ "iat_rva": iat_rva,
+ "int_rva": int_rva,
+ "bound_iat_rva": bound_iat_rva,
+ "unload_iat_rva": unload_iat_rva,
+ "timestamp": timestamp,
+ "is_bound": is_bound,
+ "imports": imports or [],
+ "errors": errors or [],
+ }
+
+
+def _make_di(
+ rva: int = 0x1000,
+ size: int = 64,
+ is_64bit: bool = True,
+ descriptors: Optional[List[Dict[str, Any]]] = None,
+ truncations: Optional[List[str]] = None,
+ errors: Optional[List[str]] = None,
+) -> Dict[str, Any]:
+ return {
+ "rva": rva,
+ "size": size,
+ "is_64bit": is_64bit,
+ "descriptors": descriptors or [],
+ "truncations": truncations or [],
+ "errors": errors or [],
+ }
+
+
+def _codes(issues) -> List:
+ return [i["issue"] for i in issues]
+
+
+def _details_for(issues, code) -> List[Dict[str, Any]]:
+ return [i["details"] for i in issues if i["issue"] == code]
+
+
+# =================================================================
+# Absence
+# =================================================================
+
+class TestAbsence:
+
+ def test_no_delay_import_struct_returns_no_issues(self):
+ assert validate_delay_imports({}, _make_analysis()) == []
+
+ def test_explicit_none_returns_no_issues(self):
+ assert validate_delay_imports(
+ {"delay_import_struct": None}, _make_analysis(),
+ ) == []
+
+
+# =================================================================
+# Top-level decode short-circuit
+# =================================================================
+
+class TestTopLevelDecodeFailure:
+
+ def test_errors_present_emits_invalid_header_and_returns_early(self):
+ di = _make_di(errors=["header_read_failed"])
+ di["truncations"] = ["delay_import_descriptor_truncated"]
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ codes = _codes(issues)
+ assert ReasonCodes.DELAY_IMPORT_DIRECTORY_INVALID_HEADER in codes
+ # Should not emit truncation issues due to early return
+ assert ReasonCodes.DELAY_IMPORT_TABLE_TRUNCATED not in codes
+ details = _details_for(issues,
+ ReasonCodes.DELAY_IMPORT_DIRECTORY_INVALID_HEADER)
+ assert details[0]["reason"] == "top_level_decode"
+
+
+# =================================================================
+# Placement
+# =================================================================
+
+class TestPlacement:
+
+ def test_in_bounds_no_issue(self):
+ di = _make_di(rva=0x1000, size=64)
+ issues = validate_delay_imports(
+ {"delay_import_struct": di},
+ _make_analysis(size_of_image=0x100000),
+ )
+ assert ReasonCodes.DELAY_IMPORT_DIRECTORY_OUT_OF_BOUNDS not in _codes(issues)
+
+ def test_extends_past_image_flagged(self):
+ di = _make_di(rva=0xFFFF0, size=0x200)
+ issues = validate_delay_imports(
+ {"delay_import_struct": di},
+ _make_analysis(size_of_image=0x100000),
+ )
+ details = _details_for(issues,
+ ReasonCodes.DELAY_IMPORT_DIRECTORY_OUT_OF_BOUNDS)
+ assert len(details) == 1
+ assert details[0]["rva"] == 0xFFFF0
+
+ def test_silent_when_size_of_image_missing(self):
+ di = _make_di(rva=0xFFFF0, size=0x200)
+ issues = validate_delay_imports(
+ {"delay_import_struct": di},
+ _make_analysis(size_of_image=None),
+ )
+ assert ReasonCodes.DELAY_IMPORT_DIRECTORY_OUT_OF_BOUNDS not in _codes(issues)
+
+
+# =================================================================
+# Truncations
+# =================================================================
+
+class TestTruncations:
+
+ def test_no_truncations_no_issues(self):
+ di = _make_di(truncations=[])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ assert ReasonCodes.DELAY_IMPORT_TABLE_TRUNCATED not in _codes(issues)
+
+ def test_single_truncation_emits_one_issue(self):
+ di = _make_di(truncations=["delay_import_descriptor_truncated"])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_TABLE_TRUNCATED)
+ assert len(details) == 1
+ assert details[0]["table"] == "delay_import_descriptor_truncated"
+
+ def test_multiple_truncations_emit_separate_issues(self):
+ di = _make_di(truncations=[
+ "delay_import_descriptor_truncated",
+ "int_truncated",
+ "iat_truncated",
+ ])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_TABLE_TRUNCATED)
+ assert len(details) == 3
+
+
+# =================================================================
+# Descriptor validation
+# =================================================================
+
+class TestDescriptorValidation:
+
+ def test_clean_descriptor_no_issues(self):
+ d = _make_descriptor()
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ assert issues == []
+
+ def test_v0_attributes_flagged(self):
+ d = _make_descriptor(attributes=0, attributes_v1=False)
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues,
+ ReasonCodes.DELAY_IMPORT_ATTRIBUTES_LEGACY_VA_MODE)
+ assert len(details) == 1
+ assert details[0]["index"] == 0
+ assert details[0]["attributes"] == 0
+
+ def test_dll_name_rva_zero_flagged(self):
+ d = _make_descriptor(errors=["dll_name_rva_zero"])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_DLL_NAME_INVALID)
+ assert len(details) == 1
+ assert details[0]["reason"] == "dll_name_rva_zero"
+
+ def test_dll_name_priority_resolution(self):
+ """dll_name_rva_zero wins over read_failed when both are present."""
+ d = _make_descriptor(errors=["read_failed", "dll_name_rva_zero"])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_DLL_NAME_INVALID)
+ assert len(details) == 1
+ assert details[0]["reason"] == "dll_name_rva_zero"
+
+ def test_dll_name_not_printable_flagged(self):
+ d = _make_descriptor(
+ dll_name="kernel\x0132.dll",
+ dll_name_valid=False,
+ errors=["dll_name_not_printable"],
+ )
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_DLL_NAME_INVALID)
+ assert details[0]["reason"] == "dll_name_not_printable"
+ assert details[0]["dll_name"] == "kernel\x0132.dll"
+
+ def test_int_rva_zero_flagged(self):
+ d = _make_descriptor(errors=["int_rva_zero"], int_rva=0)
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_DESCRIPTOR_INVALID)
+ # Find the INT-specific one
+ int_details = [d for d in details if d["table"] == "int"]
+ assert len(int_details) == 1
+ assert int_details[0]["reason"] == "int_rva_zero"
+
+ def test_iat_rva_zero_flagged(self):
+ d = _make_descriptor(errors=["iat_rva_zero"], iat_rva=0)
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_DESCRIPTOR_INVALID)
+ iat_details = [d for d in details if d["table"] == "iat"]
+ assert len(iat_details) == 1
+ assert iat_details[0]["reason"] == "iat_rva_zero"
+
+ def test_int_iat_mismatch_flagged(self):
+ d = _make_descriptor(errors=["int_iat_length_mismatch"])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_INT_IAT_MISMATCH)
+ assert len(details) == 1
+ assert details[0]["dll_name"] == "test.dll"
+
+ def test_no_double_emission_per_descriptor(self):
+ """A descriptor with multiple DLL-name errors emits only one issue."""
+ d = _make_descriptor(
+ errors=["dll_name_rva_zero", "read_failed", "unterminated"],
+ )
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ dll_issues = [
+ i for i in issues
+ if i["issue"] == ReasonCodes.DELAY_IMPORT_DLL_NAME_INVALID
+ ]
+ assert len(dll_issues) == 1
+
+ def test_entry_with_unknown_error_tag_skipped(self):
+ """
+ Cover the defensive guard: an entry whose errors contain only tags
+ not in _ENTRY_ERROR_PRIORITY (e.g., a future parser tag that the
+ validator's priority list hasn't been updated to recognise) is
+ silently skipped without emitting a reason code.
+ """
+ e = _make_entry(errors=["future_parser_tag_not_yet_recognised"])
+ d = _make_descriptor(imports=[e])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ # No DELAY_IMPORT_ENTRY_INVALID should fire — the unknown tag is
+ # not in the priority list, so the validator skips emission rather
+ # than reporting a spurious "unknown" reason.
+ assert ReasonCodes.DELAY_IMPORT_ENTRY_INVALID not in _codes(issues)
+
+
+# =================================================================
+# Entry validation
+# =================================================================
+
+class TestEntryValidation:
+
+ def test_clean_entry_no_issues(self):
+ e = _make_entry()
+ d = _make_descriptor(imports=[e])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ assert ReasonCodes.DELAY_IMPORT_ENTRY_INVALID not in _codes(issues)
+
+ def test_ordinal_zero_flagged(self):
+ e = _make_entry(
+ is_ordinal=True, ordinal=0,
+ errors=["ordinal_zero"],
+ )
+ d = _make_descriptor(imports=[e])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_ENTRY_INVALID)
+ assert len(details) == 1
+ assert details[0]["reason"] == "ordinal_zero"
+ assert details[0]["is_ordinal"] is True
+ assert details[0]["ordinal"] == 0
+
+ def test_int_entry_missing_flagged(self):
+ e = _make_entry(errors=["int_entry_missing"])
+ d = _make_descriptor(imports=[e])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_ENTRY_INVALID)
+ assert details[0]["reason"] == "int_entry_missing"
+
+ def test_int_entry_zero_flagged(self):
+ e = _make_entry(errors=["int_entry_zero"])
+ d = _make_descriptor(imports=[e])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_ENTRY_INVALID)
+ assert details[0]["reason"] == "int_entry_zero"
+
+ def test_name_unterminated_flagged(self):
+ e = _make_entry(
+ name=None, name_valid=False,
+ errors=["name_unterminated"],
+ )
+ d = _make_descriptor(imports=[e])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_ENTRY_INVALID)
+ assert details[0]["reason"] == "name_unterminated"
+
+ def test_name_not_printable_flagged(self):
+ e = _make_entry(
+ name="Foo\x01Bar", name_valid=False,
+ errors=["name_not_printable"],
+ )
+ d = _make_descriptor(imports=[e])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_ENTRY_INVALID)
+ assert details[0]["reason"] == "name_not_printable"
+ assert details[0]["name"] == "Foo\x01Bar"
+
+ def test_entry_priority_resolution(self):
+ """int_entry_missing wins over name_unterminated."""
+ e = _make_entry(errors=["name_unterminated", "int_entry_missing"])
+ d = _make_descriptor(imports=[e])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_ENTRY_INVALID)
+ assert details[0]["reason"] == "int_entry_missing"
+
+ def test_multiple_bad_entries_each_flagged(self):
+ e1 = _make_entry(index=0, errors=["int_entry_missing"])
+ e2 = _make_entry(index=1, errors=["name_unterminated"])
+ e3 = _make_entry(index=2) # clean
+ d = _make_descriptor(imports=[e1, e2, e3])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ details = _details_for(issues, ReasonCodes.DELAY_IMPORT_ENTRY_INVALID)
+ assert len(details) == 2
+
+
+# =================================================================
+# Combined scenarios
+# =================================================================
+
+class TestCombinedAnomalies:
+
+ def test_v0_with_cascading_dll_errors_emits_both(self):
+ d = _make_descriptor(
+ attributes=0, attributes_v1=False,
+ errors=["dll_name_not_printable"],
+ )
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ codes = set(_codes(issues))
+ assert ReasonCodes.DELAY_IMPORT_ATTRIBUTES_LEGACY_VA_MODE in codes
+ assert ReasonCodes.DELAY_IMPORT_DLL_NAME_INVALID in codes
+
+ def test_multiple_descriptors_each_independently_validated(self):
+ d1 = _make_descriptor(index=0, errors=["dll_name_rva_zero"])
+ d2 = _make_descriptor(index=1, attributes=0, attributes_v1=False)
+ di = _make_di(descriptors=[d1, d2])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ codes = set(_codes(issues))
+ assert ReasonCodes.DELAY_IMPORT_DLL_NAME_INVALID in codes
+ assert ReasonCodes.DELAY_IMPORT_ATTRIBUTES_LEGACY_VA_MODE in codes
+
+
+# =================================================================
+# Output contract
+# =================================================================
+
+class TestOutputContract:
+
+ def test_returns_list(self):
+ result = validate_delay_imports(
+ {"delay_import_struct": _make_di()}, _make_analysis(),
+ )
+ assert isinstance(result, list)
+
+ def test_clean_returns_empty_list(self):
+ result = validate_delay_imports(
+ {"delay_import_struct": _make_di()}, _make_analysis(),
+ )
+ assert result == []
+
+ def test_each_issue_has_issue_and_details(self):
+ d = _make_descriptor(errors=["dll_name_rva_zero"])
+ di = _make_di(descriptors=[d])
+ issues = validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ for issue in issues:
+ assert "issue" in issue
+ assert "details" in issue
+ assert isinstance(issue["details"], dict)
+
+
+# =================================================================
+# Determinism
+# =================================================================
+
+class TestDeterminism:
+
+ def test_repeated_validation_produces_identical_issues(self):
+ d = _make_descriptor(
+ attributes=0, attributes_v1=False,
+ errors=["dll_name_not_printable", "int_iat_length_mismatch"],
+ imports=[
+ _make_entry(index=0, errors=["int_entry_missing"]),
+ _make_entry(index=1, errors=["name_unterminated"]),
+ ],
+ )
+ di = _make_di(
+ truncations=["delay_import_descriptor_truncated"],
+ descriptors=[d],
+ )
+ metadata = {"delay_import_struct": di}
+ analysis = _make_analysis()
+
+ results = [
+ validate_delay_imports(metadata, analysis) for _ in range(20)
+ ]
+ for r in results[1:]:
+ assert r == results[0]
+
+ def test_priority_resolution_deterministic(self):
+ d = _make_descriptor(
+ errors=["read_failed", "dll_name_rva_zero", "unterminated"],
+ )
+ di = _make_di(descriptors=[d])
+ results = [
+ validate_delay_imports(
+ {"delay_import_struct": di}, _make_analysis(),
+ )
+ for _ in range(20)
+ ]
+ for r in results[1:]:
+ assert r == results[0]
+ # Confirm priority winner
+ details = _details_for(results[0],
+ ReasonCodes.DELAY_IMPORT_DLL_NAME_INVALID)
+ assert details[0]["reason"] == "dll_name_rva_zero"
diff --git a/tests/unit/validators/test_validator_exports.py b/tests/unit/validators/test_validator_exports.py
new file mode 100644
index 0000000..4b4daa7
--- /dev/null
+++ b/tests/unit/validators/test_validator_exports.py
@@ -0,0 +1,699 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Unit tests for iocx.validators.exports.validate_exports.
+
+Strategy:
+- Input is the export_struct dict produced by parser_exports.
+- Build dicts directly; isolate validator logic from parser behaviour.
+- Each test asserts on the set of REASONCODES emitted and the details
+ payload.
+"""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional
+
+import pytest
+
+from iocx.reason_codes import ReasonCodes
+from iocx.validators.exports import validate_exports
+
+
+# =================================================================
+# Input builders
+# =================================================================
+
+_NOT_PROVIDED = object()
+
+
+def _make_analysis(size_of_image: Optional[int] = 0x100000) -> Dict[str, Any]:
+ return {"size_of_image": size_of_image}
+
+
+def _make_header(
+ base: int = 1,
+ num_functions: int = 0,
+ num_names: int = 0,
+ addr_functions: int = 0x1100,
+ addr_names: int = 0x1200,
+ addr_name_ordinals: int = 0x1300,
+ **overrides,
+) -> Dict[str, int]:
+ h = {
+ "Characteristics": 0,
+ "TimeDateStamp": 0,
+ "MajorVersion": 0,
+ "MinorVersion": 0,
+ "Name": 0x1000,
+ "Base": base,
+ "NumberOfFunctions": num_functions,
+ "NumberOfNames": num_names,
+ "AddressOfFunctions": addr_functions if num_functions else 0,
+ "AddressOfNames": addr_names if num_names else 0,
+ "AddressOfNameOrdinals": addr_name_ordinals if num_names else 0,
+ }
+ h.update(overrides)
+ return h
+
+
+def _make_function(
+ index: int = 0,
+ ordinal: int = 1,
+ address_rva: Optional[int] = 0x2000,
+ is_forwarder: bool = False,
+ forwarder: Optional[str] = None,
+ forwarder_valid: bool = False,
+ name: Optional[str] = None,
+ name_rva: Optional[int] = None,
+) -> Dict[str, Any]:
+ return {
+ "index": index,
+ "ordinal": ordinal,
+ "address_rva": address_rva,
+ "is_forwarder": is_forwarder,
+ "forwarder": forwarder,
+ "forwarder_valid": forwarder_valid,
+ "name": name,
+ "name_rva": name_rva,
+ }
+
+
+def _make_name_pointer(
+ index: int = 0,
+ name_rva: Optional[int] = 0x1400,
+ ordinal_index: Optional[int] = 0,
+ name: Optional[str] = "Foo",
+ name_valid: bool = True,
+ errors: Optional[List[str]] = None,
+) -> Dict[str, Any]:
+ return {
+ "index": index,
+ "name_rva": name_rva,
+ "ordinal_index": ordinal_index,
+ "name": name,
+ "name_valid": name_valid,
+ "errors": errors or [],
+ }
+
+
+def _make_exp(
+ rva: int = 0x1000,
+ size: int = 200,
+ header: Any = _NOT_PROVIDED,
+ functions: Optional[List[Dict[str, Any]]] = None,
+ name_pointers: Optional[List[Dict[str, Any]]] = None,
+ truncations: Optional[List[str]] = None,
+ errors: Optional[List[str]] = None,
+) -> Dict[str, Any]:
+ return {
+ "rva": rva,
+ "size": size,
+ "header": _make_header() if header is _NOT_PROVIDED else header,
+ "functions": functions or [],
+ "name_pointers": name_pointers or [],
+ "truncations": truncations or [],
+ "errors": errors or [],
+ }
+
+
+def _codes(issues) -> List[Any]:
+ return [i["issue"] for i in issues]
+
+
+def _details_for(issues, code) -> List[Dict[str, Any]]:
+ return [i["details"] for i in issues if i["issue"] == code]
+
+
+# =================================================================
+# Absence
+# =================================================================
+
+class TestAbsence:
+
+ def test_no_export_struct_returns_no_issues(self):
+ assert validate_exports({}, _make_analysis()) == []
+
+ def test_explicit_none_returns_no_issues(self):
+ assert validate_exports({"export_struct": None}, _make_analysis()) == []
+
+
+# =================================================================
+# Top-level decode short-circuit
+# =================================================================
+
+class TestTopLevelDecodeFailure:
+
+ def test_errors_present_emits_invalid_returns_early(self):
+ exp = _make_exp(errors=["header_read_failed"])
+ # Add other issues that should NOT be emitted due to early return
+ exp["truncations"] = ["eat_truncated"]
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ codes = _codes(issues)
+ assert ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER in codes
+ assert ReasonCodes.EXPORT_TABLE_TRUNCATED not in codes
+ details = _details_for(issues, ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER)
+ assert details[0]["reason"] == "top_level_decode"
+ assert details[0]["errors"] == ["header_read_failed"]
+
+
+# =================================================================
+# Placement
+# =================================================================
+
+class TestPlacement:
+
+ def test_in_bounds_no_issue(self):
+ exp = _make_exp(rva=0x1000, size=200)
+ analysis = _make_analysis(size_of_image=0x100000)
+ issues = validate_exports({"export_struct": exp}, analysis)
+ assert ReasonCodes.EXPORT_DIRECTORY_OUT_OF_BOUNDS not in _codes(issues)
+
+ def test_extends_past_image_flagged(self):
+ exp = _make_exp(rva=0xFFF00, size=0x200)
+ analysis = _make_analysis(size_of_image=0x100000)
+ issues = validate_exports({"export_struct": exp}, analysis)
+ details = _details_for(issues, ReasonCodes.EXPORT_DIRECTORY_OUT_OF_BOUNDS)
+ assert len(details) == 1
+ assert details[0]["rva"] == 0xFFF00
+
+ def test_silent_when_size_of_image_missing(self):
+ exp = _make_exp(rva=0xFFF00, size=0x200)
+ analysis = _make_analysis(size_of_image=None)
+ issues = validate_exports({"export_struct": exp}, analysis)
+ assert ReasonCodes.EXPORT_DIRECTORY_OUT_OF_BOUNDS not in _codes(issues)
+
+ def test_silent_when_rva_none(self):
+ exp = _make_exp(rva=None, size=0)
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_DIRECTORY_OUT_OF_BOUNDS not in _codes(issues)
+
+
+# =================================================================
+# Truncations
+# =================================================================
+
+class TestTruncations:
+
+ def test_no_truncations_no_issues(self):
+ exp = _make_exp(truncations=[])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_TABLE_TRUNCATED not in _codes(issues)
+
+ def test_single_truncation_emits_one_issue(self):
+ exp = _make_exp(truncations=["eat_truncated"])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_TABLE_TRUNCATED)
+ assert len(details) == 1
+ assert details[0]["table"] == "eat_truncated"
+
+ def test_multiple_truncations_emit_separate_issues(self):
+ exp = _make_exp(truncations=["eat_truncated", "enpt_truncated", "eot_truncated"])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_TABLE_TRUNCATED)
+ assert len(details) == 3
+ tables = [d["table"] for d in details]
+ assert set(tables) == {"eat_truncated", "enpt_truncated", "eot_truncated"}
+
+
+# =================================================================
+# Header consistency
+# =================================================================
+
+class TestHeaderConsistency:
+
+ def test_clean_header_no_issues(self):
+ header = _make_header(num_functions=2, num_names=2)
+ exp = _make_exp(header=header)
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER not in _codes(issues)
+
+ def test_eat_rva_zero_with_nonzero_count_flagged(self):
+ header = _make_header(num_functions=5, addr_functions=0)
+ exp = _make_exp(header=header)
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER)
+ reasons = [d["reason"] for d in details]
+ assert "eat_rva_zero_with_nonzero_count" in reasons
+
+ def test_enpt_rva_zero_with_nonzero_count_flagged(self):
+ header = _make_header(num_functions=5, num_names=3, addr_names=0)
+ exp = _make_exp(header=header)
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER)
+ reasons = [d["reason"] for d in details]
+ assert "enpt_rva_zero_with_nonzero_count" in reasons
+
+ def test_eot_rva_zero_with_nonzero_count_flagged(self):
+ header = _make_header(num_functions=5, num_names=3, addr_name_ordinals=0)
+ exp = _make_exp(header=header)
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER)
+ reasons = [d["reason"] for d in details]
+ assert "eot_rva_zero_with_nonzero_count" in reasons
+
+ def test_num_names_exceeds_num_functions_flagged(self):
+ header = _make_header(num_functions=2, num_names=5)
+ exp = _make_exp(header=header)
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER)
+ reasons = [d["reason"] for d in details]
+ assert "num_names_exceeds_num_functions" in reasons
+
+ def test_multiple_consistency_failures_emit_multiple_issues(self):
+ header = _make_header(
+ num_functions=5,
+ num_names=10, # > num_functions
+ addr_functions=0, # zero with count
+ )
+ exp = _make_exp(header=header)
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER)
+ assert len(details) >= 2
+
+ def test_header_none_skips_consistency_check(self):
+ exp = _make_exp(header=None, truncations=["export_directory_header"])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ # Should emit truncation but not consistency issues
+ assert ReasonCodes.EXPORT_TABLE_TRUNCATED in _codes(issues)
+ consistency = [
+ d for d in _details_for(issues, ReasonCodes.EXPORT_DIRECTORY_INVALID_HEADER)
+ if d.get("reason") in {"eat_rva_zero_with_nonzero_count",
+ "enpt_rva_zero_with_nonzero_count",
+ "eot_rva_zero_with_nonzero_count",
+ "num_names_exceeds_num_functions"}
+ ]
+ assert consistency == []
+
+
+# =================================================================
+# Name pointer validation
+# =================================================================
+
+class TestNamePointers:
+
+ def test_clean_name_pointer_no_issues(self):
+ np = _make_name_pointer()
+ exp = _make_exp(
+ header=_make_header(num_functions=1, num_names=1),
+ name_pointers=[np],
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_NAME_RVA_INVALID not in _codes(issues)
+ assert ReasonCodes.EXPORT_NAME_NOT_ASCII not in _codes(issues)
+ assert ReasonCodes.EXPORT_NAME_ORDINAL_INDEX_INVALID not in _codes(issues)
+
+ def test_name_rva_zero_flagged(self):
+ np = _make_name_pointer(errors=["name_rva_zero"])
+ exp = _make_exp(name_pointers=[np])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_NAME_RVA_INVALID)
+ assert len(details) == 1
+ assert details[0]["reason"] == "name_rva_zero"
+
+ def test_name_rva_priority_resolution(self):
+ """name_rva_missing wins over read_failed when both are present."""
+ np = _make_name_pointer(errors=["read_failed", "name_rva_missing"])
+ exp = _make_exp(name_pointers=[np])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_NAME_RVA_INVALID)
+ assert len(details) == 1
+ assert details[0]["reason"] == "name_rva_missing"
+
+ def test_unterminated_flagged_with_correct_reason(self):
+ np = _make_name_pointer(errors=["unterminated"])
+ exp = _make_exp(name_pointers=[np])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_NAME_RVA_INVALID)
+ assert details[0]["reason"] == "unterminated"
+
+ def test_non_ascii_flagged_with_correct_reason(self):
+ np = _make_name_pointer(name="caf\ufffd", errors=["non_ascii"])
+ exp = _make_exp(name_pointers=[np])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_NAME_NOT_ASCII)
+ assert len(details) == 1
+ assert details[0]["reason"] == "non_ascii"
+
+ def test_name_not_printable_ascii_flagged(self):
+ np = _make_name_pointer(
+ name="Foo\x01Bar",
+ name_valid=False,
+ errors=["name_not_printable_ascii"],
+ )
+ exp = _make_exp(name_pointers=[np])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_NAME_NOT_ASCII)
+ assert len(details) == 1
+ assert details[0]["reason"] == "name_not_printable_ascii"
+
+ def test_name_encoding_priority_resolution(self):
+ """non_ascii wins over name_not_printable_ascii."""
+ np = _make_name_pointer(
+ errors=["name_not_printable_ascii", "non_ascii"],
+ )
+ exp = _make_exp(name_pointers=[np])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_NAME_NOT_ASCII)
+ assert len(details) == 1
+ assert details[0]["reason"] == "non_ascii"
+
+ def test_ordinal_index_missing_flagged(self):
+ np = _make_name_pointer(errors=["ordinal_index_missing"])
+ exp = _make_exp(name_pointers=[np])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_NAME_ORDINAL_INDEX_INVALID)
+ assert len(details) == 1
+ assert details[0]["reason"] == "missing"
+
+ def test_ordinal_index_out_of_range_flagged(self):
+ np = _make_name_pointer(
+ ordinal_index=99,
+ errors=["ordinal_index_out_of_range"],
+ )
+ exp = _make_exp(
+ header=_make_header(num_functions=5, num_names=1),
+ name_pointers=[np],
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_NAME_ORDINAL_INDEX_INVALID)
+ assert len(details) == 1
+ assert details[0]["reason"] == "out_of_range"
+ assert details[0]["ordinal_index"] == 99
+ assert details[0]["num_functions"] == 5
+
+ def test_no_double_emission_per_entry(self):
+ """An entry with multiple RVA-class errors emits only one issue."""
+ np = _make_name_pointer(
+ errors=["name_rva_zero", "name_rva_missing", "read_failed"],
+ )
+ exp = _make_exp(name_pointers=[np])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ rva_issues = [
+ i for i in issues
+ if i["issue"] == ReasonCodes.EXPORT_NAME_RVA_INVALID
+ ]
+ assert len(rva_issues) == 1
+
+
+# =================================================================
+# Name pointer ordering
+# =================================================================
+
+class TestNamePointerOrdering:
+
+ def test_sorted_names_no_issue(self):
+ nps = [
+ _make_name_pointer(index=0, name="Alpha"),
+ _make_name_pointer(index=1, name="Beta"),
+ _make_name_pointer(index=2, name="Gamma"),
+ ]
+ exp = _make_exp(
+ header=_make_header(num_functions=3, num_names=3),
+ name_pointers=nps,
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_NAME_POINTER_TABLE_UNSORTED not in _codes(issues)
+
+ def test_unsorted_names_flagged(self):
+ nps = [
+ _make_name_pointer(index=0, name="Zeta"),
+ _make_name_pointer(index=1, name="Alpha"),
+ ]
+ exp = _make_exp(
+ header=_make_header(num_functions=2, num_names=2),
+ name_pointers=nps,
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_NAME_POINTER_TABLE_UNSORTED)
+ assert len(details) == 1
+ assert details[0]["name_count"] == 2
+ assert details[0]["first_violation_index"] == 1
+
+ def test_unreadable_name_skips_ordering_check(self):
+ nps = [
+ _make_name_pointer(index=0, name=None, name_valid=False),
+ _make_name_pointer(index=1, name="Zeta"),
+ ]
+ exp = _make_exp(name_pointers=nps)
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_NAME_POINTER_TABLE_UNSORTED not in _codes(issues)
+
+ def test_empty_name_pointers_no_issue(self):
+ exp = _make_exp(name_pointers=[])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_NAME_POINTER_TABLE_UNSORTED not in _codes(issues)
+
+
+# =================================================================
+# Function entries
+# =================================================================
+
+class TestFunctions:
+
+ def test_clean_function_no_issues(self):
+ fn = _make_function(address_rva=0x2000)
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_FUNCTION_RVA_INVALID not in _codes(issues)
+
+ def test_max_ordinal_exceeds_u16_flagged(self):
+ header = _make_header(base=0xFFF0, num_functions=32)
+ exp = _make_exp(header=header, functions=[])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_ORDINAL_OUT_OF_RANGE)
+ assert len(details) == 1
+ assert details[0]["reason"] == "max_exceeds_u16"
+ assert details[0]["base"] == 0xFFF0
+ assert details[0]["max_ordinal"] == 0xFFF0 + 31
+
+ def test_max_ordinal_in_range_no_issue(self):
+ header = _make_header(base=1, num_functions=100)
+ exp = _make_exp(header=header)
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_ORDINAL_OUT_OF_RANGE not in _codes(issues)
+
+ def test_function_rva_within_image_no_issue(self):
+ fn = _make_function(address_rva=0x5000)
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports(
+ {"export_struct": exp},
+ _make_analysis(size_of_image=0x100000),
+ )
+ assert ReasonCodes.EXPORT_FUNCTION_RVA_INVALID not in _codes(issues)
+
+ def test_function_rva_beyond_image_flagged(self):
+ fn = _make_function(address_rva=0x200000)
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports(
+ {"export_struct": exp},
+ _make_analysis(size_of_image=0x100000),
+ )
+ details = _details_for(issues, ReasonCodes.EXPORT_FUNCTION_RVA_INVALID)
+ assert len(details) == 1
+ assert details[0]["reason"] == "exceeds_image"
+ assert details[0]["address_rva"] == 0x200000
+
+ def test_zero_rva_function_skipped(self):
+ """An EAT entry of 0 is 'unused slot' — not flagged."""
+ fn = _make_function(address_rva=0)
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_FUNCTION_RVA_INVALID not in _codes(issues)
+
+ def test_none_rva_function_skipped(self):
+ fn = _make_function(address_rva=None)
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_FUNCTION_RVA_INVALID not in _codes(issues)
+
+ def test_function_rva_check_silent_when_size_of_image_missing(self):
+ fn = _make_function(address_rva=0x200000)
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports(
+ {"export_struct": exp},
+ _make_analysis(size_of_image=None),
+ )
+ assert ReasonCodes.EXPORT_FUNCTION_RVA_INVALID not in _codes(issues)
+
+
+# =================================================================
+# Forwarders
+# =================================================================
+
+class TestForwarders:
+
+ def test_valid_forwarder_no_issue(self):
+ fn = _make_function(
+ address_rva=0x1050,
+ is_forwarder=True,
+ forwarder="KERNEL32.LoadLibraryA",
+ forwarder_valid=True,
+ )
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_FORWARDER_MALFORMED not in _codes(issues)
+
+ def test_unreadable_forwarder_flagged(self):
+ fn = _make_function(
+ address_rva=0x1050,
+ is_forwarder=True,
+ forwarder=None,
+ forwarder_valid=False,
+ )
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_FORWARDER_MALFORMED)
+ assert len(details) == 1
+ assert details[0]["reason"] == "unreadable"
+
+ def test_malformed_forwarder_format_flagged(self):
+ fn = _make_function(
+ address_rva=0x1050,
+ is_forwarder=True,
+ forwarder="NoDotInThisString",
+ forwarder_valid=False,
+ )
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.EXPORT_FORWARDER_MALFORMED)
+ assert len(details) == 1
+ assert details[0]["reason"] == "format"
+ assert details[0]["forwarder"] == "NoDotInThisString"
+
+ def test_forwarder_skips_function_rva_check(self):
+ """A forwarder's address RVA points into the export directory; it
+ should not also trigger EXPORT_FUNCTION_RVA_INVALID."""
+ fn = _make_function(
+ address_rva=0x1050,
+ is_forwarder=True,
+ forwarder="KERNEL32.X",
+ forwarder_valid=True,
+ )
+ exp = _make_exp(
+ header=_make_header(num_functions=1),
+ functions=[fn],
+ )
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ assert ReasonCodes.EXPORT_FUNCTION_RVA_INVALID not in _codes(issues)
+
+
+# =================================================================
+# Combined scenarios
+# =================================================================
+
+class TestCombinedAnomalies:
+
+ def test_multiple_pathology_classes_emit_independently(self):
+ # Bad placement, truncated EAT, bad name pointer
+ exp = _make_exp(
+ rva=0xFFF00,
+ size=0x200,
+ truncations=["eat_truncated"],
+ name_pointers=[
+ _make_name_pointer(errors=["name_rva_zero"]),
+ ],
+ )
+ analysis = _make_analysis(size_of_image=0x100000)
+ issues = validate_exports({"export_struct": exp}, analysis)
+ codes = set(_codes(issues))
+ assert ReasonCodes.EXPORT_DIRECTORY_OUT_OF_BOUNDS in codes
+ assert ReasonCodes.EXPORT_TABLE_TRUNCATED in codes
+ assert ReasonCodes.EXPORT_NAME_RVA_INVALID in codes
+
+
+# =================================================================
+# Output contract
+# =================================================================
+
+class TestOutputContract:
+
+ def test_returns_list(self):
+ result = validate_exports({"export_struct": _make_exp()}, _make_analysis())
+ assert isinstance(result, list)
+
+ def test_clean_exports_return_empty_list(self):
+ result = validate_exports({"export_struct": _make_exp()}, _make_analysis())
+ assert result == []
+
+ def test_each_issue_has_issue_and_details(self):
+ np = _make_name_pointer(errors=["name_rva_zero"])
+ exp = _make_exp(name_pointers=[np])
+ issues = validate_exports({"export_struct": exp}, _make_analysis())
+ for issue in issues:
+ assert "issue" in issue
+ assert "details" in issue
+ assert isinstance(issue["details"], dict)
+
+
+# =================================================================
+# Determinism
+# =================================================================
+
+class TestDeterminism:
+
+ def test_repeated_validation_produces_identical_issues(self):
+ np = _make_name_pointer(
+ name=None,
+ name_valid=False,
+ errors=["read_failed", "name_rva_missing"],
+ )
+ fn = _make_function(
+ is_forwarder=True,
+ forwarder="BAD",
+ forwarder_valid=False,
+ )
+ exp = _make_exp(
+ truncations=["eat_truncated", "eot_truncated"],
+ name_pointers=[np],
+ functions=[fn],
+ )
+ metadata = {"export_struct": exp}
+ analysis = _make_analysis()
+
+ results = [validate_exports(metadata, analysis) for _ in range(20)]
+ for r in results[1:]:
+ assert r == results[0]
+
+ def test_priority_resolution_deterministic(self):
+ np = _make_name_pointer(
+ errors=["read_failed", "name_rva_zero", "name_rva_missing", "unterminated"],
+ )
+ exp = _make_exp(name_pointers=[np])
+ results = [
+ validate_exports({"export_struct": exp}, _make_analysis())
+ for _ in range(20)
+ ]
+ for r in results[1:]:
+ assert r == results[0]
+ # Confirm priority winner
+ details = _details_for(results[0], ReasonCodes.EXPORT_NAME_RVA_INVALID)
+ assert details[0]["reason"] == "name_rva_missing"
diff --git a/tests/unit/validators/test_validator_resources.py b/tests/unit/validators/test_validator_resources.py
index f346b2f..f00ae11 100644
--- a/tests/unit/validators/test_validator_resources.py
+++ b/tests/unit/validators/test_validator_resources.py
@@ -339,3 +339,124 @@ def test_resources_directory_outside_rsrc_skips_validation():
# Because the directory is outside .rsrc, validate_directory() returns immediately
# → no issues should be produced
assert issues == []
+
+
+class TestLanguageLayerNamedEntry:
+ """
+ Cover the depth-2 named-entry detection: a Language-layer directory
+ that contains a named entry instead of an integer LCID.
+ """
+
+ def _make_resources_with_named_language_entry(self) -> dict:
+ """
+ Build a minimal resources_struct dict with this shape:
+ Type (depth 0)
+ └─ Name (depth 1)
+ └─ Language (depth 2) ← contains a named entry (anomaly)
+ └─ data leaf
+ """
+ # Data leaf at depth 3 — well-formed, in-bounds
+ data_leaf_entry = {
+ "name": None,
+ "id": 0x0409,
+ "is_directory": False,
+ "directory": None,
+ "data_rva": 0x1100,
+ "data_size": 100,
+ "raw_offset": 0x500,
+ }
+
+ # Depth 2: Language directory containing a NAMED entry (the anomaly)
+ # plus a valid ID entry as a sibling, to prove the validator emits
+ # per-violation rather than per-directory.
+ lang_dir = {
+ "rva": 0x1080,
+ "size": 32,
+ "entries": [
+ {
+ "name": "EN-US", # ← anomaly: named instead of LCID
+ "id": None,
+ "is_directory": True,
+ "directory": {
+ "rva": 0x1090,
+ "size": 24,
+ "entries": [data_leaf_entry],
+ },
+ "data_rva": None,
+ "data_size": None,
+ "raw_offset": None,
+ },
+ ],
+ }
+
+ # Depth 1: Name directory
+ name_dir = {
+ "rva": 0x1040,
+ "size": 24,
+ "entries": [
+ {
+ "name": None,
+ "id": 1,
+ "is_directory": True,
+ "directory": lang_dir,
+ "data_rva": None,
+ "data_size": None,
+ "raw_offset": None,
+ },
+ ],
+ }
+
+ # Depth 0: Type directory (root)
+ root = {
+ "rva": 0x1000,
+ "size": 24,
+ "entries": [
+ {
+ "name": None,
+ "id": 16, # RT_VERSION
+ "is_directory": True,
+ "directory": name_dir,
+ "data_rva": None,
+ "data_size": None,
+ "raw_offset": None,
+ },
+ ],
+ }
+
+ return {
+ "root": root,
+ "string_tables": [],
+ }
+
+ def _make_analysis(self) -> dict:
+ return {
+ "sections": [{
+ "name": ".rsrc",
+ "virtual_address": 0x1000,
+ "virtual_size": 0x2000,
+ "raw_address": 0x400,
+ "raw_size": 0x2000,
+ }],
+ "file_size": 0x10000,
+ "overlay_offset": 0x9000,
+ }
+
+ def test_named_language_entry_flagged(self):
+ from iocx.reason_codes import ReasonCodes
+ from iocx.validators.resources import validate_resources
+
+ resources = self._make_resources_with_named_language_entry()
+ metadata = {"resources_struct": resources}
+ issues = validate_resources(metadata, self._make_analysis())
+
+ codes = [i["issue"] for i in issues]
+ assert ReasonCodes.RESOURCE_DIRECTORY_LANGUAGE_NOT_ID in codes
+
+ # Confirm the details payload is shaped correctly
+ details = [
+ i["details"] for i in issues
+ if i["issue"] == ReasonCodes.RESOURCE_DIRECTORY_LANGUAGE_NOT_ID
+ ]
+ assert len(details) == 1
+ assert details[0]["name"] == "EN-US"
+ assert details[0]["rva"] == 0x1080 # the language directory's RVA
diff --git a/tests/unit/validators/test_validator_version_info.py b/tests/unit/validators/test_validator_version_info.py
new file mode 100644
index 0000000..8c1c940
--- /dev/null
+++ b/tests/unit/validators/test_validator_version_info.py
@@ -0,0 +1,645 @@
+# Copyright (c) 2026 MalX Labs and contributors
+# SPDX-License-Identifier: MPL-2.0
+
+"""
+Unit tests for iocx.validators.version_info.validate_version_info.
+
+Strategy:
+- Input is the version_info_struct dict produced by parser_version_info.
+ We construct dicts directly rather than running the parser, which
+ isolates validator logic from parser behaviour.
+- The analysis dict supplies sections and overlay_offset; minimal stubs
+ cover everything the validator reads.
+- Each test asserts on the set of REASONCODES emitted and the details
+ payload, since both are part of the contract.
+"""
+
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional
+
+import pytest
+
+from iocx.reason_codes import ReasonCodes
+from iocx.validators.version_info import validate_version_info
+
+
+# =================================================================
+# Input builders
+# =================================================================
+
+def _make_analysis(
+ rsrc_va: int = 0x1000,
+ rsrc_vs: int = 0x2000,
+ include_rsrc: bool = True,
+ extra_sections: Optional[List[Dict[str, Any]]] = None,
+) -> Dict[str, Any]:
+ """Build a minimal analysis dict with optional .rsrc section."""
+ sections: List[Dict[str, Any]] = []
+ if include_rsrc:
+ sections.append({
+ "name": ".rsrc",
+ "virtual_address": rsrc_va,
+ "virtual_size": rsrc_vs,
+ "raw_address": 0x400,
+ "raw_size": rsrc_vs,
+ })
+ if extra_sections:
+ sections.extend(extra_sections)
+ return {
+ "sections": sections,
+ "file_size": 0x10000,
+ "overlay_offset": 0x9000,
+ }
+
+
+def _make_ffi(
+ signature_ok: bool = True,
+ struct_version_ok: bool = True,
+ signature: int = 0xFEEF04BD,
+ struct_version: int = 0x00010000,
+) -> Dict[str, Any]:
+ """Build a fixed_file_info sub-dict."""
+ return {
+ "signature": signature,
+ "signature_ok": signature_ok,
+ "struct_version": struct_version,
+ "struct_version_ok": struct_version_ok,
+ "file_version": (0x00010000, 0x00020003),
+ "product_version": (0x00010000, 0x00020003),
+ "file_flags_mask": 0,
+ "file_flags": 0,
+ "file_os": 0x00040004,
+ "file_type": 1,
+ "file_subtype": 0,
+ "file_date": (0, 0),
+ }
+
+
+_NOT_PROVIDED = object()
+
+
+def _make_vi(
+ *,
+ rva: Optional[int] = 0x1100,
+ size: Optional[int] = 100,
+ decoded: bool = True,
+ header_ok: bool = True,
+ length_consistent: bool = True,
+ fixed_file_info: Any = _NOT_PROVIDED,
+ string_file_info: Any = _NOT_PROVIDED,
+ var_file_info: Any = _NOT_PROVIDED,
+ errors: Any = _NOT_PROVIDED,
+ w_type: int = 0,
+) -> Dict[str, Any]:
+ """Build a complete version_info_struct dict with sensible defaults."""
+ return {
+ "rva": rva,
+ "size": size,
+ "decoded": decoded,
+ "header_ok": header_ok,
+ "length_consistent": length_consistent,
+ "w_type": w_type,
+ "fixed_file_info": _make_ffi() if fixed_file_info is _NOT_PROVIDED else fixed_file_info,
+ "string_file_info": [] if string_file_info is _NOT_PROVIDED else string_file_info,
+ "var_file_info": [] if var_file_info is _NOT_PROVIDED else var_file_info,
+ "errors": [] if errors is _NOT_PROVIDED else errors,
+ }
+
+
+def _codes(issues) -> List:
+ """Extract REASONCODES from a list of StructuralIssue dicts."""
+ return [issue["issue"] for issue in issues]
+
+
+def _details_for(issues, code) -> List[Dict[str, Any]]:
+ """Return the details payloads for all issues matching a given code."""
+ return [issue["details"] for issue in issues if issue["issue"] == code]
+
+
+# =================================================================
+# Absence / clean cases
+# =================================================================
+
+class TestAbsence:
+ """Absence of RT_VERSION is never a structural defect."""
+
+ def test_no_version_info_struct_returns_no_issues(self):
+ metadata = {} # version_info_struct missing entirely
+ issues = validate_version_info(metadata, _make_analysis())
+ assert issues == []
+
+ def test_explicit_none_returns_no_issues(self):
+ metadata = {"version_info_struct": None}
+ issues = validate_version_info(metadata, _make_analysis())
+ assert issues == []
+
+
+class TestCleanBlob:
+ """A well-formed version_info_struct produces no issues."""
+
+ def test_minimal_clean_blob(self):
+ metadata = {"version_info_struct": _make_vi()}
+ issues = validate_version_info(metadata, _make_analysis())
+ assert issues == []
+
+ def test_clean_blob_with_all_substructures(self):
+ sfi = [{
+ "tables": [{
+ "lang_codepage": "040904B0",
+ "strings": {"CompanyName": "MalX Labs"},
+ "errors": [],
+ }],
+ "errors": [],
+ }]
+ vfi = [{
+ "vars": [{
+ "key": "Translation",
+ "translations": [{"lang": 0x0409, "codepage": 0x04B0}],
+ }],
+ "errors": [],
+ }]
+ metadata = {"version_info_struct": _make_vi(
+ string_file_info=sfi,
+ var_file_info=vfi,
+ )}
+ issues = validate_version_info(metadata, _make_analysis())
+ assert issues == []
+
+
+# =================================================================
+# Placement validation
+# =================================================================
+
+class TestPlacement:
+
+ def test_placement_inside_rsrc_no_issue(self):
+ vi = _make_vi(rva=0x1100, size=100)
+ analysis = _make_analysis(rsrc_va=0x1000, rsrc_vs=0x2000)
+ issues = validate_version_info({"version_info_struct": vi}, analysis)
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER not in _codes(issues)
+
+ def test_placement_rva_before_rsrc_flagged(self):
+ vi = _make_vi(rva=0x500, size=100)
+ analysis = _make_analysis(rsrc_va=0x1000, rsrc_vs=0x2000)
+ issues = validate_version_info({"version_info_struct": vi}, analysis)
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER)
+ assert any(d.get("reason") == "placement" for d in details)
+
+ def test_placement_rva_extends_past_rsrc_flagged(self):
+ vi = _make_vi(rva=0x2F00, size=0x200)
+ analysis = _make_analysis(rsrc_va=0x1000, rsrc_vs=0x2000)
+ issues = validate_version_info({"version_info_struct": vi}, analysis)
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER)
+ placement_details = [d for d in details if d.get("reason") == "placement"]
+ assert len(placement_details) == 1
+ assert placement_details[0]["rva"] == 0x2F00
+ assert placement_details[0]["size"] == 0x200
+
+ def test_placement_no_rsrc_section_skipped(self):
+ """If there's no .rsrc section, placement isn't checked."""
+ vi = _make_vi(rva=0x5000, size=100)
+ analysis = _make_analysis(include_rsrc=False)
+ issues = validate_version_info({"version_info_struct": vi}, analysis)
+ placement_details = [d for d in _details_for(
+ issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER
+ ) if d.get("reason") == "placement"]
+ assert placement_details == []
+
+ def test_placement_rva_none_skipped(self):
+ """If the parser couldn't determine an RVA, placement isn't checked."""
+ vi = _make_vi(rva=None, size=None)
+ analysis = _make_analysis()
+ issues = validate_version_info({"version_info_struct": vi}, analysis)
+ placement_details = [d for d in _details_for(
+ issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER
+ ) if d.get("reason") == "placement"]
+ assert placement_details == []
+
+ def test_placement_size_none_treated_as_zero(self):
+ """size=None is coerced to 0 by `vi['size'] or 0`."""
+ vi = _make_vi(rva=0x1100, size=None)
+ analysis = _make_analysis(rsrc_va=0x1000, rsrc_vs=0x2000)
+ issues = validate_version_info({"version_info_struct": vi}, analysis)
+ # 0x1100 + 0 is within .rsrc, so no placement issue
+ placement_details = [d for d in _details_for(
+ issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER
+ ) if d.get("reason") == "placement"]
+ assert placement_details == []
+
+ def test_placement_exact_boundary_no_issue(self):
+ """RVA at rsrc_va and rva+size == rsrc_va + rsrc_vs is in-bounds."""
+ vi = _make_vi(rva=0x1000, size=0x2000)
+ analysis = _make_analysis(rsrc_va=0x1000, rsrc_vs=0x2000)
+ issues = validate_version_info({"version_info_struct": vi}, analysis)
+ placement_details = [d for d in _details_for(
+ issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER
+ ) if d.get("reason") == "placement"]
+ assert placement_details == []
+
+
+# =================================================================
+# Top-level header validation
+# =================================================================
+
+class TestTopLevelHeader:
+
+ def test_undecoded_emits_invalid_header_and_returns_early(self):
+ """If decoded=False, no further sub-structure issues should be emitted."""
+ vi = _make_vi(
+ decoded=False,
+ header_ok=False,
+ length_consistent=False,
+ fixed_file_info=None,
+ errors=["too_short"],
+ )
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+
+ codes = _codes(issues)
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER in codes
+ # Should not emit FFI/SFI/VFI codes because the validator returns early
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO not in codes
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO not in codes
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_VARFILEINFO not in codes
+
+ # Details should carry the "undecoded" reason and the parser errors
+ header_details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER)
+ undecoded = [d for d in header_details if d.get("reason") == "undecoded"]
+ assert len(undecoded) == 1
+ assert undecoded[0]["errors"] == ["too_short"]
+
+ def test_szkey_mismatch_emits_invalid_header(self):
+ vi = _make_vi(header_ok=False)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER)
+ szkey_details = [d for d in details if d.get("reason") == "szkey_mismatch"]
+ assert len(szkey_details) == 1
+
+ def test_length_inconsistent_emits_invalid_header(self):
+ vi = _make_vi(length_consistent=False)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER)
+ length_details = [d for d in details if d.get("reason") == "length_inconsistent"]
+ assert len(length_details) == 1
+
+ def test_both_szkey_and_length_emit_two_separate_issues(self):
+ vi = _make_vi(header_ok=False, length_consistent=False)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER)
+ reasons = [d.get("reason") for d in details]
+ assert "szkey_mismatch" in reasons
+ assert "length_inconsistent" in reasons
+
+ def test_undecoded_with_empty_errors_list(self):
+ """decoded=False with no errors recorded — still emits header issue."""
+ vi = _make_vi(decoded=False, errors=[])
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER)
+ undecoded = [d for d in details if d.get("reason") == "undecoded"]
+ assert undecoded[0]["errors"] == []
+
+
+# =================================================================
+# VS_FIXEDFILEINFO validation
+# =================================================================
+
+class TestFixedFileInfo:
+
+ def test_clean_ffi_no_issue(self):
+ vi = _make_vi(fixed_file_info=_make_ffi())
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO not in _codes(issues)
+
+ def test_absent_ffi_without_errors_no_issue(self):
+ """Legitimate omission: FFI is None and no parser errors flag it."""
+ vi = _make_vi(fixed_file_info=None, errors=[])
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO not in _codes(issues)
+
+ def test_absent_ffi_with_parse_errors_flagged(self):
+ vi = _make_vi(
+ fixed_file_info=None,
+ errors=["fixed_file_info_truncated"],
+ )
+ print("VI errors:", vi.get("errors")) # debug
+ print("VI ffi:", vi.get("fixed_file_info")) # debug
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ print("Issues:", issues) # debug
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO)
+ assert len(details) == 1
+ assert details[0]["reason"] == "parse_failed"
+ assert "fixed_file_info_truncated" in details[0]["errors"]
+
+ def test_absent_ffi_with_non_ffi_errors_not_flagged(self):
+ """Parser errors unrelated to FFI shouldn't trigger an FFI issue."""
+ vi = _make_vi(
+ fixed_file_info=None,
+ errors=["unknown_child", "child_length_invalid"],
+ )
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO not in _codes(issues)
+
+ def test_bad_signature_flagged(self):
+ ffi = _make_ffi(signature_ok=False, signature=0xDEADBEEF)
+ vi = _make_vi(fixed_file_info=ffi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO)
+ sig_details = [d for d in details if d.get("reason") == "signature"]
+ assert len(sig_details) == 1
+ assert sig_details[0]["signature"] == 0xDEADBEEF
+
+ def test_bad_struct_version_flagged(self):
+ ffi = _make_ffi(struct_version_ok=False, struct_version=0x00020000)
+ vi = _make_vi(fixed_file_info=ffi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO)
+ sv_details = [d for d in details if d.get("reason") == "struct_version"]
+ assert len(sv_details) == 1
+ assert sv_details[0]["struct_version"] == 0x00020000
+
+ def test_both_signature_and_struct_version_emit_two_issues(self):
+ ffi = _make_ffi(signature_ok=False, struct_version_ok=False)
+ vi = _make_vi(fixed_file_info=ffi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO)
+ reasons = [d.get("reason") for d in details]
+ assert "signature" in reasons
+ assert "struct_version" in reasons
+
+
+# =================================================================
+# StringFileInfo validation
+# =================================================================
+
+class TestStringFileInfo:
+
+ def test_no_string_file_info_no_issue(self):
+ vi = _make_vi(string_file_info=[])
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO not in _codes(issues)
+
+ def test_clean_string_file_info_no_issue(self):
+ sfi = [{
+ "tables": [{
+ "lang_codepage": "040904B0",
+ "strings": {"CompanyName": "X"},
+ "errors": [],
+ }],
+ "errors": [],
+ }]
+ vi = _make_vi(string_file_info=sfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO not in _codes(issues)
+
+ def test_sfi_with_top_level_errors_flagged(self):
+ """A StringFileInfo with errors on the wrapper itself."""
+ sfi = [{
+ "tables": [],
+ "errors": ["string_table_header"],
+ }]
+ vi = _make_vi(string_file_info=sfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO)
+ assert len(details) == 1
+ assert details[0]["errors"] == ["string_table_header"]
+ assert details[0]["tables"] == 0
+
+ def test_sfi_with_top_level_errors_skips_table_iteration(self):
+ """Continue branch: top-level errors short-circuit table iteration."""
+ sfi = [{
+ "tables": [
+ # This table has errors but should NOT produce a second issue
+ # because the wrapper-level error already triggered `continue`.
+ {"lang_codepage": "BAD", "strings": {}, "errors": ["lang_codepage_key"]},
+ ],
+ "errors": ["string_table_length"],
+ }]
+ vi = _make_vi(string_file_info=sfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO)
+ assert len(details) == 1 # only the wrapper-level error
+
+ def test_sfi_table_with_errors_flagged(self):
+ """A StringTable with its own errors but a clean wrapper."""
+ sfi = [{
+ "tables": [{
+ "lang_codepage": "ENGLISHX",
+ "strings": {"CompanyName": "X"},
+ "errors": ["lang_codepage_key"],
+ }],
+ "errors": [],
+ }]
+ vi = _make_vi(string_file_info=sfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO)
+ assert len(details) == 1
+ assert details[0]["errors"] == ["lang_codepage_key"]
+ assert details[0]["lang_codepage"] == "ENGLISHX"
+
+ def test_sfi_multiple_tables_each_with_errors_flagged_separately(self):
+ sfi = [{
+ "tables": [
+ {"lang_codepage": "BAD1", "strings": {}, "errors": ["lang_codepage_key"]},
+ {"lang_codepage": "BAD2", "strings": {}, "errors": ["string_length"]},
+ ],
+ "errors": [],
+ }]
+ vi = _make_vi(string_file_info=sfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO)
+ assert len(details) == 2
+ codepages = [d["lang_codepage"] for d in details]
+ assert "BAD1" in codepages
+ assert "BAD2" in codepages
+
+ def test_sfi_table_with_no_errors_not_flagged(self):
+ """A clean table inside an SFI with no wrapper errors produces nothing."""
+ sfi = [{
+ "tables": [{
+ "lang_codepage": "040904B0",
+ "strings": {"X": "Y"},
+ "errors": [],
+ }],
+ "errors": [],
+ }]
+ vi = _make_vi(string_file_info=sfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO not in _codes(issues)
+
+ def test_multiple_sfi_children(self):
+ """Multiple StringFileInfo wrappers (unusual but legal)."""
+ sfi = [
+ {"tables": [], "errors": ["string_table_header"]},
+ {"tables": [], "errors": ["string_table_length"]},
+ ]
+ vi = _make_vi(string_file_info=sfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO)
+ assert len(details) == 2
+
+
+# =================================================================
+# VarFileInfo validation
+# =================================================================
+
+class TestVarFileInfo:
+
+ def test_no_var_file_info_no_issue(self):
+ vi = _make_vi(var_file_info=[])
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_VARFILEINFO not in _codes(issues)
+
+ def test_clean_var_file_info_no_issue(self):
+ vfi = [{
+ "vars": [{
+ "key": "Translation",
+ "translations": [{"lang": 0x0409, "codepage": 0x04B0}],
+ }],
+ "errors": [],
+ }]
+ vi = _make_vi(var_file_info=vfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_VARFILEINFO not in _codes(issues)
+
+ def test_vfi_with_errors_flagged(self):
+ vfi = [{
+ "vars": [{"key": "Translation", "translations": []}],
+ "errors": ["translation_not_dword_aligned"],
+ }]
+ vi = _make_vi(var_file_info=vfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_VARFILEINFO)
+ assert len(details) == 1
+ assert "translation_not_dword_aligned" in details[0]["errors"]
+ assert details[0]["vars"] == 1
+
+ def test_vfi_with_empty_vars_count(self):
+ """vars list empty but errors present — details vars count should be 0."""
+ vfi = [{"vars": [], "errors": ["var_header"]}]
+ vi = _make_vi(var_file_info=vfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_VARFILEINFO)
+ assert details[0]["vars"] == 0
+
+ def test_multiple_vfi_children_each_with_errors(self):
+ vfi = [
+ {"vars": [], "errors": ["var_header"]},
+ {"vars": [], "errors": ["var_length"]},
+ ]
+ vi = _make_vi(var_file_info=vfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ details = _details_for(issues, ReasonCodes.RESOURCE_VERSIONINFO_INVALID_VARFILEINFO)
+ assert len(details) == 2
+
+ def test_vfi_no_errors_not_flagged(self):
+ """A VarFileInfo with vars but no errors produces nothing."""
+ vfi = [{
+ "vars": [{"key": "Translation", "translations": []}],
+ "errors": [],
+ }]
+ vi = _make_vi(var_file_info=vfi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_VARFILEINFO not in _codes(issues)
+
+
+# =================================================================
+# Combination scenarios
+# =================================================================
+
+class TestCombinedAnomalies:
+ """Multiple anomalies in a single blob — each fires independently."""
+
+ def test_szkey_plus_bad_ffi_signature_emits_both(self):
+ ffi = _make_ffi(signature_ok=False, signature=0xDEADBEEF)
+ vi = _make_vi(header_ok=False, fixed_file_info=ffi)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ codes = _codes(issues)
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER in codes
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_FIXEDINFO in codes
+
+ def test_placement_plus_sfi_plus_vfi_all_emit(self):
+ sfi = [{"tables": [], "errors": ["string_table_header"]}]
+ vfi = [{"vars": [], "errors": ["var_header"]}]
+ vi = _make_vi(
+ rva=0x5000, # outside .rsrc
+ size=100,
+ string_file_info=sfi,
+ var_file_info=vfi,
+ )
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ codes = _codes(issues)
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_HEADER in codes
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_STRINGFILEINFO in codes
+ assert ReasonCodes.RESOURCE_VERSIONINFO_INVALID_VARFILEINFO in codes
+
+
+# =================================================================
+# Output shape contract
+# =================================================================
+
+class TestOutputContract:
+ """Pin the shape of returned StructuralIssue objects."""
+
+ def test_returns_list(self):
+ result = validate_version_info({"version_info_struct": _make_vi()}, _make_analysis())
+ assert isinstance(result, list)
+
+ def test_returns_empty_list_for_clean_blob(self):
+ result = validate_version_info({"version_info_struct": _make_vi()}, _make_analysis())
+ assert result == []
+
+ def test_returns_empty_list_when_vi_absent(self):
+ assert validate_version_info({}, _make_analysis()) == []
+ assert validate_version_info({"version_info_struct": None}, _make_analysis()) == []
+
+ def test_each_issue_has_issue_and_details(self):
+ vi = _make_vi(header_ok=False)
+ issues = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ for issue in issues:
+ assert "issue" in issue
+ assert "details" in issue
+ assert isinstance(issue["details"], dict)
+
+
+# =================================================================
+# Determinism
+# =================================================================
+
+class TestDeterminism:
+ """Same input → same output, always."""
+
+ def test_repeated_validation_produces_identical_issues(self):
+ ffi = _make_ffi(signature_ok=False)
+ sfi = [{
+ "tables": [{
+ "lang_codepage": "BAD",
+ "strings": {},
+ "errors": ["lang_codepage_key"],
+ }],
+ "errors": [],
+ }]
+ vfi = [{"vars": [], "errors": ["translation_not_dword_aligned"]}]
+ vi = _make_vi(
+ header_ok=False,
+ fixed_file_info=ffi,
+ string_file_info=sfi,
+ var_file_info=vfi,
+ )
+ metadata = {"version_info_struct": vi}
+ analysis = _make_analysis()
+
+ results = [validate_version_info(metadata, analysis) for _ in range(20)]
+ codes_sequence = [_codes(r) for r in results]
+ for seq in codes_sequence[1:]:
+ assert seq == codes_sequence[0]
+
+ def test_issue_ordering_is_stable(self):
+ """The order in which issues are emitted should be deterministic."""
+ vi = _make_vi(
+ header_ok=False,
+ length_consistent=False,
+ fixed_file_info=_make_ffi(signature_ok=False, struct_version_ok=False),
+ )
+ result1 = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ result2 = validate_version_info({"version_info_struct": vi}, _make_analysis())
+ assert _codes(result1) == _codes(result2)