diff --git a/dissect/target/helpers/record.py b/dissect/target/helpers/record.py index 85ebe87f6..3cf1eee1f 100644 --- a/dissect/target/helpers/record.py +++ b/dissect/target/helpers/record.py @@ -180,7 +180,8 @@ def DynamicDescriptor(types): # noqa [ *COMMON_INTERFACE_ELEMENTS, ("varint", "vlan"), - ("string", "proxy"), + ("net.ipnetwork[]", "network"), ("varint", "interface_service_order"), + ("boolean", "dhcp"), ], ) diff --git a/dissect/target/plugins/os/unix/bsd/osx/_os.py b/dissect/target/plugins/os/unix/bsd/osx/_os.py index 2a6aa692f..ef5dfdbc2 100644 --- a/dissect/target/plugins/os/unix/bsd/osx/_os.py +++ b/dissect/target/plugins/os/unix/bsd/osx/_os.py @@ -40,24 +40,8 @@ def hostname(self) -> Optional[str]: @export(property=True) def ips(self) -> Optional[list[str]]: ips = set() - - # Static configured IP-addresses - if (preferences := self.target.fs.path(self.SYSTEM)).exists(): - network = plistlib.load(preferences.open()).get("NetworkServices") - - for interface in network.values(): - for addresses in [interface.get("IPv4"), interface.get("IPv6")]: - ips.update(addresses.get("Addresses", [])) - - # IP-addresses configured by DHCP - if (dhcp := self.target.fs.path("/private/var/db/dhcpclient/leases")).exists(): - for lease in dhcp.iterdir(): - if lease.is_file(): - lease = plistlib.load(lease.open()) - - if ip := lease.get("IPAddress"): - ips.add(ip) - + for ip in self.target.network.ips(): + ips.add(str(ip)) return list(ips) @export(property=True) diff --git a/dissect/target/plugins/os/unix/bsd/osx/network.py b/dissect/target/plugins/os/unix/bsd/osx/network.py new file mode 100644 index 000000000..3f3d3d2cf --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/osx/network.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import plistlib +from functools import cache, lru_cache +from typing import Iterator + +from dissect.target.helpers.record import MacInterfaceRecord +from dissect.target.plugins.general.network import NetworkPlugin +from dissect.target.target import Target + + +class MacNetworkPlugin(NetworkPlugin): + def __init__(self, target: Target): + super().__init__(target) + self._plistnetwork = cache(self._plistnetwork) + self._plistlease = lru_cache(32)(self._plistlease) + + def _plistlease(self, devname: str) -> dict: + for lease in self.target.fs.glob_ext(f"/private/var/db/dhcpclient/leases/{devname}*"): + return plistlib.load(lease.open()) + return {} + + def _plistnetwork(self) -> dict: + if (preferences := self.target.fs.path("/Library/Preferences/SystemConfiguration/preferences.plist")).exists(): + return plistlib.load(preferences.open()) + + def _interfaces(self) -> Iterator[MacInterfaceRecord]: + plistnetwork = self._plistnetwork() + current_set = plistnetwork.get("CurrentSet") + sets = plistnetwork.get("Sets", {}) + for name, _set in sets.items(): + if f"/Sets/{name}" == current_set: + item = _set + for key in ["Network", "Global", "IPv4", "ServiceOrder"]: + item = item.get(key, {}) + service_order = item + break + + network = plistnetwork.get("NetworkServices", {}) + vlans = plistnetwork.get("VirtualNetworkInterfaces", {}).get("VLAN", {}) + + vlan_lookup = {key: vlan.get("Tag") for key, vlan in vlans.items()} + + for _id, interface in network.items(): + dns = set() + gateways = set() + ips = set() + device = interface.get("Interface", {}) + name = device.get("DeviceName") + _type = device.get("Type") + vlan = vlan_lookup.get(name) + dhcp = False + subnetmask = [] + network = [] + interface_service_order = service_order.index(_id) if _id in service_order else None + try: + for addr in interface.get("DNS", {}).get("ServerAddresses", {}): + dns.add(addr) + for addresses in [interface.get("IPv4", {}), interface.get("IPv6", {})]: + subnetmask += filter(lambda mask: mask != "", addresses.get("SubnetMasks", [])) + if router := addresses.get("Router"): + gateways.add(router) + if addresses.get("ConfigMethod", "") == "DHCP": + ips.add(self._plistlease(name).get("IPAddress")) + dhcp = True + else: + for addr in addresses.get("Addresses", []): + ips.add(addr) + + if subnetmask: + network = self.calculate_network(ips, subnetmask) + + yield MacInterfaceRecord( + name=name, + type=_type, + enabled=not interface.get("__INACTIVE__", False), + dns=list(dns), + ip=list(ips), + gateway=list(gateways), + source="NetworkServices", + vlan=vlan, + network=network, + interface_service_order=interface_service_order, + dhcp=dhcp, + _target=self.target, + ) + + except Exception as e: + self.target.log.warning("Error reading configuration for network device %s: %s", name, e) + continue diff --git a/tests/plugins/os/unix/bsd/osx/test_network.py b/tests/plugins/os/unix/bsd/osx/test_network.py new file mode 100644 index 000000000..d21ea6ec5 --- /dev/null +++ b/tests/plugins/os/unix/bsd/osx/test_network.py @@ -0,0 +1,172 @@ +from typing import Iterator +from unittest import mock + +import pytest + +from dissect.target.plugins.os.unix.bsd.osx.network import MacNetworkPlugin +from dissect.target.target import Target + + +@pytest.fixture +def almost_empty_plist() -> Iterator[dict]: + yield {"CurrentSet": {}} + + +@pytest.fixture +def fake_plist() -> Iterator[dict]: + yield { + "CurrentSet": "/Sets/1", + "NetworkServices": { + "1": { + "DNS": {"ServerAddresses": ["8.8.8.8"]}, + "IPv4": { + "Addresses": ["192.122.13.34"], + "Router": "8.8.8.8", + }, + "Interface": { + "DeviceName": "en0", + "Type": "Ethernet", + }, + }, + }, + "Sets": { + "1": { + "Network": { + "Global": {"IPv4": {"ServiceOrder": ["1"]}}, + }, + }, + }, + "VirtualNetworkInterfaces": {"VLAN": {"vlan0": {"Tag": 2}}}, + } + + +@pytest.fixture +def vlan0(fake_plist: dict) -> Iterator[dict]: + fake_plist["NetworkServices"]["1"]["Interface"].update({"DeviceName": "vlan0"}) + yield fake_plist + + +@pytest.fixture +def inactive(fake_plist: dict) -> Iterator[dict]: + fake_plist["NetworkServices"]["1"].update({"__INACTIVE__": True}) + yield fake_plist + + +@pytest.fixture +def ipv6(fake_plist: dict) -> Iterator[dict]: + del fake_plist["NetworkServices"]["1"]["IPv4"] + fake_plist["NetworkServices"]["1"]["IPv6"] = {"Addresses": ["::1"]} + yield fake_plist + + +@pytest.fixture +def reorder(fake_plist: dict) -> Iterator[dict]: + fake_plist["Sets"]["1"]["Network"]["Global"]["IPv4"]["ServiceOrder"] = ["2", "1"] + yield fake_plist + + +@pytest.fixture +def double(fake_plist: dict) -> Iterator[dict]: + fake_plist["NetworkServices"]["2"] = fake_plist["NetworkServices"]["1"] + yield fake_plist + + +@pytest.fixture +def dhcp(fake_plist: dict) -> Iterator[dict]: + fake_plist["NetworkServices"]["1"]["IPv4"].update({"ConfigMethod": "DHCP"}) + yield fake_plist + + +@pytest.mark.parametrize( + "lease,netinfo_param,expected,count", + [ + ({"IPAddress": None}, "almost_empty_plist", [], 0), + ( + {}, + "fake_plist", + [ + (0, "hostname", ["dummys Mac"]), + (0, "domain", ["None"]), + (0, "name", ["en0"]), + (0, "type", ["Ethernet"]), + (0, "ip", ["192.122.13.34"]), + (0, "gateway", ["8.8.8.8"]), + (0, "dns", ["8.8.8.8"]), + (0, "vlan", ["None"]), + (0, "enabled", ["True"]), + (0, "interface_service_order", ["0"]), + (0, "mac", ["None"]), + (0, "vlan", ["None"]), + ], + 1, + ), + ( + {"IPAddress": "10.0.0.2"}, + "dhcp", + [ + (0, "ip", sorted(["10.0.0.2"])), + ], + 1, + ), + ( + {}, + "vlan0", + [ + (0, "vlan", ["2"]), + ], + 1, + ), + ( + {}, + "inactive", + [ + (0, "enabled", ["False"]), + ], + 1, + ), + ( + {}, + "ipv6", + [ + (0, "ip", ["::1"]), + ], + 1, + ), + ( + {}, + "reorder", + [ + (0, "interface_service_order", ["1"]), + ], + 1, + ), + ( + {}, + "double", + [ + (0, "enabled", ["True"]), + (1, "enabled", ["True"]), + ], + 2, + ), + ], +) +def test_macos_network( + target_osx: Target, lease: dict, netinfo_param: str, expected: dict, count: int, request: pytest.FixtureRequest +) -> None: + plistnetwork = request.getfixturevalue(netinfo_param) + with mock.patch( + "dissect.target.plugins.os.unix.bsd.osx.network.MacNetworkPlugin._plistlease", return_value=lease + ), mock.patch( + "dissect.target.plugins.os.unix.bsd.osx.network.MacNetworkPlugin._plistnetwork", return_value=plistnetwork + ): + network = MacNetworkPlugin(target_osx) + + interfaces = list(network.interfaces()) + assert len(interfaces) == count + for index, key, value in expected: + attr = getattr(interfaces[index], key) + if not isinstance(attr, list): + attr = [attr] + attr = list(sorted(map(str, attr))) + assert attr == value