1use serde_json::value::Value;
2use serde::{Deserialize, Serialize};
3use ldap3::SearchEntry;
4use log::{debug, error, trace};
5use std::collections::HashMap;
6use std::error::Error;
7use std::collections::HashSet;
8use x509_parser::prelude::*;
9
10use crate::enums::regex::{OBJECT_SID_RE1, SID_PART1_RE1};
11use crate::objects::common::{LdapObject, AceTemplate, SPNTarget, Link, Member};
12use crate::utils::date::{convert_timestamp, string_to_epoch};
13use crate::utils::crypto::convert_encryption_types;
14use crate::enums::acl::{parse_ntsecuritydescriptor, parse_gmsa};
15use crate::enums::secdesc::LdapSid;
16use crate::enums::sid::sid_maker;
17use crate::enums::spntasks::check_spn;
18use crate::enums::uacflags::get_flag;
19
20#[derive(Debug, Clone, Deserialize, Serialize, Default)]
22pub struct User {
23 #[serde(rename ="ObjectIdentifier")]
24 object_identifier: String,
25 #[serde(rename ="IsDeleted")]
26 is_deleted: bool,
27 #[serde(rename ="IsACLProtected")]
28 is_acl_protected: bool,
29 #[serde(rename ="Properties")]
30 properties: UserProperties,
31 #[serde(rename ="PrimaryGroupSID")]
32 primary_group_sid: String,
33 #[serde(rename ="SPNTargets")]
34 spn_targets: Vec<SPNTarget>,
35 #[serde(rename ="UnconstrainedDelegation")]
36 unconstrained_delegation: bool,
37 #[serde(rename ="DomainSID")]
38 domain_sid: String,
39 #[serde(rename ="Aces")]
40 aces: Vec<AceTemplate>,
41 #[serde(rename ="AllowedToDelegate")]
42 allowed_to_delegate: Vec<Member>,
43 #[serde(rename ="HasSIDHistory")]
44 has_sid_history: Vec<String>,
45 #[serde(rename ="ContainedBy")]
46 contained_by: Option<Member>,
47}
48
49impl User {
50 pub fn new() -> Self {
52 Self { ..Default::default()}
53 }
54
55 pub fn properties(&self) -> &UserProperties {
57 &self.properties
58 }
59 pub fn aces(&self) -> &Vec<AceTemplate> {
60 &self.aces
61 }
62
63 pub fn properties_mut(&mut self) -> &mut UserProperties {
65 &mut self.properties
66 }
67 pub fn aces_mut(&mut self) -> &mut Vec<AceTemplate> {
68 &mut self.aces
69 }
70 pub fn object_identifier_mut(&mut self) -> &mut String {
71 &mut self.object_identifier
72 }
73
74 pub fn parse(
77 &mut self,
78 result: SearchEntry,
79 domain: &str,
80 dn_sid: &mut HashMap<String, String>,
81 sid_type: &mut HashMap<String, String>,
82 domain_sid: &str,
83 schema_guid_map: &HashMap<String, String>,
84 ) -> Result<(), Box<dyn Error>> {
85 let result_dn: String = result.dn.to_uppercase();
86 let result_attrs: HashMap<String, Vec<String>> = result.attrs;
87 let result_bin: HashMap<String, Vec<Vec<u8>>> = result.bin_attrs;
88
89 debug!("Parse user: {result_dn}");
91
92 for (key, value) in &result_attrs {
94 trace!(" {key:?}:{value:?}");
95 }
96 for (key, value) in &result_bin {
98 trace!(" {key:?}:{value:?}");
99 }
100
101 self.properties.domain = domain.to_uppercase();
103 self.properties.distinguishedname = result_dn;
104 self.properties.enabled = true;
105 self.domain_sid = domain_sid.to_string();
106
107 let mut group_id: String ="".to_owned();
109 for (key, value) in &result_attrs {
110 match key.as_str() {
111 "sAMAccountName" => {
112 let name = &value[0];
113 let email = format!("{}@{}",name.to_owned(),domain);
114 self.properties.name = email.to_uppercase();
115 self.properties.samaccountname = name.to_string();
116 }
117 "description" => {
118 self.properties.description = Some(value[0].to_owned());
119 }
120 "mail" => {
121 self.properties.email = value[0].to_owned();
122 }
123 "title" => {
124 self.properties.title = value[0].to_owned();
125 }
126 "userPassword" => {
127 self.properties.userpassword = value[0].to_owned();
128 }
129 "unixUserPassword" => {
130 self.properties.unixpassword = value[0].to_owned();
131 }
132 "unicodepwd" => {
133 self.properties.unicodepassword = value[0].to_owned();
134 }
135 "sfupassword" => {
136 }
138 "displayName" => {
139 self.properties.displayname = value[0].to_owned();
140 }
141 "adminCount" => {
142 let isadmin = &value[0];
143 let mut admincount = false;
144 if isadmin =="1" {
145 admincount = true;
146 }
147 self.properties.admincount = admincount;
148 }
149 "homeDirectory" => {
150 self.properties.homedirectory = value[0].to_owned();
151 }
152 "scriptpath" => {
153 self.properties.logonscript = value[0].to_owned();
154 }
155 "profilePath" | "profilepath" => {
156 if let Some(profile_path) = value.first() {
157 self.properties.profilepath = profile_path.to_owned();
158 }
159 }
160 "userAccountControl" => {
161 let uac = &value[0].parse::<u32>().unwrap_or(0);
162 self.properties.useraccountcontrol = *uac;
163 let uac_flags = get_flag(*uac);
164 for flag in uac_flags {
166 if flag.contains("AccountDisable") {
167 self.properties.enabled = false;
168 };
169 if flag.contains("PasswordNotRequired") {
171 self.properties.passwordnotreqd = true;
172 };
173 if flag.contains("DontExpirePassword") {
174 self.properties.pwdneverexpires = true;
175 };
176 if flag.contains("DontReqPreauth") {
177 self.properties.dontreqpreauth = true;
178 };
179 if flag.contains("TrustedForDelegation") {
181 self.properties.unconstraineddelegation = true;
182 self.unconstrained_delegation = true;
183 };
184 if flag.contains("NotDelegated") {
185 self.properties.sensitive = true;
186 };
187 if flag.contains("TrustedToAuthForDelegation") {
189 self.properties.trustedtoauth = true;
190 };
191 }
192 }
193 "msDS-AllowedToDelegateTo" => {
194 let mut vec_members2: Vec<Member> = Vec::new();
195 let mut seen = HashSet::<String>::new();
196
197 for spn_raw in value {
198 let spn = spn_raw.trim().replace('\\', "/");
200 let host_part = spn
202 .split_once('/') .map(|(_, rest)| rest)
204 .unwrap_or(spn.as_str());
205
206 let host = host_part.split(':').next().unwrap_or(host_part);
208
209 let fqdn_upper = host.trim().to_ascii_uppercase();
211 if fqdn_upper.is_empty() {
212 error!("Skipping empty host in SPN: {:?}", spn_raw);
213 continue;
214 }
215
216 if seen.insert(fqdn_upper.clone()) {
218 let mut m = Member::new();
219 *m.object_identifier_mut() = fqdn_upper; *m.object_type_mut() = "Computer".to_string();
221 vec_members2.push(m);
222 }
223 }
224
225 self.allowed_to_delegate = vec_members2;
226 }
227 "lastLogon" => {
228 let lastlogon = &value[0].parse::<i64>().unwrap_or(0);
229 if lastlogon.is_positive() {
230 let epoch = convert_timestamp(*lastlogon);
231 self.properties.lastlogon = epoch;
232 }
233 }
234 "lastLogonTimestamp" => {
235 let lastlogontimestamp = &value[0].parse::<i64>().unwrap_or(0);
236 if lastlogontimestamp.is_positive() {
237 let epoch = convert_timestamp(*lastlogontimestamp);
238 self.properties.lastlogontimestamp = epoch;
239 }
240 }
241 "pwdLastSet" => {
242 let pwdlastset = &value[0].parse::<i64>().unwrap_or(0);
243 if pwdlastset.is_positive() {
244 let epoch = convert_timestamp(*pwdlastset);
245 self.properties.pwdlastset = epoch;
246 }
247 }
248 "whenCreated" => {
249 let epoch = string_to_epoch(&value[0])?;
250 if epoch.is_positive() {
251 self.properties.whencreated = epoch;
252 }
253 }
254 "servicePrincipalName" => {
255 let mut targets: Vec<SPNTarget> = Vec::new();
257 let mut result: Vec<String> = Vec::new();
258 let mut added: bool = false;
259 for v in value {
260 result.push(v.to_owned());
261 let _target = match check_spn(v).to_owned() {
263 Some(_target) => {
264 if !added {
265 targets.push(_target.to_owned());
266 added = true;
267 }
268 },
269 None => {}
270 };
271 }
272 self.properties.serviceprincipalnames = result;
273 self.properties.hasspn = true;
274 self.spn_targets = targets;
275 }
276 "primaryGroupID" => {
277 group_id = value[0].to_owned();
278 }
279 "isDeleted" => {
280 self.is_deleted = true;
281 }
282 "msDS-SupportedEncryptionTypes" => {
283 self.properties.supportedencryptiontypes = convert_encryption_types(value[0].parse::<i32>().unwrap_or(0));
284 }
285 _ => {}
286 }
287 }
288
289 let mut sid: String = "".to_owned();
291 for (key, value) in &result_bin {
292 match key.as_str() {
293 "objectSid" => {
294 sid = sid_maker(LdapSid::parse(&value[0]).unwrap().1, domain);
295 self.object_identifier = sid.to_owned();
296
297 for domain_sid in OBJECT_SID_RE1.captures_iter(&sid) {
298 self.properties.domainsid = domain_sid[0].to_owned().to_string();
299 }
300 }
301 "nTSecurityDescriptor" => {
302 let relations_ace = parse_ntsecuritydescriptor(
304 self,
305 &value[0],
306 "User",
307 &result_attrs,
308 &result_bin,
309 domain,
310 schema_guid_map,
311 );
312 self.aces_mut().extend(relations_ace);
313 }
314 "sIDHistory" => {
315 let mut list_sid_history: Vec<String> = Vec::new();
318 for bsid in value {
319 debug!("sIDHistory: {:?}", &bsid);
320 list_sid_history.push(sid_maker(LdapSid::parse(bsid).unwrap().1, domain));
321 }
323 self.properties.sidhistory = list_sid_history;
324 }
325 "msDS-GroupMSAMembership" => {
326 let mut relations_ace = parse_ntsecuritydescriptor(
328 self,
329 &value[0],
330 "User",
331 &result_attrs,
332 &result_bin,
333 domain,
334 schema_guid_map,
335 );
336 parse_gmsa(&mut relations_ace, self);
339 }
341 "userCertificate" => {
342 let res = X509Certificate::from_der(&value[0]);
345 match res {
346 Ok((_rem, _cert)) => {},
347 _ => error!("CA x509 certificate parsing failed: {:?}", res),
348 }
349 }
350 _ => {}
351 }
352 }
353
354 #[allow(irrefutable_let_patterns)]
356 if let id = group_id {
357 if let Some(part1) = SID_PART1_RE1.find(&sid) {
358 self.primary_group_sid = format!("{}{}", part1.as_str(), id);
359 } else {
360 eprintln!("[!] Regex did not match any part of the SID");
361 }
362 }
363
364 dn_sid.insert(
366 self.properties.distinguishedname.to_owned(),
367 self.object_identifier.to_owned(),
368 );
369 sid_type.insert(
371 self.object_identifier.to_owned(),
372 "User".to_string(),
373 );
374
375 Ok(())
378 }
379}
380
381impl LdapObject for User {
383 fn to_json(&self) -> Value {
385 serde_json::to_value(self).unwrap()
386 }
387
388 fn get_object_identifier(&self) -> &String {
390 &self.object_identifier
391 }
392 fn get_is_acl_protected(&self) -> &bool {
393 &self.is_acl_protected
394 }
395 fn get_aces(&self) -> &Vec<AceTemplate> {
396 &self.aces
397 }
398 fn get_spntargets(&self) -> &Vec<SPNTarget> {
399 &self.spn_targets
400 }
401 fn get_allowed_to_delegate(&self) -> &Vec<Member> {
402 &self.allowed_to_delegate
403 }
404 fn get_links(&self) -> &Vec<Link> {
405 panic!("Not used by current object.");
406 }
407 fn get_contained_by(&self) -> &Option<Member> {
408 &self.contained_by
409 }
410 fn get_child_objects(&self) -> &Vec<Member> {
411 panic!("Not used by current object.");
412 }
413 fn get_haslaps(&self) -> &bool {
414 &false
415 }
416
417 fn get_aces_mut(&mut self) -> &mut Vec<AceTemplate> {
419 &mut self.aces
420 }
421 fn get_spntargets_mut(&mut self) -> &mut Vec<SPNTarget> {
422 &mut self.spn_targets
423 }
424 fn get_allowed_to_delegate_mut(&mut self) -> &mut Vec<Member> {
425 &mut self.allowed_to_delegate
426 }
427
428 fn set_is_acl_protected(&mut self, is_acl_protected: bool) {
430 self.is_acl_protected = is_acl_protected;
431 self.properties.isaclprotected = is_acl_protected;
432 }
433 fn set_aces(&mut self, aces: Vec<AceTemplate>) {
434 self.aces = aces;
435 }
436 fn set_spntargets(&mut self, spn_targets: Vec<SPNTarget>) {
437 self.spn_targets = spn_targets;
438 }
439 fn set_allowed_to_delegate(&mut self, allowed_to_delegate: Vec<Member>) {
440 self.allowed_to_delegate = allowed_to_delegate;
441 }
442 fn set_links(&mut self, _links: Vec<Link>) {
443 }
445 fn set_contained_by(&mut self, contained_by: Option<Member>) {
446 self.contained_by = contained_by;
447 }
448 fn set_child_objects(&mut self, _child_objects: Vec<Member>) {
449 }
451}
452
453#[derive(Debug, Clone, Deserialize, Serialize, Default)]
455pub struct UserProperties {
456 domain: String,
457 name: String,
458 domainsid: String,
459 isaclprotected: bool,
460 distinguishedname: String,
461 highvalue: bool,
462 description: Option<String>,
463 whencreated: i64,
464 sensitive: bool,
465 dontreqpreauth: bool,
466 passwordnotreqd: bool,
467 unconstraineddelegation: bool,
468 pwdneverexpires: bool,
469 enabled: bool,
470 trustedtoauth: bool,
471 lastlogon: i64,
472 lastlogontimestamp: i64,
473 pwdlastset: i64,
474 serviceprincipalnames: Vec<String>,
475 hasspn: bool,
476 displayname: String,
477 email: String,
478 title: String,
479 homedirectory: String,
480 logonscript: String,
481 useraccountcontrol: u32,
482 samaccountname: String,
483 userpassword: String,
484 unixpassword: String,
485 unicodepassword: String,
486 sfupassword: String,
487 profilepath: String,
488 admincount: bool,
489 supportedencryptiontypes: Vec<String>,
490 sidhistory: Vec<String>,
491 allowedtodelegate: Vec<String>
492}
493
494impl UserProperties {
495 pub fn name(&self) -> &String {
497 &self.name
498 }
499 pub fn domainsid(&self) -> &String {
500 &self.domainsid
501 }
502 pub fn isaclprotected(&self) -> &bool {
503 &self.isaclprotected
504 }
505
506 pub fn name_mut(&mut self) -> &mut String {
508 &mut self.name
509 }
510 pub fn domainsid_mut(&mut self) -> &mut String {
511 &mut self.domainsid
512 }
513 pub fn isaclprotected_mut(&mut self) -> &mut bool {
514 &mut self.isaclprotected
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 fn parse_user_with_attrs(attrs: HashMap<String, Vec<String>>) -> User {
523 let mut user = User::new();
524 let result = SearchEntry {
525 dn: "CN=Test User,OU=Users,DC=example,DC=local".to_string(),
526 attrs,
527 bin_attrs: HashMap::new(),
528 };
529 let mut dn_sid = HashMap::new();
530 let mut sid_type = HashMap::new();
531 let schema_guid_map = HashMap::new();
532
533 user.parse(
534 result,
535 "example.local",
536 &mut dn_sid,
537 &mut sid_type,
538 "S-1-5-21-1-2-3",
539 &schema_guid_map,
540 )
541 .unwrap();
542
543 user
544 }
545
546 #[test]
547 fn parse_sets_profilepath_from_ldap_profile_path() {
548 let mut attrs = HashMap::new();
549 attrs.insert(
550 "sAMAccountName".to_string(),
551 vec!["rh.profilepath".to_string()],
552 );
553 attrs.insert(
554 "profilePath".to_string(),
555 vec![r"\\FILE01\Profiles\rh.profilepath".to_string()],
556 );
557
558 let user = parse_user_with_attrs(attrs);
559
560 assert_eq!(
561 user.properties.profilepath,
562 r"\\FILE01\Profiles\rh.profilepath"
563 );
564 assert_eq!(
565 user.to_json()["Properties"]["profilepath"],
566 r"\\FILE01\Profiles\rh.profilepath"
567 );
568 }
569
570 #[test]
571 fn parse_defaults_profilepath_to_empty_string_when_absent() {
572 let mut attrs = HashMap::new();
573 attrs.insert(
574 "sAMAccountName".to_string(),
575 vec!["rh.profilepath.control".to_string()],
576 );
577
578 let user = parse_user_with_attrs(attrs);
579
580 assert_eq!(user.properties.profilepath, "");
581 assert_eq!(user.to_json()["Properties"]["profilepath"], "");
582 }
583}