ntdsextract2 1.4.21

Display contents of Active Directory database files (ntds.dit)
use crate::cache::{self, MetaDataCache, RecordId, RecordPointer};
use crate::cache::{ColumnIndex, WithValue};
use crate::ntds::{Error, NtdsAttributeId, SdTable};
use crate::value::FromValue;
use crate::win32_types::{
    Rdn, SamAccountType, TruncatedWindowsFileTime, UserAccountControl, WindowsFileTime,
};
use crate::win32_types::{SecurityDescriptor, TimelineEntry};
use crate::ColumnInfoMapping;
use bodyfile::Bodyfile3Line;
use chrono::{DateTime, Utc};
use concat_idents::concat_idents;
use dfir_windows_types::{Guid, Sid};
use flow_record::derive::*;
use flow_record::prelude::*;
use getset::Getters;
use serde::ser::SerializeStruct;
use serde::Serialize;
use std::collections::HashMap;
use std::sync::Arc;
use term_table::row::Row;
use term_table::table_cell::{Alignment, TableCell};

use super::{AttributeName, AttributeValue};

#[derive(Getters, Serialize)]
#[getset(get = "pub")]
pub struct EntryAttribute<'info, 'db, 'record> {
    column: String,
    attribute: AttributeName,
    value: AttributeValue,
    r#type: &'static str,
    original_record: &'record DataTableRecord<'info, 'db>,
}

#[derive(Getters)]
pub struct DataTableRecord<'info, 'db> {
    inner: cache::Record<'info, 'db>,

    #[getset(get = "pub")]
    ptr: RecordPointer,
}

macro_rules! record_attribute {
    ($name: ident, $id: ident, $type: ty) => {
        pub fn $name(&self) -> crate::ntds::Result<$type> {
            self.get_value(NtdsAttributeId::$id)
        }

        concat_idents!(fn_name=$name, _opt {
            pub fn fn_name(&self) -> crate::ntds::Result<Option<$type>> {
                self.get_value_opt(NtdsAttributeId::$id)
            }
        });

        concat_idents!(fn_name=has_, $name {
            pub fn fn_name(&self, other: &$type) -> crate::ntds::Result<bool> {
                self.has_value(NtdsAttributeId::$id, other)
            }
        });
    };
}

macro_rules! record_attributes {
    [ $( ( $name: ident, $id: ident, $type: ty) ),+ $(,)? ] => {
        $(record_attribute!($name, $id, $type); )+
        pub fn is_known_attribute(id: NtdsAttributeId) -> bool {match id {
                $(
                    $crate::ntds::attribute_id::NtdsAttributeId::$id
                )|+ => true,
                _ => false
            }
        }
        pub fn format_attribute(&self, id: NtdsAttributeId) -> crate::ntds::Result<Option<String>> {
            match id {
                $(
                    $crate::ntds::attribute_id::NtdsAttributeId::$id => {
                        concat_idents!(fn_name=$name, _opt {Ok(self.fn_name()?.as_ref().map(|v| v.to_string()))})}
                ),+
                _ => self.inner.with_value(id, |v| Ok(v.map(|v| v.to_string())))
            }
        }
    };
}

impl<'info, 'db> DataTableRecord<'info, 'db> {
    record_attributes![
        (ds_record_id, DsRecordId, RecordId),
        (object_category, AttObjectCategory, RecordId),
        //(att_object_type_id, AttObjectCategory, RecordId),
        (ds_parent_record_id, DsParentRecordId, RecordId),
        (ds_record_time, DsRecordTime, TruncatedWindowsFileTime),
        (ds_ancestors, DsAncestors, i32),
        (att_object_sid, AttObjectSid, Sid),
        (att_when_created, AttWhenCreated, TruncatedWindowsFileTime),
        (att_when_changed, AttWhenChanged, TruncatedWindowsFileTime),
        (att_object_name, AttCommonName, Rdn),
        (att_object_name2, AttRdn, Rdn),
        (att_sam_account_name, AttSamAccountName, String),
        (att_sam_account_type, AttSamAccountType, SamAccountType),
        (att_user_principal_name, AttUserPrincipalName, String),
        (att_service_principal_name, AttServicePrincipalName, String),
        (
            att_user_account_control,
            AttUserAccountControl,
            UserAccountControl
        ),
        (att_last_logon, AttLastLogon, WindowsFileTime),
        (
            att_last_logon_time_stamp,
            AttLastLogonTimestamp,
            WindowsFileTime
        ),
        (att_account_expires, AttAccountExpires, WindowsFileTime),
        (att_password_last_set, AttPwdLastSet, WindowsFileTime),
        (att_bad_pwd_time, AttBadPasswordTime, WindowsFileTime),
        (att_logon_count, AttLogonCount, i32),
        (att_bad_pwd_count, AttBadPwdCount, i32),
        (att_primary_group_id, AttPrimaryGroupId, i32),
        (att_comment, AttComment, String),
        (att_dns_host_name, AttDnsHostName, String),
        (att_os_name, AttOperatingSystem, String),
        (att_os_version, AttOperatingSystemVersion, String),
        (att_link_id, AttLinkId, u32),
        (att_ldap_display_name, AttLdapDisplayName, String),
        (att_creator_sid, AttMsDsCreatorSid, Sid),
        (att_admin_count, AttAdminCount, i32),
        (att_is_deleted, AttIsDeleted, bool),
        (att_last_known_parent, AttLastKnownParent, RecordId),
        (att_nt_security_descriptor, AttNtSecurityDescriptor, i64),
        (att_object_guid, AttObjectGuid, Guid),
        (att_rights_guid, AttRightsGuid, Guid),
    ];
}

