rusthound_ce/modules/gpo/
types.rs1use std::fmt;
2
3#[derive(Debug)]
5pub enum GpoError {
6 InvalidEncoding(String),
8 MalformedContent(String),
10 Io(std::io::Error),
12}
13
14impl fmt::Display for GpoError {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 match self {
17 Self::InvalidEncoding(msg) => write!(f, "Invalid policy encoding: {msg}"),
18 Self::MalformedContent(msg) => write!(f, "Malformed policy content: {msg}"),
19 Self::Io(err) => write!(f, "Policy I/O error: {err}"),
20 }
21 }
22}
23
24impl std::error::Error for GpoError {
25 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
26 match self {
27 Self::Io(err) => Some(err),
28 _ => None,
29 }
30 }
31}
32
33impl From<std::io::Error> for GpoError {
34 fn from(err: std::io::Error) -> Self {
35 Self::Io(err)
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Default)]
41pub struct PrivilegeAssignment {
42 privilege: String,
43 principals: Vec<String>,
44}
45
46impl PrivilegeAssignment {
47 pub fn new(privilege: impl Into<String>, principals: Vec<String>) -> Self {
49 Self {
50 privilege: privilege.into(),
51 principals,
52 }
53 }
54
55 pub fn privilege(&self) -> &str {
57 &self.privilege
58 }
59
60 pub fn principals(&self) -> &[String] {
62 &self.principals
63 }
64
65 pub fn normalized_principals(&self) -> impl Iterator<Item = &str> {
67 self.principals
68 .iter()
69 .map(|p| p.strip_prefix('*').unwrap_or(p))
70 }
71
72 pub fn sid_candidates(&self) -> impl Iterator<Item = &str> {
75 self.normalized_principals()
76 .filter(|p| p.starts_with("S-1-") || p.starts_with("s-1-"))
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Default)]
82pub struct GptTmplPolicy {
83 privilege_rights: Vec<PrivilegeAssignment>,
84 restricted_groups: Vec<RestrictedGroupDirective>,
85}
86
87impl GptTmplPolicy {
88 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn with_privilege_rights(privilege_rights: Vec<PrivilegeAssignment>) -> Self {
95 Self {
96 privilege_rights,
97 restricted_groups: Vec::new(),
98 }
99 }
100
101 pub fn with_entries(
103 privilege_rights: Vec<PrivilegeAssignment>,
104 restricted_groups: Vec<RestrictedGroupDirective>,
105 ) -> Self {
106 Self {
107 privilege_rights,
108 restricted_groups,
109 }
110 }
111
112 pub fn privilege_rights(&self) -> &[PrivilegeAssignment] {
114 &self.privilege_rights
115 }
116
117 pub fn get_privilege(&self, privilege_name: &str) -> Option<&PrivilegeAssignment> {
119 self.privilege_rights
120 .iter()
121 .find(|p| p.privilege.eq_ignore_ascii_case(privilege_name))
122 }
123
124 pub fn restricted_groups(&self) -> &[RestrictedGroupDirective] {
126 &self.restricted_groups
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum RestrictedGroupOperation {
135 ReplaceMembers,
137 AddToParentGroups,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct RestrictedGroupDirective {
144 target: String,
145 operation: RestrictedGroupOperation,
146 principals: Vec<String>,
147}
148
149impl RestrictedGroupDirective {
150 pub fn new(
151 target: impl Into<String>,
152 operation: RestrictedGroupOperation,
153 principals: Vec<String>,
154 ) -> Self {
155 Self {
156 target: target.into(),
157 operation,
158 principals,
159 }
160 }
161
162 pub fn target(&self) -> &str {
163 &self.target
164 }
165
166 pub fn operation(&self) -> RestrictedGroupOperation {
167 self.operation
168 }
169
170 pub fn principals(&self) -> &[String] {
171 &self.principals
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum GppGroupAction {
178 Create,
179 Delete,
180 Replace,
181 Update,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum GppMemberAction {
187 Add,
188 Remove,
189}
190
191fn nonempty_identity(value: Option<String>) -> Option<String> {
192 value.filter(|value| !value.trim().is_empty())
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct GppGroupMember {
197 sid: Option<String>,
198 name: Option<String>,
199 action: GppMemberAction,
200}
201
202impl GppGroupMember {
203 pub fn new(sid: Option<String>, name: Option<String>, action: GppMemberAction) -> Self {
204 Self {
205 sid: nonempty_identity(sid),
206 name: nonempty_identity(name),
207 action,
208 }
209 }
210
211 pub fn principal(&self) -> Option<&str> {
213 self.sid.as_deref().or(self.name.as_deref())
214 }
215
216 pub fn sid(&self) -> Option<&str> {
217 self.sid.as_deref()
218 }
219
220 pub fn name(&self) -> Option<&str> {
221 self.name.as_deref()
222 }
223
224 pub fn action(&self) -> GppMemberAction {
225 self.action
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct GppLocalGroup {
231 sid: Option<String>,
232 name: Option<String>,
233 action: GppGroupAction,
234 delete_all_users: bool,
235 delete_all_groups: bool,
236 has_item_level_targeting: bool,
237 members: Vec<GppGroupMember>,
238}
239
240impl GppLocalGroup {
241 #[allow(clippy::too_many_arguments)]
242 pub fn new(
243 sid: Option<String>,
244 name: Option<String>,
245 action: GppGroupAction,
246 delete_all_users: bool,
247 delete_all_groups: bool,
248 has_item_level_targeting: bool,
249 members: Vec<GppGroupMember>,
250 ) -> Self {
251 Self {
252 sid: nonempty_identity(sid),
253 name: nonempty_identity(name),
254 action,
255 delete_all_users,
256 delete_all_groups,
257 has_item_level_targeting,
258 members,
259 }
260 }
261
262 pub fn target(&self) -> Option<&str> {
264 self.sid.as_deref().or(self.name.as_deref())
265 }
266
267 pub fn sid(&self) -> Option<&str> {
268 self.sid.as_deref()
269 }
270
271 pub fn name(&self) -> Option<&str> {
272 self.name.as_deref()
273 }
274
275 pub fn action(&self) -> GppGroupAction {
276 self.action
277 }
278
279 pub fn delete_all_users(&self) -> bool {
280 self.delete_all_users
281 }
282
283 pub fn delete_all_groups(&self) -> bool {
284 self.delete_all_groups
285 }
286
287 pub fn has_item_level_targeting(&self) -> bool {
290 self.has_item_level_targeting
291 }
292
293 pub fn members(&self) -> &[GppGroupMember] {
294 &self.members
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn privilege_assignment_normalized_principals_and_sid_candidates() {
304 let assignment = PrivilegeAssignment::new(
305 "SeRemoteInteractiveLogonRight",
306 vec![
307 "*S-1-5-32-544".to_string(),
308 "S-1-5-32-545".to_string(),
309 "*DOMAIN\\Administrators".to_string(),
310 "LocalUser".to_string(),
311 ],
312 );
313
314 assert_eq!(assignment.privilege(), "SeRemoteInteractiveLogonRight");
315 assert_eq!(assignment.principals().len(), 4);
316
317 let normalized: Vec<&str> = assignment.normalized_principals().collect();
318 assert_eq!(
319 normalized,
320 vec![
321 "S-1-5-32-544",
322 "S-1-5-32-545",
323 "DOMAIN\\Administrators",
324 "LocalUser"
325 ]
326 );
327
328 let sids: Vec<&str> = assignment.sid_candidates().collect();
329 assert_eq!(sids, vec!["S-1-5-32-544", "S-1-5-32-545"]);
330 }
331
332 #[test]
333 fn gpttmpl_policy_lookup_is_case_insensitive() {
334 let policy = GptTmplPolicy::with_privilege_rights(vec![
335 PrivilegeAssignment::new("SeDebugPrivilege", vec!["*S-1-5-32-544".to_string()]),
336 PrivilegeAssignment::new(
337 "SeRemoteInteractiveLogonRight",
338 vec!["*S-1-5-32-555".to_string()],
339 ),
340 ]);
341
342 assert!(policy.get_privilege("sedebugprivilege").is_some());
343 assert!(policy.get_privilege("SEDEBUGPRIVILEGE").is_some());
344 assert!(policy.get_privilege("SeDebugPrivilege").is_some());
345 assert!(policy.get_privilege("SeNonExistentPrivilege").is_none());
346 }
347
348 #[test]
349 fn gpo_error_display_formatting() {
350 let err = GpoError::MalformedContent("invalid syntax at line 5".to_string());
351 assert_eq!(
352 err.to_string(),
353 "Malformed policy content: invalid syntax at line 5"
354 );
355
356 let err2 = GpoError::InvalidEncoding("unsupported encoding".to_string());
357 assert_eq!(
358 err2.to_string(),
359 "Invalid policy encoding: unsupported encoding"
360 );
361 }
362}