Skip to main content

rusthound_ce/json/checker/
common.rs

1use std::collections::HashMap;
2use std::error::Error;
3
4use once_cell::sync::Lazy;
5use rayon::prelude::*;
6use regex::Regex;
7use crate::enums::ldaptype::*;
8use crate::objects::common::Link;
9use crate::objects::{
10    user::User,
11    computer::Computer,
12    group::Group,
13    ou::Ou,
14    domain::Domain,
15    trust::Trust,
16    common::{Member, GPOChange, LdapObject}
17};
18//use log::{info,debug,trace};
19use crate::transport::ldap::prepare_ldap_dc;
20use crate::utils::format::domain_to_dc;
21use crate::enums::regex::{COMMON_RE1,DOMAIN_SID_RE1};
22use indicatif::ProgressBar;
23use log::{error,trace};
24
25/// Function to add default groups
26/// <https://github.com/fox-it/BloodHound.py/blob/645082e3462c93f31b571db945cde1fd7b837fb9/bloodhound/enumeration/memberships.py#L411>
27pub fn add_default_groups(
28    vec_groups: &mut Vec<Group>,
29    vec_computers: &[Computer],
30    domain: String
31) -> Result<(), Box<dyn Error>> {
32    let mut member_sid = "".to_owned();
33    let mut domain_sid = "".to_owned();
34
35    let mut template_member = Member::new();
36    *template_member.object_type_mut() = "Computer".to_string();
37
38    // ENTERPRISE DOMAIN CONTROLLERS
39    let mut edc_group = Group::new();
40    let mut sid = domain.to_uppercase();
41    sid.push_str("-S-1-5-9");
42
43    let mut name = "ENTERPRISE DOMAIN CONTROLLERS@".to_owned();
44    name.push_str(&domain.to_uppercase());
45
46    let mut vec_members: Vec<Member> = Vec::new();
47    for computer in vec_computers {
48        if computer.properties().get_is_dc().to_owned()
49        {
50            // *template_member.object_identifier_mut() = computer.object_identifier().to_string();
51            // vec_members.push(template_member.to_owned());
52            // let re = Regex::new(r"^S-[0-9]{1}-[0-9]{1}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}")?;
53            // let mut sids: Vec<String> = Vec::new();
54            // for sid in re.captures_iter(&computer.object_identifier().to_string())
55            // {
56            //     sids.push(sid[0].to_owned().to_string());
57            // }
58            // domain_sid = sids[0].to_string();
59            *template_member.object_identifier_mut() = computer.object_identifier().clone();
60            vec_members.push(template_member.clone());
61            if let Some(capture) = COMMON_RE1.captures(computer.object_identifier()) {
62
63                member_sid = capture.get(0).map(|m| m.as_str().to_string()).unwrap_or_default();
64
65                if let Some(capture) = DOMAIN_SID_RE1.captures(computer.object_identifier()) {
66                    domain_sid = capture.get(0).unwrap().as_str().to_string();
67                }
68
69            }
70        }
71    }
72
73    *edc_group.object_identifier_mut() = sid;
74    *edc_group.properties_mut().name_mut() = name;
75    *edc_group.members_mut() = vec_members;
76    *edc_group.properties_mut().domain_mut() = domain.to_owned();
77    *edc_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
78    vec_groups.push(edc_group);
79
80    // ACCOUNT OPERATORS
81    let mut account_operators_group = Group::new();
82    sid = domain.to_owned().to_uppercase();
83    sid.push_str("-S-1-5-32-548");
84    let mut name = "ACCOUNT OPERATORS@".to_owned();
85    name.push_str(&domain.to_uppercase());
86    
87    *account_operators_group.object_identifier_mut() = sid;
88    *account_operators_group.properties_mut().name_mut() = name;
89    *account_operators_group.properties_mut().highvalue_mut() = true;
90    *account_operators_group.properties_mut().domain_mut() = domain.to_owned();
91    *account_operators_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
92    vec_groups.push(account_operators_group);
93
94    // WINDOWS AUTHORIZATION ACCESS GROUP
95    let mut waag_group = Group::new();
96    sid = domain.to_uppercase();
97    sid.push_str("-S-1-5-32-560");
98    let mut name = "WINDOWS AUTHORIZATION ACCESS GROUP@".to_owned();
99    name.push_str(&domain.to_uppercase());
100    *waag_group.object_identifier_mut() = sid;
101    *waag_group.properties_mut().name_mut() = name;
102    *waag_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
103    vec_groups.push(waag_group);
104
105    // EVERYONE
106    let mut everyone_group = Group::new();
107    sid = domain.to_uppercase();
108    sid.push_str("-S-1-1-0");
109    let mut name = "EVERYONE@".to_owned();
110    name.push_str(&domain.to_uppercase());
111
112    let mut vec_everyone_members: Vec<Member> = Vec::new();
113    let mut member_id = member_sid.to_owned();
114    member_id.push_str("-515");
115    *template_member.object_identifier_mut() = member_id.to_owned();
116    *template_member.object_type_mut() = "Group".to_string();
117    vec_everyone_members.push(template_member.to_owned());
118
119    member_id = member_sid.to_owned();
120    member_id.push_str("-513");
121    *template_member.object_identifier_mut() = member_id.to_owned();
122    *template_member.object_type_mut() = "Group".to_string();
123    vec_everyone_members.push(template_member.to_owned());
124
125    *everyone_group.object_identifier_mut() = sid;
126    *everyone_group.properties_mut().name_mut() = name;
127    *everyone_group.properties_mut().domain_mut() = domain.to_owned();
128    *everyone_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
129    *everyone_group.members_mut() = vec_everyone_members;
130    vec_groups.push(everyone_group);
131
132    // AUTHENTICATED USERS
133    let mut auth_users_group = Group::new();
134    sid = domain.to_uppercase();
135    sid.push_str("-S-1-5-11");
136    let mut name = "AUTHENTICATED USERS@".to_owned();
137    name.push_str(&domain.to_uppercase());
138
139    let mut vec_auth_users_members: Vec<Member> = Vec::new();
140    member_id = member_sid.to_owned();
141    member_id.push_str("-515");
142    *template_member.object_identifier_mut() = member_id.to_owned();
143    *template_member.object_type_mut() = "Group".to_string();
144    vec_auth_users_members.push(template_member.to_owned());
145
146    member_id = member_sid.to_owned();
147    member_id.push_str("-513");
148    *template_member.object_identifier_mut() = member_id.to_owned();
149    *template_member.object_type_mut() = "Group".to_string();
150    vec_auth_users_members.push(template_member.to_owned());
151
152    *auth_users_group.object_identifier_mut() = sid;
153    *auth_users_group.properties_mut().name_mut() = name;
154    *auth_users_group.properties_mut().domain_mut() = domain.to_owned();
155    *auth_users_group.members_mut() = vec_auth_users_members;
156    *auth_users_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
157    vec_groups.push(auth_users_group);
158
159    // ADMINISTRATORS
160    let mut administrators_group = Group::new();
161    sid = domain.to_uppercase();
162    sid.push_str("-S-1-5-32-544");
163    let mut name = "ADMINISTRATORS@".to_owned();
164    name.push_str(&domain.to_uppercase());
165
166    *administrators_group.object_identifier_mut() = sid;
167    *administrators_group.properties_mut().name_mut() = name;
168    *administrators_group.properties_mut().highvalue_mut() = true;
169    *administrators_group.properties_mut().domain_mut() = domain.to_owned();
170    *administrators_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
171    vec_groups.push(administrators_group);
172
173    // PRE-WINDOWS 2000 COMPATIBLE ACCESS
174    let mut pw2000ca_group = Group::new();
175    sid = domain.to_uppercase();
176    sid.push_str("-S-1-5-32-554");
177    let mut name = "PRE-WINDOWS 2000 COMPATIBLE ACCESS@".to_owned();
178    name.push_str(&domain.to_uppercase());
179            
180    *pw2000ca_group.object_identifier_mut() = sid;
181    *pw2000ca_group.properties_mut().name_mut() = name;
182    *pw2000ca_group.properties_mut().domain_mut() = domain.to_owned();
183    *pw2000ca_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
184    vec_groups.push(pw2000ca_group);    
185
186    // INTERACTIVE
187    let mut interactive_group = Group::new();
188    sid = domain.to_uppercase();
189    sid.push_str("-S-1-5-4");
190    let mut name = "INTERACTIVE@".to_owned();
191    name.push_str(&domain.to_uppercase());
192
193    *interactive_group.object_identifier_mut() = sid;
194    *interactive_group.properties_mut().name_mut() = name;
195    *interactive_group.properties_mut().domain_mut() = domain.to_owned();
196    *interactive_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
197    vec_groups.push(interactive_group);
198
199    // PRINT OPERATORS
200    let mut print_operators_group = Group::new();
201    sid = domain.to_uppercase();
202    sid.push_str("-S-1-5-32-550");
203    let mut name = "PRINT OPERATORS@".to_owned();
204    name.push_str(&domain.to_uppercase());
205            
206    *print_operators_group.object_identifier_mut() = sid;
207    *print_operators_group.properties_mut().name_mut() = name;
208    *print_operators_group.properties_mut().highvalue_mut() = true;
209    *print_operators_group.properties_mut().domain_mut() = domain.to_owned();
210    *print_operators_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
211    vec_groups.push(print_operators_group); 
212
213    // TERMINAL SERVER LICENSE SERVERS
214    let mut tsls_group = Group::new();
215    sid = domain.to_uppercase();
216    sid.push_str("-S-1-5-32-561");
217    let mut name = "TERMINAL SERVER LICENSE SERVERS@".to_owned();
218    name.push_str(&domain.to_uppercase());
219            
220    *tsls_group.object_identifier_mut() = sid;
221    *tsls_group.properties_mut().name_mut() = name;
222    *tsls_group.properties_mut().domain_mut() = domain.to_owned();
223    *tsls_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
224    vec_groups.push(tsls_group); 
225
226    // INCOMING FOREST TRUST BUILDERS
227    let mut iftb_group = Group::new();
228    sid = domain.to_uppercase();
229    sid.push_str("-S-1-5-32-557");
230    let mut name = "INCOMING FOREST TRUST BUILDERS@".to_owned();
231    name.push_str(&domain.to_uppercase());
232            
233    *iftb_group.object_identifier_mut() = sid;
234    *iftb_group.properties_mut().name_mut() = name;
235    *iftb_group.properties_mut().domain_mut() = domain.to_owned();
236    *iftb_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
237    vec_groups.push(iftb_group); 
238 
239    // THIS ORGANIZATION 
240    let mut this_organization_group = Group::new();
241    sid = domain.to_uppercase();
242    sid.push_str("-S-1-5-15");
243    let mut name = "THIS ORGANIZATION@".to_owned();
244    name.push_str(&domain.to_uppercase());
245            
246    *this_organization_group.object_identifier_mut() = sid;
247    *this_organization_group.properties_mut().name_mut() = name;
248    *this_organization_group.properties_mut().domain_mut() = domain.to_owned();
249    *this_organization_group.properties_mut().domainsid_mut() = domain_sid.to_owned();
250    vec_groups.push(this_organization_group);
251    Ok(())
252}
253
254/// Function to add default user
255/// <https://github.com/fox-it/BloodHound.py/blob/645082e3462c93f31b571db945cde1fd7b837fb9/bloodhound/enumeration/memberships.py#L411>
256pub fn add_default_users(
257    vec_users: &mut Vec<User>,
258    domain: String
259) -> Result<(), Box<dyn Error>> {
260    // NT AUTHORITY
261    let mut ntauthority_user = User::new();
262    let mut sid = domain.to_uppercase();
263    sid.push_str("-S-1-5-20");
264    let mut name = "NT AUTHORITY@".to_owned();
265    name.push_str(&domain.to_uppercase());
266    *ntauthority_user.properties_mut().name_mut() = name;
267    *ntauthority_user.object_identifier_mut() = sid;
268
269    if let Some(first_user) = vec_users.get(0) {
270        *ntauthority_user.properties_mut().domainsid_mut() = first_user.properties().domainsid().to_string();
271    } else {
272        error!("vec_users is empty, skipping domain SID assignment");
273    }
274
275    vec_users.push(ntauthority_user);
276    Ok(())
277}
278
279/// Build a reverse index from SID to DN once, so per-object lookups are O(1)
280/// instead of an O(n) linear scan over `dn_sid`. `dn_sid` values (SIDs) are
281/// effectively unique; when a SID maps to several DNs the first one wins, which
282/// matches the previous `.find()` behaviour.
283fn build_sid_to_dn(dn_sid: &HashMap<String, String>) -> HashMap<&str, &str> {
284    let mut sid_to_dn: HashMap<&str, &str> = HashMap::with_capacity(dn_sid.len());
285    for (dn, sid) in dn_sid {
286        // When a SID maps to several DNs, keep the lexicographically smallest so
287        // the choice is deterministic instead of dependent on HashMap iteration
288        // order (which otherwise makes e.g. ContainedBy vary run-to-run).
289        sid_to_dn
290            .entry(sid.as_str())
291            .and_modify(|existing| {
292                if dn.as_str() < *existing {
293                    *existing = dn.as_str();
294                }
295            })
296            .or_insert(dn.as_str());
297    }
298    sid_to_dn
299}
300
301/// This function is to push user SID in ChildObjects v2
302pub fn add_childobjects_members<T: LdapObject + Send>(
303    vec_replaced: &mut [T],
304    dn_sid: &HashMap<String, String>,
305    sid_type: &HashMap<String, String>,
306) -> Result<(), Box<dyn Error>> {
307    // Reverse index SID -> DN, built once (was an O(n) scan per object).
308    let sid_to_dn = build_sid_to_dn(dn_sid);
309
310    // Uppercase every DN a single time instead of once per object. The old code
311    // re-allocated an uppercased copy of every `dn_sid` key for every object,
312    // which was the O(n^2) allocation storm behind the multi-GB RAM use.
313    let dn_upper: Vec<(String, &String)> =
314        dn_sid.iter().map(|(dn, sid)| (dn.to_uppercase(), sid)).collect();
315
316    // Objects are independent; process them across all CPU cores.
317    vec_replaced.par_iter_mut().for_each(|object| {
318        // Get the SID, DN, and name of the current object
319        let sid = object.get_object_identifier().to_uppercase();
320        let dn: &str = sid_to_dn.get(sid.as_str()).copied().unwrap_or(sid.as_str());
321        let name = get_name_from_full_distinguishedname(dn);
322
323        // Filter direct members from the pre-uppercased dn list
324        let direct_members: Vec<Member> = dn_upper
325            .iter()
326            .filter_map(|(dn_object_upper, value_sid)| {
327                // Check if dn_object is related to the current object's DN
328                if dn_object_upper.contains(dn)
329                    && dn_object_upper.as_str() != dn
330                    && dn_object_upper
331                        .split(',')
332                        .nth(1)
333                        .and_then(|s| s.split('=').nth(1))
334                        == Some(name.as_str())
335                {
336                    let mut member = Member::new();
337                    *member.object_identifier_mut() = (*value_sid).clone();
338                    *member.object_type_mut() =
339                        sid_type.get(*value_sid).unwrap_or(value_sid).to_string();
340                    if !member.object_identifier().is_empty() {
341                        return Some(member);
342                    }
343                }
344                None
345            })
346            .collect();
347
348        // Set direct members for the object
349        object.set_child_objects(direct_members);
350    });
351
352    Ok(())
353}
354
355/// This function is to push user SID in ChildObjects for Ou v2
356pub fn add_childobjects_members_for_ou(
357    vec_replaced: &mut [Ou],
358    dn_sid: &HashMap<String, String>,
359    sid_type: &HashMap<String, String>,
360) -> Result<(), Box<dyn Error>> {
361    // Progress bar setup
362    let total = vec_replaced.len();
363    let pb = ProgressBar::new(total as u64);
364
365    // Cache common values to avoid repeated allocations
366    let null = "NULL".to_string();
367
368    for (count, object) in vec_replaced.iter_mut().enumerate() {
369        // Update progress bar periodically
370        if count % (total / 100).max(1) == 0 {
371            pb.set_position(count as u64);
372        }
373
374        let mut direct_members = Vec::new();
375        let mut affected_computers = Vec::new();
376
377        // Fetch properties of the current object
378        let dn = object.properties().distinguishedname();
379        let mut name = object.properties().name().to_owned();
380        let sid = dn_sid.get(dn).unwrap_or(&null);
381        let otype = sid_type.get(sid).unwrap_or(&null);
382
383        // Adjust the name if not a domain
384        if otype != "Domain" {
385            if let Some(first_part) = name.split('@').next() {
386                name = first_part.to_string();
387            }
388        }
389
390        // Process all dn_sid entries
391        for (dn_object, value_sid) in dn_sid {
392            let dn_object_upper = dn_object.to_uppercase();
393
394            // Parse the "first" component of the DN
395            let first = dn_object_upper
396                .split(',')
397                .nth(1)
398                .and_then(|part| part.split('=').nth(1))
399                .unwrap_or("");
400
401            if otype != "Domain" {
402                // For non-domain objects
403                if dn_object_upper.contains(dn) && &dn_object_upper != dn && first == name {
404                    let mut member = Member::new();
405                    *member.object_identifier_mut() = value_sid.clone();
406                    let object_type = sid_type.get(value_sid).unwrap_or(&null).to_string();
407                    *member.object_type_mut() = object_type.clone();
408
409                    direct_members.push(member.clone());
410
411                    // Add computers to affected_computers if applicable
412                    if object_type == "Computer" {
413                        affected_computers.push(member);
414                    }
415                }
416            } else {
417                // For domain objects
418                if let Some(cn) = name.split('.').next() {
419                    if first.contains(cn) {
420                        let mut member = Member::new();
421                        *member.object_identifier_mut() = value_sid.clone();
422                        *member.object_type_mut() = sid_type.get(value_sid).unwrap_or(&null).to_string();
423                        direct_members.push(member);
424                    }
425                }
426            }
427        }
428
429        // Set child objects and GPO changes for OUs
430        *object.child_objects_mut() = direct_members;
431        if otype == "OU" {
432            let mut gpo_changes = GPOChange::new();
433            *gpo_changes.affected_computers_mut() = affected_computers;
434            *object.gpo_changes_mut() = gpo_changes;
435        }
436    }
437
438    pb.finish_and_clear();
439    Ok(())
440}
441
442/// This function checks GUID for all Gplinks and replaces them with the correct GUIDs
443pub fn replace_guid_gplink<T: LdapObject + Send>(
444    vec_replaced: &mut [T],
445    dn_sid: &HashMap<String, String>,
446) -> Result<(), Box<dyn Error>> {
447    // Objects are independent; process them across all CPU cores.
448    vec_replaced.par_iter_mut().for_each(|object| {
449        // Process links if they exist
450        if !object.get_links().is_empty() {
451            // Replace GUIDs in links
452            let updated_links: Vec<Link> = object
453                .get_links()
454                .iter()
455                .map(|link| {
456                    let mut new_link = link.clone(); // Clone the Link to create a new instance
457                    if let Some(new_guid) = dn_sid
458                        .iter()
459                        .find(|(key, _)| key.contains(link.guid()))
460                        .map(|(_, guid)| guid.to_owned())
461                    {
462                        *new_link.guid_mut() = new_guid;
463                    }
464                    new_link
465                })
466                .collect();
467
468            // Update the object's links
469            object.set_links(updated_links);
470        }
471    });
472
473    Ok(())
474}
475
476/// This function pushes computer SIDs into the domain's GPO changes v2
477pub fn add_affected_computers(
478    vec_domains: &mut [Domain],
479    sid_type: &HashMap<String, String>,
480) -> Result<(), Box<dyn Error>> {
481    // Filter only "Computer" SIDs and map them to Member objects
482    let vec_affected_computers: Vec<Member> = sid_type
483        .iter()
484        .filter(|&(_, obj_type)| obj_type == "Computer")
485        .map(|(sid, _)| {
486            let mut member = Member::new();
487            *member.object_type_mut() = "Computer".to_string();
488            *member.object_identifier_mut() = sid.clone();
489            member
490        })
491        .collect();
492
493    // Update the GPO changes of the first domain
494    if let Some(domain) = vec_domains.get_mut(0) {
495        let mut gpo_changes = GPOChange::new();
496        *gpo_changes.affected_computers_mut() = vec_affected_computers;
497        *domain.gpo_changes_mut() = gpo_changes;
498    }
499    Ok(())
500}
501
502/// This function pushes computer SIDs into GPO changes for each OU
503pub fn add_affected_computers_for_ou(
504    vec_ous: &mut [Ou],
505    dn_sid: &HashMap<String, String>,
506    sid_type: &HashMap<String, String>,
507) -> Result<(), Box<dyn Error>> {
508    // Group every computer under its parent DN a single time, so each OU is an
509    // O(1) lookup instead of a full scan over all computers (was O(OUs*computers)).
510    let mut computers_by_parent: HashMap<String, Vec<Member>> = HashMap::new();
511    for (dn, sid) in dn_sid {
512        if sid_type.get(sid).map(|t| t == "Computer").unwrap_or(false) {
513            let parent = get_contained_by_name_from_distinguishedname(
514                &get_cn_object_name_from_full_distinguishedname(dn),
515                dn,
516            );
517            let mut member = Member::new();
518            *member.object_identifier_mut() = sid.to_string();
519            *member.object_type_mut() = "Computer".to_string();
520            computers_by_parent.entry(parent).or_default().push(member);
521        }
522    }
523
524    // Map each OU's identifier to its DN, using a reverse index built once
525    // instead of scanning `dn_sid` per OU.
526    let sid_to_dn = build_sid_to_dn(dn_sid);
527
528    // For each OU, attach the pre-grouped affected computers (empty list when
529    // none, matching the previous behaviour of always setting gpo_changes).
530    for ou in vec_ous.iter_mut() {
531        let Some(ou_dn) = sid_to_dn.get(ou.get_object_identifier().as_str()) else {
532            continue;
533        };
534        let affected = computers_by_parent.get(*ou_dn).cloned().unwrap_or_default();
535        let mut gpo_changes = GPOChange::new();
536        *gpo_changes.affected_computers_mut() = affected;
537        *ou.gpo_changes_mut() = gpo_changes;
538    }
539    Ok(())
540}
541
542/// This function replaces FQDN by SID in users' SPNTargets or computers' AllowedToDelegate
543pub fn replace_fqdn_by_sid<T: LdapObject + Send>(
544    object_type: Type,
545    vec_src: &mut [T],
546    fqdn_sid: &HashMap<String, String>,
547) -> Result<(), Box<dyn Error>> {
548    // Objects are independent and `fqdn_sid` is read-only, so process across all
549    // CPU cores.
550    match object_type {
551        Type::User => {
552            vec_src.par_iter_mut().for_each(|obj| {
553                // Process SPNTargets
554                for target in obj.get_spntargets_mut().iter_mut() {
555                    let sid = fqdn_sid
556                        .get(target.computer_sid())
557                        .unwrap_or_else(|| target.computer_sid());
558                    *target.computer_sid_mut() = sid.to_string();
559                }
560
561                // Process AllowedToDelegate
562                for target in obj.get_allowed_to_delegate_mut().iter_mut() {
563                    let sid = fqdn_sid
564                        .get(target.object_identifier())
565                        .unwrap_or_else(|| target.object_identifier());
566                    *target.object_identifier_mut() = sid.to_string();
567                }
568            });
569        }
570        Type::Computer => {
571            vec_src.par_iter_mut().for_each(|obj| {
572                // Process AllowedToDelegate
573                for delegate in obj.get_allowed_to_delegate_mut().iter_mut() {
574                    let sid = fqdn_sid
575                        .get(delegate.object_identifier())
576                        .unwrap_or_else(|| delegate.object_identifier());
577                    *delegate.object_identifier_mut() = sid.to_string();
578                }
579            });
580        }
581        _ => {}
582    }
583
584    Ok(())
585}
586
587/// This function checks and replaces object names by SIDs in group members v2
588pub fn replace_sid_members(
589    vec_groups: &mut [Group],
590    dn_sid: &HashMap<String, String>,
591    sid_type: &HashMap<String, String>,
592    vec_trusts: &[Trust],
593) -> Result<(), Box<dyn Error>> {
594    let default_type = "Group".to_string();
595
596    // Groups are independent and the maps/trusts are read-only, so resolve
597    // members across all CPU cores.
598    vec_groups.par_iter_mut().for_each(|group| {
599        for member in group.members_mut() {
600            let member_dn = member.object_identifier();
601
602            // 1) Skip clearly invalid entries
603            if member_dn.trim().is_empty() || member_dn.eq_ignore_ascii_case("SID") {
604                error!("Skipping empty/invalid member DN in group");
605                continue;
606            }
607
608            // 2) Try to resolve via dn_sid
609            if let Some(sid) = dn_sid.get(member_dn) {
610                if !sid.is_empty() && sid != "NULL" {
611                    let type_object =
612                        sid_type.get(sid).cloned().unwrap_or_else(|| default_type.clone());
613                    *member.object_identifier_mut() = sid.clone();
614                    *member.object_type_mut() = type_object;
615                    continue;
616                }
617            }
618
619            // 3) Fallback: try trusts
620            match sid_maker_from_another_domain(vec_trusts, member_dn) {
621                Ok(generated_sid) if !generated_sid.is_empty() => {
622                    *member.object_identifier_mut() = generated_sid;
623                    *member.object_type_mut() = default_type.clone();
624                }
625                Ok(_) => {
626                    error!("Could not resolve SID for member DN: {}", member_dn);
627                }
628                Err(e) => {
629                    error!("Failed to resolve member DN {}: {}", member_dn, e);
630                }
631            }
632        }
633    });
634
635    Ok(())
636}
637
638/// Make the SID from domain present in trust v2
639fn sid_maker_from_another_domain(
640    vec_trusts: &[Trust],
641    object_identifier: &String,
642) -> Result<String, Box<dyn Error>> {
643    // Regex for SID matching, compiled once instead of on every call.
644    static SID_REGEX: Lazy<Regex> =
645        Lazy::new(|| Regex::new(r"S-[0-9]+-[0-9]+-[0-9]+(?:-[0-9]+)+").unwrap());
646    let sid_regex = &*SID_REGEX;
647
648    // Check if the object_identifier matches any trusted domain
649    for trust in vec_trusts {
650        let ldap_dc = prepare_ldap_dc(trust.target_domain_name());
651        if object_identifier.contains(&ldap_dc[0]) {
652            let id = get_id_from_objectidentifier(object_identifier)?;
653            return Ok(format!("{}{}", trust.target_domain_name(), id))
654        }
655    }
656
657    // Check if object_identifier contains an SID
658    if object_identifier.contains("CN=S-") {
659        if let Some(capture) = sid_regex.captures(object_identifier).and_then(|cap| cap.get(0)) {
660            return Ok(capture.as_str().to_owned())
661        }
662    }
663
664    // Default case: return the object_identifier as-is
665    Ok(object_identifier.to_string())
666}
667
668// Get id from objectidentifier for all common group (Administrators ...) v2
669// https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers
670fn get_id_from_objectidentifier(
671    object_identifier: &str
672) -> Result<String, Box<dyn Error>> {
673
674    // Static mapping of built-in group names to their well-known RIDs.
675    //
676    // NOTE: the French-language entries are intentional and REQUIRED, not
677    // untranslated text. On a French-locale Domain Controller the built-in
678    // groups are returned by LDAP under their localized names (e.g. "Domain
679    // Admins" is literally "Administrateurs du domaine"), so each RID is
680    // matched from both its English and French display name. Removing the
681    // French names would break well-known-SID resolution on French domains.
682    // Additional locales can be added here following the same pattern.
683    const NAME_TO_RID: [(&str, &str); 16] = [
684        // -512  Domain Admins
685        ("DOMAIN ADMINS", "-512"),
686        ("ADMINISTRATEURS DU DOMAINE", "-512"),
687        // -513  Domain Users
688        ("DOMAIN USERS", "-513"),
689        ("UTILISATEURS DU DOMAINE", "-513"),
690        // -514  Domain Guests
691        ("DOMAIN GUESTS", "-514"),
692        ("INVITES DE DOMAINE", "-514"),
693        // -515  Domain Computers
694        ("DOMAIN COMPUTERS", "-515"),
695        ("ORDINATEURS DE DOMAINE", "-515"),
696        // -516  Domain Controllers
697        ("DOMAIN CONTROLLERS", "-516"),
698        ("CONTRÔLEURS DE DOMAINE", "-516"),
699        // -517  Cert Publishers
700        ("CERT PUBLISHERS", "-517"),
701        ("EDITEURS DE CERTIFICATS", "-517"),
702        // -518  Schema Admins
703        ("SCHEMA ADMINS", "-518"),
704        ("ADMINISTRATEURS DU SCHEMA", "-518"),
705        // -519  Enterprise Admins
706        ("ENTERPRISE ADMINS", "-519"),
707        ("ADMINISTRATEURS DE L'ENTREPRISE", "-519"),
708    ];
709
710    // Iterate over the static array to find a match
711    for (name, rid) in NAME_TO_RID.iter() {
712        if object_identifier.contains(name) {
713            return Ok(rid.to_string())
714        }
715    }
716
717    // Default case if no match is found
718    Ok("NULL_ID1".to_string())
719}
720
721/// This function push trust domain values in domain
722pub fn add_trustdomain(
723    vec_domains: &mut Vec<Domain>,
724    vec_trusts: &mut [Trust]
725) -> Result<(), Box<dyn Error>> {
726    if !&vec_trusts[0].target_domain_sid().to_string().contains("SID") {
727        let mut trusts: Vec<Trust> = Vec::new();
728        for trust in vec_trusts {
729            trusts.push(trust.to_owned());
730            let mut new_domain = Domain::new();
731            *new_domain.object_identifier_mut() = trust.target_domain_sid().to_string();
732            *new_domain.properties_mut().name_mut() = trust.target_domain_name().to_string();
733            *new_domain.properties_mut().domain_mut() = trust.target_domain_name().to_string();
734            *new_domain.properties_mut().distinguishedname_mut() = domain_to_dc(trust.target_domain_name());
735            *new_domain.properties_mut().highvalue_mut() = true;
736            vec_domains.push(new_domain);
737        }
738        *vec_domains[0].trusts_mut() = trusts.to_owned();
739    }
740    Ok(())
741}
742
743/// This function checks PrincipalSID for all ACEs and adds the PrincipalType ("Group", "User", "Computer") v2
744pub fn add_type_for_ace<T: LdapObject + Send>(
745    object: &mut [T],
746    sid_type: &HashMap<String, String>,
747) -> Result<(), Box<dyn Error>> {
748    // Default type for unmatched SIDs
749    let default_type = "Group".to_string();
750
751    // Each object's ACEs are independent and `sid_type` is read-only here, so
752    // this is embarrassingly parallel: spread it across all CPU cores.
753    object.par_iter_mut().for_each(|obj| {
754        for ace in obj.get_aces_mut() {
755            // Fetch the type from sid_type or use the default
756            let type_object = sid_type
757                .get(ace.principal_sid())
758                .unwrap_or(&default_type)
759                .clone();
760
761            // Update the principal type
762            *ace.principal_type_mut() = type_object;
763        }
764    });
765
766    Ok(())
767}
768
769/// This function checks PrincipalSID for all AllowedToAct objects and adds the PrincipalType ("Group", "User", "Computer") v2
770pub fn add_type_for_allowtedtoact(
771    computer: &mut [Computer],
772    sid_type: &HashMap<String, String>,
773) -> Result<(), Box<dyn Error>> {
774    // Default type for unmatched SIDs
775    let default_type = "Computer".to_string();
776
777    // Computers are independent; process them across all CPU cores.
778    computer.par_iter_mut().for_each(|comp| {
779        // Process all AllowedToAct objects
780        for allowed in comp.allowed_to_act_mut() {
781            let type_object = sid_type
782                .get(allowed.object_identifier())
783                .unwrap_or(&default_type)
784                .clone();
785
786            *allowed.object_type_mut() = type_object;
787        }
788    });
789
790    Ok(())
791}
792
793/// This function pushes user SID into ChildObjects for Ou v2
794pub fn add_contained_by_for<T: LdapObject + Send>(
795    vec_replaced: &mut [T],
796    dn_sid: &HashMap<String, String>,
797    sid_type: &HashMap<String, String>,
798) -> Result<(), Box<dyn Error>> {
799    // Default type for unmatched SIDs
800    let default_type = "Group".to_string();
801
802    // Reverse index SID -> DN, built once. This used to be an O(n) linear scan
803    // over `dn_sid` *per object* (`dn_sid.iter().find_map(...)`), i.e. O(n^2)
804    // over the whole dataset — the dominant hang on large domains.
805    let sid_to_dn = build_sid_to_dn(dn_sid);
806
807    // Each object is independent and the maps are read-only here, so fan the
808    // work out across all CPU cores.
809    vec_replaced.par_iter_mut().for_each(|object| {
810        // Fetch SID and DN for the current object
811        let sid = object.get_object_identifier();
812        let Some(dn) = sid_to_dn.get(sid.as_str()).copied() else {
813            return;
814        };
815
816        let otype = sid_type.get(sid).unwrap_or(&default_type);
817        if otype != "Domain" {
818            // Extract CN name and contained-by name
819            let dn_owned = dn.to_string();
820            let cn_name = get_cn_object_name_from_full_distinguishedname(&dn_owned);
821            let contained_by_name = get_contained_by_name_from_distinguishedname(&cn_name, &dn_owned);
822
823            // Check if the contained-by name exists in dn_sid
824            if let Some(sid_contained_by) = dn_sid.get(&contained_by_name) {
825                let type_contained_by = sid_type.get(sid_contained_by).unwrap_or(&default_type);
826
827                // Create and set the contained_by Member
828                let mut contained_by = Member::new();
829                *contained_by.object_identifier_mut() = sid_contained_by.to_string();
830                *contained_by.object_type_mut() = type_contained_by.to_string();
831                object.set_contained_by(Some(contained_by));
832            }
833        }
834    });
835
836    Ok(())
837}
838
839/// Function to get name from DN
840pub fn get_name_from_full_distinguishedname(dn_object: &str) -> String {
841    // Example:
842    // dn_object = CN=G0H4N,CN=USERS,DC=ESSOS,DC=LOCAL
843    trace!("get_name_from_full_distinguishedname() {:?}",&dn_object);
844    let split1 = dn_object.split(",");
845    let vec1 = split1.collect::<Vec<&str>>();
846    let split2 = vec1[0].split("=");
847    let vec2 = split2.collect::<Vec<&str>>();
848    let name = vec2[1].to_owned();
849    // name = G0H4N
850    name
851}
852
853/// Function to get CN=name from DN
854fn get_cn_object_name_from_full_distinguishedname(dn_object: &String) -> String {
855    // Example:
856    // dn_object = CN=G0H4N,CN=USERS,DC=ESSOS,DC=LOCAL
857    let name = dn_object.to_owned();
858    let split = name.split(",");
859    let vec = split.collect::<Vec<&str>>();
860    let name = vec[0].to_owned();
861    // name = CN=G0H4N
862    name
863}
864
865/// Function to get first degree contained by name from DN
866fn get_contained_by_name_from_distinguishedname(cn_name: &str, dn_object: &str) -> String {
867    // Example:
868    // dn_object = CN=G0H4N,CN=USERS,DC=ESSOS,DC=LOCAL
869    let name = format!("{},",cn_name);
870    let split = dn_object.split(&name);
871    let vec = split.collect::<Vec<&str>>();
872    let dn_contained_by = vec[1].to_owned();
873    // dn_contained_by = CN=USERS,DC=ESSOS,DC=LOCAL
874    dn_contained_by
875}
876
877//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
878
879#[cfg(test)]
880mod tests {
881    
882    use crate::json::checker::common::{
883        get_name_from_full_distinguishedname,
884        get_cn_object_name_from_full_distinguishedname,
885        get_contained_by_name_from_distinguishedname
886    };
887    
888    #[test]
889    #[rustfmt::skip]
890    pub fn test_get_name_from_full_distinguishedname() {
891        // Example:
892        // dn_object = CN=G0H4N,CN=USERS,DC=ESSOS,DC=LOCAL
893        let dn_object = "CN=G0H4N,CN=USERS,DC=ESSOS,DC=LOCAL".to_string();
894        let cn_name =  get_name_from_full_distinguishedname(&dn_object);
895        println!("dn_object: {:?}",dn_object);
896        println!("cn_name: {:?}",cn_name);
897        assert_eq!(cn_name, "G0H4N".to_string());
898    }
899
900    #[test]
901    #[rustfmt::skip]
902    pub fn test_get_cn_object_name_from_full_distinguishedname() {
903        // Example:
904        // dn_object = CN=G0H4N,CN=USERS,DC=ESSOS,DC=LOCAL
905        let dn_object = "CN=G0H4N,CN=USERS,DC=ESSOS,DC=LOCAL".to_string();
906        let cn_name =  get_cn_object_name_from_full_distinguishedname(&dn_object);
907        println!("dn_object: {:?}",dn_object);
908        println!("cn_name: {:?}",cn_name);
909        assert_eq!(cn_name, "CN=G0H4N".to_string());
910    }
911    
912    #[test]
913    #[rustfmt::skip]
914    pub fn test_get_contained_by_name_from_name() {
915        // Example:
916        // dn_object = CN=G0H4N,CN=USERS,DC=ESSOS,DC=LOCAL
917        let dn_object = "CN=G0H4N,CN=USERS,DC=ESSOS,DC=LOCAL".to_string();
918        let cn_name = "CN=G0H4N".to_string();
919        let contained_by_dn =  get_contained_by_name_from_distinguishedname(&cn_name, &dn_object);
920        println!("dn_object: {:?}",dn_object);
921        println!("contained_by_dn: {:?}",contained_by_dn);
922        assert_eq!(contained_by_dn, "CN=USERS,DC=ESSOS,DC=LOCAL".to_string());
923    }
924}