dissect.database 1.2.dev9__py3-none-any.whl → 1.2.dev10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
11
11
 
12
12
  from dissect.database.ese.ntds.objects import (
13
13
  Computer,
14
+ DnsNode,
14
15
  DomainDNS,
15
16
  Group,
16
17
  GroupPolicyContainer,
@@ -118,6 +119,10 @@ class NTDS:
118
119
  """Get all secret objects from the database."""
119
120
  yield from self.search(objectClass="secret")
120
121
 
122
+ def dns_nodes(self) -> Iterator[DnsNode]:
123
+ """Get all DnsNode objects from the database."""
124
+ yield from self.search(objectClass="dnsNode")
125
+
121
126
  def backup_keys(self) -> Iterator[BackupKey]:
122
127
  """Get all DPAPI backup keys from the database."""
123
128
  if not self.pek.unlocked:
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from dissect.cstruct import cstruct
4
+
5
+ dns_record_def = """
6
+
7
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/39b03b89-2264-4063-8198-d62f62a6441a
8
+ enum DNS_RECORD_TYPE : WORD {
9
+ ZERO = 0x0000, // An empty record type ([RFC1034] section 3.6 and [RFC1035] section 3.2.2).
10
+ A = 0x0001, // An A record type, used for storing an IP address ([RFC1035] section 3.2.2).
11
+ NS = 0x0002, // An authoritative name-server
12
+ // record type ([RFC1034] section 3.6 and [RFC1035] section 3.2.2).
13
+ MD = 0x0003, // A mail-destination record type ([RFC1035] section 3.2.2).
14
+ MF = 0x0004, // A mail forwarder record type ([RFC1035] section 3.2.2).
15
+ CNAME = 0x0005, // A record type that contains the canonical name of a DNS alias ([RFC1035] section 3.2.2).
16
+ SOA = 0x0006, // A Start of Authority (SOA) record type ([RFC1035] section 3.2.2).
17
+ MB = 0x0007, // A mailbox record type ([RFC1035] section 3.2.2).
18
+ MG = 0x0008, // A mail group member record type ([RFC1035] section 3.2.2).
19
+ MR = 0x0009, // A mail-rename record type ([RFC1035] section 3.2.2).
20
+ NULL = 0x000A, // A record type for completion queries ([RFC1035] section 3.2.2).
21
+ WKS = 0x000B, // A record type for a well-known service ([RFC1035] section 3.2.2).
22
+ PTR = 0x000C, // A record type containing FQDN pointer ([RFC1035] section 3.2.2).
23
+ HINFO = 0x000D, // A host information record type ([RFC1035] section 3.2.2).
24
+ MINFO = 0x000E, // A mailbox or mailing list information record type ([RFC1035] section 3.2.2).
25
+ MX = 0x000F, // A mail-exchanger record type ([RFC1035] section 3.2.2).
26
+ TXT = 0x0010, // A record type containing a text string ([RFC1035] section 3.2.2).
27
+ RP = 0x0011, // A responsible-person record type [RFC1183].
28
+ AFSDB = 0x0012, // A record type containing AFS database location [RFC1183].
29
+ X25 = 0x0013, // An X25 PSDN address record type [RFC1183].
30
+ ISDN = 0x0014, // An ISDN address record type [RFC1183].
31
+ RT = 0x0015, // A route through record type [RFC1183].
32
+ SIG = 0x0018, // A cryptographic public key signature record type [RFC2931].
33
+ KEY = 0x0019, // A record type containing public key used in DNSSEC [RFC2535].
34
+ AAAA = 0x001C, // An IPv6 address record type [RFC3596].
35
+ LOC = 0x001D, // A location information record type [RFC1876].
36
+ NXT = 0x001E, // A next-domain record type [RFC2065].
37
+ SRV = 0x0021, // A server selection record type [RFC2782].
38
+ ATMA = 0x0022, // An Asynchronous Transfer Mode (ATM) address record type [ATMA].
39
+ NAPTR = 0x0023, // An NAPTR record type [RFC2915].
40
+ DNAME = 0x0027, // A DNAME record type [RFC2672].
41
+ DS = 0x002B, // A DS record type [RFC4034].
42
+ RRSIG = 0x002E, // An RRSIG record type [RFC4034].
43
+ NSEC = 0x002F, // An NSEC record type [RFC4034].
44
+ DNSKEY = 0x0030, // A DNSKEY record type [RFC4034].
45
+ DHCID = 0x0031, // A DHCID record type [RFC4701].
46
+ NSEC3 = 0x0032, // An NSEC3 record type [RFC5155].
47
+ NSEC3PARAM = 0x0033, // An NSEC3PARAM record type [RFC5155].
48
+ TLSA = 0x0034, // A TLSA record type [RFC6698].
49
+ ALL = 0x00FF, // A query-only type requesting all records [RFC1035].
50
+ WINS = 0xFF01, // A record type containing Windows Internet Name Service (WINS)
51
+ // forward lookup data [MS-WINSRADNS_TYPE_WINSR].
52
+ WINSR = 0xFF02 // A record type containing WINS reverse lookup data [MS-WINSRA].
53
+ };
54
+
55
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/6912b338-5472-4f59-b912-0edb536b6ed8
56
+ typedef struct DNS_RECORD_HEADER {
57
+ WORD DataLength;
58
+ DNS_RECORD_TYPE Type;
59
+ BYTE Version; // Must be 0x05
60
+ BYTE Rank;
61
+ WORD Flags; // Must be 0x00
62
+ DWORD Serial;
63
+ DWORD TtlSeconds; // Big Endian
64
+ DWORD Reserved; // MUST be 0x00000000.
65
+ DWORD TimeStamp;
66
+ CHAR Data[DataLength];
67
+ };
68
+
69
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/3fd41adc-c69e-407b-979e-721251403132
70
+ // MS docs indicate that structure is 4 byte aligned, and that the string MUST NOT be null-terminated.
71
+ // But observed reality is a null terminated string (null char not counted in NameLength)
72
+ typedef struct DNS_RPC_NAME{
73
+ BYTE NameLength;
74
+ CHAR dnsName[NameLength];
75
+ };
76
+
77
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/db37cab7-f121-43ba-81c5-ca0e198d4b9a
78
+ typedef struct DNS_RPC_RECORD_SRV {
79
+ WORD Priority;
80
+ WORD Weight;
81
+ WORD Port;
82
+ DNS_RPC_NAME nameTarget;
83
+ };
84
+
85
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/f647d391-6614-4c3e-b38b-4df971590eb6
86
+ typedef struct DNS_RPC_RECORD_NAME_PREFERENCE {
87
+ WORD Preference;
88
+ DNS_RPC_NAME nameExchange;
89
+ };
90
+
91
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/dcd3ec16-d6bf-4bb4-9128-6172f9e5f066
92
+ typedef struct DNS_RPC_RECORD_SOA {
93
+ DWORD Serial;
94
+ DWORD Refresh;
95
+ DWORD Retry;
96
+ DWORD Expire;
97
+ DWORD MinimumTtl;
98
+ DNS_RPC_NAME namePrimaryServer;
99
+ BYTE _pad;
100
+ DNS_RPC_NAME ZoneAdministratorEmail;
101
+ };
102
+
103
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/def7736a-dd09-4b4a-b8d6-6a702a7ecde0
104
+ typedef struct DNS_RPC_RECORD_TS {
105
+ QWORD EntombedTime;
106
+ };
107
+ """
108
+ c_dns_record = cstruct(dns_record_def)
109
+ DNS_RECORD_TYPE = c_dns_record.DNS_RECORD_TYPE
@@ -0,0 +1,146 @@
1
+ # Generated by cstruct-stubgen
2
+ from typing import BinaryIO, Literal, TypeAlias, overload
3
+
4
+ import dissect.cstruct as __cs__
5
+
6
+ class _c_dns_record(__cs__.cstruct):
7
+ class DNS_RECORD_TYPE(__cs__.Enum):
8
+ ZERO = ...
9
+ A = ...
10
+ NS = ...
11
+ MD = ...
12
+ MF = ...
13
+ CNAME = ...
14
+ SOA = ...
15
+ MB = ...
16
+ MG = ...
17
+ MR = ...
18
+ NULL = ...
19
+ WKS = ...
20
+ PTR = ...
21
+ HINFO = ...
22
+ MINFO = ...
23
+ MX = ...
24
+ TXT = ...
25
+ RP = ...
26
+ AFSDB = ...
27
+ X25 = ...
28
+ ISDN = ...
29
+ RT = ...
30
+ SIG = ...
31
+ KEY = ...
32
+ AAAA = ...
33
+ LOC = ...
34
+ NXT = ...
35
+ SRV = ...
36
+ ATMA = ...
37
+ NAPTR = ...
38
+ DNAME = ...
39
+ DS = ...
40
+ RRSIG = ...
41
+ NSEC = ...
42
+ DNSKEY = ...
43
+ DHCID = ...
44
+ NSEC3 = ...
45
+ NSEC3PARAM = ...
46
+ TLSA = ...
47
+ ALL = ...
48
+ WINS = ...
49
+ WINSR = ...
50
+
51
+ class DNS_RECORD_HEADER(__cs__.Structure):
52
+ DataLength: _c_dns_record.uint16
53
+ Type: _c_dns_record.DNS_RECORD_TYPE
54
+ Version: _c_dns_record.uint8
55
+ Rank: _c_dns_record.uint8
56
+ Flags: _c_dns_record.uint16
57
+ Serial: _c_dns_record.uint32
58
+ TtlSeconds: _c_dns_record.uint32
59
+ Reserved: _c_dns_record.uint32
60
+ TimeStamp: _c_dns_record.uint32
61
+ Data: __cs__.CharArray
62
+ @overload
63
+ def __init__(
64
+ self,
65
+ DataLength: _c_dns_record.uint16 | None = ...,
66
+ Type: _c_dns_record.DNS_RECORD_TYPE | None = ...,
67
+ Version: _c_dns_record.uint8 | None = ...,
68
+ Rank: _c_dns_record.uint8 | None = ...,
69
+ Flags: _c_dns_record.uint16 | None = ...,
70
+ Serial: _c_dns_record.uint32 | None = ...,
71
+ TtlSeconds: _c_dns_record.uint32 | None = ...,
72
+ Reserved: _c_dns_record.uint32 | None = ...,
73
+ TimeStamp: _c_dns_record.uint32 | None = ...,
74
+ Data: __cs__.CharArray | None = ...,
75
+ ): ...
76
+ @overload
77
+ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
78
+
79
+ class DNS_RPC_NAME(__cs__.Structure):
80
+ NameLength: _c_dns_record.uint8
81
+ dnsName: __cs__.CharArray
82
+ @overload
83
+ def __init__(self, NameLength: _c_dns_record.uint8 | None = ..., dnsName: __cs__.CharArray | None = ...): ...
84
+ @overload
85
+ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
86
+
87
+ class DNS_RPC_RECORD_SRV(__cs__.Structure):
88
+ Priority: _c_dns_record.uint16
89
+ Weight: _c_dns_record.uint16
90
+ Port: _c_dns_record.uint16
91
+ nameTarget: _c_dns_record.DNS_RPC_NAME
92
+ @overload
93
+ def __init__(
94
+ self,
95
+ Priority: _c_dns_record.uint16 | None = ...,
96
+ Weight: _c_dns_record.uint16 | None = ...,
97
+ Port: _c_dns_record.uint16 | None = ...,
98
+ nameTarget: _c_dns_record.DNS_RPC_NAME | None = ...,
99
+ ): ...
100
+ @overload
101
+ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
102
+
103
+ class DNS_RPC_RECORD_NAME_PREFERENCE(__cs__.Structure):
104
+ Preference: _c_dns_record.uint16
105
+ nameExchange: _c_dns_record.DNS_RPC_NAME
106
+ @overload
107
+ def __init__(
108
+ self, Preference: _c_dns_record.uint16 | None = ..., nameExchange: _c_dns_record.DNS_RPC_NAME | None = ...
109
+ ): ...
110
+ @overload
111
+ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
112
+
113
+ class DNS_RPC_RECORD_SOA(__cs__.Structure):
114
+ Serial: _c_dns_record.uint32
115
+ Refresh: _c_dns_record.uint32
116
+ Retry: _c_dns_record.uint32
117
+ Expire: _c_dns_record.uint32
118
+ MinimumTtl: _c_dns_record.uint32
119
+ namePrimaryServer: _c_dns_record.DNS_RPC_NAME
120
+ _pad: _c_dns_record.uint8
121
+ ZoneAdministratorEmail: _c_dns_record.DNS_RPC_NAME
122
+ @overload
123
+ def __init__(
124
+ self,
125
+ Serial: _c_dns_record.uint32 | None = ...,
126
+ Refresh: _c_dns_record.uint32 | None = ...,
127
+ Retry: _c_dns_record.uint32 | None = ...,
128
+ Expire: _c_dns_record.uint32 | None = ...,
129
+ MinimumTtl: _c_dns_record.uint32 | None = ...,
130
+ namePrimaryServer: _c_dns_record.DNS_RPC_NAME | None = ...,
131
+ _pad: _c_dns_record.uint8 | None = ...,
132
+ ZoneAdministratorEmail: _c_dns_record.DNS_RPC_NAME | None = ...,
133
+ ): ...
134
+ @overload
135
+ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
136
+
137
+ class DNS_RPC_RECORD_TS(__cs__.Structure):
138
+ EntombedTime: _c_dns_record.uint64
139
+ @overload
140
+ def __init__(self, EntombedTime: _c_dns_record.uint64 | None = ...): ...
141
+ @overload
142
+ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
143
+
144
+ # Technically `c_dns_record` is an instance of `_c_dns_record`, but then we can't use it in type hints
145
+ c_dns_record: TypeAlias = _c_dns_record
146
+ DNS_RECORD_TYPE: TypeAlias = _c_dns_record.DNS_RECORD_TYPE
@@ -1,13 +1,443 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime
4
+ import logging
5
+ import socket
6
+ import typing
7
+ from functools import cached_property
8
+ from typing import Any, ClassVar, NamedTuple
9
+
10
+ from dissect.cstruct.utils import swap16, swap32
11
+
12
+ from dissect.database.ese.ntds.objects.c_dnsnode import DNS_RECORD_TYPE, c_dns_record
3
13
  from dissect.database.ese.ntds.objects.top import Top
4
14
 
15
+ if typing.TYPE_CHECKING:
16
+ from dissect.database.ese.ntds.objects.object import DecoderMap
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ def parse_rfc1035_dns_name(data: bytes) -> str:
21
+ """Parse DNS name as specified in ``rfc1035#section-3.1`` format.
22
+
23
+ References:
24
+ - https://datatracker.ietf.org/doc/html/rfc1035#section-3.1
25
+ """
26
+ if not data:
27
+ return ""
28
+ _nb_segment = data[0]
29
+ name_parts = []
30
+ offset = 1
31
+ # Domain names in messages are expressed in terms of a sequence of labels.
32
+ # Each label is represented as a one octet length field followed by that
33
+ # number of octets. Since every domain name ends with the null label of
34
+ # the root, a domain name is terminated by a length byte of zero.
35
+ while offset < len(data):
36
+ length = data[offset]
37
+ if length == 0:
38
+ name_parts.append("")
39
+ break
40
+ # The high order two bits of every length octet must be zero, and the
41
+ # remaining six bits of the length field limit the label to 63 octets or
42
+ # less.
43
+ if length > 63: # Compression pointer
44
+ return "<error>"
45
+
46
+ offset += 1
47
+ if offset + length > len(data):
48
+ return "<error>"
49
+
50
+ part = data[offset : offset + length].decode("utf-8", errors="backslashreplace")
51
+ name_parts.append(part)
52
+ offset += length
53
+ return ".".join(name_parts) if name_parts else ""
54
+
55
+
56
+ class DnsARecord(NamedTuple):
57
+ """``A`` resource records."""
58
+
59
+ ipv4_address: str
60
+
61
+ @property
62
+ def ip_address(self) -> str:
63
+ return self.ipv4_address
64
+
65
+ @classmethod
66
+ def from_bytes(cls, data: bytes) -> DnsARecord:
67
+ """Parse ``A`` record (IPv4 address).
68
+
69
+ References:
70
+ - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/117c2ff9-9094-45b2-83c2-5e44518e0bac
71
+
72
+ Raises:
73
+ EOFError: Issue while unpacking structure.
74
+ """
75
+ if len(data) >= 4:
76
+ ip = socket.inet_ntop(socket.AF_INET, data[:4])
77
+ return cls(ipv4_address=ip)
78
+ raise EOFError("A records with less than 4 bytes")
79
+
80
+
81
+ class DnsAAAARecord(NamedTuple):
82
+ """``AAAA`` resource records."""
83
+
84
+ ipv6_address: str
85
+
86
+ @property
87
+ def ip_address(self) -> str:
88
+ return self.ipv6_address
89
+
90
+ @classmethod
91
+ def from_bytes(cls, data: bytes) -> DnsAAAARecord:
92
+ """Parse ``AAAA`` record (IPv6 address).
93
+
94
+ References:
95
+ - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/ee33fef1-6e82-42d0-8107-0f6d21be072a
96
+
97
+ Raises:
98
+ EOFError: Issue while unpacking structure.
99
+ """
100
+ if len(data) >= 16:
101
+ ip = socket.inet_ntop(socket.AF_INET6, data[:16])
102
+ return cls(ipv6_address=ip)
103
+ raise EOFError("AAAA records with less than 16 bytes")
104
+
105
+
106
+ class SOARecord(NamedTuple):
107
+ """The ``DNS_RPC_RECORD_SOA`` structure contains information about a ``SOA`` record."""
108
+
109
+ name_primary_server: str
110
+ # Serial does not match value seen using DNS request/management interface
111
+ # As this is not the most important field, we simply ignore it instead a showing a errored value
112
+ # serial: int
113
+ refresh: int
114
+ retry: int
115
+ minimum_ttl: int
116
+ zone_administrator_email: str
117
+
118
+ @classmethod
119
+ def from_bytes(cls, data: bytes) -> SOARecord | None:
120
+ """Parse ``SOA`` records.
121
+
122
+ References:
123
+ https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/dcd3ec16-d6bf-4bb4-9128-6172f9e5f066
124
+
125
+ Raises:
126
+ EOFError: Issue while unpacking structure.
127
+ """
128
+ record = c_dns_record.DNS_RPC_RECORD_SOA(data)
129
+ return cls(
130
+ name_primary_server=parse_rfc1035_dns_name(record.namePrimaryServer.dnsName),
131
+ # Serial does not match value seen using DNS request/management interface
132
+ # As this is not the most important field, we simply ignore it instead a showing an errored value
133
+ # serial=swap32(dns_rpc_record_soa.Serial, int_len=4),
134
+ refresh=swap32(record.Refresh),
135
+ retry=swap32(record.Retry),
136
+ minimum_ttl=swap32(record.MinimumTtl),
137
+ zone_administrator_email=parse_rfc1035_dns_name(record.ZoneAdministratorEmail.dnsName),
138
+ )
139
+
140
+
141
+ class NodeNameRecord(NamedTuple):
142
+ """The ``DNS_RPC_RECORD_NODE_NAME`` structure contains information about a DNS record referring to another DNS name.
143
+
144
+ This corresponds to the following types:
145
+ - ``DNS_TYPE_PTR``
146
+ - ``DNS_TYPE_NS``
147
+ - ``DNS_TYPE_CNAME``
148
+ - ``DNS_TYPE_DNAME``
149
+ - ``DNS_TYPE_MB``
150
+ - ``DNS_TYPE_MR``
151
+ - ``DNS_TYPE_MG``
152
+ - ``DNS_TYPE_MD``
153
+ - ``DNS_TYPE_MF``
154
+ """
155
+
156
+ name_node: str
157
+
158
+ @classmethod
159
+ def from_bytes(cls, data: bytes) -> NodeNameRecord | None:
160
+ """Parse Node Name type record (e.g. ``CNAME``, ``PTR``).
161
+
162
+ References:
163
+ - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/8f986756-f151-4f5b-bfcf-0d85be8b0d7e
164
+
165
+ Raises:
166
+ EOFError: Issue while unpacking structure.
167
+ """
168
+ return NodeNameRecord(parse_rfc1035_dns_name(c_dns_record.DNS_RPC_NAME(data).dnsName))
169
+
170
+
171
+ class StringRecord(NamedTuple):
172
+ """The ``DNS_RPC_RECORD_STRING`` structure contains information about a DNS record containing text data.
173
+
174
+ This corresponds to the following types:
175
+ - ``DNS_TYPE_HINFO``
176
+ - ``DNS_TYPE_ISDN``
177
+ - ``DNS_TYPE_TXT``
178
+ - ``DNS_TYPE_X25``
179
+ - ``DNS_TYPE_LOC``
180
+ """
181
+
182
+ string_data: str
183
+
184
+ @classmethod
185
+ def from_bytes(cls, data: bytes) -> StringRecord | None:
186
+ """Parse Node Name type record (e.g. ``TXT``).
187
+
188
+ Test using GUI does not allow to create record with a line length > 255 char.
189
+
190
+ References:
191
+ - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/69166ff5-36c1-4542-9243-13b8931fa447
192
+
193
+ Raises:
194
+ EOFError: Issue while unpacking structure.
195
+ """
196
+ records = []
197
+ data_consumed = 0
198
+
199
+ while data_consumed < len(data):
200
+ rpc_name = c_dns_record.DNS_RPC_NAME(data[data_consumed:])
201
+ data_consumed += len(rpc_name)
202
+
203
+ records.append(rpc_name.dnsName.decode("utf-8", errors="backslashreplace"))
204
+ return cls("\n".join(records))
205
+
206
+
207
+ class NamePreferenceRecord(NamedTuple):
208
+ """The ``DNS_RPC_RECORD_NAME_PREFERENCE`` structure specifies information about a DNS
209
+ record referring to another DNS name with a preference.
210
+
211
+ This corresponds to the following types:
212
+ - ``DNS_TYPE_MX``
213
+ - ``DNS_TYPE_AFSDB``
214
+ - ``DNS_TYPE_RT``
215
+ """
216
+
217
+ name_exchange: str
218
+ preference: int
219
+
220
+ @classmethod
221
+ def from_bytes(cls, data: bytes) -> NamePreferenceRecord | None:
222
+ """Parse ``DNS_RPC_RECORD_NAME_PREFERENCE`` record (e.g. ``MX``).
223
+
224
+ References:
225
+ - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/f647d391-6614-4c3e-b38b-4df971590eb6
226
+
227
+ Raises:
228
+ EOFError: Issue while unpacking structure.
229
+ """
230
+ record = c_dns_record.DNS_RPC_RECORD_NAME_PREFERENCE(data)
231
+ return cls(
232
+ preference=swap16(record.Preference),
233
+ name_exchange=parse_rfc1035_dns_name(record.nameExchange.dnsName),
234
+ )
235
+
236
+
237
+ class SRVRecord(NamedTuple):
238
+ """``SRV`` resource records."""
239
+
240
+ name_target: str
241
+ port: int
242
+ weight: int
243
+ priority: int
244
+
245
+ @classmethod
246
+ def from_bytes(cls, data: bytes) -> SRVRecord | None:
247
+ """Parse ``SRV`` record.
248
+
249
+ References:
250
+ - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/db37cab7-f121-43ba-81c5-ca0e198d4b9a
251
+
252
+ Raises:
253
+ EOFError: Issue while unpacking structure.
254
+ """
255
+ record = c_dns_record.DNS_RPC_RECORD_SRV(data)
256
+ target = parse_rfc1035_dns_name(record.nameTarget.dnsName)
257
+ return SRVRecord(
258
+ priority=record.Priority,
259
+ weight=swap16(record.Weight),
260
+ port=swap16(record.Port),
261
+ name_target=target,
262
+ )
263
+
264
+
265
+ class TombStonedRecord(NamedTuple):
266
+ """``ZERO`` resource records."""
267
+
268
+ entombed_time: datetime.datetime
269
+
270
+ @classmethod
271
+ def from_bytes(cls, data: bytes) -> TombStonedRecord | None:
272
+ """The ``DNS_RPC_RECORD_TS`` specifies information for a node that has been tombstoned,
273
+ used for record type ``DNS_TYPE_ZERO``.
274
+
275
+ References:
276
+ - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/69166ff5-36c1-4542-9243-13b8931fa447
277
+
278
+ Raises:
279
+ EOFError: Issue while unpacking structure.
280
+ """
281
+ record = c_dns_record.DNS_RPC_RECORD_TS(data).EntombedTime
282
+ if record == 0:
283
+ return None
284
+ base_date = datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc)
285
+ return TombStonedRecord(base_date + datetime.timedelta(microseconds=record / 10))
286
+
287
+
288
+ class DnsRecord:
289
+ """DNS resource record definitions.
290
+
291
+ References:
292
+ - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/6912b338-5472-4f59-b912-0edb536b6ed8
293
+ """
294
+
295
+ def __init__(self, data: bytes):
296
+ self.raw = data
297
+ self.header = c_dns_record.DNS_RECORD_HEADER(data)
298
+ self.type = self.header.Type
299
+ self.ttl_seconds = swap32(self.header.TtlSeconds)
300
+
301
+ def __repr__(self) -> str:
302
+ return (
303
+ f"<DnsRecord type={self.type.name!r} ttl_seconds={self.ttl_seconds!r} "
304
+ f"timestamp={self.timestamp} data={self.data}>"
305
+ )
306
+
307
+ @property
308
+ def timestamp(self) -> datetime.datetime | None:
309
+ """Timestamp is stored in hours since 1601-01-01.
310
+
311
+ Raises:
312
+ OverflowError: Number of hours cause an overflow.
313
+ """
314
+ if self.header.TimeStamp == 0:
315
+ return None
316
+ # Windows timestamp is hours since 1601-01-01
317
+ base_date = datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc)
318
+ return base_date + datetime.timedelta(hours=self.header.TimeStamp)
319
+
320
+ @property
321
+ def data(
322
+ self,
323
+ ) -> (
324
+ bytes
325
+ | DnsARecord
326
+ | DnsAAAARecord
327
+ | NodeNameRecord
328
+ | NamePreferenceRecord
329
+ | StringRecord
330
+ | TombStonedRecord
331
+ | SRVRecord
332
+ | SOARecord
333
+ | None
334
+ ):
335
+ """Parse the data part of a record, which contains a structure that depends on the record type.
336
+
337
+ Raises:
338
+ EOFError: Issue while unpacking structure.
339
+ """
340
+ header_data = self.header.Data
341
+
342
+ # Process most common DNS records types
343
+ match self.type:
344
+ case DNS_RECORD_TYPE.A:
345
+ return DnsARecord.from_bytes(header_data)
346
+ case c_dns_record.DNS_RECORD_TYPE.AAAA:
347
+ return DnsAAAARecord.from_bytes(header_data)
348
+ case (
349
+ DNS_RECORD_TYPE.PTR
350
+ | DNS_RECORD_TYPE.NS
351
+ | DNS_RECORD_TYPE.CNAME
352
+ | DNS_RECORD_TYPE.DNAME
353
+ | DNS_RECORD_TYPE.MB
354
+ | DNS_RECORD_TYPE.MR
355
+ | DNS_RECORD_TYPE.MG
356
+ | DNS_RECORD_TYPE.MD
357
+ | DNS_RECORD_TYPE.MF
358
+ ):
359
+ return NodeNameRecord.from_bytes(header_data)
360
+ case DNS_RECORD_TYPE.MX | DNS_RECORD_TYPE.AFSDB | DNS_RECORD_TYPE.RT:
361
+ return NamePreferenceRecord.from_bytes(header_data)
362
+ case DNS_RECORD_TYPE.SRV:
363
+ return SRVRecord.from_bytes(header_data)
364
+ case DNS_RECORD_TYPE.SOA:
365
+ return SOARecord.from_bytes(header_data)
366
+ case (
367
+ DNS_RECORD_TYPE.HINFO
368
+ | DNS_RECORD_TYPE.ISDN
369
+ | DNS_RECORD_TYPE.TXT
370
+ | DNS_RECORD_TYPE.X25
371
+ | DNS_RECORD_TYPE.LOC
372
+ ):
373
+ return StringRecord.from_bytes(header_data)
374
+ case DNS_RECORD_TYPE.ZERO:
375
+ return TombStonedRecord.from_bytes(header_data)
376
+ return header_data
377
+
378
+ def as_dict(self) -> dict[str, Any]:
379
+ """Return a dictionary representation of the record, with parsed data if possible."""
380
+ try:
381
+ data = self.data
382
+ except EOFError:
383
+ log.warning("Error processing DNS record: failed to parse data (record type: %s)", self.type.name)
384
+ data = None
385
+
386
+ try:
387
+ timestamp = self.timestamp
388
+ except OverflowError:
389
+ log.warning("Error processing DNS record: invalid record timestamp")
390
+ timestamp = None
391
+ return {
392
+ "type": self.type.name,
393
+ "ttl_seconds": self.ttl_seconds,
394
+ "timestamp": timestamp,
395
+ # isinstance(X, NamedTuple) does not work, but NamedTuple are subtype of tuple
396
+ "data": data._asdict() if isinstance(data, tuple) else data,
397
+ }
398
+
5
399
 
