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#[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 pub fn new() -> Self {
33 Self {
34 ..Default::default()
35 }
36 }
37
38 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 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 gpo: {result_dn}");
85
86 for (key, value) in &result_attrs {
88 trace!(" {key:?}:{value:?}");
89 }
90 for (key, value) in &result_bin {
92 trace!(" {key:?}:{value:?}");
93 }
94
95 self.properties.domain = domain.to_uppercase();
97 self.properties.distinguishedname = result_dn;
98 self.properties.domainsid = domain_sid.to_string();
99
100 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 (key, value) in &result_bin {
132 match key.as_str() {
133 "objectGUID" => {
134 let guid = decode_guid_le(&value[0]);
136 self.object_identifier = guid.to_owned();
137 self.properties.objectguid = guid;
138 }
139 "nTSecurityDescriptor" => {
140 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 dn_sid.insert(
158 self.properties.distinguishedname.to_string(),
159 self.object_identifier.to_string(),
160 );
161 sid_type.insert(self.object_identifier.to_string(), "Gpo".to_string());
163
164 Ok(())
167 }
168}
169
170impl LdapObject for Gpo {
171 fn to_json(&self) -> Value {
173 serde_json::to_value(self).unwrap()
174 }
175
176 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 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 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 }
227 fn set_allowed_to_delegate(&mut self, _allowed_to_delegate: Vec<Member>) {
228 }
230 fn set_links(&mut self, _links: Vec<Link>) {
231 }
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 }
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#[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}