use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::OnceLock;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub use crate::types::{ProcessId, ThreadId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct EventId(u32);
impl EventId {
pub fn new(id: u32) -> Self {
EventId(id)
}
pub fn as_u32(&self) -> u32 {
self.0
}
}
impl std::fmt::Display for EventId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RecordId(u64);
impl RecordId {
pub fn new(id: u64) -> Self {
RecordId(id)
}
pub fn as_u64(&self) -> u64 {
self.0
}
}
impl std::fmt::Display for RecordId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EventLevel {
AuditSuccess = 0,
AuditFailure = 1,
Critical = 2,
Error = 3,
Warning = 4,
#[default]
Informational = 5,
Verbose = 6,
}
impl EventLevel {
pub fn from_code(code: u8) -> Option<Self> {
match code {
0 => Some(EventLevel::AuditSuccess),
1 => Some(EventLevel::AuditFailure),
2 => Some(EventLevel::Critical),
3 => Some(EventLevel::Error),
4 => Some(EventLevel::Warning),
5 => Some(EventLevel::Informational),
6 => Some(EventLevel::Verbose),
_ => None,
}
}
}
impl std::fmt::Display for EventLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EventLevel::AuditSuccess => write!(f, "AuditSuccess"),
EventLevel::AuditFailure => write!(f, "AuditFailure"),
EventLevel::Critical => write!(f, "Critical"),
EventLevel::Error => write!(f, "Error"),
EventLevel::Warning => write!(f, "Warning"),
EventLevel::Informational => write!(f, "Informational"),
EventLevel::Verbose => write!(f, "Verbose"),
}
}
}
struct StringCache {
providers: HashMap<&'static str, Cow<'static, str>>,
channels: HashMap<&'static str, Cow<'static, str>>,
}
impl StringCache {
fn new() -> Self {
let mut cache = StringCache {
providers: HashMap::new(),
channels: HashMap::new(),
};
const COMMON_PROVIDERS: &[&str] = &[
"Security",
"System",
"Application",
"Microsoft-Windows-Sysmon/Operational",
"Microsoft-Windows-PowerShell/Operational",
"Microsoft-Windows-WinRM/Operational",
"Microsoft-Windows-DNS-Client/Operational",
"Perflib",
"Windows Defender",
"Google Chrome",
];
const COMMON_CHANNELS: &[&str] = &[
"Security",
"System",
"Application",
"Operational",
"Analytic",
"Debug",
"Setup",
"Forwarded Events",
];
for provider in COMMON_PROVIDERS {
cache.providers.insert(provider, Cow::Borrowed(provider));
}
for channel in COMMON_CHANNELS {
cache.channels.insert(channel, Cow::Borrowed(channel));
}
cache
}
}
static STRING_CACHE: OnceLock<StringCache> = OnceLock::new();
pub fn intern_provider(name: &str) -> Cow<'static, str> {
let cache = STRING_CACHE.get_or_init(StringCache::new);
if let Some(cached) = cache.providers.get(name) {
return cached.clone();
}
Cow::Owned(name.to_string())
}
pub fn intern_channel(name: &str) -> Cow<'static, str> {
let cache = STRING_CACHE.get_or_init(StringCache::new);
if let Some(cached) = cache.channels.get(name) {
return cached.clone();
}
Cow::Owned(name.to_string())
}
pub fn intern_field_name(name: &str) -> Cow<'static, str> {
match name {
"SubjectUserName" => Cow::Borrowed("SubjectUserName"),
"SubjectDomainName" => Cow::Borrowed("SubjectDomainName"),
"SubjectUserSid" => Cow::Borrowed("SubjectUserSid"),
"SubjectLogonId" => Cow::Borrowed("SubjectLogonId"),
"TargetUserName" => Cow::Borrowed("TargetUserName"),
"TargetDomainName" => Cow::Borrowed("TargetDomainName"),
"TargetUserSid" => Cow::Borrowed("TargetUserSid"),
"TargetLogonId" => Cow::Borrowed("TargetLogonId"),
"LogonType" => Cow::Borrowed("LogonType"),
"IpAddress" => Cow::Borrowed("IpAddress"),
"IpPort" => Cow::Borrowed("IpPort"),
"WorkstationName" => Cow::Borrowed("WorkstationName"),
"AuthenticationPackageName" => Cow::Borrowed("AuthenticationPackageName"),
"ProcessName" => Cow::Borrowed("ProcessName"),
"ProcessId" => Cow::Borrowed("ProcessId"),
"CommandLine" => Cow::Borrowed("CommandLine"),
"NewProcessName" => Cow::Borrowed("NewProcessName"),
"NewProcessId" => Cow::Borrowed("NewProcessId"),
"ParentProcessName" => Cow::Borrowed("ParentProcessName"),
"ObjectName" => Cow::Borrowed("ObjectName"),
"AccessList" => Cow::Borrowed("AccessList"),
"PrivilegeList" => Cow::Borrowed("PrivilegeList"),
"Image" => Cow::Borrowed("Image"),
"ImageLoaded" => Cow::Borrowed("ImageLoaded"),
"ParentImage" => Cow::Borrowed("ParentImage"),
"ParentCommandLine" => Cow::Borrowed("ParentCommandLine"),
"ParentProcessId" => Cow::Borrowed("ParentProcessId"),
"Hashes" => Cow::Borrowed("Hashes"),
"User" => Cow::Borrowed("User"),
"IntegrityLevel" => Cow::Borrowed("IntegrityLevel"),
"SourceIp" => Cow::Borrowed("SourceIp"),
"SourcePort" => Cow::Borrowed("SourcePort"),
"SourceHostname" => Cow::Borrowed("SourceHostname"),
"DestinationIp" => Cow::Borrowed("DestinationIp"),
"DestinationPort" => Cow::Borrowed("DestinationPort"),
"DestinationHostname" => Cow::Borrowed("DestinationHostname"),
"Protocol" => Cow::Borrowed("Protocol"),
"TargetFilename" => Cow::Borrowed("TargetFilename"),
"TargetObject" => Cow::Borrowed("TargetObject"),
"Details" => Cow::Borrowed("Details"),
"ScriptBlockText" => Cow::Borrowed("ScriptBlockText"),
"Path" => Cow::Borrowed("Path"),
"MessageNumber" => Cow::Borrowed("MessageNumber"),
"MessageTotal" => Cow::Borrowed("MessageTotal"),
"EventID" => Cow::Borrowed("EventID"),
"Level" => Cow::Borrowed("Level"),
"Keywords" => Cow::Borrowed("Keywords"),
"Message" => Cow::Borrowed("Message"),
"Data" => Cow::Borrowed("Data"),
_ => Cow::Owned(name.to_string()),
}
}
#[derive(Debug, Clone, Default)]
pub struct Event {
pub id: EventId,
pub level: EventLevel,
pub provider: Cow<'static, str>,
pub channel: Cow<'static, str>,
pub computer: String,
pub timestamp: Option<SystemTime>,
pub record_id: Option<RecordId>,
pub process_id: Option<ProcessId>,
pub thread_id: Option<ThreadId>,
pub data: Option<std::collections::HashMap<Cow<'static, str>, String>>,
pub formatted_message: Option<String>,
}
impl Event {
pub fn new() -> Self {
Event {
id: EventId(0),
level: EventLevel::Informational,
provider: Cow::Borrowed(""),
channel: Cow::Borrowed(""),
computer: String::new(),
timestamp: None,
record_id: None,
process_id: None,
thread_id: None,
data: None,
formatted_message: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderFormat {
Values,
Xml,
}
#[derive(Debug, Clone)]
pub struct CorruptedEvent {
pub record_id: Option<u64>,
pub component: Cow<'static, str>,
pub reason: Cow<'static, str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChannelFilter {
All,
Operational,
AdminOrHigher,
IncludeAnalytic,
}
#[derive(Debug, Clone, Default)]
pub struct EventQueryResult {
pub events: Vec<Event>,
pub corrupted: Vec<CorruptedEvent>,
pub total_processed: usize,
}
use crate::error::{Error, Result};
use std::ffi::c_void;
use windows::Win32::System::EventLog::*;
pub fn extract_event_id(handle: EVT_HANDLE) -> Result<u32> {
extract_variant_field(handle, 0).and_then(|v| {
unsafe {
if v.Type == 8u32 {
Ok(v.Anonymous.UInt32Val)
} else {
Err(Error::Other(crate::error::OtherError::new(
"EventID field is not UInt32",
)))
}
}
})
}
pub fn extract_provider(handle: EVT_HANDLE) -> Result<String> {
extract_variant_field(handle, 2).and_then(|v| {
unsafe {
if v.Type == 21u32 {
let pwstr = v.Anonymous.StringVal;
if !pwstr.is_null() {
let len = (0..).take_while(|&i| *pwstr.0.offset(i) != 0).count();
let slice = std::slice::from_raw_parts(pwstr.0, len);
Ok(String::from_utf16_lossy(slice))
} else {
Err(Error::Other(crate::error::OtherError::new(
"Provider field is null",
)))
}
} else {
Err(Error::Other(crate::error::OtherError::new(
"Provider field is not string",
)))
}
}
})
}
pub fn extract_level(handle: EVT_HANDLE) -> Result<u8> {
extract_variant_field(handle, 1).and_then(|v| {
unsafe {
if v.Type == 2u32 {
Ok(v.Anonymous.ByteVal)
} else {
Err(Error::Other(crate::error::OtherError::new(
"Level field is not byte",
)))
}
}
})
}
pub fn extract_timestamp(handle: EVT_HANDLE) -> Result<SystemTime> {
extract_variant_field(handle, 5).and_then(|v| {
unsafe {
if v.Type == 17u32 {
let filetime = v.Anonymous.FileTimeVal;
let intervals = filetime * 100;
let duration = Duration::from_nanos(intervals);
let epoch = UNIX_EPOCH - Duration::from_secs(11644473600); Ok(epoch + duration)
} else {
Err(Error::Other(crate::error::OtherError::new(
"Timestamp field is not FILETIME",
)))
}
}
})
}
pub fn extract_record_id(handle: EVT_HANDLE) -> Result<u64> {
extract_variant_field(handle, 6).and_then(|v| {
unsafe {
if v.Type == 10u32 {
Ok(v.Anonymous.UInt64Val)
} else {
Err(Error::Other(crate::error::OtherError::new(
"RecordID field is not UInt64",
)))
}
}
})
}
fn extract_variant_field(event_handle: EVT_HANDLE, field_index: usize) -> Result<EVT_VARIANT> {
let mut buffer = vec![0u8; 8192];
let mut buffer_used = 0u32;
let mut prop_count = 0u32;
let result = unsafe {
EvtRender(
EVT_HANDLE::default(),
event_handle,
EvtRenderEventValues.0,
buffer.len() as u32,
Some(buffer.as_mut_ptr() as *mut c_void),
&mut buffer_used,
&mut prop_count,
)
};
if result.is_err() {
return Err(Error::Other(crate::error::OtherError::new(
"Failed to render event values",
)));
}
let variant_ptr = buffer.as_ptr() as *const EVT_VARIANT;
let variants = unsafe { std::slice::from_raw_parts(variant_ptr, prop_count as usize) };
if field_index >= variants.len() {
return Err(Error::Other(crate::error::OtherError::new(Cow::Owned(
format!("Field index {} out of range", field_index),
))));
}
Ok(variants[field_index])
}