6
400
  class DnsNode(Top):
7
401
  """Represents a DNS node object in the Active Directory.
8
402
 
9
403
  References:
10
404
  - https://learn.microsoft.com/en-us/windows/win32/adschema/c-dnsnode
405
+ - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/6912b338-5472-4f59-b912-0edb536b6ed8
11
406
  """
12
407
 
13
408
  __object_class__ = "dnsNode"
409
+ __decoders__: ClassVar[DecoderMap] = {"dnsRecord": lambda x, value: [DnsRecord(x) for x in value] if value else []}
410
+
411
+ def __repr_body__(self) -> str:
412
+ return f"dns_name={self.dns_name} dns_record={self.dns_record}"
413
+
414
+ @property
415
+ def dns_record(self) -> list[DnsRecord]:
416
+ """Return DNS records as objects.
417
+
418
+ Raises:
419
+ EOFError: Issue while unpacking structure.
420
+ """
421
+ return self.get("dnsRecord")
422
+
423
+ @cached_property
424
+ def dns_name(self) -> str:
425
+ """Create a DNS name from node and parent names.
426
+
427
+ Examples:
428
+ DC=NORTH,DC=SEVENKINGDOMS.LOCAL,CN=MICROSOFTDNS,DC=DOMAINDNSZONES,DC=SEVENKINGDOMS,DC=LOCAL ->
429
+ north.sevenkingdoms.local
430
+ """
431
+ node = self.distinguished_name
432
+ ret = [self.name] if self.name != "@" else [] # @ means same as parent folder
433
+ while (i := node.parent).object.__object_class__ in ["dnsNode", "dnsZone"]:
434
+ ret.append(i.object.name)
435
+ node = i
436
+ return ".".join(ret).replace("\n", "\\n")
437
+
438
+ def as_dict(self) -> dict[str, Any]:
439
+ result = super().as_dict()
440
+ result["dns_name"] = self.dns_name
441
+ if "dnsRecord" in result:
442
+ result["dnsRecord"] = [r.as_dict() for r in result.get("dnsRecord", [])]
443
+ return result
@@ -405,6 +405,9 @@ class DN(str):
405
405
 
