From de5ccb98c440b230027beae65b41713913e43178 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:43:39 +0200 Subject: [PATCH 01/13] deduplicate ips output --- dissect/target/plugins/os/unix/bsd/osx/_os.py | 5 +---- dissect/target/plugins/os/unix/linux/_os.py | 9 ++++----- dissect/target/plugins/os/windows/_os.py | 2 +- dissect/target/tools/info.py | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/osx/_os.py b/dissect/target/plugins/os/unix/bsd/osx/_os.py index ef5dfdbc2..0f25e6012 100644 --- a/dissect/target/plugins/os/unix/bsd/osx/_os.py +++ b/dissect/target/plugins/os/unix/bsd/osx/_os.py @@ -39,10 +39,7 @@ def hostname(self) -> Optional[str]: @export(property=True) def ips(self) -> Optional[list[str]]: - ips = set() - for ip in self.target.network.ips(): - ips.add(str(ip)) - return list(ips) + return list(set([str(ip) for ip in self.target.network.ips()])) @export(property=True) def version(self) -> Optional[str]: diff --git a/dissect/target/plugins/os/unix/linux/_os.py b/dissect/target/plugins/os/unix/linux/_os.py index e49391e71..1658714d6 100644 --- a/dissect/target/plugins/os/unix/linux/_os.py +++ b/dissect/target/plugins/os/unix/linux/_os.py @@ -34,17 +34,16 @@ def detect(cls, target: Target) -> Filesystem | None: @export(property=True) def ips(self) -> list[str]: """Returns a list of static IP addresses and DHCP lease IP addresses found on the host system.""" - ips = [] + ips = set() for ip_set in self.network_manager.get_config_value("ips"): for ip in ip_set: - ips.append(ip) + ips.add(ip) for ip in parse_unix_dhcp_log_messages(self.target, iter_all=False): - if ip not in ips: - ips.append(ip) + ips.add(ip) - return ips + return list(ips) @export(property=True) def dns(self) -> list[str]: diff --git a/dissect/target/plugins/os/windows/_os.py b/dissect/target/plugins/os/windows/_os.py index 6d4e1a627..e8ae68580 100644 --- a/dissect/target/plugins/os/windows/_os.py +++ b/dissect/target/plugins/os/windows/_os.py @@ -99,7 +99,7 @@ def hostname(self) -> Optional[str]: @export(property=True) def ips(self) -> list[str]: - return self.target.network.ips() + return list(set([str(ip) for ip in self.target.network.ips()])) def _get_version_reg_value(self, value_name: str) -> Any: try: diff --git a/dissect/target/tools/info.py b/dissect/target/tools/info.py index e105d2df3..026308160 100644 --- a/dissect/target/tools/info.py +++ b/dissect/target/tools/info.py @@ -137,7 +137,7 @@ def print_target_info(target: Target) -> None: continue if isinstance(value, list): - value = ", ".join(value) + value = ", ".join([str(v) for v in value]) if isinstance(value, datetime): value = value.isoformat(timespec="microseconds") From f8551127a69817e0171a103c7df870bbb0ef22f2 Mon Sep 17 00:00:00 2001 From: Computer Network Investigation <121175071+JSCU-CNI@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:14:42 +0200 Subject: [PATCH 02/13] Apply suggestions from code review Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/plugins/os/unix/bsd/osx/_os.py | 2 +- dissect/target/plugins/os/windows/_os.py | 2 +- dissect/target/tools/info.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/osx/_os.py b/dissect/target/plugins/os/unix/bsd/osx/_os.py index 0f25e6012..d61766788 100644 --- a/dissect/target/plugins/os/unix/bsd/osx/_os.py +++ b/dissect/target/plugins/os/unix/bsd/osx/_os.py @@ -39,7 +39,7 @@ def hostname(self) -> Optional[str]: @export(property=True) def ips(self) -> Optional[list[str]]: - return list(set([str(ip) for ip in self.target.network.ips()])) + return list(set(map(str, self.target.network.ips())) @export(property=True) def version(self) -> Optional[str]: diff --git a/dissect/target/plugins/os/windows/_os.py b/dissect/target/plugins/os/windows/_os.py index e8ae68580..a073381d1 100644 --- a/dissect/target/plugins/os/windows/_os.py +++ b/dissect/target/plugins/os/windows/_os.py @@ -99,7 +99,7 @@ def hostname(self) -> Optional[str]: @export(property=True) def ips(self) -> list[str]: - return list(set([str(ip) for ip in self.target.network.ips()])) + return list(set(map(str, self.target.network.ips())) def _get_version_reg_value(self, value_name: str) -> Any: try: diff --git a/dissect/target/tools/info.py b/dissect/target/tools/info.py index 026308160..5b1f466a2 100644 --- a/dissect/target/tools/info.py +++ b/dissect/target/tools/info.py @@ -137,7 +137,7 @@ def print_target_info(target: Target) -> None: continue if isinstance(value, list): - value = ", ".join([str(v) for v in value]) + value = ", ".join(map(str, value)) if isinstance(value, datetime): value = value.isoformat(timespec="microseconds") From 65cc4ce563ab9e3bf365bf345616ac2146d91e01 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:58:44 +0200 Subject: [PATCH 03/13] fix broken suggestions --- dissect/target/plugins/os/unix/bsd/osx/_os.py | 2 +- dissect/target/plugins/os/windows/_os.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/osx/_os.py b/dissect/target/plugins/os/unix/bsd/osx/_os.py index f1aa1cf7c..6c99e6e62 100644 --- a/dissect/target/plugins/os/unix/bsd/osx/_os.py +++ b/dissect/target/plugins/os/unix/bsd/osx/_os.py @@ -41,7 +41,7 @@ def hostname(self) -> Optional[str]: @export(property=True) def ips(self) -> Optional[list[str]]: - return list(set(map(str, self.target.network.ips())) + return list(set(map(str, self.target.network.ips()))) @export(property=True) def version(self) -> Optional[str]: diff --git a/dissect/target/plugins/os/windows/_os.py b/dissect/target/plugins/os/windows/_os.py index a073381d1..3acdaf879 100644 --- a/dissect/target/plugins/os/windows/_os.py +++ b/dissect/target/plugins/os/windows/_os.py @@ -99,7 +99,7 @@ def hostname(self) -> Optional[str]: @export(property=True) def ips(self) -> list[str]: - return list(set(map(str, self.target.network.ips())) + return list(set(map(str, self.target.network.ips()))) def _get_version_reg_value(self, value_name: str) -> Any: try: From e719965f5061c2476dbf2760a9a9cfb760381207 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:01:57 +0200 Subject: [PATCH 04/13] add (broken) tests --- dissect/target/plugins/os/unix/linux/_os.py | 3 +- dissect/target/plugins/os/windows/network.py | 28 ++++++---- tests/conftest.py | 17 ++++-- tests/plugins/os/unix/test_ips.py | 24 +++++++++ tests/plugins/os/windows/test_network.py | 55 +++++++++++++++++++- 5 files changed, 109 insertions(+), 18 deletions(-) diff --git a/dissect/target/plugins/os/unix/linux/_os.py b/dissect/target/plugins/os/unix/linux/_os.py index be93fe7d2..fb2e6367d 100644 --- a/dissect/target/plugins/os/unix/linux/_os.py +++ b/dissect/target/plugins/os/unix/linux/_os.py @@ -37,8 +37,7 @@ def ips(self) -> list[str]: ips = set() for ip_set in self.network_manager.get_config_value("ips"): - for ip in ip_set: - ips.add(ip) + ips.update(ip_set) for ip in parse_unix_dhcp_log_messages(self.target, iter_all=False): ips.add(ip) diff --git a/dissect/target/plugins/os/windows/network.py b/dissect/target/plugins/os/windows/network.py index b65144b0f..c84135554 100644 --- a/dissect/target/plugins/os/windows/network.py +++ b/dissect/target/plugins/os/windows/network.py @@ -242,19 +242,25 @@ def _interfaces(self) -> Iterator[WindowsInterfaceRecord]: continue # Extract the network device name for given interface id - name_key = self.target.registry.key( - f"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Network\\" - f"{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{net_cfg_instance_id}\\Connection" - ) - if value_name := _try_value(name_key, "Name"): - device_info["name"] = value_name + try: + name_key = self.target.registry.key( + f"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Network\\" + f"{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{net_cfg_instance_id}\\Connection" + ) + if value_name := _try_value(name_key, "Name"): + device_info["name"] = value_name + except RegistryKeyNotFoundError: + pass # Extract the metric value from the REGISTRY_KEY_INTERFACE key - interface_key = self.target.registry.key( - f"HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{net_cfg_instance_id}" - ) - if value_metric := _try_value(interface_key, "InterfaceMetric"): - device_info["metric"] = value_metric + try: + interface_key = self.target.registry.key( + f"HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{net_cfg_instance_id}" + ) + if value_metric := _try_value(interface_key, "InterfaceMetric"): + device_info["metric"] = value_metric + except RegistryKeyNotFoundError: + pass # Extract the rest of the device information device_info["mac"] = _try_value(subkey, "NetworkAddress") diff --git a/tests/conftest.py b/tests/conftest.py index 53b8a295f..4826cf799 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -227,14 +227,23 @@ def hive_hklm() -> Iterator[VirtualHive]: hive = VirtualHive() # set current control set to ControlSet001 and mock it - controlset_key = "SYSTEM\\ControlSet001" + change_controlset(hive, 1) + + yield hive + + +def change_controlset(hive: VirtualHive, num: int) -> None: + """Update the current control set of the given HKLM hive.""" + + if not isinstance(num, int) or num > 999 or num < 1: + raise ValueError("ControlSet integer must be between 1 and 999") + + controlset_key = "SYSTEM\\ControlSet" + str(num).zfill(3) hive.map_key(controlset_key, VirtualKey(hive, controlset_key)) select_key = "SYSTEM\\Select" hive.map_key(select_key, VirtualKey(hive, select_key)) - hive.map_value(select_key, "Current", VirtualValue(hive, "Current", 1)) - - yield hive + hive.map_value(select_key, "Current", VirtualValue(hive, "Current", num)) @pytest.fixture diff --git a/tests/plugins/os/unix/test_ips.py b/tests/plugins/os/unix/test_ips.py index d8591e453..c86d8505e 100644 --- a/tests/plugins/os/unix/test_ips.py +++ b/tests/plugins/os/unix/test_ips.py @@ -222,3 +222,27 @@ def test_clean_ips(input: str, expected_output: set) -> None: """Test the cleaning of dirty ip addresses.""" assert NetworkManager.clean_ips({input}) == expected_output + + +def test_regression_ips_unique_strings(target_unix: Target, fs_unix: VirtualFilesystem) -> None: + """Regression test for https://github.com/fox-it/dissect.target/issues/877""" + + config = """ + network: + ethernets: + eth0: + addresses: ['1.2.3.4'] + """ + fs_unix.map_file_fh("/etc/netplan/01-netcfg.yaml", BytesIO(textwrap.dedent(config).encode())) + fs_unix.map_file_fh("/etc/netplan/02-netcfg.yaml", BytesIO(textwrap.dedent(config).encode())) + + syslog = "Apr 4 13:37:04 localhost dhclient[4]: bound to 1.2.3.4 -- renewal in 1337 seconds." + fs_unix.map_file_fh("/var/log/syslog", BytesIO(textwrap.dedent(syslog).encode())) + + target_unix.add_plugin(LinuxPlugin) + + assert isinstance(target_unix.ips, list) + assert all([isinstance(ip, str) for ip in target_unix.ips]) + + assert len(target_unix.ips) == 1 + assert target_unix.ips == ["1.2.3.4"] diff --git a/tests/plugins/os/windows/test_network.py b/tests/plugins/os/windows/test_network.py index ed865f1eb..9bbaef3c8 100644 --- a/tests/plugins/os/windows/test_network.py +++ b/tests/plugins/os/windows/test_network.py @@ -2,11 +2,15 @@ from dataclasses import dataclass from datetime import datetime -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest +from dissect.target.helpers.regutil import VirtualHive, VirtualKey +from dissect.target.plugins.os.windows._os import WindowsPlugin +from dissect.target.plugins.os.windows.network import WindowsNetworkPlugin from dissect.target.target import Target +from tests.conftest import change_controlset @dataclass @@ -354,3 +358,52 @@ def test_network_dhcp_and_static( assert network.dns() == dns assert network.gateways() == gateways assert network.macs() == macs + + +@patch( + "dissect.target.plugins.os.windows.registry.RegistryPlugin.controlsets", + property(MagicMock(return_value=["ControlSet001", "ControlSet002", "ControlSet003"])), +) +def test_regression_duplicate_ips(target_win: Target, hive_hklm: VirtualHive) -> None: + """Regression test for https://github.com/fox-it/dissect.target/issues/877""" + + change_controlset(hive_hklm, 3) + + # register the interfaces + kvs = [ + ( + "SYSTEM\\ControlSet001\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}\\0001", + "{some-net-cfg-instance-uuid}", + ), + ( + "SYSTEM\\ControlSet002\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}\\0001", + "{some-net-cfg-instance-uuid}", + ), + ( + "SYSTEM\\ControlSet003\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}\\0001", + "{some-net-cfg-instance-uuid}", + ), + ] + for name, value in kvs: + key = VirtualKey(hive_hklm, name) + key.add_value("NetCfgInstanceId", value) + hive_hklm.map_key(name, key) + + # register interface dhcp ip addresses for three different control sets + kvs = [ + ("SYSTEM\\ControlSet001\\Services\\Tcpip\\Parameters\\Interfaces\\{some-net-cfg-instance-uuid}", "1.2.3.4"), + ("SYSTEM\\ControlSet002\\Services\\Tcpip\\Parameters\\Interfaces\\{some-net-cfg-instance-uuid}", "1.2.3.4"), + ("SYSTEM\\ControlSet003\\Services\\Tcpip\\Parameters\\Interfaces\\{some-net-cfg-instance-uuid}", "5.6.7.8"), + ] + for name, value in kvs: + key = VirtualKey(hive_hklm, name) + key.add_value("DhcpIPAddress", value) + hive_hklm.map_key(name, key) + + target_win.add_plugin(WindowsPlugin) + target_win.add_plugin(WindowsNetworkPlugin) + + assert isinstance(target_win.ips, list) + assert all([isinstance(ip, str) for ip in target_win.ips]) + assert len(target_win.ips) == 2 + assert target_win.ips == ["1.2.3.4", "5.6.7.8"] From 3a63a26d40af8c7aa4c27dd7f592bf19efd7f89f Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:31:36 +0200 Subject: [PATCH 05/13] fix tests --- dissect/target/plugins/general/network.py | 7 +- dissect/target/plugins/os/windows/generic.py | 2 +- dissect/target/plugins/os/windows/network.py | 161 ++++++++++--------- tests/plugins/os/windows/test_network.py | 36 ++--- 4 files changed, 107 insertions(+), 99 deletions(-) diff --git a/dissect/target/plugins/general/network.py b/dissect/target/plugins/general/network.py index cf550146c..b77c78b45 100644 --- a/dissect/target/plugins/general/network.py +++ b/dissect/target/plugins/general/network.py @@ -49,18 +49,21 @@ def interfaces(self) -> Iterator[InterfaceRecord]: @export def ips(self) -> list[IPAddress]: + # TODO: deduplicate using set: https://github.com/fox-it/flow.record/pull/148 return list(self._get_record_type("ip")) @export def gateways(self) -> list[IPAddress]: + # TODO: deduplicate using set: https://github.com/fox-it/flow.record/pull/148 return list(self._get_record_type("gateway")) @export def macs(self) -> list[str]: - return list(self._get_record_type("mac")) + return list(set(self._get_record_type("mac"))) @export - def dns(self) -> list[str]: + def dns(self) -> list[str | IPAddress]: + # TODO: deduplicate using set: https://github.com/fox-it/flow.record/pull/148 return list(self._get_record_type("dns")) @internal diff --git a/dissect/target/plugins/os/windows/generic.py b/dissect/target/plugins/os/windows/generic.py index 600a4a279..e1882259e 100644 --- a/dissect/target/plugins/os/windows/generic.py +++ b/dissect/target/plugins/os/windows/generic.py @@ -163,7 +163,7 @@ def domain(self): if val: val = val.strip("\\") return val - except RegistryError: + except (RegistryError, AttributeError): continue @export(property=True) diff --git a/dissect/target/plugins/os/windows/network.py b/dissect/target/plugins/os/windows/network.py index c84135554..f5d2089a7 100644 --- a/dissect/target/plugins/os/windows/network.py +++ b/dissect/target/plugins/os/windows/network.py @@ -1,7 +1,8 @@ from __future__ import annotations from enum import IntEnum -from typing import Iterator +from functools import lru_cache +from typing import Any, Iterator from dissect.util.ts import wintimestamp @@ -12,6 +13,7 @@ from dissect.target.helpers.record import WindowsInterfaceRecord from dissect.target.helpers.regutil import RegistryKey from dissect.target.plugins.general.network import NetworkPlugin +from dissect.target.target import Target class IfTypes(IntEnum): @@ -222,13 +224,32 @@ def _try_value(subkey: RegistryKey, value: str) -> str | list | None: return None +def _get_config_value(key: RegistryKey, name: str) -> set: + value = _try_value(key, name) + if not value or value in ("", "0.0.0.0", None, [], ["0.0.0.0"]): + return set() + + if isinstance(value, list): + return set(value) + + return {value} + + class WindowsNetworkPlugin(NetworkPlugin): + """Windows network plugin.""" + + def __init__(self, target: Target): + super().__init__(target) + self._extract_network_device_config = lru_cache(4096)(self._extract_network_device_config) + def _interfaces(self) -> Iterator[WindowsInterfaceRecord]: + """Yields found Windows interfaces used by :class:`NetworkPlugin` `interfaces` export func.""" + # Get all the network interfaces - for keys in self.target.registry.keys( + for key in self.target.registry.keys( "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}" ): - for subkey in keys.subkeys(): + for subkey in key.subkeys(): device_info = {} if (net_cfg_instance_id := _try_value(subkey, "NetCfgInstanceId")) is None: @@ -237,11 +258,8 @@ def _interfaces(self) -> Iterator[WindowsInterfaceRecord]: # Extract the network device configuration for given interface id config = self._extract_network_device_config(net_cfg_instance_id) - if config is None or all(not conf for conf in config): - # if no configuration is found or all configurations are empty, skip this network interface - continue - # Extract the network device name for given interface id + # Extract a network device name for given interface id try: name_key = self.target.registry.key( f"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Network\\" @@ -274,26 +292,57 @@ def _interfaces(self) -> Iterator[WindowsInterfaceRecord]: # Yield a record for each non-empty configuration for conf in config: - if conf: - # Create a copy of device_info to avoid overwriting - record_info = device_info.copy() - record_info.update(conf) - yield WindowsInterfaceRecord( - **record_info, - source=f"HKLM\\SYSTEM\\{subkey.path}", - _target=self.target, - ) + # If no configuration is found or all configurations are empty, + # skip this network interface. + if not conf or not any( + [ + conf["dns"], + conf["ip"], + conf["gateway"], + conf["subnetmask"], + conf["search_domain"], + ] + ): + continue + + # Create a copy of device_info to avoid overwriting + record_info = device_info.copy() + record_info.update(conf) + yield WindowsInterfaceRecord( + **record_info, + source=f"HKLM\\SYSTEM\\{subkey.path}", + _target=self.target, + ) def _extract_network_device_config( self, interface_id: str ) -> list[dict[str, str | list], dict[str, str | list]] | None: - dhcp_config = {} - static_config = {} + """Extract network device configuration from the given interface_id for all ControlSets on the system.""" + + dhcp_config = { + "gateway": set(), + "ip": set(), + "dns": set(), + "subnetmask": set(), + "search_domain": set(), + "network": set(), + } + + static_config = { + "ip": set(), + "dns": set(), + "subnetmask": set(), + "search_domain": set(), + "gateway": set(), + "network": set(), + } # Get the registry keys for the given interface id try: - keys = self.target.registry.key( - f"HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{interface_id}" + keys = list( + self.target.registry.keys( + f"HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{interface_id}" + ) ) except RegistryKeyNotFoundError: return None @@ -301,69 +350,33 @@ def _extract_network_device_config( if not len(keys): return None - # Extract DHCP configuration from the registry - dhcp_gateway = _try_value(keys, "DhcpDefaultGateway") - if dhcp_gateway not in ["", "0.0.0.0", None, []]: - dhcp_config["gateway"] = dhcp_gateway - - dhcp_ip = _try_value(keys, "DhcpIPAddress") - if dhcp_ip not in ["", "0.0.0.0", None]: - dhcp_config["ip"] = [dhcp_ip] - - dhcp_dns = _try_value(keys, "DhcpNameServer") - if dhcp_dns not in ["", "0.0.0.0", None]: - dhcp_config["dns"] = dhcp_dns.split(" ") - - dhcp_subnetmask = _try_value(keys, "DhcpSubnetMask") - if dhcp_subnetmask not in ["", "0.0.0.0", None]: - dhcp_config["subnetmask"] = [dhcp_subnetmask] - - dhcp_domain = _try_value(keys, "DhcpDomain") - if dhcp_domain not in ["", None]: - dhcp_config["search_domain"] = [dhcp_domain] + for key in keys: + # Extract DHCP configuration from the registry + dhcp_config["gateway"].update(_get_config_value(key, "DhcpDefaultGateway")) + dhcp_config["ip"].update(_get_config_value(key, "DhcpIPAddress")) + dhcp_config["subnetmask"].update(_get_config_value(key, "DhcpSubnetMask")) + dhcp_config["search_domain"].update(_get_config_value(key, "DhcpDomain")) + dhcp_config["dns"].update(_get_config_value(key, "DhcpNameServer")) + + # Extract static configuration from the registry + static_config["gateway"].update(_get_config_value(key, "DefaultGateway")) + static_config["dns"].update(_get_config_value(key, "NameServer")) + static_config["search_domain"].update(_get_config_value(key, "Domain")) + static_config["ip"].update(_get_config_value(key, "IPAddress")) + static_config["subnetmask"].update(_get_config_value(key, "SubnetMask")) if len(dhcp_config) > 0: - dhcp_enable = _try_value(keys, "EnableDHCP") - dhcp_config["enabled"] = dhcp_enable == 1 + dhcp_config["enabled"] = _try_value(key, "EnableDHCP") == 1 dhcp_config["dhcp"] = True - # Extract static configuration from the registry - static_gateway = _try_value(keys, "DefaultGateway") - if static_gateway not in ["", None, []]: - static_config["gateway"] = static_gateway - - static_ip = _try_value(keys, "IPAddress") - if static_ip not in ["", "0.0.0.0", ["0.0.0.0"], None, []]: - static_config["ip"] = static_ip if isinstance(static_ip, list) else [static_ip] - - static_dns = _try_value(keys, "NameServer") - if static_dns not in ["", "0.0.0.0", None]: - static_config["dns"] = static_dns.split(",") - - static_subnetmask = _try_value(keys, "SubnetMask") - if static_subnetmask not in ["", "0.0.0.0", ["0.0.0.0"], None, []]: - static_config["subnetmask"] = ( - static_subnetmask if isinstance(static_subnetmask, list) else [static_subnetmask] - ) - - static_domain = _try_value(keys, "Domain") - if static_domain not in ["", None]: - static_config["search_domain"] = [static_domain] - if len(static_config) > 0: static_config["enabled"] = None static_config["dhcp"] = False - # Combine ip and subnetmask for extraction - combined_configs = [ - (dhcp_config, dhcp_config.get("ip", []), dhcp_config.get("subnetmask", [])), - (static_config, static_config.get("ip", []), static_config.get("subnetmask", [])), - ] - # Iterate over combined ip/subnet lists - for config, ips, subnet_masks in combined_configs: - for network_address in self.calculate_network(ips, subnet_masks): - config.setdefault("network", []).append(network_address) + for config in (dhcp_config, static_config): + if (ips := config.get("ip")) and (masks := config.get("subnetmask")): + config["network"].update(set(self.calculate_network(ips, masks))) # Return both configurations return [dhcp_config, static_config] diff --git a/tests/plugins/os/windows/test_network.py b/tests/plugins/os/windows/test_network.py index 9bbaef3c8..19d8bc0ee 100644 --- a/tests/plugins/os/windows/test_network.py +++ b/tests/plugins/os/windows/test_network.py @@ -106,7 +106,7 @@ class MockRegVal: "dns": [], "mac": [], "network": [], - "search_domain": None, + "search_domain": [], "first_connected": None, "vlan": None, "name": None, @@ -140,7 +140,7 @@ class MockRegVal: "first_connected": None, "dns": [], "network": [], - "search_domain": None, + "search_domain": [], "vlan": None, "metric": None, "name": None, @@ -174,7 +174,9 @@ def test_windows_network( with ( patch("dissect.target.plugins.os.windows.generic.GenericPlugin", return_value=""), - patch("dissect.target.plugins.os.windows._os.WindowsPlugin.hostname", return_value="hostname"), + patch( + "dissect.target.plugins.os.windows._os.WindowsPlugin.hostname", property(MagicMock(return_value="hostname")) + ), patch.object(target_win, "registry", mock_registry), ): network = target_win.network @@ -227,7 +229,9 @@ def test_windows_network_none( mock_registry.values.return_value = list(mock_value_dict.values()) with ( - patch("dissect.target.plugins.os.windows._os.WindowsPlugin.hostname", return_value="hostname"), + patch( + "dissect.target.plugins.os.windows._os.WindowsPlugin.hostname", property(MagicMock(return_value="hostname")) + ), patch.object(target_win, "registry", mock_registry), ): network = target_win.network @@ -323,26 +327,19 @@ def test_network_dhcp_and_static( with ( patch("dissect.target.plugins.os.windows.generic.GenericPlugin", return_value=""), - patch("dissect.target.plugins.os.windows._os.WindowsPlugin.hostname", return_value="hostname"), + patch( + "dissect.target.plugins.os.windows._os.WindowsPlugin.hostname", property(MagicMock(return_value="hostname")) + ), patch.object(target_win, "registry", mock_registry), ): - ips = [] - dns = [] - gateways = [] - macs = [] - network = target_win.network interfaces = list(network.interfaces()) for interface, expected in zip(interfaces, expected_values): assert interface.ip == expected["ip"] - ips.extend(interface.ip) assert interface.dns == expected["dns"] - dns.extend(interface.dns) assert interface.gateway == expected["gateway"] - gateways.extend(interface.gateway) assert interface.mac == expected["mac"] - macs.append(interface.mac) assert interface.network == expected["network"] assert interface.first_connected == expected["first_connected"] assert interface.type == expected["type"] @@ -354,11 +351,6 @@ def test_network_dhcp_and_static( assert interface.dhcp == expected["dhcp"] assert interface.enabled == expected["enabled"] - assert network.ips() == ips - assert network.dns() == dns - assert network.gateways() == gateways - assert network.macs() == macs - @patch( "dissect.target.plugins.os.windows.registry.RegistryPlugin.controlsets", @@ -376,11 +368,11 @@ def test_regression_duplicate_ips(target_win: Target, hive_hklm: VirtualHive) -> "{some-net-cfg-instance-uuid}", ), ( - "SYSTEM\\ControlSet002\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}\\0001", + "SYSTEM\\ControlSet002\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}\\0002", "{some-net-cfg-instance-uuid}", ), ( - "SYSTEM\\ControlSet003\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}\\0001", + "SYSTEM\\ControlSet003\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}\\0003", "{some-net-cfg-instance-uuid}", ), ] @@ -406,4 +398,4 @@ def test_regression_duplicate_ips(target_win: Target, hive_hklm: VirtualHive) -> assert isinstance(target_win.ips, list) assert all([isinstance(ip, str) for ip in target_win.ips]) assert len(target_win.ips) == 2 - assert target_win.ips == ["1.2.3.4", "5.6.7.8"] + assert sorted(target_win.ips) == ["1.2.3.4", "5.6.7.8"] From f02cf5630143a6e2762cf09d34c0d937f95fe8a1 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:00:04 +0200 Subject: [PATCH 06/13] deduplicate ip addresses in network plugin --- dissect/target/plugins/general/network.py | 9 +++------ dissect/target/plugins/os/windows/network.py | 4 ++-- pyproject.toml | 2 +- tests/plugins/os/windows/test_network.py | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/dissect/target/plugins/general/network.py b/dissect/target/plugins/general/network.py index b77c78b45..3867ff94f 100644 --- a/dissect/target/plugins/general/network.py +++ b/dissect/target/plugins/general/network.py @@ -49,13 +49,11 @@ def interfaces(self) -> Iterator[InterfaceRecord]: @export def ips(self) -> list[IPAddress]: - # TODO: deduplicate using set: https://github.com/fox-it/flow.record/pull/148 - return list(self._get_record_type("ip")) + return list(set(self._get_record_type("ip"))) @export def gateways(self) -> list[IPAddress]: - # TODO: deduplicate using set: https://github.com/fox-it/flow.record/pull/148 - return list(self._get_record_type("gateway")) + return list(set(self._get_record_type("gateway"))) @export def macs(self) -> list[str]: @@ -63,8 +61,7 @@ def macs(self) -> list[str]: @export def dns(self) -> list[str | IPAddress]: - # TODO: deduplicate using set: https://github.com/fox-it/flow.record/pull/148 - return list(self._get_record_type("dns")) + return list(set(self._get_record_type("dns"))) @internal def with_ip(self, ip_addr: str) -> Iterator[InterfaceRecord]: diff --git a/dissect/target/plugins/os/windows/network.py b/dissect/target/plugins/os/windows/network.py index f5d2089a7..f720fb17d 100644 --- a/dissect/target/plugins/os/windows/network.py +++ b/dissect/target/plugins/os/windows/network.py @@ -2,7 +2,7 @@ from enum import IntEnum from functools import lru_cache -from typing import Any, Iterator +from typing import Iterator from dissect.util.ts import wintimestamp @@ -273,7 +273,7 @@ def _interfaces(self) -> Iterator[WindowsInterfaceRecord]: # Extract the metric value from the REGISTRY_KEY_INTERFACE key try: interface_key = self.target.registry.key( - f"HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{net_cfg_instance_id}" + f"HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{net_cfg_instance_id}" # noqa: E501 ) if value_metric := _try_value(interface_key, "InterfaceMetric"): device_info["metric"] = value_metric diff --git a/pyproject.toml b/pyproject.toml index 6881e0486..bf09c2df0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "dissect.regf>=3.3,<4", "dissect.util>=3,<4", "dissect.volume>=2,<4", - "flow.record~=3.16.0", + "flow.record~=3.17.0", "structlog", ] dynamic = ["version"] diff --git a/tests/plugins/os/windows/test_network.py b/tests/plugins/os/windows/test_network.py index 19d8bc0ee..2398e66d7 100644 --- a/tests/plugins/os/windows/test_network.py +++ b/tests/plugins/os/windows/test_network.py @@ -335,7 +335,17 @@ def test_network_dhcp_and_static( network = target_win.network interfaces = list(network.interfaces()) + ips = set() + dns = set() + gateways = set() + macs = set() + for interface, expected in zip(interfaces, expected_values): + ips.update(interface.ip) + dns.update(interface.dns) + gateways.update(interface.gateway) + macs.add(interface.mac) + assert interface.ip == expected["ip"] assert interface.dns == expected["dns"] assert interface.gateway == expected["gateway"] @@ -351,6 +361,11 @@ def test_network_dhcp_and_static( assert interface.dhcp == expected["dhcp"] assert interface.enabled == expected["enabled"] + assert network.ips() == list(ips) + assert network.dns() == list(dns) + assert network.gateways() == list(gateways) + assert network.macs() == list(macs) + @patch( "dissect.target.plugins.os.windows.registry.RegistryPlugin.controlsets", From 834c21c040fb29971aefbd735086d07d3e872b66 Mon Sep 17 00:00:00 2001 From: Computer Network Investigation <121175071+JSCU-CNI@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:59:52 +0200 Subject: [PATCH 07/13] Apply suggestions from code review Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/plugins/os/windows/network.py | 4 ++-- tests/conftest.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dissect/target/plugins/os/windows/network.py b/dissect/target/plugins/os/windows/network.py index f720fb17d..a209f3d8d 100644 --- a/dissect/target/plugins/os/windows/network.py +++ b/dissect/target/plugins/os/windows/network.py @@ -240,10 +240,10 @@ class WindowsNetworkPlugin(NetworkPlugin): def __init__(self, target: Target): super().__init__(target) - self._extract_network_device_config = lru_cache(4096)(self._extract_network_device_config) + self._extract_network_device_config = lru_cache(128)(self._extract_network_device_config) def _interfaces(self) -> Iterator[WindowsInterfaceRecord]: - """Yields found Windows interfaces used by :class:`NetworkPlugin` `interfaces` export func.""" + """Yields found Windows interfaces used by :class:`NetworkPlugin` ``interfaces`` export func.""" # Get all the network interfaces for key in self.target.registry.keys( diff --git a/tests/conftest.py b/tests/conftest.py index 4826cf799..8cd7049fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -238,7 +238,7 @@ def change_controlset(hive: VirtualHive, num: int) -> None: if not isinstance(num, int) or num > 999 or num < 1: raise ValueError("ControlSet integer must be between 1 and 999") - controlset_key = "SYSTEM\\ControlSet" + str(num).zfill(3) + controlset_key = f"SYSTEM\\ControlSet{num:>03}" hive.map_key(controlset_key, VirtualKey(hive, controlset_key)) select_key = "SYSTEM\\Select" From 9b10787acdda29200b6d87487041f5712f8bb827 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:52:29 +0200 Subject: [PATCH 08/13] fix tests --- tests/plugins/general/test_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/general/test_network.py b/tests/plugins/general/test_network.py index 6e3a1e9b3..5e0c7cf29 100644 --- a/tests/plugins/general/test_network.py +++ b/tests/plugins/general/test_network.py @@ -34,7 +34,7 @@ def test_base_network_plugin(target_bare: Target, network_record: InterfaceRecor assert network.ips() == ["10.42.42.10"] assert network.gateways() == ["10.42.42.1"] assert network.macs() == ["DE:AD:BE:EF:00:00"] - assert network.dns() == ["8.8.8.8", "1.1.1.1"] + assert list(map(str, network.dns())) == ["8.8.8.8", "1.1.1.1"] assert len(list(network.in_cidr("10.42.42.0/24"))) == 1 assert len(list(network.in_cidr("10.43.42.0/24"))) == 0 From 73d45e2a9087deefe765265ebd45019288ceac01 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:43:59 +0200 Subject: [PATCH 09/13] fix test race condition --- tests/plugins/general/test_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/general/test_network.py b/tests/plugins/general/test_network.py index 5e0c7cf29..a8131235f 100644 --- a/tests/plugins/general/test_network.py +++ b/tests/plugins/general/test_network.py @@ -34,7 +34,7 @@ def test_base_network_plugin(target_bare: Target, network_record: InterfaceRecor assert network.ips() == ["10.42.42.10"] assert network.gateways() == ["10.42.42.1"] assert network.macs() == ["DE:AD:BE:EF:00:00"] - assert list(map(str, network.dns())) == ["8.8.8.8", "1.1.1.1"] + assert sorted(list(map(str, network.dns()))) == ["1.1.1.1", "8.8.8.8"] assert len(list(network.in_cidr("10.42.42.0/24"))) == 1 assert len(list(network.in_cidr("10.43.42.0/24"))) == 0 From bfc080f913c238fa1684585658981a1d8fca40c6 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:05:16 +0100 Subject: [PATCH 10/13] noqa --- dissect/target/plugins/os/windows/network.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dissect/target/plugins/os/windows/network.py b/dissect/target/plugins/os/windows/network.py index 2174f9025..02a4c7f6e 100644 --- a/dissect/target/plugins/os/windows/network.py +++ b/dissect/target/plugins/os/windows/network.py @@ -261,10 +261,7 @@ def _interfaces(self) -> Iterator[WindowsInterfaceRecord]: # Extract a network device name for given interface id try: - name_key = self.target.registry.key( - f"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Network\\" - f"{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{net_cfg_instance_id}\\Connection" - ) + name_key = self.target.registry.key(f"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{net_cfg_instance_id}\\Connection") # noqa: E501 if value_name := _try_value(name_key, "Name"): device_info["name"] = value_name except RegistryKeyNotFoundError: From 0af446169b26147d7fb556c6c962157b01cbc448 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:45:20 +0100 Subject: [PATCH 11/13] implement review comment --- dissect/target/plugins/os/windows/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/network.py b/dissect/target/plugins/os/windows/network.py index 02a4c7f6e..dfc22f26c 100644 --- a/dissect/target/plugins/os/windows/network.py +++ b/dissect/target/plugins/os/windows/network.py @@ -243,7 +243,7 @@ def __init__(self, target: Target): self._extract_network_device_config = lru_cache(128)(self._extract_network_device_config) def _interfaces(self) -> Iterator[WindowsInterfaceRecord]: - """Yields found Windows interfaces used by :class:`NetworkPlugin` ``interfaces`` export func.""" + """Yields found Windows interfaces used by :meth:`NetworkPlugin.interfaces() `.""" # noqa: E501 # Get all the network interfaces for key in self.target.registry.keys( From c3aff4dbac0b2c7b1cf08ceb39008240729ca693 Mon Sep 17 00:00:00 2001 From: JSCU-CNI <121175071+JSCU-CNI@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:34:51 +0100 Subject: [PATCH 12/13] revert AttributeError catch --- dissect/target/plugins/os/windows/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/generic.py b/dissect/target/plugins/os/windows/generic.py index 2c97a0ef9..3083a0176 100644 --- a/dissect/target/plugins/os/windows/generic.py +++ b/dissect/target/plugins/os/windows/generic.py @@ -165,7 +165,7 @@ def domain(self) -> str | None: if val: val = val.strip("\\") return val - except (RegistryError, AttributeError): + except RegistryError: continue @export(property=True) From db9e4257cd81be24c9c91ed18c1275b346fabd6c Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:47:44 +0100 Subject: [PATCH 13/13] Fix linting --- dissect/target/plugins/os/windows/network.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/windows/network.py b/dissect/target/plugins/os/windows/network.py index dfc22f26c..dac8de698 100644 --- a/dissect/target/plugins/os/windows/network.py +++ b/dissect/target/plugins/os/windows/network.py @@ -261,13 +261,15 @@ def _interfaces(self) -> Iterator[WindowsInterfaceRecord]: # Extract a network device name for given interface id try: - name_key = self.target.registry.key(f"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{net_cfg_instance_id}\\Connection") # noqa: E501 + name_key = self.target.registry.key( + f"HKLM\\SYSTEM\\CurrentControlSet\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{net_cfg_instance_id}\\Connection" # noqa: E501 + ) if value_name := _try_value(name_key, "Name"): device_info["name"] = value_name except RegistryKeyNotFoundError: pass - # Extract the metric value from the REGISTRY_KEY_INTERFACE key + # Extract the metric value from the interface registry key try: interface_key = self.target.registry.key( f"HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{net_cfg_instance_id}" # noqa: E501