impl<'info, 'db> DataTableRecord<'info, 'db> {
    pub fn new(inner: cache::Record<'info, 'db>, ptr: RecordPointer) -> Self {
        Self { inner, ptr }
    }

    fn get_value<T>(&self, column: NtdsAttributeId) -> crate::ntds::Result<T>
    where
        T: FromValue,
    {
        self.inner.with_value(column, |v| match v {
            None => Err(Error::ValueIsMissing),
            Some(v) => Ok(<T>::from_value(v)?),
        })
    }
    fn get_value_opt<T>(&self, column: NtdsAttributeId) -> crate::ntds::Result<Option<T>>
    where
        T: FromValue,
    {
        self.inner.with_value(column, |v| match v {
            None => Ok(None),
            Some(v) => Ok(Some(<T>::from_value(v)?)),
        })
    }
    fn has_value<T>(&self, column: NtdsAttributeId, other: &T) -> crate::ntds::Result<bool>
    where
        T: FromValue + Eq,
    {
        self.inner.with_value(column, |v| match v {
            None => Ok(false),
            Some(v) => Ok(&(<T>::from_value(v)?) == other),
        })
    }

    pub fn mapping(&self) -> &ColumnInfoMapping {
        self.inner.esedbinfo().mapping()
    }
    pub fn all_attributes(&self) -> HashMap<NtdsAttributeId, EntryAttribute> {
        (0..*self.inner.count())
            .map(ColumnIndex::from)
            .filter_map(|idx| {
                let column = &self.inner.columns()[idx];
                if column.attribute_id().is_some() {
                    Some(column)
                } else {
                    None
                }
            })
            .map(|column| {
                self.inner.with_value(*column.index(), |v| {
                    Ok(v.map(|x| {
                        (
                            column.attribute_id().unwrap(),
                            EntryAttribute {
                                column: column.name().to_string(),
                                attribute: column
                                    .attribute_name()
                                    .as_ref()
                                    .cloned()
                                    .unwrap_or(AttributeName::from(column.name().to_string())),
                                value: AttributeValue::from(x.to_string()),
                                r#type: x.type_name(),
                                original_record: self,
                            },
                        )
                    }))
                })
            })
            .filter_map(Result::ok)
            .flatten()
            .collect()
    }

    pub fn object_type_name(&self, metadata: &MetaDataCache) -> anyhow::Result<String> {
        Ok(if let Some(type_id) = self.object_category_opt()? {
            metadata
                .record(&type_id)
                .map(|entry| entry.rdn().name().to_string())
                .unwrap_or("Object".to_string())
        } else {
            "Object".to_string()
        })
    }