406
406
  __slots__ = ("object", "parent")
407
407
 
408
+ object: Object
409
+ parent: DN | None
410
+
408
411
  def __new__(cls, value: str, object: Object, parent: DN | None = None):
409
412
  instance = super().__new__(cls, value)
410
413
  instance.object = object
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.database
3
- Version: 1.2.dev9
3
+ Version: 1.2.dev10
4
4
  Summary: A Dissect module implementing parsers for various database formats, including Berkeley DB, Microsofts Extensible Storage Engine (ESE) and SQLite3
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License-Expression: Apache-2.0
@@ -30,16 +30,18 @@ dissect/database/ese/ntds/c_pek.pyi,sha256=lHfhvHWABT1vLmknu66phOXsylgwzGIWVdbqD
30
30
  dissect/database/ese/ntds/c_sd.py,sha256=DYICZsWCBzj0OtvhO6vhzzVjK9YP6tzBCsgKgr0pc-k,4294
31
31
  dissect/database/ese/ntds/c_sd.pyi,sha256=7717Y0EBVu37Liu26rqsDWkLIdSCqWn9KK9svtniLqY,5279
32
32
  dissect/database/ese/ntds/database.py,sha256=fPMgTzHm1rsPZ3Hvt7SQqtjdXQNNLa51wGEK3i5_afw,16776
