use std::env::temp_dir;
use std::fs::File;
use std::io::{BufReader, Read};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{bail, Result};
use clap::Parser;
use dfir_windows_types::{Guid, Sid};
use hashbrown::HashSet;
use libesedb::EseDb;
use libntdsextract2::cli::{
AclCommand, Args, Commands, EntrySearchField, OutputOptions, SdDisplayOption,
};
use libntdsextract2::{
use_member_of_attribute, CDatabase, CsvSerialization, EntryId, EsedbInfo, JsonSerialization,
Resolve,
};
use sddl::{AccessMaskFlag, Contains};
use serde::Serialize;
use sha1::{Digest, Sha1};
use simplelog::{Config, TermLogger};
mod progress_bar;
mod walk;
use cap::Cap;
use std::{alloc, fs};
use crate::walk::run_tui;
macro_rules! do_with_serialization {
($cmd: expr, $db: expr, $function: ident, $options: expr) => {
if $cmd.flat_serialization() {
$db.$function::<CsvSerialization>($options)
} else {
$db.$function::<JsonSerialization>($options)
}
};
}
#[global_allocator]
static ALLOCATOR: Cap<alloc::System> = Cap::new(alloc::System, usize::MAX);
fn main() -> Result<()> {
ALLOCATOR.set_limit(4096 * 1024 * 1024).unwrap();
let cli = Args::parse();
if !matches!(cli.command(), Commands::Walk { .. }) {
let _ = TermLogger::init(
cli.verbose().log_level_filter(),
Config::default(),
simplelog::TerminalMode::Stderr,
simplelog::ColorChoice::Auto,
);
}
let ntds_path = Path::new(cli.ntds_file());
if !(ntds_path.exists() && ntds_path.is_file()) {
eprintln!("unable to open '{}'", cli.ntds_file());
std::process::exit(-1);
}
let esedb = EseDb::open(cli.ntds_file())?;
if matches!(cli.command(), Commands::Info {}) {
EsedbInfo::print_metainfo(&esedb)?;
return Ok(());
}
let info = EsedbInfo::try_from(&esedb)?;
let cache_file = if *cli.use_cache() {
if let Some(cache_file) = cli.cache_file() {
let cache_file = PathBuf::from(cache_file).canonicalize()?;
if !cache_file.exists() {
if let Some(parent) = cache_file.parent() {
let permissions = fs::metadata(parent)?.permissions();
if permissions.readonly() {
bail!(
"directory '{}' must be writable in order to store a cache file there",
parent.to_string_lossy()
);
}
let permissions = fs::metadata(&cache_file)?.permissions();
if permissions.mode() & 0o177 != 0 {
bail!(
"cache file '{}' has too lose permissions set",
cache_file.to_string_lossy()
);
}
Some(cache_file)
} else {
bail!("invalid path: '{}'", cache_file.to_string_lossy());
}
} else {
if !cache_file.is_file() {
bail!(
"Path '{}' does not point to a file",
cache_file.to_string_lossy()
);
}
Some(cache_file)
}
} else {
let buffer = {
let mut reader = BufReader::new(File::open(cli.ntds_file()).unwrap());
let mut buffer = [0; 4096];
let _ = reader.read(&mut buffer)?;
buffer
};
let hash = {
let mut hasher = Sha1::new();
hasher.update(buffer);
let result = hasher.finalize();
hex::encode(&result[0..8])
};
let mut cache_file = temp_dir();
cache_file.push(format!("ntdsextract2_cache_{}", hash));
if cache_file.exists() {
let permissions = fs::metadata(&cache_file)?.permissions();
if permissions.mode() & 0o177 != 0 {
bail!(
"cache file '{}' has too loose permissions set: {:o}",
cache_file.to_string_lossy(),
permissions.mode()
);
}
}
Some(cache_file)
}
} else {
None
};
let database = CDatabase::new(
&info,
cli.command().show_security_descriptor() != SdDisplayOption::Hide
|| matches!(cli.command(), Commands::Acl { .. })
|| matches!(cli.command(), Commands::Walk { .. }),
cache_file,
)?;
let mut options = OutputOptions::default();
options.set_display_all_attributes(cli.command().display_all_attributes());
options.set_flat_serialization(cli.command().flat_serialization());
options.set_format(cli.command().format());
options.set_include_dn(cli.command().include_dn());
options.set_show_security_descriptor(cli.command().show_security_descriptor());
use_member_of_attribute(cli.command().member_of_attribute());
match cli.command() {
Commands::Group { .. } => {
do_with_serialization!(cli.command(), database, show_groups, &options)
}
Commands::User { .. } => {
do_with_serialization!(cli.command(), database, show_users, &options)
}
Commands::Computer { .. } => {
do_with_serialization!(cli.command(), database, show_computers, &options)
}
Commands::Types { .. } => {
do_with_serialization!(cli.command(), database, show_type_names, &options)
}
Commands::Timeline {
all_objects,
include_deleted,
format,
} => {
options.set_show_all_objects(*all_objects);
database.show_timeline(&options, *include_deleted, format)
}
Commands::Tree { max_depth } => Ok(database.show_tree(*max_depth)?),
Commands::Entry {
search_field,
id,
entry_format,
} => {
let entry_id = match search_field {
EntrySearchField::EntryId => EntryId::Id(id.parse::<i32>()?.into()),
EntrySearchField::Sid => EntryId::Rid(id.parse::<u32>()?),
EntrySearchField::Guid => EntryId::Guid(Guid::from_str(id)?),
};
Ok(database.show_entry(entry_id, *entry_format)?)
}
Commands::Search { regex, ignore_case } => {
let regex = if *ignore_case {
format!("(?i:{regex})")
} else {
regex.to_owned()
};
database.search_entries(®ex)
}
Commands::Acl { acl_command } => {
match acl_command {
AclCommand::GA { format, include_dn } => {
database.show_objects_by_permission(*include_dn, format, |ace| {
if ace.header().mask().contains(AccessMaskFlag::GENERIC_ALL)
| ace.header().mask().contains(
AccessMaskFlag::WRITE_OWNER
| AccessMaskFlag::WRITE_DACL
| AccessMaskFlag::READ_CONTROL
| AccessMaskFlag::STANDARD_DELETE
| AccessMaskFlag::CONTROL_ACCESS
| AccessMaskFlag::LIST_OBJECT
| AccessMaskFlag::DELETE_TREE
| AccessMaskFlag::WRITE_PROPERTY
| AccessMaskFlag::READ_PROPERTY
| AccessMaskFlag::SELF_WRITE
| AccessMaskFlag::LIST_CHILDREN
| AccessMaskFlag::DELETE_CHILD
| AccessMaskFlag::CREATE_CHILD,
)
{
Some(ace.sid().sid())
} else {
None
}
})
}
AclCommand::AllCr { format, include_dn } => {
database.show_objects_by_permission(*include_dn, format, |ace| {
if ace.header().mask().contains(AccessMaskFlag::CONTROL_ACCESS)
&& ace.can_have_object_type()
&& ace.object_type().is_none()
{
return Some(ace.sid().sid());
}
None
})
}
AclCommand::DCSync { format } => {
let domain_entry = database.domain_entry();
let sd = domain_entry
.security_descriptor()
.as_ref()
.expect("Domain entry has no security descriptor");
let ds_replication_get_changes =
Guid::from_str("1131f6aa-9c07-11d1-f79f-00c04fc2dcd2")?;
let ds_replication_get_changes_all =
Guid::from_str("1131f6ad-9c07-11d1-f79f-00c04fc2dcd2")?;
let mut get_changes_users = HashSet::new();
let mut get_changes_all_users = HashSet::new();
if let Some(dacl) = sd.as_ref().dacl() {
for ace in dacl.ace_list().iter() {
if ace.header().mask().contains(AccessMaskFlag::CONTROL_ACCESS) {
if let Some(object_guid) = ace.object_type() {
if object_guid == &ds_replication_get_changes {
get_changes_users.insert(ace.sid());
} else if object_guid == &ds_replication_get_changes_all {
get_changes_all_users.insert(ace.sid());
}
} else {
get_changes_users.insert(ace.sid());
get_changes_all_users.insert(ace.sid());
}
}
}
}
#[derive(Serialize)]
struct UserInfo<'e> {
sam_account_name: &'e str,
distinguished_name: String,
sid: Sid,
}
let mut writer = format.create_writer()?;
for sid_info in get_changes_users.intersection(&get_changes_all_users) {
match database.resolve(sid_info.sid()) {
None => log::error!("unable to find entry for SID {}", sid_info.sid()),
Some(e) => {
let user_info = UserInfo {
sam_account_name: e
.sam_account_name()
.as_ref()
.map(|v| &v[..])
.unwrap_or(""),
distinguished_name: database
.object_tree()
.dn_of(e.record_ptr())
.unwrap_or("".to_string()),
sid: e.sid().as_ref().unwrap().clone(),
};
writer.write(user_info)?;
}
}
}
drop(writer);
Ok(())
}
AclCommand::Hidden { include_dn, format } => {
database.show_hidden_objects(*include_dn, format)
}
AclCommand::ExtendedRights { command } => command.run(&database),
}
}
Commands::Walk {} => run_tui(database),
Commands::Info {} => Ok(()),
}
}