Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions dissect/database/ese/ntds/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from dissect.database.ese.ntds.query import Query
from dissect.database.ese.ntds.schema import Schema
from dissect.database.ese.ntds.sd import SecurityDescriptor
from dissect.database.ese.ntds.util import DN, DatabaseFlags, SearchFlags, encode_value
from dissect.database.ese.ntds.util import DN, DatabaseFlag, SearchFlag, encode_value

if TYPE_CHECKING:
from collections.abc import Iterator
Expand Down Expand Up @@ -44,14 +44,14 @@ def __init__(self, fh: BinaryIO):
self.data._make_dn.cache_clear()

@cached_property
def flags(self) -> DatabaseFlags | None:
def flags(self) -> DatabaseFlag | None:
"""Return the database flags."""
if self.hiddeninfo is None:
return None

result = DatabaseFlags(0)
result = DatabaseFlag(0)
flags = self.hiddeninfo.get("flags_col")
for idx, member in enumerate(DatabaseFlags.__members__.values()):
for idx, member in enumerate(DatabaseFlag.__members__.values()):
if flags[idx] == ord(b"1"):
result = member if result is None else result | member

Expand Down Expand Up @@ -256,9 +256,9 @@ def _get_index(self, attribute: str) -> Index:
if schema.search_flags is None:
raise ValueError(f"Attribute is not indexed: {attribute!r}")

if SearchFlags.Indexed in schema.search_flags:
if SearchFlag.Indexed in schema.search_flags:
name = f"INDEX_{schema.id:08x}"
elif SearchFlags.TupleIndexed in schema.search_flags:
elif SearchFlag.TupleIndexed in schema.search_flags:
name = f"INDEX_T_{schema.id:08x}"
else:
# TODO add ContainerIndexed
Expand Down
15 changes: 12 additions & 3 deletions dissect/database/ese/ntds/ntds.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
TrustedDomain,
User,
)
from dissect.database.ese.ntds.objects.organizationalunit import OrganizationalUnit
from dissect.database.ese.ntds.pek import PEK


Expand Down Expand Up @@ -83,19 +84,23 @@ def search(self, **kwargs: str) -> Iterator[Object]:

def groups(self) -> Iterator[Group]:
"""Get all group objects from the database."""
yield from self.search(objectCategory="group")
yield from self.search(objectClass="group")

def servers(self) -> Iterator[Server]:
"""Get all server objects from the database."""
yield from self.search(objectCategory="server")
yield from self.search(objectClass="server")

def users(self) -> Iterator[User]:
"""Get all user objects from the database."""
yield from self.search(objectCategory="person", objectClass="user")

def computers(self) -> Iterator[Computer]:
"""Get all computer objects from the database."""
yield from self.search(objectCategory="computer")
yield from self.search(objectClass="computer")

def domains(self) -> Iterator[DomainDNS]:
"""Get all domain objects from the database."""
yield from self.search(objectClass="domainDNS")

def trusts(self) -> Iterator[TrustedDomain]:
"""Get all trust objects from the database."""
Expand All @@ -105,6 +110,10 @@ def group_policies(self) -> Iterator[GroupPolicyContainer]:
"""Get all group policy objects (GPO) objects from the database."""
yield from self.search(objectClass="groupPolicyContainer")

def organizational_units(self) -> Iterator[OrganizationalUnit]:
"""Get all organizational unit (OU) objects from the database."""
yield from self.search(objectClass="organizationalUnit")

def secrets(self) -> Iterator[Secret]:
"""Get all secret objects from the database."""
yield from self.search(objectClass="secret")
Expand Down
15 changes: 15 additions & 0 deletions dissect/database/ese/ntds/objects/attributeschema.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

from dissect.database.ese.ntds.objects.top import Top
from dissect.database.ese.ntds.util import SearchFlag, SystemFlagAttribute

if TYPE_CHECKING:
from dissect.database.ese.ntds.objects.object import DecoderMap


class AttributeSchema(Top):
Expand All @@ -11,3 +17,12 @@ class AttributeSchema(Top):
"""

__object_class__ = "attributeSchema"
__decoders__: ClassVar[DecoderMap] = {
"searchFlags": lambda db, value: SearchFlag(value),
"systemFlags": lambda db, value: SystemFlagAttribute(value),
}

@property
def search_flags(self) -> SearchFlag | None:
"""Return the searchFlags of this attribute schema."""
return self.get("searchFlags")
5 changes: 5 additions & 0 deletions dissect/database/ese/ntds/objects/certificationauthority.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ class CertificationAuthority(Top):
"""