    pub fn to_bodyfile(&self, metadata: &MetaDataCache) -> anyhow::Result<Vec<Bodyfile3Line>> {
        let my_name = self
            .att_sam_account_name()
            .or(self.att_object_name().map(|s| s.name().to_string()));

        let object_type_name = self.object_type_name(metadata)?;
        let object_type_caption =
            if let Some(last_known_parent) = self.att_last_known_parent_opt()? {
                metadata
                    .record(&last_known_parent)
                    .and_then(|entry| metadata.dn(entry))
                    .map(|e| format!("{object_type_name}, deleted from {e}"))
                    .unwrap_or(format!("deleted {object_type_name}"))
            } else if self.att_is_deleted_opt()?.unwrap_or(false) {
                format!("deleted {object_type_name}")
            } else {
                object_type_name
            };

        let inode = self.ptr.ds_record_id().to_string();
        if let Ok(upn) = &my_name {
            Ok(vec![
                self.ds_record_time().map(|ts| {
                    ts.cr_entry(upn, "record creation time", &object_type_caption)
                        .with_inode(&inode)
                }),
                self.att_when_created().map(|ts| {
                    ts.cr_entry(upn, "object created", &object_type_caption)
                        .with_inode(&inode)
                }),
                self.att_when_changed().map(|ts| {
                    ts.cr_entry(upn, "object changed", &object_type_caption)
                        .with_inode(&inode)
                }),
                self.att_last_logon().map(|ts| {
                    ts.c_entry(upn, "last logon on this DC", &object_type_caption)
                        .with_inode(&inode)
                }),
                self.att_last_logon_time_stamp().map(|ts| {
                    ts.c_entry(upn, "last logon on any DC", &object_type_caption)
                        .with_inode(&inode)
                }),
                self.att_bad_pwd_time().map(|ts| {
                    ts.c_entry(upn, "bad pwd time", &object_type_caption)
                        .with_inode(&inode)
                }),
                self.att_password_last_set().map(|ts| {
                    ts.c_entry(upn, "password last set", object_type_caption)
                        .with_inode(&inode)
                }),
            ]
            .into_iter()
            .flatten()
            .collect())
        } else {
            Ok(Vec::new())
        }
    }
    pub fn to_flow_record(&self, metadata: &MetaDataCache) -> anyhow::Result<NtdsEntry> {
        let name = self
            .att_sam_account_name()
            .or(self.att_object_name().map(|s| s.name().to_string()))?;

        let object_type = self.object_type_name(metadata)?;
        let deleted_from = self
            .att_last_known_parent_opt()?
            .and_then(|last_known_parent| {
                metadata
                    .record(&last_known_parent)
                    .and_then(|entry| metadata.dn(entry))
            });

        Ok(NtdsEntry {
            name,
            object_type,
            record_id: self.ptr.ds_record_id().inner(),
            is_deleted: self.att_is_deleted_opt()?.unwrap_or(false),
            deleted_from,
            record_time: self.ds_record_time_opt()?.map(|ts| ts.into()),
            when_created: self.att_when_created_opt()?.map(|ts| ts.into()),
            when_changed: self.att_when_changed_opt()?.map(|ts| ts.into()),
            last_logon: self.att_last_logon_opt()?.map(|ts| ts.into()),
            last_logon_timestamp: self.att_last_logon_time_stamp_opt()?.map(|ts| ts.into()),
            bad_pwd_time: self.att_bad_pwd_time_opt()?.map(|ts| ts.into()),
            password_last_set: self.att_password_last_set_opt()?.map(|ts| ts.into()),
        })
    }
    pub fn security_descriptor(
        &self,
        sd_table: &Arc<SdTable>,
    ) -> anyhow::Result<Option<SecurityDescriptor>> {
        Ok(self
            .att_nt_security_descriptor_opt()?
            .and_then(|sd_id| sd_table.descriptor(&sd_id))
            .map(Result::unwrap))
    }
}

impl<'info, 'db> WithValue<NtdsAttributeId> for DataTableRecord<'info, 'db> {
    fn with_value<T>(
        &self,
        index: NtdsAttributeId,
        function: impl FnMut(Option<&cache::Value>) -> crate::ntds::Result<T>,
    ) -> crate::ntds::Result<T> {
        self.inner.with_value(index, function)
    }
}

impl<'info, 'db> WithValue<ColumnIndex> for DataTableRecord<'info, 'db> {
    fn with_value<T>(
        &self,
        index: ColumnIndex,
        function: impl FnMut(Option<&cache::Value>) -> crate::ntds::Result<T>,
    ) -> crate::ntds::Result<T> {
        self.inner.with_value(index, function)
    }
}

impl<'info, 'db> From<&DataTableRecord<'info, 'db>> for term_table::Table {
    fn from(value: &DataTableRecord<'info, 'db>) -> Self {
        let mut table = term_table::Table::new();
        let all_attributes = value.all_attributes();
        let mut keys: Vec<_> = all_attributes.keys().collect();
        keys.sort();

        table.add_row(Row::new(vec![
            TableCell::builder("Attribute")
                .alignment(Alignment::Center)
                .build(),
            TableCell::builder("Type")
                .alignment(Alignment::Center)
                .build(),
            TableCell::builder("Value")
                .alignment(Alignment::Center)
                .build(),
        ]));

        for id in keys {
            let attribute = &all_attributes[id];
            table.add_row(Row::new(vec![
                TableCell::new(attribute.attribute()),
                TableCell::new(attribute.r#type().to_owned()),
                TableCell::new(attribute.value()),
            ]));
        }

        table
    }
}

impl<'info, 'db> Serialize for DataTableRecord<'info, 'db> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let all_attributes = self.all_attributes();
        let mut ser = serializer.serialize_struct("record", all_attributes.len())?;
        for (id, att) in all_attributes {
            let key: &'static str = id.into();
            ser.serialize_field(key, att.value())?;
        }
        ser.end()
    }
}

#[derive(FlowRecord)]
#[flow_record(version = 1, source = "ntdsextract2", classification = "ntds")]
pub struct NtdsEntry {
    name: String,
    object_type: String,
    record_id: i32,
    record_time: Option<DateTime<Utc>>,
    when_created: Option<DateTime<Utc>>,
    when_changed: Option<DateTime<Utc>>,
    last_logon: Option<DateTime<Utc>>,
    last_logon_timestamp: Option<DateTime<Utc>>,
    bad_pwd_time: Option<DateTime<Utc>>,
    password_last_set: Option<DateTime<Utc>>,
    is_deleted: bool,
    deleted_from: Option<String>,
}