use anyhow::{bail, Result};
use clap::Subcommand;
use dfir_windows_types::{Guid, Sid};
use sddl::{AccessMaskFlag, Contains};
use serde::Serialize;
use hashbrown::{hash_map::Entry, HashMap, HashSet};
use std::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,
},
Subjects {
extended_right: Option<String>,
#[clap(long("object"))]
object: Option<Sid>,
},
}
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::Subjects {
extended_right,
object,
} => self.list_extended_right_subjects(
database,
extended_right.as_ref().map(|x| x.as_str()),
object.as_ref(),
),
}
}
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: Option<&str>,
object: Option<&Sid>,
) -> std::result::Result<(), anyhow::Error> {
let format = OutputFormat::Json;
#[derive(Serialize, Eq, PartialEq, Clone)]
struct Subject {
subject_name: Option<Rdn>,
subject_guid: Option<Guid>,
subject_sid: Sid,
permission_for_objects: HashMap<String, HashSet<ObjectInfo>>,
}
#[derive(Serialize, Eq, PartialEq, Hash, Clone)]
struct ObjectInfo {
object_name: Option<Rdn>,
object_guid: Option<Guid>,
object_sid: Option<Sid>,
}
let right_guid = match extended_right {
None => None,
Some(r) => Some(self.resolve_extended_right(database, r)?),
};
let mut extended_rights_mapping: HashMap<Sid, Subject> = HashMap::new();
let sd_map = self.find_sd_with_extended_right(database, right_guid)?;
for (sd_id, acl) in sd_map.iter() {
let mut permission_for_objects = HashSet::new();
for obj in self.find_objects_with_sd(database, *sd_id)? {
let object_name = obj.att_object_name_opt()?;
let object_guid = obj.att_object_guid_opt()?;
let object_sid = obj.att_object_sid_opt()?;
if let Some(object) = object {
if let Some(object_sid) = object_sid {
if object == &object_sid {
let object_sid = Some(object_sid);
let obj_info = ObjectInfo {
object_name,
object_guid,
object_sid,
};
permission_for_objects.insert(obj_info);
}
}
} else {
let obj_info = ObjectInfo {
object_name,
object_guid,
object_sid,
};
permission_for_objects.insert(obj_info);
}
}
if !permission_for_objects.is_empty() {
for sid in acl.sids().iter().cloned() {
let entry = database
.data_table()
.data_table()
.metadata()
.entry_with_sid(&sid);
match extended_rights_mapping.entry(sid.clone()) {
Entry::Occupied(mut sid_entry) => {
for object in permission_for_objects.clone().into_iter() {
match sid_entry
.get_mut()
.permission_for_objects
.entry(extended_right.unwrap_or("<ALL_RIGHTS>").to_owned())
{
Entry::Occupied(mut entry) => {
(*entry.get_mut()).insert(object);
}
Entry::Vacant(entry) => {
let mut permission_for_objects = HashSet::new();
permission_for_objects.insert(object);
entry.insert(permission_for_objects);
}
};
}
}
Entry::Vacant(sid_entry) => {
let mut pfs = HashMap::new();
pfs.insert(
extended_right.unwrap_or("<ALL_RIGHTS>").to_owned(),
permission_for_objects.clone(),
);
let subject = Subject {
subject_sid: sid,
subject_name: entry.map(|e| e.rdn()).cloned(),
subject_guid: entry.and_then(|e| e.guid().as_ref()).cloned(),
permission_for_objects: pfs,
};
sid_entry.insert(subject);
}
}
}
}
}
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().sid()),
Some(guid) => {
if let Some(object_type) = ace.object_type() {
if guid == *object_type {
Some(ace.sid().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)
}
}