33
- dissect/database/ese/ntds/ntds.py,sha256=Uws58HSUKUUIs3qEc3zlrQfwNK-cKwyzkAt3320aHs8,5506
33
+ dissect/database/ese/ntds/ntds.py,sha256=zKtLcID8GGtpuzZ9kYaNRa_qRij5HFmtZZWS-f15zr4,5681
34
34
  dissect/database/ese/ntds/pek.py,sha256=BEmxO175T8QkGVvFQLN9MI9uDCcK4jztuZetbwbbYqU,4154
35
35
  dissect/database/ese/ntds/query.py,sha256=pDLLCVdrCRNq3ripvMDiyxXoFWsVJLwwsRMplRf7C9s,8063
36
36
  dissect/database/ese/ntds/schema.py,sha256=qAC9STfmJqUMp7633ZcOZOQgWI-UymwX3h4PUUjUV1U,16641
37
37
  dissect/database/ese/ntds/sd.py,sha256=Y-oYnJPcLMDB_4X8TLEGtt-n_nC4HLA0WqIS8qYAwAs,5995
38
- dissect/database/ese/ntds/util.py,sha256=3aIvxX3B2-LXQgmHA6rrFiN0obIZHdfD1-wKfecqaI8,20451
38
+ dissect/database/ese/ntds/util.py,sha256=31j6YwGmIVirTlvpQ_l_C-JIEm6JoTUdS4dH1we2x3g,20493
39
39
  dissect/database/ese/ntds/objects/__init__.py,sha256=LpZ9nHGxmuhVPQkD-qwh-OwFM1T4OzjcklAcEF2Zmrs,10868
40
40
  dissect/database/ese/ntds/objects/applicationsettings.py,sha256=j7UzmF8yxm3LR2lLnmGb7vFvUhYokCUOdhJXnk9xxzE,358
41
41
  dissect/database/ese/ntds/objects/attributeschema.py,sha256=E8JioZAc_OgnK60nbrxHFxgFBvR1HKAz0tCdj4xt6DI,892
42
42
  dissect/database/ese/ntds/objects/builtindomain.py,sha256=KEo-YQl-r748Hnkod21qej2qsLGj7jM4vt6nbufMPWQ,334
43
+ dissect/database/ese/ntds/objects/c_dnsnode.py,sha256=lk7emP30eftyWWeDSBCeA4NWRSd-Oesp2xOZt9g4_mk,6087
44
+ dissect/database/ese/ntds/objects/c_dnsnode.pyi,sha256=DQXT0zRuVXuQRckuw_XjrpQf_onWYAflwv98bXVSKcU,5063
43
45
  dissect/database/ese/ntds/objects/certificationauthority.py,sha256=XWGzSImJ7Qo1TEMTUznAslp-810lWe8Xpk-Cjo99nIc,532
44
46
  dissect/database/ese/ntds/objects/classschema.py,sha256=Hrpfgl_mR6ciHL7qya63mvzsrouFIy4BJVHU5wJm_Hg,1544
