From 22f7e139003ddfe0964cf92f7385409e956621e4 Mon Sep 17 00:00:00 2001 From: Miauwkeru Date: Thu, 6 Feb 2025 14:56:01 +0000 Subject: [PATCH 1/4] Add ipinterface type to fieltype.net --- flow/record/fieldtypes/net/__init__.py | 11 +++++- flow/record/fieldtypes/net/ip.py | 52 ++++++++++++++++++++++++++ flow/record/jsonpacker.py | 6 +++ flow/record/whitelist.py | 2 + tests/test_fieldtype_ip.py | 42 +++++++++++++++++++++ 5 files changed, 112 insertions(+), 1 deletion(-) diff --git a/flow/record/fieldtypes/net/__init__.py b/flow/record/fieldtypes/net/__init__.py index b603ad8e..524ca23a 100644 --- a/flow/record/fieldtypes/net/__init__.py +++ b/flow/record/fieldtypes/net/__init__.py @@ -1,12 +1,21 @@ from __future__ import annotations from flow.record.fieldtypes import string -from flow.record.fieldtypes.net.ip import IPAddress, IPNetwork, ipaddress, ipnetwork +from flow.record.fieldtypes.net.ip import ( + IPAddress, + IPInterface, + IPNetwork, + ipaddress, + ipinterface, + ipnetwork, +) __all__ = [ "IPAddress", + "IPInterface", "IPNetwork", "ipaddress", + "ipinterface", "ipnetwork", ] diff --git a/flow/record/fieldtypes/net/ip.py b/flow/record/fieldtypes/net/ip.py index 9d699579..20e05e21 100644 --- a/flow/record/fieldtypes/net/ip.py +++ b/flow/record/fieldtypes/net/ip.py @@ -2,10 +2,13 @@ from ipaddress import ( IPv4Address, + IPv4Interface, IPv4Network, IPv6Address, + IPv6Interface, IPv6Network, ip_address, + ip_interface, ip_network, ) from typing import Union @@ -15,6 +18,7 @@ _IPNetwork = Union[IPv4Network, IPv6Network] _IPAddress = Union[IPv4Address, IPv6Address] +_IPInterface = Union[IPv4Interface, IPv6Interface] class ipaddress(FieldType): @@ -98,8 +102,56 @@ def _pack(self) -> str: def _unpack(data: str) -> ipnetwork: return ipnetwork(data) + @property + def netmask(self) -> ipaddress: + return ipaddress(self.val.netmask) + + +class ipinterface(FieldType): + val = None + _type = "net.ipinterface" + + def __init__(self, addr: int) -> None: + self.val = ip_interface(addr) + + def __eq__(self, b: str | int | bytes | _IPInterface) -> bool: + try: + return self.val == ip_interface(b) + except ValueError: + return False + + def __hash__(self) -> int: + return hash(self.val) + + def __str__(self) -> str: + return str(self.val) + + def __repr__(self) -> str: + return f"{self._type}({str(self)!r})" + + @property + def ip(self) -> ipaddress: + return ipaddress(self.val.ip) + + @property + def network(self) -> ipnetwork: + return ipnetwork(self.val.network) + + @property + def netmask(self) -> ipnetwork: + return self.network.netmask + + def _pack(self) -> str: + return self.val.compressed + + @staticmethod + def _unpack(data: str) -> ipinterface: + return ipinterface(data) + # alias: net.IPAddress -> net.ipaddress # alias: net.IPNetwork -> net.ipnetwork +# alias: net.IPInterface -> net.ipinterface IPAddress = ipaddress IPNetwork = ipnetwork +IPInterface = ipinterface diff --git a/flow/record/jsonpacker.py b/flow/record/jsonpacker.py index 86c03fb4..bca37073 100644 --- a/flow/record/jsonpacker.py +++ b/flow/record/jsonpacker.py @@ -79,6 +79,12 @@ def pack_obj(self, obj: Any) -> dict | str: "executable": obj.executable, "args": obj.args, } + if isinstance(obj, fieldtypes.net.ipinterface): + return { + "ip": str(obj.ip), + "subnetmask": str(obj.netmask), + "network": str(obj.network), + } raise TypeError(f"Unpackable type {type(obj)}") diff --git a/flow/record/whitelist.py b/flow/record/whitelist.py index c1b41ced..8b6b45de 100644 --- a/flow/record/whitelist.py +++ b/flow/record/whitelist.py @@ -24,8 +24,10 @@ "bytes", "record", "net.ipaddress", + "net.ipinterface", "net.ipnetwork", "net.IPAddress", + "net.IPInterface", "net.IPNetwork", "path", ] diff --git a/tests/test_fieldtype_ip.py b/tests/test_fieldtype_ip.py index c0d7c4af..d39c62ee 100644 --- a/tests/test_fieldtype_ip.py +++ b/tests/test_fieldtype_ip.py @@ -128,6 +128,48 @@ def test_record_ipnetwork() -> None: assert "::1" not in data +def test_record_ipinterface() -> None: + TestRecord = RecordDescriptor( + "test/ipinterface", + [ + ("net.ipinterface", "interface"), + ], + ) + + # ipv4 + r = TestRecord("192.168.0.0/24") + assert r.interface == "192.168.0.0/24" + assert "bad.ip" not in r.interface.network + assert "192.168.0.1" in r.interface.network + assert isinstance(r.interface, net.ipinterface) + assert repr(r.interface) == "net.ipinterface('192.168.0.0/24')" + assert hash(r.interface) == hash(net.ipinterface("192.168.0.0/24")) + + r = TestRecord("192.168.1.1") + assert r.interface.ip == "192.168.1.1" + assert r.interface.network == "192.168.1.1/32" + assert r.interface == "192.168.1.1/32" + assert r.interface.netmask == "255.255.255.255" + + r = TestRecord("192.168.1.24/255.255.255.0") + assert r.interface == "192.168.1.24/24" + assert r.interface.ip == "192.168.1.24" + assert r.interface.network == "192.168.1.0/24" + assert r.interface.netmask == "255.255.255.0" + + # ipv6 - https://en.wikipedia.org/wiki/IPv6_address + r = TestRecord("::1") + assert r.interface == "::1" + assert r.interface == "::1/128" + + # Test whether it functions in a set + data = {TestRecord(x).interface for x in ["192.168.0.0/24", "192.168.0.0/24", "::1", "::1"]} + assert len(data) == 2 + assert net.ipinterface("::1") in data + assert net.ipinterface("192.168.0.0/24") in data + assert "::1" not in data + + @pytest.mark.parametrize("PSelector", [Selector, CompiledSelector]) def test_selector_ipaddress(PSelector: type[Selector]) -> None: TestRecord = RecordDescriptor( From 07a72ac2cec2a72e465b32ceb5ba211821248484 Mon Sep 17 00:00:00 2001 From: Miauwkeru Date: Mon, 10 Feb 2025 10:43:32 +0000 Subject: [PATCH 2/4] Resolve comments --- flow/record/fieldtypes/net/__init__.py | 2 -- flow/record/fieldtypes/net/ip.py | 26 +++++++++--------- flow/record/whitelist.py | 1 - tests/test_fieldtype_ip.py | 37 ++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/flow/record/fieldtypes/net/__init__.py b/flow/record/fieldtypes/net/__init__.py index 524ca23a..5c46c053 100644 --- a/flow/record/fieldtypes/net/__init__.py +++ b/flow/record/fieldtypes/net/__init__.py @@ -3,7 +3,6 @@ from flow.record.fieldtypes import string from flow.record.fieldtypes.net.ip import ( IPAddress, - IPInterface, IPNetwork, ipaddress, ipinterface, @@ -12,7 +11,6 @@ __all__ = [ "IPAddress", - "IPInterface", "IPNetwork", "ipaddress", "ipinterface", diff --git a/flow/record/fieldtypes/net/ip.py b/flow/record/fieldtypes/net/ip.py index 20e05e21..a202550f 100644 --- a/flow/record/fieldtypes/net/ip.py +++ b/flow/record/fieldtypes/net/ip.py @@ -19,16 +19,18 @@ _IPNetwork = Union[IPv4Network, IPv6Network] _IPAddress = Union[IPv4Address, IPv6Address] _IPInterface = Union[IPv4Interface, IPv6Interface] +_ConversionTypes = Union[str, int, bytes] +_IP = Union[_IPNetwork, _IPAddress, _IPInterface] class ipaddress(FieldType): - val = None + val: _IPAddress = None _type = "net.ipaddress" - def __init__(self, addr: str | int | bytes): + def __init__(self, addr: _ConversionTypes | _IPAddress): self.val = ip_address(addr) - def __eq__(self, b: str | int | bytes | _IPAddress) -> bool: + def __eq__(self, b: _ConversionTypes | _IPAddress) -> bool: try: return self.val == ip_address(b) except ValueError: @@ -57,13 +59,13 @@ def _unpack(data: int) -> ipaddress: class ipnetwork(FieldType): - val = None + val: _IPNetwork = None _type = "net.ipnetwork" - def __init__(self, addr: str | int | bytes): + def __init__(self, addr: _ConversionTypes | _IPNetwork): self.val = ip_network(addr) - def __eq__(self, b: str | int | bytes | _IPNetwork) -> bool: + def __eq__(self, b: _ConversionTypes | _IPNetwork) -> bool: try: return self.val == ip_network(b) except ValueError: @@ -108,13 +110,13 @@ def netmask(self) -> ipaddress: class ipinterface(FieldType): - val = None + val: _IPInterface = None _type = "net.ipinterface" - def __init__(self, addr: int) -> None: + def __init__(self, addr: _ConversionTypes | _IP) -> None: self.val = ip_interface(addr) - def __eq__(self, b: str | int | bytes | _IPInterface) -> bool: + def __eq__(self, b: _ConversionTypes | _IP) -> bool: try: return self.val == ip_interface(b) except ValueError: @@ -138,8 +140,8 @@ def network(self) -> ipnetwork: return ipnetwork(self.val.network) @property - def netmask(self) -> ipnetwork: - return self.network.netmask + def netmask(self) -> ipaddress: + return ipaddress(self.val.netmask) def _pack(self) -> str: return self.val.compressed @@ -151,7 +153,5 @@ def _unpack(data: str) -> ipinterface: # alias: net.IPAddress -> net.ipaddress # alias: net.IPNetwork -> net.ipnetwork -# alias: net.IPInterface -> net.ipinterface IPAddress = ipaddress IPNetwork = ipnetwork -IPInterface = ipinterface diff --git a/flow/record/whitelist.py b/flow/record/whitelist.py index 8b6b45de..0259f70d 100644 --- a/flow/record/whitelist.py +++ b/flow/record/whitelist.py @@ -27,7 +27,6 @@ "net.ipinterface", "net.ipnetwork", "net.IPAddress", - "net.IPInterface", "net.IPNetwork", "path", ] diff --git a/tests/test_fieldtype_ip.py b/tests/test_fieldtype_ip.py index d39c62ee..a1c224ee 100644 --- a/tests/test_fieldtype_ip.py +++ b/tests/test_fieldtype_ip.py @@ -162,6 +162,17 @@ def test_record_ipinterface() -> None: assert r.interface == "::1" assert r.interface == "::1/128" + r = TestRecord("64:ff9b::2/96") + assert r.interface == "64:ff9b::2/96" + assert r.interface.ip == "64:ff9b::2" + assert r.interface.network == "64:ff9b::/96" + assert r.interface.netmask == "ffff:ffff:ffff:ffff:ffff:ffff::" + + # instantiate from different types + assert TestRecord(1).interface == "0.0.0.1/32" + assert TestRecord(0x7F0000FF).interface == "127.0.0.255/32" + assert TestRecord(b"\x7f\xff\xff\xff").interface == "127.255.255.255/32" + # Test whether it functions in a set data = {TestRecord(x).interface for x in ["192.168.0.0/24", "192.168.0.0/24", "::1", "::1"]} assert len(data) == 2 @@ -170,6 +181,32 @@ def test_record_ipinterface() -> None: assert "::1" not in data +def test_record_ipinterface_types() -> None: + TestRecord = RecordDescriptor( + "test/ipinterface", + [ + ( + "net.ipinterface", + "interface", + ) + ], + ) + + r = TestRecord("192.168.0.255/24") + _if = r.interface + assert isinstance(_if, net.ipinterface) + assert isinstance(_if.ip, net.ipaddress) + assert isinstance(_if.network, net.ipnetwork) + assert isinstance(_if.netmask, net.ipaddress) + + r = TestRecord("64:ff9b::/96") + _if = r.interface + assert isinstance(_if, net.ipinterface) + assert isinstance(_if.ip, net.ipaddress) + assert isinstance(_if.network, net.ipnetwork) + assert isinstance(_if.netmask, net.ipaddress) + + @pytest.mark.parametrize("PSelector", [Selector, CompiledSelector]) def test_selector_ipaddress(PSelector: type[Selector]) -> None: TestRecord = RecordDescriptor( From ab2449b0a5ecf668f0c65c513c3bfe44faf21fb4 Mon Sep 17 00:00:00 2001 From: Miauwkeru Date: Wed, 12 Feb 2025 14:35:40 +0000 Subject: [PATCH 3/4] Move code for packing ipinterface --- flow/record/jsonpacker.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/flow/record/jsonpacker.py b/flow/record/jsonpacker.py index bca37073..01857bf2 100644 --- a/flow/record/jsonpacker.py +++ b/flow/record/jsonpacker.py @@ -68,7 +68,7 @@ def pack_obj(self, obj: Any) -> dict | str: "sha1": obj.sha1, "sha256": obj.sha256, } - if isinstance(obj, (fieldtypes.net.ipaddress, fieldtypes.net.ipnetwork)): + if isinstance(obj, (fieldtypes.net.ipaddress, fieldtypes.net.ipnetwork, fieldtypes.net.ipinterface)): return str(obj) if isinstance(obj, bytes): return base64.b64encode(obj).decode() @@ -79,12 +79,6 @@ def pack_obj(self, obj: Any) -> dict | str: "executable": obj.executable, "args": obj.args, } - if isinstance(obj, fieldtypes.net.ipinterface): - return { - "ip": str(obj.ip), - "subnetmask": str(obj.netmask), - "network": str(obj.network), - } raise TypeError(f"Unpackable type {type(obj)}") From 6268302b88735b933db8dfba5cdc08d4d9f0c4fe Mon Sep 17 00:00:00 2001 From: Miauwkeru Date: Mon, 17 Feb 2025 11:56:20 +0000 Subject: [PATCH 4/4] Rename _IP to _IPTypes --- flow/record/fieldtypes/net/ip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flow/record/fieldtypes/net/ip.py b/flow/record/fieldtypes/net/ip.py index a202550f..85965d89 100644 --- a/flow/record/fieldtypes/net/ip.py +++ b/flow/record/fieldtypes/net/ip.py @@ -20,7 +20,7 @@ _IPAddress = Union[IPv4Address, IPv6Address] _IPInterface = Union[IPv4Interface, IPv6Interface] _ConversionTypes = Union[str, int, bytes] -_IP = Union[_IPNetwork, _IPAddress, _IPInterface] +_IPTypes = Union[_IPNetwork, _IPAddress, _IPInterface] class ipaddress(FieldType): @@ -113,10 +113,10 @@ class ipinterface(FieldType): val: _IPInterface = None _type = "net.ipinterface" - def __init__(self, addr: _ConversionTypes | _IP) -> None: + def __init__(self, addr: _ConversionTypes | _IPTypes) -> None: self.val = ip_interface(addr) - def __eq__(self, b: _ConversionTypes | _IP) -> bool: + def __eq__(self, b: _ConversionTypes | _IPTypes) -> bool: try: return self.val == ip_interface(b) except ValueError: