moonevtx

    Windows EVTX and BINXML codec with CRC verification, XML rendering and an in-memory writer

    evtx
    binxml
    windows-event-log
    forensics
    python-evtx
    Download zip
    Author
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    31

    No dependencies

    #MoonEvtx

    MoonBit library for Windows Event Log .evtx files. It reads the 4 KiB file header, 64 KiB chunks, record stream and Microsoft BINXML, checks CRC-32 the same way python-evtx does, renders XML/JSON, filters extracted events, and writes a well-formed EVTX buffer in memory.

    This is a behavior port of python-evtx (Apache-2.0), not a Windows Event Log API wrapper and not a text XML parser.

    #Why it exists

    Forensics and IR tools often need to inspect .evtx without wevtutil or Win32. MoonBit did not have an EVTX/BINXML codec. Adjacent packages cover live Win32 FFI, application loggers, or text XML; they do not decode EVTX chunks.

    #Requirements

    • MoonBit toolchain with moonc >= 0.10.14
    • A C compiler is required only for local native checks/tests; the portable wasm-gc and js targets need no system compiler

    #Install

    moon add kekeshuo/moonevtx@0.1.1

    Or copy this repository and run moon check. The examples in this repository use the dependency alias @evtx.

    #Example

    let bytes = @evtx.write_events(@evtx.sample_event_drafts()).unwrap()
    let file = @evtx.parse_evtx(bytes).unwrap()
    file.verify().unwrap()
    let xml = file.records[0].xml(@evtx.PythonEvtx).unwrap()
    let events = file.events(@evtx.PythonEvtx).unwrap()

    Runnable programs:

    moon run examples/inspect moon run examples/filter moon run examples/roundtrip

    inspect prints header/chunk CRC status, EventID 1000/1001/4624, and python-evtx-compatible XML.

    #What it does

    • Parse ElfFile / ElfChnk / record magic 0x00002a2a
    • CRC-32 over header 0x00..0x78 and chunk header+tables / record payload
    • Recursive-descent BINXML (open/close, attributes, substitutions, resident templates)
    • XML rendering matching python-evtx Views.py
    • Event field extraction, filter, dump, JSON
    • In-memory writer: 4096-byte header + 65536-byte chunks, 8-byte record padding, resident templates

    #What it does not do

    • Call OpenEventLog / wevtutil
    • Parse text XML or Windows XML Eventing XSLT
    • Ship third-party captured logs (tests generate fixtures)

    #Tests

    The CI job checks all supported backends, builds the library, and runs the core suite:

    moon check --target wasm-gc --deny-warn moon check --target wasm --deny-warn moon check --target js --deny-warn moon check --target native --deny-warn moon build --target wasm-gc moon test --target wasm-gc moon test --target js moon test --target native

    Goldens: 3 records, EventIDs 1000/1001/4624, TimeCreated 2024-03-09 16:00:00+00:00, record sizes 896/888/888, file length 69632, header/chunk CRC verify.

    #License

    Apache-2.0. See LICENSE and NOTICE.

    BinAttr

    pub(all) struct BinAttr {
    name : String
    value : BinNode
    } derive(
    Debug
    )

    One attribute on an open-start element.

    BinAttr::to_repr

    BinNode

    pub(all) enum BinNode {
    EndOfStream
    StreamStart
    OpenStart(String, Array[BinAttr], Array[BinNode])
    ValueText(String)
    CData(String)
    CharRef(Int)
    EntityRef(String)
    PITarget(String)
    PIData(String)
    NormalSub(Int, Int)
    OptionalSub(Int, Int)
    } derive(
    Debug
    )

    Procedural BINXML node. Close tokens are consumed and not stored.

    BinNode::to_repr

    ChunkHeader

    pub(all) struct ChunkHeader {
    offset : Int
    first_record : UInt64
    last_record : UInt64
    log_first_record : UInt64
    log_last_record : UInt64
    header_size : UInt
    last_record_offset : UInt
    next_record_offset : UInt
    data_checksum : UInt
    header_checksum : UInt
    header_ok : Bool
    data_ok : Bool
    } derive(
    Debug
    )

    One 64 KiB chunk header.

    Event

    pub(all) struct Event {
    record_id : UInt64
    event_id : UInt
    level : Int
    provider : String
    channel : String
    computer : String
    timestamp : UInt64
    time_text : String
    message : String
    data : Array[(String, String)]
    } derive(
    Debug
    )

    Extracted Windows Event Log fields.

    Event::json

    fn Event::json(self : Event) -> String

    JSON object for one extracted event.

    Event::matches

    fn Event::matches(self : Event, spec : FilterSpec) -> Bool

    Event::to_repr

    EventDraft

    pub(all) struct EventDraft {
    provider : String
    event_id : UInt
    level : Int
    timestamp : UInt64
    channel : String
    computer : String
    message : String
    } derive(
    Debug
    )

    Input event used by the in-memory EVTX writer.

    EvtxError

    pub(all) enum EvtxError {
    Truncated(String, Int, Int)
    InvalidMagic(String, String)
    InvalidChecksum(String, UInt, UInt)
    InvalidHeader(String)
    InvalidChunk(String)
    InvalidRecord(String)
    UnexpectedToken(Int, Int)
    InvalidName(String)
    UnsupportedType(Int, Int)
    InvalidSubstitution(String)
    InvalidDraft(String)
    InvalidXmlName(String)
    } derive(Eq,
    Debug
    )

    Stable errors returned by the EVTX decoder, renderer and writer.

    EvtxError::code

    fn EvtxError::code(self : EvtxError) -> String

    EvtxError::equal

    fn EvtxError::equal(EvtxError, EvtxError) -> Bool

    EvtxError::message

    fn EvtxError::message(self : EvtxError) -> String

    EvtxError::not_equal

    fn EvtxError::not_equal(x : EvtxError, y : EvtxError) -> Bool

    EvtxFile

    pub(all) struct EvtxFile {
    data : Bytes
    header : FileHeader
    chunks : Array[ChunkHeader]
    records : Array[EvtxRecord]
    }

    In-memory EVTX file.

    EvtxFile::events

    fn EvtxFile::events(self : EvtxFile, style : TimeStyle) -> Result[Array[Event], EvtxError]

    EvtxFile::filter

    fn EvtxFile::filter(self : EvtxFile, spec : FilterSpec, style : TimeStyle) -> Result[Array[Event], EvtxError]

    EvtxFile::is_dirty

    fn EvtxFile::is_dirty(self : EvtxFile) -> Bool

    True when the log was opened dirty (flag bit 0).

    EvtxFile::is_full

    fn EvtxFile::is_full(self : EvtxFile) -> Bool

    True when the log reported a full-file write failure (flag bit 1).

    EvtxFile::record_count

    fn EvtxFile::record_count(self : EvtxFile) -> Int

    EvtxFile::verify

    fn EvtxFile::verify(self : EvtxFile) -> Result[Unit, EvtxError]

    Fail if header/chunk checksums do not match python-evtx.

    EvtxFile::xml_records

    fn EvtxFile::xml_records(self : EvtxFile, style : TimeStyle) -> Result[Array[String], EvtxError]

    EvtxRecord

    pub(all) struct EvtxRecord {
    number : UInt64
    timestamp : UInt64
    size : UInt
    chunk_offset : Int
    offset : Int
    root : ParsedRoot
    } derive(
    Debug
    )

    Parsed event record.

    EvtxRecord::event

    fn EvtxRecord::event(self : EvtxRecord, style : TimeStyle) -> Result[Event, EvtxError]

    Pull conventional System/EventData fields out of a parsed record.

    EvtxRecord::hexdump

    fn EvtxRecord::hexdump(self : EvtxRecord, file : EvtxFile) -> Result[String, EvtxError]

    EvtxRecord::xml

    fn EvtxRecord::xml(self : EvtxRecord, style : TimeStyle) -> Result[String, EvtxError]

    FileHeader

    pub(all) struct FileHeader {
    oldest_chunk : UInt64
    current_chunk_number : UInt64
    next_record_number : UInt64
    header_size : UInt
    minor_version : Int
    major_version : Int
    header_chunk_size : Int
    chunk_count : Int
    flags : UInt
    checksum : UInt
    checksum_ok : Bool
    } derive(
    Debug
    )

    EVTX file header (4096-byte prefix, checksum over the first 0x78 bytes).

    FilterSpec

    pub(all) struct FilterSpec {
    event_id : UInt?
    provider : String?
    channel : String?
    computer : String?
    level : Int?
    contains : String?
    since : UInt64?
    until : UInt64?
    } derive(
    Debug
    )

    Predicate over extracted events. Absent fields are ignored.

    ParsedRoot

    pub(all) struct ParsedRoot {
    template : Array[BinNode]
    substitutions : Array[SubCell]
    } derive(
    Debug
    )

    Root BINXML: template body plus substitution table.

    SubCell

    pub(all) struct SubCell {
    type_code : Int
    payload : Bytes
    nested : Array[ParsedRoot]
    } derive(
    Debug
    )

    Substitution cell. Nested BINXML lives in nested when type is BXml.

    SubCell::to_repr

    TimeStyle

    pub(all) enum TimeStyle {
    PythonEvtx
    Iso8601
    } derive(Eq,
    Debug
    )

    FILETIME / SYSTEMTIME rendering style.

    TimeStyle::equal

    fn TimeStyle::equal(TimeStyle, TimeStyle) -> Bool

    TimeStyle::not_equal

    fn TimeStyle::not_equal(x : TimeStyle, y : TimeStyle) -> Bool

    append_bytes

    fn append_bytes(target : Array[Byte], source : Bytes) -> Unit

    Append immutable bytes onto a growable buffer.

    base64_encode

    fn base64_encode(data : Bytes) -> String

    Standard Base64 with = padding, matching python-evtx BinaryTypeNode.

    bytes_from_ascii

    fn bytes_from_ascii(text : String) -> Bytes

    bytes_slice

    fn bytes_slice(data : Bytes, start : Int, end : Int) -> Bytes

    Copy [start, end) out of an immutable buffer.

    bytes_slice_result

    fn bytes_slice_result(data : Bytes, start : Int, end : Int) -> Result[Bytes, EvtxError]

    Bounded slice that fails instead of panicking.

    concat_bytes

    fn concat_bytes(left : Bytes, right : Bytes) -> Bytes

    Concatenate two byte strings.

    crc32_bytes

    fn crc32_bytes(data : Bytes) -> UInt

    CRC-32 of a whole byte string.

    crc32_slice

    fn crc32_slice(data : Bytes, start : Int, end : Int) -> Result[UInt, EvtxError]

    CRC-32 of data[start, end) with the ISO 3309 final XOR.

    crc32_two_slices

    fn crc32_two_slices(data : Bytes, a0 : Int, a1 : Int, b0 : Int, b1 : Int) -> Result[UInt, EvtxError]

    Concatenate two ranges before hashing, matching chunk header checksums.

    dump_file

    fn dump_file(file : EvtxFile, style : TimeStyle) -> Result[String, EvtxError]

    Human-readable file dump covering header, chunks and record summaries.

    empty_filter

    fn empty_filter() -> FilterSpec

    Empty filter that keeps every event.

    encode_guid_text

    fn encode_guid_text(text : String, buf : Array[Byte]) -> Result[Unit, EvtxError]

    Encode a mixed-endian GUID from an already-formatted lowercase string.

    escape_xml_value

    fn escape_xml_value(text : String) -> String

    Escape a BINXML string for an XML text node, matching Views.py: xml.sax.saxutils.escape then xmlcharrefreplace then drop restricted chars.

    events_json

    fn events_json(events : Array[Event]) -> String

    file_stats

    fn file_stats(file : EvtxFile, style : TimeStyle) -> Result[String, EvtxError]

    Count records by EventID / channel / level.

    filetime_to_unix

    fn filetime_to_unix(filetime : UInt64) -> Int64

    Convert a Windows FILETIME (100-ns ticks from 1601-01-01) to Unix seconds.

    filter_events

    fn filter_events(events : Array[Event], spec : FilterSpec) -> Array[Event]

    format_filetime

    fn format_filetime(filetime : UInt64, style : TimeStyle) -> String

    python-evtx datetime.isoformat(" ") for UTC FILETIME values.

    format_guid

    fn format_guid(data : Bytes, pos : Int) -> Result[String, EvtxError]

    python-evtx unpack_guid: mixed-endian, lowercase, no braces.

    format_guid_node

    fn format_guid_node(data : Bytes, pos : Int) -> Result[String, EvtxError]

    GuidTypeNode.string() wraps unpack_guid in braces.

    format_sid

    fn format_sid(data : Bytes, pos : Int, length : Int) -> Result[(String, Int), EvtxError]

    Windows SID string S-{version}-{authority}-{rid}*.

    format_systemtime

    fn format_systemtime(data : Bytes, pos : Int, style : TimeStyle) -> Result[String, EvtxError]

    SYSTEMTIME: year, month, dow, day, hour, minute, second, millisecond.

    format_variant

    fn format_variant(data : Bytes, pos : Int, type_code : Int, length : Int?, style : TimeStyle) -> Result[(String, Int), EvtxError]

    Format a variant payload using python-evtx VariantTypeNode.string() rules.

    hex_decode

    fn hex_decode(text : String) -> Result[Bytes, EvtxError]

    Decode a hex string into bytes. Odd length is rejected.

    hex_encode

    fn hex_encode(data : Bytes) -> String

    Encode bytes as lowercase hexadecimal.

    hexdump_range

    fn hexdump_range(data : Bytes, start : Int, end : Int) -> Result[String, EvtxError]

    Hex dump of a record body, 16 bytes per row.

    parse_evtx

    fn parse_evtx(data : Bytes) -> Result[EvtxFile, EvtxError]

    Parse an EVTX buffer. Checksums are recorded rather than required.

    parse_evtx_styled

    fn parse_evtx_styled(data : Bytes, style : TimeStyle) -> Result[EvtxFile, EvtxError]

    Parse with an explicit FILETIME rendering style used while walking BINXML values.

    render_root

    fn render_root(root : ParsedRoot, style : TimeStyle) -> Result[String, EvtxError]

    Render a parsed root using python-evtx Views.render_root_node_with_subs.

    sample_event_drafts

    fn sample_event_drafts() -> Array[EventDraft]

    Three Application/Security events matching the python-evtx fixture goldens.

    text_contains

    fn text_contains(hay : String, needle : String) -> Bool

    unix_to_filetime

    fn unix_to_filetime(unix : Int64) -> UInt64

    Encode Unix seconds as FILETIME.

    utf16_units

    fn utf16_units(text : String) -> Array[Int]

    Encode Unicode text as UTF-16LE code units without a terminator.

    validate_xml_name

    fn validate_xml_name(name : String) -> Result[String, EvtxError]

    Stricter XML Name check used by python-evtx Views.validate_name.

    variant_type_name

    fn variant_type_name(type_code : Int) -> String

    BINXML / substitution type codes used by python-evtx NODE_TYPES.

    write_events

    fn write_events(events : Array[EventDraft]) -> Result[Bytes, EvtxError]

    Encode events as a complete EVTX file (4 KiB header + 64 KiB chunks).