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 #[serde(rename = "Links")]
29 links: Vec<Link>,
30}
31
32impl Gpo {
33 pub fn new() -> Self {
35 Self {
36 ..Default::default()
37 }
38 }
39
40 pub fn computer_configuration_enabled(&self) -> bool {
42 if self.properties.gpostatus.is_empty() {
43 warn!(
44 "GPO {} has no flags value; treating computer configuration as enabled for SharpHound compatibility",
45 self.properties.distinguishedname
46 );
47 return true;
48 }
49
50 match self.properties.gpostatus.parse::<u32>() {
51 Ok(flags) => flags & 0x2 == 0,
52 Err(_) => {
53 warn!(
54 "GPO {} has invalid flags value {:?}; skipping computer configuration",
55 self.properties.distinguishedname, self.properties.gpostatus
56 );
57 false
58 }
59 }
60 }
61
62 pub(crate) fn sysvol_guid(&self) -> Option<String> {
63 self.properties
64 .gpcpath
65 .rsplit(['\\', '/'])
66 .find(|part| !part.is_empty())
67 .map(str::to_uppercase)
68 }
69
70 pub fn parse(
73 &mut self,
74 result: SearchEntry,
75 domain: &str,
76 dn_sid: &mut HashMap<String, String>,
77 sid_type: &mut HashMap<String, String>,
78 domain_sid: &str,
79 schema_guid_map: &HashMap<String, String>,
80 ) -> Result<(), Box<dyn Error>> {
81 let result_dn: String = result.dn.to_uppercase();
82 let result_attrs: HashMap<String, Vec<String>> = result.attrs;
83 let result_bin: HashMap<String, Vec<Vec<u8>>> = result.bin_attrs;
84
85 debug!("Parse gpo: {result_dn}");
87
88 for (key, value) in &result_attrs {
90 trace!(" {key:?}:{value:?}");
91 }
92 for (key, value) in &result_bin {
94 trace!(" {key:?}:{value:?}");
95 }
96
97 self.properties.domain = domain.to_uppercase();
99 self.properties.distinguishedname = result_dn;
100 self.properties.domainsid = domain_sid.to_string();
101
102 for (key, value) in &result_attrs {
104 match key.as_str() {
105 "displayName" => {
106 let name = &value[0];
107 let email = format!("{}@{}", name.to_owned(), domain);
108 self.properties.name = email.to_uppercase();
109 }
110 "description" => {
111 self.properties.description = value.first().cloned();
112 }
113 "whenCreated" => {
114 let epoch = string_to_epoch(&value[0])?;
115 if epoch.is_positive() {
116 self.properties.whencreated = epoch;
117 }
118 }
119 "gPCFileSysPath" => {
120 self.properties.gpcpath = value[0].to_owned();
121 }
122 "flags" => {
123 self.properties.gpostatus = value.first().cloned().unwrap_or_default();
124 }
125 "isDeleted" => {
126 self.is_deleted = true;
127 }
128 _ => {}
129 }
130 }
131
132 for (key, value) in &result_bin {
134 match key.as_str() {
135 "objectGUID" => {
136 self.object_identifier = decode_guid_le(&value[0]).to_owned();
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 self.links = links;
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 }
239}
240
241#[derive(Debug, Clone, Deserialize, Serialize, Default)]
243pub struct GpoProperties {
244 domain: String,
245 name: String,
246 distinguishedname: String,
247 domainsid: String,
248 isaclprotected: bool,
249 highvalue: bool,
250 description: Option<String>,
251 whencreated: i64,
252 gpcpath: String,
253 gpostatus: String,
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 fn parse_gpo_with_flags(flags: Option<&str>) -> Gpo {
261 let mut attrs = HashMap::from([
262 ("displayName".to_string(), vec!["Test GPO".to_string()]),
263 (
264 "gPCFileSysPath".to_string(),
265 vec![r"\\example.local\SYSVOL\example.local\Policies\{00000000-0000-0000-0000-000000000000}".to_string()],
266 ),
267 ]);
268 if let Some(flags) = flags {
269 attrs.insert("flags".to_string(), vec![flags.to_string()]);
270 }
271 let result = SearchEntry {
272 dn: "CN={00000000-0000-0000-0000-000000000000},CN=Policies,CN=System,DC=example,DC=local".to_string(),
273 attrs,
274 bin_attrs: HashMap::new(),
275 };
276 let mut gpo = Gpo::new();
277 let mut dn_sid = HashMap::new();
278 let mut sid_type = HashMap::new();
279
280 gpo.parse(
281 result,
282 "example.local",
283 &mut dn_sid,
284 &mut sid_type,
285 "S-1-5-21-111111111-222222222-333333333",
286 &HashMap::new(),
287 )
288 .unwrap();
289
290 gpo
291 }
292
293 #[test]
294 fn parse_preserves_gpo_status_for_all_defined_flag_values() {
295 for flags in ["0", "1", "2", "3"] {
296 let gpo = parse_gpo_with_flags(Some(flags));
297 assert_eq!(
298 gpo.to_json()["Properties"]["gpostatus"],
299 flags,
300 "flags={flags} should be retained as gpostatus",
301 );
302 }
303 }
304
305 #[test]
306 fn computer_configuration_applicability_follows_flags_bit_one() {
307 let cases = [
308 (Some("0"), true),
309 (Some("1"), true),
310 (Some("2"), false),
311 (Some("3"), false),
312 (Some("4"), true),
313 (Some("6"), false),
314 (None, true),
315 (Some(""), true),
316 (Some("not-a-number"), false),
317 (Some("4294967296"), false),
318 ];
319
320 for (flags, expected) in cases {
321 let gpo = parse_gpo_with_flags(flags);
322 assert_eq!(
323 gpo.computer_configuration_enabled(),
324 expected,
325 "unexpected computer applicability for flags={flags:?}",
326 );
327 }
328 }
329}