evtxtools 1.12.1

tools for the analysis of evtx files
Documentation
use std::{collections::{BTreeMap, HashMap}, rc::Weak, cell::RefCell, fmt::Display};

use anyhow::bail;
use chrono::{DateTime, Utc};
use evtx::SerializedEvtxRecord;
use serde_json::{Value, json};


pub (crate) struct Process {
    pub (crate) timestamp: DateTime<Utc>,
    pub (crate) event_record_id: u64,
    pub (crate) subject_user_sid: String,
    pub (crate) subject_user_name: String,
    pub (crate) subject_domain_name: String,
    pub (crate) subject_logon_id: String,
    pub (crate) new_process_id: u64,
    pub (crate) new_process_name: String,
    pub (crate) token_elevation_type: String,
    pub (crate) process_id: u64,
    pub (crate) command_line: String,
    pub (crate) target_user_sid: String,
    pub (crate) target_user_name: String,
    pub (crate) target_domain_name: String,
    pub (crate) target_logon_id: String,
    pub (crate) parent_process_name: Option<String>,
    pub (crate) mandatory_label: Option<String>,
    pub (crate) children: BTreeMap<DateTime<Utc>, Weak<RefCell<Self>>>,
    pub (crate) is_root: bool,
}

impl From<&Process> for Value {
    fn from(process: &Process) -> Self {
        let children: BTreeMap<_, _> = process
            .children
            .values()
            .filter_map(|x| x.upgrade())
            .map(|p| {
                let p: &Process = &p.borrow();
                let v: Value = p.into();
                (p.timestamp, v)
            })
            .collect();
        let mut result: HashMap<_, _> = vec![
            ("timestamp".to_owned(), json!(process.timestamp)),
            ("event_record_id".to_owned(), json!(process.event_record_id)),
            ("SubjectUserSid".to_owned(), json!(process.subject_user_sid)),
            (
                "SubjectUserName".to_owned(),
                json!(process.subject_user_name),
            ),
            (
                "SubjectDomainName".to_owned(),
                json!(process.subject_domain_name),
            ),
            ("SubjectLogonId".to_owned(), json!(process.subject_logon_id)),
            ("NewProcessId".to_owned(), json!(process.new_process_id)),
            ("NewProcessName".to_owned(), json!(process.new_process_name)),
            (
                "TokenElevationType".to_owned(),
                json!(process.token_elevation_type),
            ),
            ("ProcessId".to_owned(), json!(process.process_id)),
            ("CommandLine".to_owned(), json!(process.command_line)),
            ("TargetUserSid".to_owned(), json!(process.target_user_sid)),
            ("TargetUserName".to_owned(), json!(process.target_user_name)),
            (
                "TargetDomainName".to_owned(),
                json!(process.target_domain_name),
            ),
            ("TargetLogonId".to_owned(), json!(process.target_logon_id)),
            (
                "ParentProcessName".to_owned(),
                json!(process.parent_process_name),
            ),
            ("MandatoryLabel".to_owned(), json!(process.mandatory_label)),
        ]
        .into_iter()
        .collect();

        result.extend(
            children
                .into_iter()
                .map(|(k, v)| (k.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), v)),
        );

        json!(result)
    }
}

impl Display for Process {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "`{}` (`0x{:04x}`, created *`{}`*, user is `{}`)",
            self.new_process_name,
            self.new_process_id,
            self.timestamp
                .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
            self.target_user_name
        )
    }
}

macro_rules! from_json {
    ($value: ident, $( $att:expr ),+ ) => {
        {
            let mut value = $value;
            $(
                value = value.get($att).ok_or(anyhow::anyhow!("missing '{}' key in {}", $att, value))?;
            )+
            value
        }
    };
}

macro_rules! from_json_or_null {
    ($value: ident, $( $att:expr ),+) => {
        {
            let mut value = $value;
            $(
                value = value.get($att).or(Some(&Value::Null)).unwrap();
            )+
            value
        }
    };
}

fn u64_from_value(value: &Value) -> anyhow::Result<u64> {
    if let Some(v) = value.as_u64() {
        Ok(v)
    } else {
        bail!("Value '{value}' is no u64")
    }
}
fn u64_from_hex_value(value: &Value) -> anyhow::Result<u64> {
    if let Some(v) = value.as_str() {
        Ok(u64::from_str_radix(v.trim_start_matches("0x"), 16)?)
    } else {
        bail!("Value '{value}' is no string")
    }
}

impl Process {
    pub fn try_from(record: SerializedEvtxRecord<Value>) -> anyhow::Result<Option<Self>> {
        let value = &record.data;
        let event = from_json!(value, "Event");
        let system = from_json!(event, "System");
        let event_id = u64_from_value({
            let event_id = from_json!(system, "EventID");
            match event_id.get("#text") {
                Some(eid) => eid,
                None => event_id,
            }
        })?;

        if event_id != 4688 {
            return Ok(None);
        }

        let event_data = from_json!(event, "EventData");

        let subject_user_sid = from_json!(event_data, "SubjectUserSid")
            .as_str()
            .unwrap()
            .into();
        let subject_user_name = from_json!(event_data, "SubjectUserName")
            .as_str()
            .unwrap()
            .into();
        let subject_domain_name = from_json!(event_data, "SubjectDomainName")
            .as_str()
            .unwrap()
            .into();
        let subject_logon_id = from_json!(event_data, "SubjectLogonId")
            .as_str()
            .unwrap()
            .into();
        let new_process_id = u64_from_hex_value(from_json!(event_data, "NewProcessId"))?;
        let new_process_name = from_json!(event_data, "NewProcessName")
            .as_str()
            .unwrap()
            .into();
        let token_elevation_type = from_json!(event_data, "TokenElevationType")
            .as_str()
            .unwrap()
            .into();
        let process_id = u64_from_hex_value(from_json!(event_data, "ProcessId"))?;
        let command_line = from_json!(event_data, "CommandLine")
            .as_str()
            .unwrap()
            .into();
        let target_user_sid = from_json!(event_data, "TargetUserSid")
            .as_str()
            .unwrap()
            .into();
        let target_user_name = from_json!(event_data, "TargetUserName")
            .as_str()
            .unwrap()
            .into();
        let target_domain_name = from_json!(event_data, "TargetDomainName")
            .as_str()
            .unwrap()
            .into();
        let target_logon_id = from_json!(event_data, "TargetLogonId")
            .as_str()
            .unwrap()
            .into();
        let parent_process_name = from_json_or_null!(event_data, "ParentProcessName")
            .as_str()
            .map(|s|s.to_owned());
        let mandatory_label = from_json_or_null!(event_data, "MandatoryLabel")
            .as_str()
            .map(|s|s.to_owned());

        Ok(Some(Self {
            timestamp: record.timestamp,
            event_record_id: record.event_record_id,
            subject_user_sid,
            subject_user_name,
            subject_domain_name,
            subject_logon_id,
            new_process_id,
            new_process_name,
            token_elevation_type,
            process_id,
            command_line,
            target_user_sid,
            target_user_name,
            target_domain_name,
            target_logon_id,
            parent_process_name,
            mandatory_label,
            children: Default::default(),
            is_root: true,
        }))
    }
}