__object_class__ = "certificationAuthority"

@property
def dns_host_name(self) -> str | None:
"""Return the dNSHostName of this Certification Authority."""
return self.get("dNSHostName")
15 changes: 15 additions & 0 deletions dissect/database/ese/ntds/objects/computer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ class Computer(User):
def __repr_body__(self) -> str:
return f"name={self.name!r}"

@property
def dns_host_name(self) -> str | None:
"""Return the dNSHostName of this computer."""
return self.get("dNSHostName")

@property
def operating_system(self) -> str | None:
"""Return the operatingSystem of this computer."""
return self.get("operatingSystem")

@property
def operating_system_version(self) -> str | None:
"""Return the operatingSystemVersion of this computer."""
return self.get("operatingSystemVersion")

def fve_recovery_information(self) -> Iterator[MSFVERecoveryInformation]:
"""Return the BitLocker recovery information objects associated with this computer."""
for child in self.children():
Expand Down
5 changes: 5 additions & 0 deletions dissect/database/ese/ntds/objects/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ class Configuration(Top):
"""

__object_class__ = "configuration"

@property
def gp_link(self) -> str | None:
"""Return the group policy link of the configuration."""
return self.get("gPLink")
14 changes: 14 additions & 0 deletions dissect/database/ese/ntds/objects/crossref.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

from dissect.database.ese.ntds.objects.top import Top
from dissect.database.ese.ntds.util import SystemFlagCrossRef

if TYPE_CHECKING:
from dissect.database.ese.ntds.objects.object import DecoderMap


class CrossRef(Top):
Expand All @@ -11,3 +17,11 @@ class CrossRef(Top):
"""

__object_class__ = "crossRef"
__decoders__: ClassVar[DecoderMap] = {
"systemFlags": lambda db, value: SystemFlagCrossRef(value),
}

@property
def behavior_version(self) -> int | None:
"""Return the msDS-Behavior-Version of this cross-reference."""
return self.get("msDS-Behavior-Version")
5 changes: 5 additions & 0 deletions dissect/database/ese/ntds/objects/crossrefcontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ class CrossRefContainer(Top):
"""

__object_class__ = "crossRefContainer"

@property
def behavior_version(self) -> int | None:
"""Return the msDS-Behavior-Version of this cross-reference container."""
return self.get("msDS-Behavior-Version")
5 changes: 5 additions & 0 deletions dissect/database/ese/ntds/objects/domaindns.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ def pek(self) -> PEK | None:
if (pek := self.get("pekList")) is not None:
return PEK(pek)
return None

@property
def behavior_version(self) -> int | None:
"""Return the msDS-Behavior-Version of this domain DNS object."""
return self.get("msDS-Behavior-Version")
10 changes: 10 additions & 0 deletions dissect/database/ese/ntds/objects/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ def sam_account_name(self) -> str:
"""Return the group's sAMAccountName."""
return self.get("sAMAccountName")

@property
def mail(self) -> str | None:
"""Return the mail address of this group."""
return self.get("mail")

@property
def admin_count(self) -> int | None:
"""Return the group's adminCount."""
return self.get("adminCount")

