use anyhow::{bail, Result};
use clap::Subcommand;
use dfir_windows_types::Guid;
use sddl::{AccessMaskFlag, Contains, Sid};
use serde::Serialize;
use std::{
collections::{hash_map::Entry, HashMap, HashSet},
str::FromStr,
};
use strum::Display;
use crate::{
cli::OutputFormat,
ntds::{DataTableRecord, InterestingAcl},
win32_types::Rdn,
CDatabase,
};
#[derive(Subcommand, Clone, Display, Eq, PartialEq)]
pub enum Command {
List {
#[clap(value_enum, short('F'), long("format"), default_value_t = OutputFormat::Csv)]
format: OutputFormat,
},
Objects {
extended_right: String,
},
}
impl Command {
pub fn run<'info, 'db>(&self, database: &CDatabase<'info, 'db>) -> Result<()> {
match self {
Command::List { format } => self.list_extended_rights(database, format),
Command::Objects { extended_right } => {
self.list_extended_right_subjects(database, extended_right)
}
}
}
fn list_extended_rights<'info, 'db>(
&self,
database: &CDatabase<'info, 'db>,
format: &OutputFormat,
) -> Result<()> {
#[derive(Serialize)]
struct ExtendedRight {
object_name: String,
rights_guid: Guid,
object_guid: Guid,
}
if let Ok(extended_rights) = database
.data_table()
.object_tree()
.get_special_records()?
.extended_rights_container()
.children()
.lock()
{
let mut writer = format.create_writer()?;
for right in extended_rights.iter() {
let object = database
.entry(crate::EntryId::Id(*right.record_ptr().ds_record_id()))?
.expect("illegal record id in object tree");
writer.write(ExtendedRight {
object_name: object.att_object_name()?.to_string(),
rights_guid: object
.att_rights_guid_opt()?
.expect("extended right without rights GUID"),
object_guid: object.att_object_guid()?,
})?;
}
drop(writer);
} else {
bail!("unable to acquire lock to extended rights container")
}
Ok(())
}
fn list_extended_right_subjects<'info, 'db>(
&self,
database: &CDatabase<'info, 'db>,
extended_right: &str,
) -> std::result::Result<(), anyhow::Error> {
let format = OutputFormat::Json;
#[derive(Serialize, Eq, PartialEq, Hash, Clone)]
struct ObjectInfo {
name: Option<Rdn>,
guid: Option<Guid>,
sid: Option<String>,
}
let right_guid = self.resolve_extended_right(database, extended_right)?;
let sd_map = self.find_sd_with_extended_right(database, Some(right_guid))?;
let mut extended_rights_mapping: HashMap<String, HashSet<ObjectInfo>> = HashMap::new();
for (sd_id, acl) in sd_map.iter() {
let mut objects = HashSet::new();
for obj in self.find_objects_with_sd(database, *sd_id)? {
let name = obj.att_object_name_opt()?;
let guid = obj.att_object_guid_opt()?;
let sid = obj.att_object_sid_opt()?.map(|sid| sid.to_string());
let obj_info = ObjectInfo { name, guid, sid };
objects.insert(obj_info);
}
for sid in acl.sids().iter().map(Sid::to_string) {
match extended_rights_mapping.entry(sid) {
Entry::Occupied(mut sid_entry) => {
for object_info in objects.iter() {
(*sid_entry.get_mut()).insert(object_info.clone());
}
}
Entry::Vacant(sid_entry) => {
sid_entry.insert(objects.clone());
}
}
}
}
let mut writer = format.create_writer()?;
writer.write(extended_rights_mapping)?;
drop(writer);
Ok(())
}
fn resolve_extended_right<'info, 'db>(
&self,
database: &CDatabase<'info, 'db>,
extended_right: &str,
) -> std::result::Result<Guid, anyhow::Error> {
if let Ok(guid) = Guid::from_str(extended_right) {
Ok(guid)
} else if let Ok(extended_rights) = database
.data_table()
.object_tree()
.get_special_records()?
.extended_rights_container()
.children()
.lock()
{
for right in extended_rights.iter() {
let object = database
.entry(crate::EntryId::Id(*right.record_ptr().ds_record_id()))?
.expect("illegal record id in object tree");
if object.att_object_name()?.as_str() == extended_right {
return Ok(object
.att_rights_guid_opt()?
.expect("extended right without rights GUID"));
}
}
bail!("unknown extended right: '{extended_right}'");
} else {
bail!("unable to acquire lock to Extended Rights container")
}
}
fn find_sd_with_extended_right<'info, 'db>(
&self,
database: &CDatabase<'info, 'db>,
right_guid: Option<Guid>,
) -> std::result::Result<HashMap<i64, InterestingAcl>, anyhow::Error> {
Ok(database.data_table().find_acls(move |ace| {
if ace.header().mask().contains(AccessMaskFlag::CONTROL_ACCESS) {
match right_guid {
None if ace.object_type().is_none() => Some(ace.sid()),
Some(guid) => {
if let Some(object_type) = ace.object_type() {
if guid == *object_type {
Some(ace.sid())
} else {
None
}
} else {
None
}
}
_ => None,
}
} else {
None
}
}))
}
fn find_objects_with_sd<'info, 'db>(
&self,
database: &'info CDatabase<'info, 'db>,
sd_id: i64,
) -> anyhow::Result<Vec<DataTableRecord<'info, 'db>>> {
let mut objects = Vec::new();
if let Some(record_ids) = database
.data_table()
.data_table()
.metadata()
.sd_objects()
.get(&sd_id)
{
for record_id in record_ids {
let record = database
.data_table()
.entry(crate::EntryId::Id(*record_id))?
.expect("illegal record pointer");
objects.push(record);
}
}
Ok(objects)
}
}