Skip to main content

adhammer_core/
snapshot.rs

1//! An immutable point-in-time capture of the directory. Checks and the graph
2//! builder read this; the collector produces it. Keeps a few precomputed indices.
3
4use crate::object::AdObject;
5use crate::sid::Sid;
6use std::collections::HashMap;
7
8#[derive(Clone, Debug, Default)]
9pub struct DomainInfo {
10    pub domain_dn: String, // DC=corp,DC=local
11    pub domain_sid: Option<Sid>,
12    pub netbios: Option<String>,
13    pub functional_level: Option<i64>,
14    pub machine_account_quota: Option<i64>,
15}
16
17#[derive(Clone, Debug, Default)]
18pub struct Snapshot {
19    pub domain: DomainInfo,
20    pub objects: Vec<AdObject>,
21    /// dn (lowercased) -> index into `objects`, for O(1) resolution while walking ACLs.
22    dn_index: HashMap<String, usize>,
23}
24
25impl Snapshot {
26    pub fn new(domain: DomainInfo, objects: Vec<AdObject>) -> Self {
27        let dn_index = objects
28            .iter()
29            .enumerate()
30            .map(|(i, o)| (o.dn.to_ascii_lowercase(), i))
31            .collect();
32        Snapshot {
33            domain,
34            objects,
35            dn_index,
36        }
37    }
38
39    pub fn by_dn(&self, dn: &str) -> Option<&AdObject> {
40        self.dn_index
41            .get(&dn.to_ascii_lowercase())
42            .map(|&i| &self.objects[i])
43    }
44
45    /// Find an object by exact SID (objectSid).
46    pub fn by_sid(&self, sid: &Sid) -> Option<&AdObject> {
47        self.objects
48            .iter()
49            .find(|o| o.bin1("objectSid").and_then(Sid::from_bytes).as_ref() == Some(sid))
50    }
51
52    /// Find a group/object by sAMAccountName (case-insensitive). Locale-dependent —
53    /// prefer `by_sid` for well-known groups.
54    pub fn by_sam(&self, sam: &str) -> Option<&AdObject> {
55        self.objects.iter().find(|o| {
56            o.one("sAMAccountName")
57                .is_some_and(|s| s.eq_ignore_ascii_case(sam))
58        })
59    }
60
61    pub fn iter_class<'a>(&'a self, class: &'a str) -> impl Iterator<Item = &'a AdObject> {
62        self.objects.iter().filter(move |o| o.has_class(class))
63    }
64
65    /// Resolve a domain RID to its object (e.g. krbtgt = 502) via objectSid.
66    pub fn by_rid(&self, rid: u32) -> Option<&AdObject> {
67        let dsid = self.domain.domain_sid.as_ref()?;
68        self.objects.iter().find(|o| {
69            o.bin1("objectSid")
70                .and_then(Sid::from_bytes)
71                .map(|s| {
72                    s.rid() == Some(rid)
73                        && s.sub_authorities[..s.sub_authorities.len() - 1]
74                            == dsid.sub_authorities[..]
75                })
76                .unwrap_or(false)
77        })
78    }
79}