def managed_by(self) -> Iterator[Object]:
"""Return the objects that manage this group."""
self._assert_local()
Expand Down
5 changes: 5 additions & 0 deletions dissect/database/ese/ntds/objects/grouppolicycontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ class GroupPolicyContainer(Container):
"""

__object_class__ = "groupPolicyContainer"

@property
def file_path(self) -> str | None:
"""Return the gPCFileSysPath of this group policy container."""
return self.get("gPCFileSysPath")
5 changes: 5 additions & 0 deletions dissect/database/ese/ntds/objects/ntdsdsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ class NTDSDSA(ApplicationSettings):

__object_class__ = "nTDSDSA"

@property
def behavior_version(self) -> int | None:
"""Return the msDS-Behavior-Version of this NTDS DSA object."""
return self.get("msDS-Behavior-Version")

def domain(self) -> DomainDNS | None:
"""Return the domain object associated with this NTDS DSA object, if any."""
self._assert_local()
Expand Down
46 changes: 36 additions & 10 deletions dissect/database/ese/ntds/objects/object.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
from __future__ import annotations

import struct
from functools import cached_property
from typing import TYPE_CHECKING, Any, ClassVar
from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias
from uuid import UUID

from dissect.database.ese.ntds.util import InstanceType, decode_value
from dissect.database.ese.ntds.util import InstanceType, SystemFlag, decode_value

if TYPE_CHECKING:
from collections.abc import Iterator
from collections.abc import Callable, Iterator
from datetime import datetime

from dissect.database.ese.ntds.database import Database
from dissect.database.ese.ntds.sd import SecurityDescriptor
from dissect.database.ese.ntds.util import DN, SystemFlags
from dissect.database.ese.ntds.util import DN
from dissect.database.ese.record import Record

DecoderMap: TypeAlias = dict[str, Callable[[Database, Any], Any]]


class Object:
"""Base class for all objects in the NTDS database.
Expand All @@ -29,7 +33,19 @@ class Object:
- https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adsc/041c6068-c710-4c74-968f-3040e4208701
"""

# Subclasses must override this to specify their object class
__object_class__: str
"""The objectClass value for this object."""

# Decoders for specific attributes to this object
__decoders__: ClassVar[DecoderMap] = {
"Ancestors": lambda db, value: [v[0] for v in struct.iter_unpack("<I", value)],
"instanceType": lambda db, value: InstanceType(value),
"systemFlags": lambda db, value: SystemFlag(value),
"objectGUID": lambda db, value: UUID(bytes_le=value),
}

# All known object classes
__known_classes__: ClassVar[dict[str, type[Object]]] = {}

def __init__(self, db: Database, record: Record):
Expand All @@ -39,6 +55,12 @@ def __init__(self, db: Database, record: Record):
def __init_subclass__(cls):
cls.__known_classes__[cls.__object_class__] = cls

# Merge parent decoders with any new ones defined on the new class
decoders = {}
for parent in reversed(cls.__mro__[1:]):
decoders |= getattr(parent, "__decoders__", {})
cls.__decoders__ = decoders | cls.__decoders__

def __repr__(self) -> str:
suffix = self.__repr_suffix__()
return f"<{self.__class__.__name__} {self.__repr_body__()}{' ' + suffix if suffix else ''}>"
Expand Down Expand Up @@ -89,15 +111,21 @@ def get(self, name: str, *, raw: bool = False) -> Any:
name: The attribute name to retrieve.
raw: Whether to return the raw value without decoding.
"""
return _get_attribute(self.db, self.record, name, raw=raw)
value = _get_attribute(self.db, self.record, name, raw=raw)

# Allow custom decoders to override the default decoding logic for specific attributes
# This is convenient for things like enums or timestamps that we want to represent as more meaningful types
if value is not None and name in self.__decoders__:
value = self.__decoders__[name](self.db, value)
return value

def as_dict(self) -> dict[str, Any]:
"""Return the object's attributes as a dictionary."""
result = {}
for key in self.record.as_dict():
if (schema := self.db.data.schema.lookup_attribute(column=key)) is not None:
key = schema.name
result[key] = _get_attribute(self.db, self.record, key)
result[key] = self.get(key)
return result

def parent(self) -> Object | None:
Expand Down Expand Up @@ -221,7 +249,7 @@ def instance_type(self) -> InstanceType | None:
return self.get("instanceType")

@property
def system_flags(self) -> SystemFlags | None:
def system_flags(self) -> SystemFlag | None:
"""Return the object's system flags."""
return self.get("systemFlags")

Expand All @@ -240,9 +268,7 @@ def distinguished_name(self) -> DN | None:
@cached_property
def sd(self) -> SecurityDescriptor | None:
"""Return the Security Descriptor for this object."""
if (sd_id := self.get("nTSecurityDescriptor")) is not None:
return self.db.sd.sd(sd_id)
return None
return self.get("nTSecurityDescriptor")

@cached_property
def well_known_objects(self) -> list[Object]:
Expand Down
17 changes: 16 additions & 1 deletion dissect/database/ese/ntds/objects/organizationalperson.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ class OrganizationalPerson(Person):
__object_class__ = "organizationalPerson"

@property
def city(self) -> str:
def city(self) -> str | None:
"""Return the city (l) of this organizational person."""
return self.get("l") # "l" (localityName) represents the city/locality.

@property
def mail(self) -> str | None:
"""Return the mail address of this organizational person."""
return self.get("mail")

@property
def title(self) -> str | None:
"""Return the title of this organizational person."""
return self.get("title")

@property
def allowed_to_act_on_behalf_of_other_identity(self) -> str | None:
"""Return the msDS-AllowedToActOnBehalfOfOtherIdentity attribute of this organizational person."""
return self.get("msDS-AllowedToActOnBehalfOfOtherIdentity")
Loading
Loading