45
47
  dissect/database/ese/ntds/objects/classstore.py,sha256=VqWGm4ZqonQQGnFE67AyGGUYuTLdBmPQzZ34X-fRG5M,321
@@ -53,7 +55,7 @@ dissect/database/ese/ntds/objects/crossrefcontainer.py,sha256=YeEnVCVYpt6p1jQ8gr
53
55
  dissect/database/ese/ntds/objects/dfsconfiguration.py,sha256=Ofgj8Nnfw4_kGTjGnqaSDB6opshqQMNAJFFk_AuTbbo,345
54
56
  dissect/database/ese/ntds/objects/displayspecifier.py,sha256=Kuc7usI-zLK0TadGAm_fnuu6DurHNYZD6-hQMlQ3F6g,345
55
57
  dissect/database/ese/ntds/objects/dmd.py,sha256=SnJLgFO5rMmsVjgYN0z0klE5tQkZxnmAaqsWefitV0U,324
56
- dissect/database/ese/ntds/objects/dnsnode.py,sha256=os48qV-nvqa80PiyDLXVSST31NI_KfFC32jlkm30FzY,309
58
+ dissect/database/ese/ntds/objects/dnsnode.py,sha256=8lqzUcos4YB-SUJLZqmJXskYNAX7f9tVhIoNr2dgZxQ,14920
57
59
  dissect/database/ese/ntds/objects/dnszone.py,sha256=rJ_3zdsxrR3FvzXU1qbbGqnh67FR64CRClO3dF7wBXU,659
