Skip to main content

rusthound_ce/objects/
gpo.rs

1use ldap3::SearchEntry;
2use log::{debug, trace, warn};
3use serde::{Deserialize, Serialize};
4use serde_json::value::Value;
5use std::collections::HashMap;
6use std::error::Error;
7
8use crate::enums::acl::parse_ntsecuritydescriptor;
9use crate::enums::decode_guid_le;
10use crate::objects::common::{AceTemplate, LdapObject, Link, Member, SPNTarget};
11use crate::utils::date::string_to_epoch;
12
13/// Gpo structure
14#[derive(Debug, Clone, Deserialize, Serialize, Default)]
15pub struct Gpo {
16    #[serde(rename = "Properties")]
17    properties: GpoProperties,
18    #[serde(rename = "Aces")]
19    aces: Vec<AceTemplate>,
20    #[serde(rename = "ObjectIdentifier")]
21    object_identifier: String,
22    #[serde(rename = "IsDeleted")]
23    is_deleted: bool,
24    #[serde(rename = "IsACLProtected")]
25    is_acl_protected: bool,
26    #[serde(rename = "ContainedBy")]
27    contained_by: Option<Member>,
28}
29
30impl Gpo {
31    // New gpo.
32    pub fn new() -> Self {
33        Self {
34            ..Default::default()
35        }
36    }
37
38    /// Whether the GPO's computer configuration is applicable.
39    pub fn computer_configuration_enabled(&self) -> bool {
40        if self.properties.gpostatus.is_empty() {
41            warn!(
42                "GPO {} has no flags value; treating computer configuration as enabled for SharpHound compatibility",
43                self.properties.distinguishedname
44            );
45            return true;
46        }
47
48        match self.properties.gpostatus.parse::<u32>() {
49            Ok(flags) => flags & 0x2 == 0,
50            Err(_) => {
51                warn!(
52                    "GPO {} has invalid flags value {:?}; skipping computer configuration",
53                    self.properties.distinguishedname, self.properties.gpostatus
54                );
55                false
56            }
57        }
58    }
59
60    pub(crate) fn sysvol_guid(&self) -> Option<String> {
61        self.properties
62            .gpcpath
63            .rsplit(['\\', '/'])
64            .find(|part| !part.is_empty())
65            .map(str::to_uppercase)
66    }
67
68    /// Function to parse and replace value for GPO object.
69    /// <https://bloodhound.readthedocs.io/en/latest/further-reading/json.html#gpos>
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 for current object
84        debug!("Parse gpo: {result_dn}");
85
86        // Trace all result attributes
87        for (key, value) in &result_attrs {
88            trace!("  {key:?}:{value:?}");
89        }
90        // Trace all bin result attributes
91        for (key, value) in &result_bin {
92            trace!("  {key:?}:{value:?}");
93        }
94
95        // Change all values...
96        self.properties.domain = domain.to_uppercase();
97        self.properties.distinguishedname = result_dn;
98        self.properties.domainsid = domain_sid.to_string();
99
100        // Check and replace value
101        for (key, value) in &result_attrs {
102            match key.as_str() {
103                "displayName" => {
104                    let name = &value[0];
105                    let email = format!("{}@{}", name.to_owned(), domain);
106                    self.properties.name = email.to_uppercase();
107                }
108                "description" => {
109                    self.properties.description = value.first().cloned();
110                }
111                "whenCreated" => {
112                    let epoch = string_to_epoch(&value[0])?;
113                    if epoch.is_positive() {
114                        self.properties.whencreated = epoch;
115                    }
116                }
117                "gPCFileSysPath" => {
118                    self.properties.gpcpath = value[0].to_owned();
119                }
120                "flags" => {
121                    self.properties.gpostatus = value.first().cloned().unwrap_or_default();
122                }
123                "isDeleted" => {
124                    self.is_deleted = true;
125                }
126                _ => {}
127            }
128        }
129
130        // For all, bins attributes
131        for (key, value) in &result_bin {
132            match key.as_str() {
133                "objectGUID" => {
134                    // objectGUID raw to string
135                    let guid = decode_guid_le(&value[0]);
136                    self.object_identifier = guid.to_owned();
137                    self.properties.objectguid = guid;
138                }
139                "nTSecurityDescriptor" => {
140                    // nTSecurityDescriptor raw to string
141                    let relations_ace = parse_ntsecuritydescriptor(
142                        self,
143                        &value[0],
144                        "Gpo",
145                        &result_attrs,
146                        &result_bin,
147                        domain,
148                        schema_guid_map,
149                    );
150                    self.aces = relations_ace;
151                }
152                _ => {}
153            }
154        }
155
156        // Push DN and SID in HashMap
157        dn_sid.insert(
158            self.properties.distinguishedname.to_string(),
159            self.object_identifier.to_string(),
160        );
161        // Push DN and Type
162        sid_type.insert(self.object_identifier.to_string(), "Gpo".to_string());
163
164        // Trace and return Gpo struct
165        // trace!("JSON OUTPUT: {:?}",serde_json::to_string(&self).unwrap());
166        Ok(())
167    }
168}
169
170impl LdapObject for Gpo {
171    // To JSON
172    fn to_json(&self) -> Value {
173        serde_json::to_value(self).unwrap()
174    }
175
176    // Get values
177    fn get_object_identifier(&self) -> &String {
178        &self.object_identifier
179    }
180    fn get_is_acl_protected(&self) -> &bool {
181        &self.is_acl_protected
182    }
183    fn get_aces(&self) -> &Vec<AceTemplate> {
184        &self.aces
185    }
186    fn get_spntargets(&self) -> &Vec<SPNTarget> {
187        panic!("Not used by current object.");
188    }
189    fn get_allowed_to_delegate(&self) -> &Vec<Member> {
190        panic!("Not used by current object.");
191    }
192    fn get_links(&self) -> &Vec<Link> {
193        panic!("Not used by current object.");
194    }
195    fn get_contained_by(&self) -> &Option<Member> {
196        &self.contained_by
197    }
198    fn get_child_objects(&self) -> &Vec<Member> {
199        panic!("Not used by current object.");
200    }
201    fn get_haslaps(&self) -> &bool {
202        &false
203    }
204
205    // Get mutable values
206    fn get_aces_mut(&mut self) -> &mut Vec<AceTemplate> {
207        &mut self.aces
208    }
209    fn get_spntargets_mut(&mut self) -> &mut Vec<SPNTarget> {
210        panic!("Not used by current object.");
211    }
212    fn get_allowed_to_delegate_mut(&mut self) -> &mut Vec<Member> {
213        panic!("Not used by current object.");
214    }
215
216    // Edit values
217    fn set_is_acl_protected(&mut self, is_acl_protected: bool) {
218        self.is_acl_protected = is_acl_protected;
219        self.properties.isaclprotected = is_acl_protected;
220    }
221    fn set_aces(&mut self, aces: Vec<AceTemplate>) {
222        self.aces = aces;
223    }
224    fn set_spntargets(&mut self, _spn_targets: Vec<SPNTarget>) {
225        // Not used by current object.
226    }
227    fn set_allowed_to_delegate(&mut self, _allowed_to_delegate: Vec<Member>) {
228        // Not used by current object.
229    }
230    fn set_links(&mut self, _links: Vec<Link>) {
231        // Not used by current object.
232    }
233    fn set_contained_by(&mut self, contained_by: Option<Member>) {
234        self.contained_by = contained_by;
235    }
236    fn set_child_objects(&mut self, _child_objects: Vec<Member>) {
237        // Not used by current object.
238    }
239    fn set_owner_rights_flags(&mut self, any: bool, any_inherited: bool) {
240        self.properties.doesanyacegrantownerrights = any;
241        self.properties.doesanyinheritedacegrantownerrights = any_inherited;
242    }
243}
244
245// Gpo properties structure
246#[derive(Debug, Clone, Deserialize, Serialize, Default)]
247pub struct GpoProperties {
248    domain: String,
249    name: String,
250    distinguishedname: String,
251    domainsid: String,
252    objectguid: String,
253    doesanyacegrantownerrights: bool,
254    doesanyinheritedacegrantownerrights: bool,
255    isaclprotected: bool,
256    highvalue: bool,
257    description: Option<String>,
258    whencreated: i64,
259    gpcpath: String,
260    gpostatus: String,
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    fn parse_gpo_with_flags(flags: Option<&str>) -> Gpo {
268        let mut attrs = HashMap::from([
269            ("displayName".to_string(), vec!["Test GPO".to_string()]),
270            (
271                "gPCFileSysPath".to_string(),
272                vec![r"\\example.local\SYSVOL\example.local\Policies\{00000000-0000-0000-0000-000000000000}".to_string()],
273            ),
274        ]);
275        if let Some(flags) = flags {
276            attrs.insert("flags".to_string(), vec![flags.to_string()]);
277        }
278        let result = SearchEntry {
279            dn: "CN={00000000-0000-0000-0000-000000000000},CN=Policies,CN=System,DC=example,DC=local".to_string(),
280            attrs,
281            bin_attrs: HashMap::new(),
282        };
283        let mut gpo = Gpo::new();
284        let mut dn_sid = HashMap::new();
285        let mut sid_type = HashMap::new();
286
287        gpo.parse(
288            result,
289            "example.local",
290            &mut dn_sid,
291            &mut sid_type,
292            "S-1-5-21-111111111-222222222-333333333",
293            &HashMap::new(),
294        )
295        .unwrap();
296
297        gpo
298    }
299
300    #[test]
301    fn parse_preserves_gpo_status_for_all_defined_flag_values() {
302        for flags in ["0", "1", "2", "3"] {
303            let gpo = parse_gpo_with_flags(Some(flags));
304            assert_eq!(
305                gpo.to_json()["Properties"]["gpostatus"],
306                flags,
307                "flags={flags} should be retained as gpostatus",
308            );
309        }
310    }
311
312    #[test]
313    fn computer_configuration_applicability_follows_flags_bit_one() {
314        let cases = [
315            (Some("0"), true),
316            (Some("1"), true),
317            (Some("2"), false),
318            (Some("3"), false),
319            (Some("4"), true),
320            (Some("6"), false),
321            (None, true),
322            (Some(""), true),
323            (Some("not-a-number"), false),
324            (Some("4294967296"), false),
325        ];
326
327        for (flags, expected) in cases {
328            let gpo = parse_gpo_with_flags(flags);
329            assert_eq!(
330                gpo.computer_configuration_enabled(),
331                expected,
332                "unexpected computer applicability for flags={flags:?}",
333            );
334        }
335    }
336}