|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +from abc import ABC |
5 | 6 | from datetime import datetime |
6 | 7 | from typing import Any, TypeAlias |
7 | 8 |
|
8 | 9 | from pytest_mh.cli import CLIBuilderArgs |
9 | 10 | from pytest_mh.conn import ProcessResult |
10 | 11 |
|
11 | 12 | from ..hosts.ad import ADHost |
12 | | -from ..misc import attrs_include_value, attrs_parse, attrs_to_hash, seconds_to_timespan |
| 13 | +from ..misc import attrs_include_value, attrs_parse, attrs_to_hash, ip_version, seconds_to_timespan |
13 | 14 | from .base import BaseObject, BaseWindowsRole, DeleteAttribute |
14 | 15 | from .generic import GenericPasswordPolicy |
15 | 16 | from .ldap import LDAPNetgroupMember |
|
27 | 28 | "ADUser", |
28 | 29 | "ADPasswordPolicy", |
29 | 30 | "GPO", |
| 31 | + "ADDNSServer", |
| 32 | + "ADDNSZone", |
30 | 33 | ] |
31 | 34 |
|
32 | 35 |
|
@@ -360,6 +363,38 @@ def test_example(client: Client, ad: AD): |
360 | 363 | """ |
361 | 364 | return ADComputer(self, name, basedn) |
362 | 365 |
|
| 366 | + def dns(self) -> ADDNSServer: |
| 367 | + """ |
| 368 | + Get DNS server object. |
| 369 | +
|
| 370 | + Get methods use dig and is parsed by jc. The data from jc contains several nested dict, |
| 371 | + but two are returned as a tuple, ``answer, authority``. |
| 372 | +
|
| 373 | + .. code-block:: python |
| 374 | + :caption: Example usage |
| 375 | +
|
| 376 | + # Create forward zone and add forward record |
| 377 | + zone = ad.dns().zone("example.test").create() |
| 378 | + zone.add_record("client", "172.16.200.15") |
| 379 | +
|
| 380 | + # Create reverse zone and add reverse record |
| 381 | + zone = ad.dns().zone("10.0.10.in-addr.arpa").create() |
| 382 | + zone.add_ptr_record("client.example.test", 15) |
| 383 | +
|
| 384 | + # Add forward record to default domain |
| 385 | + ad.dns().zone(ad.domain).add_record("client", "1.2.3.4") |
| 386 | +
|
| 387 | + # Add a global forwarder |
| 388 | + ad.dns().add_forwarder("1.1.1.1") |
| 389 | +
|
| 390 | + # Remove a global forwarder |
| 391 | + ad.dns().remove_forwarder("1.1.1.1") |
| 392 | +
|
| 393 | + # Clear all forwarders |
| 394 | + ad.dns().clear_forwarders() |
| 395 | + """ |
| 396 | + return ADDNSServer(self) |
| 397 | + |
363 | 398 | def gpo(self, name: str) -> GPO: |
364 | 399 | """ |
365 | 400 | Get group policy object. |
@@ -2114,4 +2149,185 @@ def lockout(self, duration: int, attempts: int) -> ADPasswordPolicy: |
2114 | 2149 | return self |
2115 | 2150 |
|
2116 | 2151 |
|
| 2152 | +class ADDNSServer(BaseObject[ADHost, AD]): |
| 2153 | + def __init__(self, role: AD): |
| 2154 | + """ |
| 2155 | + :param role: AD host object. |
| 2156 | + :type role: ADHost |
| 2157 | + """ |
| 2158 | + super().__init__(role) |
| 2159 | + |
| 2160 | + self.domain: str = role.domain |
| 2161 | + """Domain name.""" |
| 2162 | + |
| 2163 | + self.server: str = role.server |
| 2164 | + """Server name.""" |
| 2165 | + |
| 2166 | + def zone(self, name: str) -> ADDNSZone: |
| 2167 | + """ |
| 2168 | + Get ADDNsServerZone object. |
| 2169 | +
|
| 2170 | + :param name: Zone name. |
| 2171 | + :type name: str |
| 2172 | + :return: ADDNSServerZone object. |
| 2173 | + :rtype: ADDNSZone |
| 2174 | + """ |
| 2175 | + return ADDNSZone(self.role, name) |
| 2176 | + |
| 2177 | + def get_forwarders(self) -> list[str] | None: |
| 2178 | + """ |
| 2179 | + Get DNS global forwarders. |
| 2180 | +
|
| 2181 | + :return: DNS global forwarders. |
| 2182 | + :rtype: list[str] |
| 2183 | + """ |
| 2184 | + forwarders = self.host.conn.run("Get-DnsServerForwarder").stdout_lines |
| 2185 | + if forwarders is not None: |
| 2186 | + parsed_forwarders = attrs_parse(forwarders, ["IPAddress"]) |
| 2187 | + if isinstance(parsed_forwarders, dict): |
| 2188 | + ip_addresses = parsed_forwarders.get("IPAddress") |
| 2189 | + if isinstance(ip_addresses, list) and len(ip_addresses) > 0: |
| 2190 | + ip_addresses = [s.strip() for s in ip_addresses] |
| 2191 | + ip_addresses = [r.replace("...", "") for r in ip_addresses] |
| 2192 | + return ip_addresses[0].strip("{}").split(",") |
| 2193 | + else: |
| 2194 | + return None |
| 2195 | + else: |
| 2196 | + return None |
| 2197 | + |
| 2198 | + def add_forwarder(self, ip_address: str) -> ADDNSServer: |
| 2199 | + """ |
| 2200 | + Add a DNS server forwarder. |
| 2201 | +
|
| 2202 | + :param ip_address: IP address., |
| 2203 | + :type ip_address: str |
| 2204 | + :return: Self. |
| 2205 | + :rtype: ADDNSServer |
| 2206 | + """ |
| 2207 | + self.host.conn.run(f"Add-DnsServerForwarder -IPAddress {ip_address}") |
| 2208 | + |
| 2209 | + return self |
| 2210 | + |
| 2211 | + def remove_forwarder(self, ip_address: str) -> None: |
| 2212 | + """ |
| 2213 | + Remove a DNS server forwarder. |
| 2214 | +
|
| 2215 | + :param ip_address: IP address. |
| 2216 | + :type ip_address: str |
| 2217 | + """ |
| 2218 | + if ip_address is not None: |
| 2219 | + self.host.conn.run(f"Remove-DnsServerForwarder {ip_address} -Force") |
| 2220 | + |
| 2221 | + def clear_forwarders(self) -> None: |
| 2222 | + """ |
| 2223 | + Clear all DNS server forwarders. |
| 2224 | +
|
| 2225 | + AD has about four global forwarders enabled by default. |
| 2226 | + """ |
| 2227 | + forwarders = self.get_forwarders() |
| 2228 | + |
| 2229 | + if isinstance(forwarders, list) and not None: |
| 2230 | + for forwarder in forwarders: |
| 2231 | + self.remove_forwarder(forwarder) |
| 2232 | + |
| 2233 | + def list_zones(self) -> list[str]: |
| 2234 | + """ |
| 2235 | + List zones. |
| 2236 | + :return: List of zones. |
| 2237 | + :rtype: list[str] |
| 2238 | + """ |
| 2239 | + result = self.host.conn.run("Get-DnsServerZone | Format-List -Property ZoneName").stdout_lines |
| 2240 | + result = [x for x in result if x not in ["\r", "", None]] |
| 2241 | + result = [y.replace("\r", "").strip() for y in result] |
| 2242 | + result = [z.split(":")[1].strip() for z in result] |
| 2243 | + |
| 2244 | + return result |
| 2245 | + |
| 2246 | + |
| 2247 | +class ADDNSZone(ADDNSServer, ABC): |
| 2248 | + """ |
| 2249 | + DNS zone management. |
| 2250 | + """ |
| 2251 | + |
| 2252 | + def __init__(self, role: AD, name: str): |
| 2253 | + """ |
| 2254 | + :param name: DNS zone name. |
| 2255 | + :type name: str |
| 2256 | + :param name: DNS zone name. |
| 2257 | + :type name: str |
| 2258 | + """ |
| 2259 | + super().__init__(role) |
| 2260 | + |
| 2261 | + self.zone_name: str = name |
| 2262 | + """Zone name.""" |
| 2263 | + |
| 2264 | + def create(self) -> ADDNSZone: |
| 2265 | + """ |
| 2266 | + Create new zone. |
| 2267 | +
|
| 2268 | + :return: ADDNSServer object. |
| 2269 | + :rtype: ADDNSServer |
| 2270 | + """ |
| 2271 | + self.host.conn.run(f"Add-DnsServerPrimaryZone -Name {self.zone_name} -ReplicationScope Forest -Passthru") |
| 2272 | + return self |
| 2273 | + |
| 2274 | + def delete(self) -> None: |
| 2275 | + """ |
| 2276 | + Delete zone. |
| 2277 | + """ |
| 2278 | + self.host.conn.run(f"Delete-DnsServerZone -Name {self.zone_name}") |
| 2279 | + |
| 2280 | + def add_record(self, name: str, data: str | int) -> ADDNSZone: |
| 2281 | + """ |
| 2282 | + Add DNS record. |
| 2283 | +
|
| 2284 | + If ``data`` is a str, a forward record will be added. |
| 2285 | + If an integer a reverse record will be added. |
| 2286 | +
|
| 2287 | + :param name: Record name. |
| 2288 | + :type name: str |
| 2289 | + :param data: Record data. |
| 2290 | + :type data: str |
| 2291 | + :return: ADDNSZone object. |
| 2292 | + :rtype: ADDNSZone |
| 2293 | + """ |
| 2294 | + args = "" |
| 2295 | + |
| 2296 | + if isinstance(data, int): |
| 2297 | + args = f"-Ptr -Name {str(data)} -AllowUpdateAny -PtrDomainName {name}.{self.zone_name}" |
| 2298 | + elif isinstance(data, str) and ip_version(data) == 4: |
| 2299 | + args = f"-A -Name {name} -IPv4Address {data}" |
| 2300 | + elif isinstance(data, str) and ip_version(data) == 6: |
| 2301 | + args = f"-A -Name {name} -IPv6Address {data}" |
| 2302 | + |
| 2303 | + self.host.conn.run(f"Add-DnsServerResourceRecord -ZoneName {self.zone_name} {args} ") |
| 2304 | + return self |
| 2305 | + |
| 2306 | + def delete_record(self, name: str) -> None: |
| 2307 | + """ |
| 2308 | + Delete DNS record. |
| 2309 | +
|
| 2310 | + :param name: Name of the record. |
| 2311 | + :type name: str |
| 2312 | + """ |
| 2313 | + if "in-addr" in self.zone_name: |
| 2314 | + record_type = "PTR" |
| 2315 | + else: |
| 2316 | + data = self.host.conn.run(f"dig +short {name}").stdout.strip() |
| 2317 | + record_type = "AAAA" if ":" in data else "A" |
| 2318 | + |
| 2319 | + self.host.conn.run( |
| 2320 | + f"Remove-DnsServerResourceRecord -ZoneName {self.zone_name} -Name {name} -RRType {record_type} -Force" |
| 2321 | + ) |
| 2322 | + |
| 2323 | + def print(self) -> str: |
| 2324 | + """ |
| 2325 | + Prints all dns records in zone as text. |
| 2326 | +
|
| 2327 | + :return: Print zone data. |
| 2328 | + :rtype: str |
| 2329 | + """ |
| 2330 | + return self.host.conn.run(f"Get-DnsServerResourceRecord -ZoneName {self.zone_name}").stdout |
| 2331 | + |
| 2332 | + |
2117 | 2333 | ADNetgroupMember: TypeAlias = LDAPNetgroupMember[ADUser, ADNetgroup] |
0 commit comments