58
60
  dissect/database/ese/ntds/objects/domain.py,sha256=aWQN-5P1MMh2aPXJoyPORxhyE_5dIt7aIfBB7Bxp8mM,304
59
61
  dissect/database/ese/ntds/objects/domaindns.py,sha256=bHJQukHnhmBJwgGMTiwWHyF5PXQYhv6sdYrJP2QH2CY,780
@@ -159,10 +161,10 @@ dissect/database/sqlite3/encryption/__init__.py,sha256=kJdFWXD9Z_O_QipC-_A9dlVfR
159
161
  dissect/database/sqlite3/encryption/sqlcipher/__init__.py,sha256=kJdFWXD9Z_O_QipC-_A9dlVfR6AOPSOoT8WBhpFbSsE,238
160
162
  dissect/database/sqlite3/encryption/sqlcipher/exception.py,sha256=GKNtzcnAKlWkvjLluruA8LfzCwjRRWubibbH8WM9l2o,121
161
163
  dissect/database/sqlite3/encryption/sqlcipher/sqlcipher.py,sha256=y_oJRKZqoJBeOQaBbniesZKm1sVTFvXZ466rJYZj2xE,11217
162
- dissect_database-1.2.dev9.dist-info/licenses/COPYRIGHT,sha256=pFH-OBYz6Xj23UB0Odz5IhoTR8nsTbJQNlCRV_wMaiE,317
163
- dissect_database-1.2.dev9.dist-info/licenses/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
164
- dissect_database-1.2.dev9.dist-info/METADATA,sha256=lpFACmhxqvnFHoHk3_cI8ydgt--hK4JpVhbHVksYfVU,5540
165
- dissect_database-1.2.dev9.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
166
- dissect_database-1.2.dev9.dist-info/entry_points.txt,sha256=ZVVKj3Nzjkgm1kBXGWyGNVUJzTbmVgivv9lgFcuLkpk,343
167
- dissect_database-1.2.dev9.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
168
- dissect_database-1.2.dev9.dist-info/RECORD,,
164
+ dissect_database-1.2.dev10.dist-info/licenses/COPYRIGHT,sha256=pFH-OBYz6Xj23UB0Odz5IhoTR8nsTbJQNlCRV_wMaiE,317
165
+ dissect_database-1.2.dev10.dist-info/licenses/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
166
+ dissect_database-1.2.dev10.dist-info/METADATA,sha256=V508YEp3xBaiq_9Y8tn_58GMSRwLaAHZzzm1mHp4vUM,5541
167
+ dissect_database-1.2.dev10.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
168
+ dissect_database-1.2.dev10.dist-info/entry_points.txt,sha256=ZVVKj3Nzjkgm1kBXGWyGNVUJzTbmVgivv9lgFcuLkpk,343
169
+ dissect_database-1.2.dev10.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
170
+ dissect_database-1.2.dev10.dist-info/RECORD,,