diff --git a/monkey/common/agent_configuration/agent_sub_configuration_schemas.py b/monkey/common/agent_configuration/agent_sub_configuration_schemas.py index d47730fec..401c04d90 100644 --- a/monkey/common/agent_configuration/agent_sub_configuration_schemas.py +++ b/monkey/common/agent_configuration/agent_sub_configuration_schemas.py @@ -1,6 +1,4 @@ -import re - -from marshmallow import Schema, ValidationError, fields, post_load, validate, validates +from marshmallow import Schema, fields, post_load, validate from .agent_sub_configurations import ( CustomPBAConfiguration, @@ -14,49 +12,19 @@ from .agent_sub_configurations import ( TCPScanConfiguration, ) from .utils import freeze_lists - -valid_windows_custom_pba_filename_regex = re.compile(r"^[^<>:\"\\\/|?*]*[^<>:\"\\\/|?* \.]+$|^$") -valid_linux_custom_pba_filename_regex = re.compile(r"^[^\0/]*$") +from .validators import ( + validate_ip, + validate_linux_filename, + validate_subnet_range, + validate_windows_filename, +) class CustomPBAConfigurationSchema(Schema): linux_command = fields.Str() - linux_filename = fields.Str( - validate=validate.Regexp(regex=valid_linux_custom_pba_filename_regex) - ) + linux_filename = fields.Str(validate=validate_linux_filename) windows_command = fields.Str() - windows_filename = fields.Str( - validate=validate.Regexp(regex=valid_windows_custom_pba_filename_regex) - ) - - @validates("windows_filename") - def validate_windows_filename_not_reserved(self, windows_filename): - # filename shouldn't start with any of these and be followed by a period - if windows_filename.split(".")[0].upper() in [ - "CON", - "PRN", - "AUX", - "NUL", - "COM1", - "COM2", - "COM3", - "COM4", - "COM5", - "COM6", - "COM7", - "COM8", - "COM9", - "LPT1", - "LPT2", - "LPT3", - "LPT4", - "LPT5", - "LPT6", - "LPT7", - "LPT8", - "LPT9", - ]: - raise ValidationError("Invalid Windows filename: reserved name used") + windows_filename = fields.Str(validate=validate_windows_filename) @post_load def _make_custom_pba_configuration(self, data, **kwargs): @@ -73,10 +41,10 @@ class PluginConfigurationSchema(Schema): class ScanTargetConfigurationSchema(Schema): - blocked_ips = fields.List(fields.Str()) - inaccessible_subnets = fields.List(fields.Str()) + blocked_ips = fields.List(fields.Str(validate=validate_ip)) + inaccessible_subnets = fields.List(fields.Str(validate=validate_subnet_range)) local_network_scan = fields.Bool() - subnets = fields.List(fields.Str()) + subnets = fields.List(fields.Str(validate=validate_subnet_range)) @post_load @freeze_lists diff --git a/monkey/common/agent_configuration/agent_sub_configurations.py b/monkey/common/agent_configuration/agent_sub_configurations.py index f58bcd9b0..4ed94d7a8 100644 --- a/monkey/common/agent_configuration/agent_sub_configurations.py +++ b/monkey/common/agent_configuration/agent_sub_configurations.py @@ -54,6 +54,20 @@ class PluginConfiguration: @dataclass(frozen=True) class ScanTargetConfiguration: + """ + Configuration of network targets to scan and exploit + + Attributes: + :param blocked_ips: IP's that won't be scanned + Example: ("1.1.1.1", "2.2.2.2") + :param inaccessible_subnets: Subnet ranges that shouldn't be accessible for the agent + Example: ("1.1.1.1", "2.2.2.2/24", "myserver") + :param local_network_scan: Whether or not the agent should scan the local network + :param subnets: Subnet ranges to scan + Example: ("192.168.1.1-192.168.2.255", "3.3.3.3", "2.2.2.2/24", + "myHostname") + """ + blocked_ips: Tuple[str, ...] inaccessible_subnets: Tuple[str, ...] local_network_scan: bool diff --git a/monkey/common/agent_configuration/validators/__init__.py b/monkey/common/agent_configuration/validators/__init__.py new file mode 100644 index 000000000..f2c6dfad5 --- /dev/null +++ b/monkey/common/agent_configuration/validators/__init__.py @@ -0,0 +1,8 @@ +from .filenames import validate_linux_filename, validate_windows_filename +from .ip_ranges import ( + validate_ip, + validate_hostname, + validate_ip_range, + validate_subnet_range, + validate_ip_network, +) diff --git a/monkey/common/agent_configuration/validators/filenames.py b/monkey/common/agent_configuration/validators/filenames.py new file mode 100644 index 000000000..2a8e4df01 --- /dev/null +++ b/monkey/common/agent_configuration/validators/filenames.py @@ -0,0 +1,24 @@ +import re +from pathlib import PureWindowsPath + +from marshmallow import ValidationError + +_valid_windows_filename_regex = re.compile(r"^[^<>:\"\\\/|?*]*[^<>:\"\\\/|?* \.]+$|^$") +_valid_linux_filename_regex = re.compile(r"^[^\0/]*$") + + +def validate_linux_filename(linux_filename: str): + if not re.match(_valid_linux_filename_regex, linux_filename): + raise ValidationError(f"Invalid Unix filename {linux_filename}: illegal characters") + + +def validate_windows_filename(windows_filename: str): + _validate_windows_filename_not_reserved(windows_filename) + if not re.match(_valid_windows_filename_regex, windows_filename): + raise ValidationError(f"Invalid Windows filename {windows_filename}: illegal characters") + + +def _validate_windows_filename_not_reserved(windows_filename: str): + # filename shouldn't start with any of these and be followed by a period + if PureWindowsPath(windows_filename).is_reserved(): + raise ValidationError(f"Invalid Windows filename {windows_filename}: reserved name used") diff --git a/monkey/common/agent_configuration/validators/ip_ranges.py b/monkey/common/agent_configuration/validators/ip_ranges.py new file mode 100644 index 000000000..6eabc9e61 --- /dev/null +++ b/monkey/common/agent_configuration/validators/ip_ranges.py @@ -0,0 +1,67 @@ +import re +from ipaddress import AddressValueError, IPv4Address, IPv4Network, NetmaskValueError + +from marshmallow import ValidationError + + +def validate_subnet_range(subnet_range: str): + try: + return validate_ip(subnet_range) + except ValidationError: + pass + + try: + return validate_ip_range(subnet_range) + except ValidationError: + pass + + try: + return validate_ip_network(subnet_range) + except ValidationError: + pass + + try: + return validate_hostname(subnet_range) + except ValidationError: + raise ValidationError(f"Invalid subnet range {subnet_range}") + + +def validate_hostname(hostname: str): + # Based on hostname syntax: https://www.rfc-editor.org/rfc/rfc1123#page-13 + hostname_segments = hostname.split(".") + if any((part.endswith("-") or part.startswith("-") for part in hostname_segments)): + raise ValidationError(f"Hostname segment can't start or end with a hyphen: {hostname}") + if not any((char.isalpha() for char in hostname_segments[-1])): + raise ValidationError(f"Last segment of a hostname must contain a letter: {hostname}") + + valid_characters_pattern = r"^[A-Za-z0-9\-]+$" + valid_characters_regex = re.compile(valid_characters_pattern) + matches = ( + re.match(valid_characters_regex, hostname_segment) for hostname_segment in hostname_segments + ) + + if not all(matches): + raise ValidationError(f"Hostname contains invalid characters: {hostname}") + + +def validate_ip_network(ip_network: str): + try: + IPv4Network(ip_network, strict=False) + except (NetmaskValueError, AddressValueError): + raise ValidationError(f"Invalid IPv4 network {ip_network}") + + +def validate_ip_range(ip_range: str): + ip_range = ip_range.replace(" ", "") + ips = ip_range.split("-") + if len(ips) != 2: + raise ValidationError(f"Invalid IP range {ip_range}") + validate_ip(ips[0]) + validate_ip(ips[1]) + + +def validate_ip(ip: str): + try: + IPv4Address(ip) + except AddressValueError: + raise ValidationError(f"Invalid IP address {ip}") diff --git a/monkey/tests/unit_tests/common/__init__.py b/monkey/tests/unit_tests/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/unit_tests/common/agent_configuration/__init__.py b/monkey/tests/unit_tests/common/agent_configuration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/unit_tests/common/configuration/test_agent_configuration.py b/monkey/tests/unit_tests/common/agent_configuration/test_agent_configuration.py similarity index 100% rename from monkey/tests/unit_tests/common/configuration/test_agent_configuration.py rename to monkey/tests/unit_tests/common/agent_configuration/test_agent_configuration.py diff --git a/monkey/tests/unit_tests/common/agent_configuration/validators/__init__.py b/monkey/tests/unit_tests/common/agent_configuration/validators/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/unit_tests/common/agent_configuration/validators/test_ip_ranges.py b/monkey/tests/unit_tests/common/agent_configuration/validators/test_ip_ranges.py new file mode 100644 index 000000000..80f24497c --- /dev/null +++ b/monkey/tests/unit_tests/common/agent_configuration/validators/test_ip_ranges.py @@ -0,0 +1,70 @@ +import pytest +from marshmallow import ValidationError + +from common.agent_configuration.validators.ip_ranges import validate_ip, validate_subnet_range + + +@pytest.mark.parametrize("ip", ["192.168.56.1", "0.0.0.0"]) +def test_validate_ip_valid(ip): + validate_ip(ip) + + +@pytest.mark.parametrize("ip", ["1.1.1", "257.256.255.255", "1.1.1.1.1"]) +def test_validate_ip_invalid(ip): + with pytest.raises(ValidationError): + validate_ip(ip) + + +@pytest.mark.parametrize("ip", ["192.168.56.1", "0.0.0.0"]) +def test_validate_subnet_range__ip_valid(ip): + validate_subnet_range(ip) + + +@pytest.mark.parametrize("ip", ["1.1.1", "257.256.255.255", "1.1.1.1.1"]) +def test_validate_subnet_range__ip_invalid(ip): + with pytest.raises(ValidationError): + validate_subnet_range(ip) + + +@pytest.mark.parametrize("ip_range", ["1.1.1.1 - 2.2.2.2", "1.1.1.255-1.1.1.1"]) +def test_validate_subnet_range__ip_range_valid(ip_range): + validate_subnet_range(ip_range) + + +@pytest.mark.parametrize( + "ip_range", + [ + "1.1.1-2.2.2.2", + "0-.1.1.1-2.2.2.2", + "a..1.1.1-2.2.2.2", + "257.1.1.1-2.2.2.2", + "1.1.1.1-2.2.2.2-3.3.3.3", + ], +) +def test_validate_subnet_range__ip_range_invalid(ip_range): + with pytest.raises(ValidationError): + validate_subnet_range(ip_range) + + +@pytest.mark.parametrize("hostname", ["infection.monkey", "1nfection-Monkey", "1.1.1.1a"]) +def test_validate_subnet_range__hostname_valid(hostname): + validate_subnet_range(hostname) + + +@pytest.mark.parametrize( + "hostname", ["hy&!he.host", "čili-peppers.are-hot", "one.two-", "one-.two", "one@two", ""] +) +def test_validate_subnet_range__hostname_invalid(hostname): + with pytest.raises(ValidationError): + validate_subnet_range(hostname) + + +@pytest.mark.parametrize("cidr_range", ["1.1.1.1/24", "1.1.1.1/0"]) +def test_validate_subnet_range__cidr_valid(cidr_range): + validate_subnet_range(cidr_range) + + +@pytest.mark.parametrize("cidr_range", ["1.1.1/24", "1.1.1.1/-1", "1.1.1.1/33", "1.1.1.1/222"]) +def test_validate_subnet_range__cidr_invalid(cidr_range): + with pytest.raises(ValidationError): + validate_subnet_range(cidr_range)