Skip to main content

rusthound_ce/objects/
group.rs

1use serde::{Deserialize, Serialize};
2use serde_json::value::Value;
3use ldap3::SearchEntry;
4use log::{debug, trace};
5use std::collections::HashMap;
6use std::error::Error;
7
8use crate::enums::{decode_guid_le, group_scope};
9use crate::enums::regex::OBJECT_SID_RE1;
10use crate::objects::common::{LdapObject, AceTemplate, SPNTarget, Link, Member};
11use crate::enums::acl::parse_ntsecuritydescriptor;
12use crate::enums::secdesc::LdapSid;
13use crate::enums::sid::{objectsid_to_vec8, sid_maker};
14use crate::utils::date::string_to_epoch;
15
16/// Group structure
17#[derive(Debug, Clone, Deserialize, Serialize, Default)]
18pub struct Group {
19    #[serde(rename = "ObjectIdentifier")]
20    object_identifier: String,
21    #[serde(rename = "IsDeleted")]
22    is_deleted: bool,
23    #[serde(rename = "IsACLProtected")]
24    is_acl_protected: bool,
25    #[serde(rename = "Properties")]
26    properties: GroupProperties,
27    #[serde(rename = "Members")]
28    members: Vec<Member>,
29    #[serde(rename = "HasSIDHistory")]
30    has_sid_history: Vec<Member>,
31    #[serde(rename = "Aces")]
32    aces: Vec<AceTemplate>,
33    #[serde(rename = "ContainedBy")]
34    contained_by: Option<Member>,
35}
36
37impl Group {
38    // New group.
39    pub fn new() -> Self { 
40        Self { ..Default::default() } 
41    }
42
43    // Immutable access.
44    pub fn members(&self) -> &Vec<Member> {
45        &self.members
46    }
47    pub fn properties(&self) -> &GroupProperties {
48        &self.properties 
49    }
50    pub fn object_identifier(&self) -> &String {
51        &self.object_identifier
52    }
53
54    // Mutable access.
55    pub fn properties_mut(&mut self) -> &mut GroupProperties {
56        &mut self.properties
57    }
58    pub fn object_identifier_mut(&mut self) -> &mut String {
59        &mut self.object_identifier
60    }
61    pub fn members_mut(&mut self) -> &mut Vec<Member> {
62        &mut self.members
63    }
64    pub fn has_sid_history_mut(&mut self) -> &mut Vec<Member> {
65        &mut self.has_sid_history
66    }
67    
68    /// Function to parse and replace value for group object.
69    /// <https://bloodhound.readthedocs.io/en/latest/further-reading/json.html#groups>
70    pub fn parse(
71        &mut self,
72        result: SearchEntry,
73        domain: &str,
74        dn_sid: &mut HashMap<String, String>,
75        sid_type: &mut HashMap<String, String>,
76        domain_sid: &str,
77        schema_guid_map: &HashMap<String, String>,
78    ) -> Result<(), Box<dyn Error>> {
79        let result_dn: String = result.dn.to_uppercase();
80        let result_attrs: HashMap<String, Vec<String>> = result.attrs;
81        let result_bin: HashMap<String, Vec<Vec<u8>>> = result.bin_attrs;
82
83        debug!("Parse group: {result_dn}");
84
85        // Trace all result attributes
86        for (key, value) in &result_attrs {
87            trace!("  {key:?}:{value:?}");
88        }
89        // Trace all bin result attributes
90        for (key, value) in &result_bin {
91            trace!("  {key:?}:{value:?}");
92        }
93
94        // Change all values...
95        self.properties.domain = domain.to_uppercase();
96        self.properties.distinguishedname = result_dn;
97        self.properties.domainsid = domain_sid.to_string();
98
99        // With a check
100        for (key, value) in &result_attrs {
101            match key.as_str() {
102                "name" => {
103                    let name = &value[0];
104                    let email = format!("{}@{}", name.to_owned(), domain);
105                    self.properties.name = email.to_uppercase();
106                }
107                "description" => {
108                    self.properties.description = Some(value[0].to_owned());
109                }
110                "adminCount" => {
111                    // adminCount is not limited to 1: any non-zero value means
112                    // the object is (or was) in a protected group, so
113                    // AdminSDHolder owns its DACL.
114                    let admincount = value[0].parse::<i32>().unwrap_or(0) != 0;
115                    self.properties.admincount = admincount;
116                    self.properties.adminsdholderprotected = admincount;
117                }
118                "groupType" => {
119                    self.properties.groupscope = group_scope(value[0].parse::<i64>().unwrap_or(0));
120                }
121                "sAMAccountName" => {
122                    self.properties.samaccountname = value[0].to_owned();
123                }
124                "member" => {
125                    if !value.is_empty() {
126                        let mut _vec_members: Vec<Member> = Vec::new();
127
128                        for member in value {
129                            let _member = member.trim();
130                            if _member.is_empty() {
131                                continue;
132                            }
133                            if _member.eq_ignore_ascii_case("SID") {
134                                continue;
135                            }
136
137                            let mut m = Member::new();
138                            *m.object_identifier_mut() = _member.to_uppercase();
139                            _vec_members.push(m);
140                        }
141                        
142                        self.members = _vec_members;
143                    }
144                }
145                "objectSid" => {
146                    // objectSid to vec and raw to string
147                    let vec_sid = objectsid_to_vec8(&value[0]);
148                    let sid = sid_maker(LdapSid::parse(&vec_sid).unwrap().1, domain);
149                    self.object_identifier = sid.to_owned();
150
151                    /*let re = Regex::new(r"^S-[0-9]{1}-[0-9]{1}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}").unwrap();
152                    for domain_sid in re.captures_iter(&sid) 
153                    {
154                        group_json["Properties"]["domainsid"] = domain_sid[0].to_owned().to_string();
155                    }*/
156
157                    // highvalue
158                    if sid.ends_with("-512") 
159                        || sid.ends_with("-516") 
160                        || sid.ends_with("-519") 
161                        || sid.ends_with("-520") 
162                    {
163                        self.properties.highvalue = true;
164                    } else if sid.ends_with("S-1-5-32-544") 
165                        || sid.ends_with("S-1-5-32-548") 
166                        || sid.ends_with("S-1-5-32-549")
167                        || sid.ends_with("S-1-5-32-550") 
168                        || sid.ends_with("S-1-5-32-551")
169                    {
170                        self.properties.highvalue = true;
171                    } else {
172                        self.properties.highvalue = false;
173                    }
174                }
175                "whenCreated" => {
176                    let epoch = string_to_epoch(&value[0])?;
177                    if epoch.is_positive() {
178                        self.properties.whencreated = epoch;
179                    }
180                }
181                "isDeleted" => {
182                    self.is_deleted = true;
183                }
184                _ => {}
185            }
186        }
187
188        // For all, bins attributs
189        for (key, value) in &result_bin {
190            match key.as_str() {
191                "objectGUID" => {
192                    // objectGUID raw to string
193                    let guid = decode_guid_le(&value[0]);
194                    self.properties.objectguid = guid;
195                }
196                "objectSid" => {
197                    // objectSid raw to string
198                    let sid = sid_maker(LdapSid::parse(&value[0]).unwrap().1, domain);
199                    self.object_identifier = sid.to_owned();
200
201                    for domain_sid in OBJECT_SID_RE1.captures_iter(&sid) {
202                        self.properties.domainsid = domain_sid[0].to_owned().to_string();
203                    }
204    
205                    // highvalue
206                    if sid.ends_with("-512") 
207                        || sid.ends_with("-516") 
208                        || sid.ends_with("-519") 
209                        || sid.ends_with("-520") 
210                    {
211                        self.properties.highvalue = true;
212                    }
213                    else if sid.ends_with("S-1-5-32-544") 
214                        || sid.ends_with("S-1-5-32-548") 
215                        || sid.ends_with("S-1-5-32-549")
216                        || sid.ends_with("S-1-5-32-550") 
217                        || sid.ends_with("S-1-5-32-551") 
218                    {
219                        self.properties.highvalue = true;
220                    }
221                    else {
222                        self.properties.highvalue = false;
223                    }
224                }
225                "nTSecurityDescriptor" => {
226                    // nTSecurityDescriptor raw to string
227                    let relations_ace = parse_ntsecuritydescriptor(
228                        self,
229                        &value[0],
230                        "Group",
231                        &result_attrs,
232                        &result_bin,
233                        domain,
234                        schema_guid_map,
235                    );
236                    self.aces = relations_ace;
237                }
238                "sIDHistory" => {
239                    let mut list_sid_history: Vec<String> = Vec::new();
240                    let mut has_sid_history: Vec<Member> = Vec::new();
241                    for bsid in value {
242                        debug!("sIDHistory: {:?}", &bsid);
243                        let sid = sid_maker(LdapSid::parse(bsid).unwrap().1, domain);
244                        let mut member = Member::new();
245                        *member.object_identifier_mut() = sid.clone();
246                        has_sid_history.push(member);
247                        list_sid_history.push(sid);
248                    }
249                    self.properties.sidhistory = list_sid_history;
250                    self.has_sid_history = has_sid_history;
251                }
252                _ => {}
253            }
254        }
255
256        // Push DN and SID in HashMap
257        dn_sid.insert(
258            self.properties.distinguishedname.to_string(),
259            self.object_identifier.to_string(),
260        );
261        // Push DN and Type
262        sid_type.insert(
263            self.object_identifier.to_string(),
264            "Group".to_string(),
265        );
266
267        // Trace and return Group struct
268        // trace!("JSON OUTPUT: {:?}",serde_json::to_string(&self).unwrap());
269        Ok(())
270    }
271}
272
273impl LdapObject for Group {
274    // To JSON
275    fn to_json(&self) -> Value {
276        serde_json::to_value(self).unwrap()
277    }
278
279    // Get values
280    fn get_object_identifier(&self) -> &String {
281        &self.object_identifier
282    }
283    fn get_is_acl_protected(&self) -> &bool {
284        &self.is_acl_protected
285    }
286    fn get_aces(&self) -> &Vec<AceTemplate> {
287        &self.aces
288    }
289    fn get_spntargets(&self) -> &Vec<SPNTarget> {
290        panic!("Not used by current object.");
291    }
292    fn get_allowed_to_delegate(&self) -> &Vec<Member> {
293        panic!("Not used by current object.");
294    }
295    fn get_links(&self) -> &Vec<Link> {
296        panic!("Not used by current object.");
297    }
298    fn get_contained_by(&self) -> &Option<Member> {
299        &self.contained_by
300    }
301    fn get_child_objects(&self) -> &Vec<Member> {
302        panic!("Not used by current object.");
303    }
304    fn get_haslaps(&self) -> &bool {
305        &false
306    }
307    
308    // Get mutable values
309    fn get_aces_mut(&mut self) -> &mut Vec<AceTemplate> {
310        &mut self.aces
311    }
312    fn get_spntargets_mut(&mut self) -> &mut Vec<SPNTarget> {
313        panic!("Not used by current object.");
314    }
315    fn get_allowed_to_delegate_mut(&mut self) -> &mut Vec<Member> {
316        panic!("Not used by current object.");
317    }
318    
319    // Edit values
320    fn set_is_acl_protected(&mut self, is_acl_protected: bool) {
321        self.is_acl_protected = is_acl_protected;
322        self.properties.isaclprotected = is_acl_protected;
323    }
324    fn set_aces(&mut self, aces: Vec<AceTemplate>) {
325        self.aces = aces;
326    }
327    fn set_spntargets(&mut self, _spn_targets: Vec<SPNTarget>) {
328        // Not used by current object.
329    }
330    fn set_allowed_to_delegate(&mut self, _allowed_to_delegate: Vec<Member>) {
331        // Not used by current object.
332    }
333    fn set_links(&mut self, _links: Vec<Link>) {
334        // Not used by current object.
335    }
336    fn set_contained_by(&mut self, contained_by: Option<Member>) {
337        self.contained_by = contained_by;
338    }
339    fn set_child_objects(&mut self, _child_objects: Vec<Member>) {
340        // Not used by current object.
341    }
342    fn set_owner_rights_flags(&mut self, any: bool, any_inherited: bool) {
343        self.properties.doesanyacegrantownerrights = any;
344        self.properties.doesanyinheritedacegrantownerrights = any_inherited;
345    }
346}
347
348// Group properties structure
349#[derive(Debug, Clone, Deserialize, Serialize, Default)]
350pub struct GroupProperties {
351    domain: String,
352    name: String,
353    distinguishedname: String,
354    domainsid: String,
355    objectguid: String,
356    doesanyacegrantownerrights: bool,
357    doesanyinheritedacegrantownerrights: bool,
358    isaclprotected: bool,
359    highvalue: bool,
360    samaccountname: String,
361    description: Option<String>,
362    whencreated: i64,
363    admincount: bool,
364    adminsdholderprotected: bool,
365    groupscope: String,
366    sidhistory: Vec<String>,
367}
368
369impl GroupProperties {
370    // Get access.
371    pub fn name(&self) -> &String {
372        &self.name
373    }
374
375    // Mutable access.
376    pub fn name_mut(&mut self) -> &mut String {
377        &mut self.name
378    }
379    pub fn domain_mut(&mut self) -> &mut String {
380        &mut self.domain
381    }
382    pub fn domainsid_mut(&mut self) -> &mut String {
383        &mut self.domainsid
384    }
385    pub fn highvalue_mut(&mut self) -> &mut bool {
386        &mut self.highvalue
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn group_type_maps_to_bloodhound_scope() {
396        // groupType is a signed 32-bit value; security groups carry 0x80000000.
397        assert_eq!(group_scope(-2147483646), "Global");      // security, global
398        assert_eq!(group_scope(-2147483643), "DomainLocal"); // security, builtin local
399        assert_eq!(group_scope(-2147483644), "DomainLocal"); // security, resource
400        assert_eq!(group_scope(-2147483640), "Universal");   // security, universal
401        assert_eq!(group_scope(2), "Global");                // distribution, global
402        assert_eq!(group_scope(8), "Universal");             // distribution, universal
403        assert_eq!(group_scope(0), "");
404    }
405}