///|
/// Input event used by the in-memory EVTX writer.
pub(all) struct EventDraft {
provider : String
event_id : UInt
level : Int
timestamp : UInt64
channel : String
computer : String
message : String
} derive(Debug)
///|
fn uint_from_int(n : Int) -> UInt {
n.reinterpret_as_uint()
}
///|
fn align8(n : Int) -> Int {
(n + 7) / 8 * 8
}
///|
fn write_name_string(buf : Array[Byte], chunk_abs : Int, name : String) -> Int {
let rel = buf.length() - chunk_abs
put_u32(buf, 0U)
put_u16(buf, name_hash16(name))
let units = utf16_units(name)
put_u16(buf, units.length())
put_utf16(buf, name)
put_u16(buf, 0)
rel
}
///|
fn write_value_wstring(buf : Array[Byte], text : String) -> Unit {
put_u8(buf, 0x05)
put_u8(buf, 0x01)
let units = utf16_units(text)
put_u16(buf, units.length())
put_utf16(buf, text)
}
///|
fn write_normal_sub(buf : Array[Byte], index : Int, type_code : Int) -> Unit {
put_u8(buf, 0x0D)
put_u16(buf, index)
put_u8(buf, type_code)
}
///|
fn write_attr_name_start(
buf : Array[Byte],
chunk_abs : Int,
name : String,
) -> Unit {
put_u8(buf, 0x06)
let off_pos = buf.length()
put_u32(buf, 0U)
let rel = write_name_string(buf, chunk_abs, name)
write_u32_at(buf, off_pos, uint_from_int(rel))
}
///|
fn write_attr_wstring(
buf : Array[Byte],
chunk_abs : Int,
name : String,
value : String,
) -> Unit {
write_attr_name_start(buf, chunk_abs, name)
write_value_wstring(buf, value)
}
///|
fn write_attr_sub(
buf : Array[Byte],
chunk_abs : Int,
name : String,
index : Int,
type_code : Int,
) -> Unit {
write_attr_name_start(buf, chunk_abs, name)
write_normal_sub(buf, index, type_code)
}
///|
fn write_element_start(
buf : Array[Byte],
chunk_abs : Int,
name : String,
) -> (Int, Int) {
let start = buf.length()
put_u8(buf, 0x01)
put_u16(buf, 0)
let size_pos = buf.length()
put_u32(buf, 0U)
let name_off_pos = buf.length()
put_u32(buf, 0U)
let rel = write_name_string(buf, chunk_abs, name)
write_u32_at(buf, name_off_pos, uint_from_int(rel))
(start, size_pos)
}
///|
fn finish_element(buf : Array[Byte], start : Int, size_pos : Int) -> Unit {
write_u32_at(buf, size_pos, uint_from_int(buf.length() - start))
}
///|
fn write_ascii8_at(buf : Array[Byte], pos : Int, text : String) -> Unit {
for i = 0; i < 8; i = i + 1 {
if i < text.length() {
buf[pos + i] = text[i].to_int().to_byte()
} else {
buf[pos + i] = (0).to_byte()
}
}
}
///|
fn write_event_template(buf : Array[Byte], chunk_abs : Int) -> Unit {
put_u8(buf, 0x0F)
put_u8(buf, 0x01)
put_u16(buf, 0x01)
let (ev_start, ev_size) = write_element_start(buf, chunk_abs, "Event")
write_attr_wstring(
buf, chunk_abs, "xmlns", "http://schemas.microsoft.com/win/2004/08/events/event",
)
put_u8(buf, 0x02)
let (sys_start, sys_size) = write_element_start(buf, chunk_abs, "System")
put_u8(buf, 0x02)
let (p_start, p_size) = write_element_start(buf, chunk_abs, "Provider")
write_attr_sub(buf, chunk_abs, "Name", 0, 0x01)
put_u8(buf, 0x02)
put_u8(buf, 0x04)
finish_element(buf, p_start, p_size)
let (id_start, id_size) = write_element_start(buf, chunk_abs, "EventID")
put_u8(buf, 0x02)
write_normal_sub(buf, 1, 0x08)
put_u8(buf, 0x04)
finish_element(buf, id_start, id_size)
let (lv_start, lv_size) = write_element_start(buf, chunk_abs, "Level")
put_u8(buf, 0x02)
write_normal_sub(buf, 2, 0x04)
put_u8(buf, 0x04)
finish_element(buf, lv_start, lv_size)
let (tm_start, tm_size) = write_element_start(buf, chunk_abs, "TimeCreated")
write_attr_sub(buf, chunk_abs, "SystemTime", 3, 0x11)
put_u8(buf, 0x02)
put_u8(buf, 0x04)
finish_element(buf, tm_start, tm_size)
let (rid_start, rid_size) = write_element_start(
buf, chunk_abs, "EventRecordID",
)
put_u8(buf, 0x02)
write_normal_sub(buf, 4, 0x0A)
put_u8(buf, 0x04)
finish_element(buf, rid_start, rid_size)
let (ch_start, ch_size) = write_element_start(buf, chunk_abs, "Channel")
put_u8(buf, 0x02)
write_normal_sub(buf, 5, 0x01)
put_u8(buf, 0x04)
finish_element(buf, ch_start, ch_size)
let (cp_start, cp_size) = write_element_start(buf, chunk_abs, "Computer")
put_u8(buf, 0x02)
write_normal_sub(buf, 6, 0x01)
put_u8(buf, 0x04)
finish_element(buf, cp_start, cp_size)
put_u8(buf, 0x04)
finish_element(buf, sys_start, sys_size)
let (ed_start, ed_size) = write_element_start(buf, chunk_abs, "EventData")
put_u8(buf, 0x02)
let (d_start, d_size) = write_element_start(buf, chunk_abs, "Data")
write_attr_wstring(buf, chunk_abs, "Name", "Message")
put_u8(buf, 0x02)
write_normal_sub(buf, 7, 0x01)
put_u8(buf, 0x04)
finish_element(buf, d_start, d_size)
put_u8(buf, 0x04)
finish_element(buf, ed_start, ed_size)
put_u8(buf, 0x04)
finish_element(buf, ev_start, ev_size)
put_u8(buf, 0x00)
}
///|
fn write_substitution_table(
buf : Array[Byte],
draft : EventDraft,
record_id : UInt64,
) -> Result[Unit, EvtxError] {
if draft.level < 0 || draft.level > 255 {
return Err(InvalidDraft("level out of range"))
}
put_u32(buf, 8U)
put_u16(buf, utf16_byte_length(draft.provider))
put_u8(buf, 0x01)
put_u8(buf, 0)
put_u16(buf, 4)
put_u8(buf, 0x08)
put_u8(buf, 0)
put_u16(buf, 1)
put_u8(buf, 0x04)
put_u8(buf, 0)
put_u16(buf, 8)
put_u8(buf, 0x11)
put_u8(buf, 0)
put_u16(buf, 8)
put_u8(buf, 0x0A)
put_u8(buf, 0)
put_u16(buf, utf16_byte_length(draft.channel))
put_u8(buf, 0x01)
put_u8(buf, 0)
put_u16(buf, utf16_byte_length(draft.computer))
put_u8(buf, 0x01)
put_u8(buf, 0)
put_u16(buf, utf16_byte_length(draft.message))
put_u8(buf, 0x01)
put_u8(buf, 0)
put_utf16(buf, draft.provider)
put_u32(buf, draft.event_id)
put_u8(buf, draft.level)
put_u64(buf, draft.timestamp)
put_u64(buf, record_id)
put_utf16(buf, draft.channel)
put_utf16(buf, draft.computer)
put_utf16(buf, draft.message)
Ok(())
}
///|
fn u32_at(buf : Array[Byte], pos : Int) -> UInt {
buf[pos].to_int().reinterpret_as_uint() |
(buf[pos + 1].to_int().reinterpret_as_uint() << 8) |
(buf[pos + 2].to_int().reinterpret_as_uint() << 16) |
(buf[pos + 3].to_int().reinterpret_as_uint() << 24)
}
///|
fn write_record(
buf : Array[Byte],
chunk_abs : Int,
record_id : UInt64,
draft : EventDraft,
) -> Result[Int, EvtxError] {
if draft.provider.length() == 0 || draft.channel.length() == 0 {
return Err(InvalidDraft("provider and channel are required"))
}
let rec_start = buf.length()
put_u32(buf, 0x00002A2AU)
let size_pos = buf.length()
put_u32(buf, 0U)
put_u64(buf, record_id)
put_u64(buf, draft.timestamp)
put_u8(buf, 0x0F)
put_u8(buf, 0x01)
put_u16(buf, 0x01)
put_u8(buf, 0x0C)
put_u8(buf, 0x01)
let tid_pos = buf.length()
put_u32(buf, 0U)
let toff_pos = buf.length()
put_u32(buf, 0U)
let tpl_rel = buf.length() - chunk_abs
write_u32_at(buf, toff_pos, uint_from_int(tpl_rel))
put_u32(buf, 0U)
let _ = match encode_guid_text("d4c3b2a1-f6e5-1807-293a-4b5c6d7e8f90", buf) {
Ok(v) => v
Err(e) => return Err(e)
}
let tid = u32_at(buf, buf.length() - 16)
write_u32_at(buf, tid_pos, tid)
let data_len_pos = buf.length()
put_u32(buf, 0U)
let body_start = buf.length()
write_event_template(buf, chunk_abs)
write_u32_at(buf, data_len_pos, uint_from_int(buf.length() - body_start))
let _ = match write_substitution_table(buf, draft, record_id) {
Ok(v) => v
Err(e) => return Err(e)
}
let content = buf.length() - rec_start
let size = align8(content + 4)
while buf.length() < rec_start + size - 4 {
buf.push((0).to_byte())
}
put_u32(buf, uint_from_int(size))
write_u32_at(buf, size_pos, uint_from_int(size))
Ok(size)
}
///|
fn patch_file_header(
buf : Array[Byte],
chunk_count : Int,
next_record : UInt64,
) -> Result[Unit, EvtxError] {
write_ascii8_at(buf, 0, "ElfFile")
write_u64_at(buf, 8, 0UL)
write_u64_at(buf, 16, (chunk_count - 1).to_int64().reinterpret_as_uint64())
write_u64_at(buf, 24, next_record)
write_u32_at(buf, 32, 0x80U)
write_u16_at(buf, 36, 1)
write_u16_at(buf, 38, 3)
write_u16_at(buf, 40, 0x1000)
write_u16_at(buf, 42, chunk_count)
write_u32_at(buf, 0x78, 0U)
let cs = match crc32_slice(Bytes::from_array(buf), 0, 0x78) {
Ok(v) => v
Err(e) => return Err(e)
}
write_u32_at(buf, 0x7C, cs)
Ok(())
}
///|
fn patch_chunk_header(
buf : Array[Byte],
chunk_abs : Int,
first : UInt64,
last : UInt64,
last_off : Int,
next_off : Int,
) -> Result[Unit, EvtxError] {
write_ascii8_at(buf, chunk_abs, "ElfChnk")
write_u64_at(buf, chunk_abs + 8, first)
write_u64_at(buf, chunk_abs + 16, last)
write_u64_at(buf, chunk_abs + 24, first)
write_u64_at(buf, chunk_abs + 32, last)
write_u32_at(buf, chunk_abs + 40, 0x80U)
write_u32_at(buf, chunk_abs + 44, uint_from_int(last_off))
write_u32_at(buf, chunk_abs + 48, uint_from_int(next_off))
let data_cs = match
crc32_slice(Bytes::from_array(buf), chunk_abs + 0x200, chunk_abs + next_off) {
Ok(v) => v
Err(e) => return Err(e)
}
write_u32_at(buf, chunk_abs + 52, data_cs)
let header_cs = match
crc32_two_slices(
Bytes::from_array(buf),
chunk_abs,
chunk_abs + 0x78,
chunk_abs + 0x80,
chunk_abs + 0x200,
) {
Ok(v) => v
Err(e) => return Err(e)
}
write_u32_at(buf, chunk_abs + 0x7C, header_cs)
Ok(())
}
///|
fn finish_chunk(
buf : Array[Byte],
chunk_abs : Int,
first : UInt64,
last : UInt64,
last_off : Int,
) -> Result[Unit, EvtxError] {
let next_off = buf.length() - chunk_abs
let end = chunk_abs + 0x10000
while buf.length() < end {
buf.push((0).to_byte())
}
patch_chunk_header(buf, chunk_abs, first, last, last_off, next_off)
}
///|
/// Encode events as a complete EVTX file (4 KiB header + 64 KiB chunks).
pub fn write_events(events : Array[EventDraft]) -> Result[Bytes, EvtxError] {
if events.length() == 0 {
return Err(InvalidDraft("at least one event is required"))
}
let buf : Array[Byte] = []
ensure_len(buf, 0x1000)
let mut chunk_abs = 0x1000
ensure_len(buf, chunk_abs + 0x200)
let mut first = 1UL
let mut last = 0UL
let mut last_off = 0x200
let mut chunk_count = 1
for i = 0; i < events.length(); i = i + 1 {
let record_id = (i + 1).to_int64().reinterpret_as_uint64()
let rec_rel = buf.length() - chunk_abs
if rec_rel > 0x10000 - 0x400 {
let _ = match finish_chunk(buf, chunk_abs, first, last, last_off) {
Ok(v) => v
Err(e) => return Err(e)
}
chunk_abs = buf.length()
ensure_len(buf, chunk_abs + 0x200)
first = record_id
last = 0UL
last_off = 0x200
chunk_count = chunk_count + 1
}
last_off = buf.length() - chunk_abs
let written = match write_record(buf, chunk_abs, record_id, events[i]) {
Ok(v) => v
Err(e) => return Err(e)
}
if written <= 0 {
return Err(InvalidDraft("empty record"))
}
last = record_id
}
let _ = match finish_chunk(buf, chunk_abs, first, last, last_off) {
Ok(v) => v
Err(e) => return Err(e)
}
let _ = match patch_file_header(buf, chunk_count, last + 1UL) {
Ok(v) => v
Err(e) => return Err(e)
}
Ok(Bytes::from_array(buf))
}
///|
/// Three Application/Security events matching the python-evtx fixture goldens.
pub fn sample_event_drafts() -> Array[EventDraft] {
[
EventDraft::{
provider: "MoonEvtx",
event_id: 1000U,
level: 4,
timestamp: unix_to_filetime(1710000000L),
channel: "Application",
computer: "HOST1",
message: "service started",
},
EventDraft::{
provider: "MoonEvtx",
event_id: 1001U,
level: 2,
timestamp: unix_to_filetime(1710000001L),
channel: "Application",
computer: "HOST1",
message: "disk warning",
},
EventDraft::{
provider: "Security",
event_id: 4624U,
level: 0,
timestamp: unix_to_filetime(1710000002L),
channel: "Security",
computer: "HOST1",
message: "logon success",
},
]
}
///|
pub extend EventDraft with @moonbitlang/core/debug.Debug::{to_repr}