diff --git a/docs/developing.md b/docs/developing.md index 0fd57f4..98835f2 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -147,7 +147,7 @@ DOCKER_REGISTRY=$(minikube ip) ./local_dev/run_e2e_tests.sh In order to run the unit tests: ```bash -python setup.py test --addopts="-m 'not e2e' --runslow" +coverage run setup.py test --addopts="-m 'not e2e' --runslow" ``` ## Cleanup diff --git a/e2e-manifests/01-deny-all-traffic-to-an-application.yml b/e2e-manifests/01-deny-all-traffic-to-an-application.yml index f543a32..4e017e6 100644 --- a/e2e-manifests/01-deny-all-traffic-to-an-application.yml +++ b/e2e-manifests/01-deny-all-traffic-to-an-application.yml @@ -24,7 +24,7 @@ spec: app: web spec: containers: - - image: nginx + - image: busybox name: web ports: - containerPort: 80 diff --git a/e2e-manifests/expected/01-deny-all-traffic-to-an-application.yml b/e2e-manifests/expected/01-deny-all-traffic-to-an-application.yml index fd76ebe..5b99dfd 100644 --- a/e2e-manifests/expected/01-deny-all-traffic-to-an-application.yml +++ b/e2e-manifests/expected/01-deny-all-traffic-to-an-application.yml @@ -1,4 +1,6 @@ 01-deny-all-traffic-to-an-application:app=web: 01-deny-all-traffic-to-an-application:app=web: - -*: + -TCP/*: + success: true + -UDP/*: success: true diff --git a/e2e-manifests/expected/labels-with-all-legal-characters.yml b/e2e-manifests/expected/labels-with-all-legal-characters.yml index 515100d..9b8c30a 100644 --- a/e2e-manifests/expected/labels-with-all-legal-characters.yml +++ b/e2e-manifests/expected/labels-with-all-legal-characters.yml @@ -1,16 +1,16 @@ illuminatio-inverted-labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC: labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC: - -*: + -TCP/*: success: true labels-with-all-legal-characters:illuminatio-inverted-test.io/test-123_XYZ=test_456-123.ABC: labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC: - -*: + -TCP/*: success: true illuminatio-inverted-labels-with-all-legal-characters:illuminatio-inverted-test.io/test-123_XYZ=test_456-123.ABC: labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC: - -*: + -TCP/*: success: true labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC: labels-with-all-legal-characters:test.io/test-123_XYZ=test_456-123.ABC: - "*": + "TCP/*": success: true diff --git a/e2e-manifests/expected/max-length-labels.yml b/e2e-manifests/expected/max-length-labels.yml index f402bae..01944d2 100644 --- a/e2e-manifests/expected/max-length-labels.yml +++ b/e2e-manifests/expected/max-length-labels.yml @@ -1,4 +1,6 @@ max-length-labels:253-characters-or-less.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/63-characters-or-less_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=63-characters-or-less_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: max-length-labels:253-characters-or-less.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/63-characters-or-less_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=63-characters-or-less_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: - -*: + -TCP/*: + success: true + -UDP/*: success: true diff --git a/e2e-manifests/expected/udp-support.yml b/e2e-manifests/expected/udp-support.yml new file mode 100644 index 0000000..9b20d20 --- /dev/null +++ b/e2e-manifests/expected/udp-support.yml @@ -0,0 +1,16 @@ +illuminatio-inverted-udp-support:app=bookstore: + udp-support:app=bookstore,role=api: + -UDP/*: + success: true +illuminatio-inverted-udp-support:illuminatio-inverted-app=bookstore: + udp-support:app=bookstore,role=api: + -UDP/*: + success: true +udp-support:app=bookstore: + udp-support:app=bookstore,role=api: + UDP/*: + success: true +udp-support:illuminatio-inverted-app=bookstore: + udp-support:app=bookstore,role=api: + -UDP/*: + success: true diff --git a/e2e-manifests/udp-support.yml b/e2e-manifests/udp-support.yml new file mode 100644 index 0000000..9e7cc4a --- /dev/null +++ b/e2e-manifests/udp-support.yml @@ -0,0 +1,79 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: udp-support + labels: + illuminatio-e2e: udp-support +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: bookstore + role: api + illuminatio-e2e: udp-support + name: apiserver + namespace: udp-support +spec: + selector: + matchLabels: + app: bookstore + role: api + template: + metadata: + labels: + app: bookstore + role: api + spec: + containers: + - image: busybox + command: + - "nc" + args: + - "-l" + - "-u" + - "0.0.0.0" + - "80" + name: apiserver + ports: + - containerPort: 80 + protocol: UDP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: bookstore + role: api + illuminatio-e2e: udp-support + name: apiserver + namespace: udp-support +spec: + ports: + - port: 80 + protocol: UDP + targetPort: 80 + selector: + app: bookstore + role: api + type: ClusterIP +--- +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: api-allow + labels: + illuminatio-e2e: udp-support + namespace: udp-support +spec: + podSelector: + matchLabels: + app: bookstore + role: api + ingress: + - from: + - podSelector: + matchLabels: + app: bookstore + ports: + - protocol: UDP diff --git a/src/illuminatio/host.py b/src/illuminatio/host.py index b18179f..22f088b 100644 --- a/src/illuminatio/host.py +++ b/src/illuminatio/host.py @@ -186,12 +186,13 @@ def __init__(self, namespace_labels, pod_labels): self.pod_labels = pod_labels def __eq__(self, other): - if isinstance(other, GenericClusterHost): - return ( - self.namespace_labels == other.namespace_labels - and self.pod_labels == other.pod_labels - ) - return False + if not isinstance(other, GenericClusterHost): + return False + + return ( + self.namespace_labels == other.namespace_labels + and self.pod_labels == other.pod_labels + ) def __hash__(self): return hash(str(self)) diff --git a/src/illuminatio/illuminatio.py b/src/illuminatio/illuminatio.py index f014c02..8539a6b 100644 --- a/src/illuminatio/illuminatio.py +++ b/src/illuminatio/illuminatio.py @@ -103,8 +103,8 @@ def generate(outfile: str): @click.option( "-t", "--target-image", - default="nginx:stable", - help="Target image that is used to generate pods (should have a webserver inside listening on port 80)", + default="busybox", + help="Target image that is used to generate pods (should be a busybox or something with nc)", ) @click.option( "-c", @@ -197,8 +197,7 @@ def execute_tests(cases, orch, cri_socket): """ orch.test_cases = cases core_api = k8s.client.CoreV1Api() - # namespace should be an argument ! - # -> illuminatio + # TODO: namespace should be an argument ! namespace_name = "illuminatio" if not orch.namespace_exists(namespace_name, core_api): diff --git a/src/illuminatio/illuminatio_runner.py b/src/illuminatio/illuminatio_runner.py index 652ede9..3383a57 100644 --- a/src/illuminatio/illuminatio_runner.py +++ b/src/illuminatio/illuminatio_runner.py @@ -165,7 +165,22 @@ def run_tests_for_target(network_ns, ports, target): # https://stackoverflow.com/questions/2805231/how-can-i-do-dns-lookups-in-python-including-referring-to-etc-hosts LOGGER.info("Target: %s", target) port_on_nums = {port.replace("-", ""): port for port in ports} - port_string = ",".join(port_on_nums.keys()) + # TODO extra method + tests + tcp_ports = list() + udp_ports = list() + + for port in port_on_nums.keys(): + port_num = port.split("/")[1] + # starts_with ! + if port.startswith("UDP"): + udp_ports.append(port_num) + elif port.startswith("TCP"): + tcp_ports.append(port_num) + else: + LOGGER.error(f"Unsupported protocol: {port}") + + tcp_port_string = ",".join(tcp_ports) + udp_port_string = ",".join(udp_ports) ipv6_arg = "" if ipaddress.ip_address(target).version == 6: @@ -173,7 +188,12 @@ def run_tests_for_target(network_ns, ports, target): nm_scanner = nmap.PortScanner() with Namespace(network_ns, "net"): - nm_scanner.scan(target, arguments=f"-n -Pn -p {port_string} {ipv6_arg}") + if len(tcp_port_string) > 0: + nm_scanner.scan(target, arguments=f"-n -Pn -p {tcp_port_string} {ipv6_arg}") + if len(udp_port_string) > 0: + nm_scanner.scan( + target, arguments=f"-n -Pn -sU -p {udp_port_string} {ipv6_arg}" + ) LOGGER.info("Ran nmap with cmd %s", nm_scanner.command_line()) return extract_results_from_nmap(nm_scanner, port_on_nums, target) @@ -183,6 +203,7 @@ def extract_results_from_nmap(nmap_res, port_on_nums, target): """ Extracts the results of an nmap scan into a dictionary """ + hosts = nmap_res.all_hosts() if len(hosts) != 1: port_string = ",".join(port_on_nums.keys()) @@ -204,13 +225,20 @@ def extract_results_from_nmap(nmap_res, port_on_nums, target): state = nmap_res[host].tcp(port)["state"] else: state = nmap_res[host][proto][port]["state"] - port_with_expectation = port_on_nums[str(port)] - should_be_blocked = "-" in port_with_expectation - was_blocked = state == "filtered" + + port_with_expectation = port_on_nums[f"{proto.upper()}/{port}"] + should_be_blocked = port_with_expectation.startswith("-") + was_blocked = False + if "filtered" in state: + # For UDP we can not say if the port is really blocked + # or the packet never arrvied so we get + # 'nmap-state': 'open|filtered' + was_blocked = True + results[port_with_expectation] = { "success": should_be_blocked == was_blocked, "string": build_result_string( - port, target, should_be_blocked, was_blocked + f"{proto.upper()}/{port}", target, should_be_blocked, was_blocked ), "nmap-state": state, } @@ -218,20 +246,6 @@ def extract_results_from_nmap(nmap_res, port_on_nums, target): return results -def get_domain_name_for(host_string): - """ - Replaces namespace:serviceName syntax with serviceName.namespace one, - appending default as namespace if None exists - """ - return ".".join( - reversed( - ("%s%s" % (("" if ":" in host_string else "default:"), host_string)).split( - ":" - ) - ) - ) - - def get_docker_network_namespace(pod_namespace, pod_name): """ Fetches and retrieves the network namespace information @@ -313,7 +327,7 @@ def get_cri_network_namespace(host_namespace, host_name): ) LOGGER.error(prc1.stderr) pod_id = prc1.stdout.strip() - # ToDo error handling + # TODO: error handling cmd2 = ["crictl", "inspectp", pod_id] prc2 = subprocess.run(cmd2, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if prc2.returncode: diff --git a/src/illuminatio/k8s_util.py b/src/illuminatio/k8s_util.py index 3325000..6111274 100644 --- a/src/illuminatio/k8s_util.py +++ b/src/illuminatio/k8s_util.py @@ -89,7 +89,7 @@ def create_pod_manifest(host: Host, additional_labels, generate_name, container) def create_service_manifest( - host: Host, additional_selector_labels, svc_labels, port_nums + host: Host, additional_selector_labels, svc_labels, target_ports ): """ Creates and returns a service manifest with given parameters @@ -104,11 +104,14 @@ def create_service_manifest( svc.spec.selector[key] = value svc.metadata.labels = svc_labels validate_cleanup_in(svc.metadata.labels) - # TODO: support for other protocols missing, target port might not work like that for multiple ports - ports = [ - k8s.client.V1ServicePort(protocol="TCP", port=portNum, target_port=80) - for portNum in port_nums - ] + ports = [] + for target_port in target_ports: + protocol, port = target_port.split("/") + # Replace '/' with '-' to be DNS-1123 conformant + # and convert to lower case + name = target_port.replace("/", "-").lower() + ports.append(k8s.client.V1ServicePort(name=name, protocol=protocol, port=int(port), target_port=80)) + svc.spec.ports = ports return svc diff --git a/src/illuminatio/rule.py b/src/illuminatio/rule.py index 440ce28..4344f04 100644 --- a/src/illuminatio/rule.py +++ b/src/illuminatio/rule.py @@ -86,6 +86,8 @@ def from_network_policy(cls, net_pol: k8s.client.V1NetworkPolicy): """ Returns a class containing the concerns and rules of a given NetworkPolicy """ + + #TODO must be fixed here concerns = {NAMESPACE: net_pol.metadata.namespace} if net_pol.spec.pod_selector.match_labels is not None: concerns[POD_SELECTOR_LABELS] = net_pol.spec.pod_selector.match_labels @@ -105,13 +107,34 @@ def from_network_policy(cls, net_pol: k8s.client.V1NetworkPolicy): return cls(concerns, allowed) +def _generate_port_list(ports: list()) -> list(): + if ports is not None: + port_list = list() + for port in ports: + protocol = "TCP" + target_port = MATCH_ALL_WILDCARD + if port.protocol is not None: + protocol = port.protocol + + if port.port is not None: + target_port = port.port + + port_list.append(f"{protocol}/{target_port}") + else: + port_list = [f"TCP/{MATCH_ALL_WILDCARD}", f"UDP/{MATCH_ALL_WILDCARD}"] + + return port_list + + # helper function def build_connections(verb, target, ports): """ Helper function to build Connection tuples """ out = [] - port_list = [p.port for p in ports] if (ports is not None) else [MATCH_ALL_WILDCARD] + + # TODO fix here + port_list = _generate_port_list(ports) if target is not None: for item in target: if item.ip_block is not None: diff --git a/src/illuminatio/test_case.py b/src/illuminatio/test_case.py index 7bde8d0..192505e 100644 --- a/src/illuminatio/test_case.py +++ b/src/illuminatio/test_case.py @@ -20,14 +20,18 @@ def __init__(self, from_host: Host, to_host: Host, on_port, should_connect): raise ValueError("shouldConnect may not be None") self.from_host = from_host self.to_host = to_host - # on_port can be None, which matches all ports - self._on_port = on_port if on_port else "*" + # on_port can be None, which matches all ports on the default protocol + self._on_port = on_port if on_port else "TCP/*" self._should_connect = should_connect - self.port_string = "%s%s" % ( - ("" if self._should_connect else "-"), - str(self._on_port), - ) - + prefix = "" + if not self._should_connect: + prefix = "-" + self.port_string = f"{prefix}{self._on_port}" + +# TODO adjust compare? +# At index 0 diff: +# NetworkTestCase(from=GenericClusterHost(namespaceLabels={}, podLabels={}), to=ClusterHost(namespace=default, podLabels={}), port=-31203) != +# NetworkTestCase(from=GenericClusterHost(namespaceLabels={}, podLabels={}), to=ClusterHost(namespace=default, podLabels={}), port=TCP/*) def __eq__(self, other): if isinstance(other, NetworkTestCase): return ( diff --git a/src/illuminatio/test_generator.py b/src/illuminatio/test_generator.py index de8e064..7281e1e 100644 --- a/src/illuminatio/test_generator.py +++ b/src/illuminatio/test_generator.py @@ -84,7 +84,7 @@ def generate_test_cases( ) if rule_host not in isolated_hosts: isolated_hosts.append(rule_host) - if rule.allowed: # means it is NOT default deny rule + if rule.allowed: # means it is NOT default deny rule for connection in rule.allowed: for port in connection.ports: on_port = port @@ -174,7 +174,7 @@ def generate_negative_cases_for_incoming_cases( match_all_host = GenericClusterHost({}, {}) if match_all_host in allowed_hosts: # All hosts are allowed to reach (on some ports or all) => results from ALLOW all - if "*" in ports_per_host[match_all_host]: + if 'TCP/*' in ports_per_host[match_all_host] or 'UDP/*' in ports_per_host[match_all_host]: self.logger.info( "Not generating negative tests for host %s" "as all connections to it are allowed", @@ -193,6 +193,7 @@ def generate_negative_cases_for_incoming_cases( time.time() - reaching_host_find_time ) else: + self.logger.error("not match_all_host") inverted_hosts = set( [ h @@ -246,13 +247,16 @@ def generate_negative_cases_for_incoming_cases( ) ) else: - cases.append(NetworkTestCase(target, host, "*", False)) + cases.append(NetworkTestCase(target, host, "TCP/*", False)) + cases.append(NetworkTestCase(target, host, "UDP/*", False)) runtimes[host_string]["casesGen"] = time.time() - overlap_calc_time else: # No hosts are allowed to reach host -> it should be totally isolated # => results from default deny policy - cases.append(NetworkTestCase(host, host, "*", False)) + cases.append(NetworkTestCase(host, host, "TCP/*", False)) + cases.append(NetworkTestCase(host, host, "UDP/*", False)) runtimes["all"] = time.time() - start_time + return cases, runtimes def get_overlapping_hosts( diff --git a/src/illuminatio/test_orchestrator.py b/src/illuminatio/test_orchestrator.py index 8538436..bebda50 100644 --- a/src/illuminatio/test_orchestrator.py +++ b/src/illuminatio/test_orchestrator.py @@ -7,6 +7,7 @@ from pkgutil import get_data import yaml import logging +import sys import kubernetes as k8s from illuminatio.host import ClusterHost, GenericClusterHost, Host @@ -99,7 +100,7 @@ def template_manifest(self, manifest_file, **kwargs): return yaml.safe_load(data_str.format(**kwargs)) except yaml.YAMLError as exc: self.logger.error(exc) - exit(1) + sys.exit(1) def refresh_cluster_resources(self, api: k8s.client.CoreV1Api): """ @@ -169,16 +170,29 @@ def create_namespace(self, name, api: k8s.client.CoreV1Api, labels=None): return resp except k8s.client.rest.ApiException as api_exception: self.logger.error(api_exception) - exit(1) + sys.exit(1) def _rewrite_ports_for_host(self, port_list, services_for_host): self.logger.debug("Rewriting portList %s", port_list) + if not services_for_host: # assign random port, a service with matching port will be created - return { - p: "%s%s" % (("-" if "-" in p else ""), str(rand_port())) - for p in port_list - } + # TODO move to extra method + random_ports = dict() + + for p in port_list: + prefix = "" + if p.startswith("-"): + prefix = "-" + + protocol = 'TCP' + if "UDP" in p: + protocol = 'UDP' + + random_ports[p] = f"{prefix}{protocol}/{rand_port()}" + + return random_ports + rewritten_ports = {} wild_card_ports = {p for p in port_list if "*" in p} numbered_ports = {p for p in port_list if "*" not in p} @@ -187,10 +201,9 @@ def _rewrite_ports_for_host(self, port_list, services_for_host): for wildcard_port in wild_card_ports: prefix = "-" if "-" in wildcard_port else "" # choose any port for wildcard - rewritten_ports[wildcard_port] = "%s%s" % ( - prefix, - str(service_ports[0].port), - ) + rewritten_ports[ + wildcard_port + ] = f"{prefix}{service_ports[0].protocol}/{service_ports[0].port}" for port in numbered_ports: prefix = "-" if "-" in port else "" port_int = int(port.replace("-", "")) @@ -201,13 +214,13 @@ def _rewrite_ports_for_host(self, port_list, services_for_host): # resulting in test to 53 being written despite no service matching them existing. # That error should be handled in test generation, an exception here would be fine if service_ports_for_port: - rewritten_ports[port] = "%s%s" % ( - prefix, - str(service_ports_for_port[0].port), - ) + rewritten_ports[ + port + ] = f"{prefix}{service_ports_for_port[0].protocol}/{service_ports_for_port[0].port}" else: # TODO change to exception, handle it higher up rewritten_ports[port] = "err" + return rewritten_ports def _get_target_names_creating_them_if_missing( @@ -244,9 +257,14 @@ def _get_target_names_creating_them_if_missing( self.logger.debug("Rewritten ports: %s", rewritten_ports) port_dict_per_host[host_string] = rewritten_ports if not services_for_host: + # TODO if udp we need to also provide: "-u" + # TODO listen to both! gen_name = "%s-test-target-pod-" % PROJECT_PREFIX target_container = k8s.client.V1Container( - image=self.oci_images["target"], name="runner" + image=self.oci_images["target"], + name="runner", + command=["sh"], + args=["-c", "nc -l -k -v 0.0.0.0 80 & nc -l -k -v -u 0.0.0.0 80"], ) pod_labels_tuple = (ROLE_LABEL, "test_target_pod") target_pod = create_pod_manifest( @@ -259,7 +277,7 @@ def _get_target_names_creating_them_if_missing( container=target_container, ) target_ports = [ - int(port.replace("-", "")) + port.replace("-", "") for port in port_dict_per_host[host_string].values() ] svc = create_service_manifest( @@ -268,6 +286,8 @@ def _get_target_names_creating_them_if_missing( {ROLE_LABEL: "test_target_svc", CLEANUP_LABEL: CLEANUP_ALWAYS}, target_ports, ) + self.logger.error("create_service_manifest") + self.logger.error(svc) target_pod_namespace = host.namespace resp = api.create_namespaced_pod( namespace=target_pod_namespace, body=target_pod @@ -301,6 +321,7 @@ def _find_or_create_cluster_resources_for_cases( from_host_mappings = {} to_host_mappings = {} port_mappings = {} + for from_host_string, target_dict in cases_dict.items(): from_host = Host.from_identifier(from_host_string) self.logger.debug("Searching pod for host %s", from_host) @@ -325,9 +346,12 @@ def _find_or_create_cluster_resources_for_cases( ROLE_LABEL: "from_host_dummy", CLEANUP_LABEL: CLEANUP_ALWAYS, } - # TODO replace 'dummy' with a more suitable name to prevent potential conflicts + container = k8s.client.V1Container( - image=self.oci_images["target"], name="dummy" + image=self.oci_images["target"], + name="runner", + command=["sh"], + args=["-c", "nc -l -k -v 0.0.0.0 80 & nc -l -k -v -u 0.0.0.0 80"], ) dummy = create_pod_manifest( from_host, additional_labels, f"{PROJECT_PREFIX}-dummy-", container @@ -335,7 +359,7 @@ def _find_or_create_cluster_resources_for_cases( resp = api.create_namespaced_pod(dummy.metadata.namespace, dummy) if isinstance(resp, k8s.client.V1Pod): self.logger.debug( - "Dummy pod %s created succesfully", resp.metadata.name + "Dummy pod %s created successfully", resp.metadata.name ) pods_for_host = [resp] self._current_pods.append(resp) @@ -421,6 +445,7 @@ def ensure_cases_are_generated(self, core_api: k8s.client.CoreV1Api): port_mappings, ) = self._find_or_create_cluster_resources_for_cases(cases_dict, core_api) self.logger.debug("concreteCases: %s", concrete_cases) + config_map_name = f"{PROJECT_PREFIX}-cases-cfgmap" self._create_or_update_case_config_map( config_map_name, concrete_cases, core_api diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 04860c5..7865bb9 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -50,6 +50,7 @@ def clean_cluster(core_v1): "01-deny-all-traffic-to-an-application", "labels-with-all-legal-characters", "max-length-labels", + "udp-support", ], ) @pytest.mark.e2e diff --git a/tests/test_illuminatio_runner.py b/tests/test_illuminatio_runner.py index ce51879..355403c 100644 --- a/tests/test_illuminatio_runner.py +++ b/tests/test_illuminatio_runner.py @@ -12,39 +12,39 @@ [ ( { - "port": "80", + "port": "TCP/80", "target": "test", "should_be_blocked": False, "was_blocked": False, }, - "Test test:80 succeeded\nCould reach test on port 80. Expected target to be reachable", + "Test test:TCP/80 succeeded\nCould reach test on port TCP/80. Expected target to be reachable", ), ( { - "port": "80", + "port": "TCP/80", "target": "test", "should_be_blocked": False, "was_blocked": True, }, - "Test test:80 failed\nCouldn't reach test on port 80. Expected target to be reachable", + "Test test:TCP/80 failed\nCouldn't reach test on port TCP/80. Expected target to be reachable", ), ( { - "port": "80", + "port": "TCP/80", "target": "test", "should_be_blocked": True, "was_blocked": False, }, - "Test test:-80 failed\nCould reach test on port 80. Expected target to not be reachable", + "Test test:-TCP/80 failed\nCould reach test on port TCP/80. Expected target to not be reachable", ), ( { - "port": "80", + "port": "TCP/80", "target": "test", "should_be_blocked": True, "was_blocked": True, }, - "Test test:-80 succeeded\nCouldn't reach test on port 80. Expected target to not be reachable", + "Test test:-TCP/80 succeeded\nCouldn't reach test on port TCP/80. Expected target to not be reachable", ), ], ) @@ -81,14 +81,14 @@ def create_nmap_mock(hosts: list()): ( { "hosts": ["123.321.123.321"], - "port_on_nums": {"80": "80"}, + "port_on_nums": {"TCP/80": "TCP/80"}, "target": "test", }, { - "80": { + "TCP/80": { "nmap-state": "open", - "string": "Test test:80 succeeded\n" - "Could reach test on port 80. Expected target to be " + "string": "Test test:TCP/80 succeeded\n" + "Could reach test on port TCP/80. Expected target to be " "reachable", "success": True, } @@ -97,26 +97,26 @@ def create_nmap_mock(hosts: list()): ( { "hosts": ["123.321.123.321"], - "port_on_nums": {"80": "-80"}, + "port_on_nums": {"TCP/80": "-TCP/80"}, "target": "test", }, { - "-80": { + "-TCP/80": { "nmap-state": "open", - "string": "Test test:-80 failed\n" - "Could reach test on port 80. Expected target to not be " + "string": "Test test:-TCP/80 failed\n" + "Could reach test on port TCP/80. Expected target to not be " "reachable", "success": False, } }, ), ( - {"hosts": ["::1"], "port_on_nums": {"80": "-80"}, "target": "test"}, + {"hosts": ["::1"], "port_on_nums": {"TCP/80": "-TCP/80"}, "target": "test"}, { - "-80": { + "-TCP/80": { "nmap-state": "open", - "string": "Test test:-80 failed\n" - "Could reach test on port 80. Expected target to not be " + "string": "Test test:-TCP/80 failed\n" + "Could reach test on port TCP/80. Expected target to not be " "reachable", "success": False, } diff --git a/tests/test_test_case.py b/tests/test_test_case.py index be4d3ef..b643462 100644 --- a/tests/test_test_case.py +++ b/tests/test_test_case.py @@ -7,73 +7,72 @@ def test_portString_shouldConnectTrue_outputsPortOnly(): - port = 80 + port = "TCP/80" test_case = NetworkTestCase(LocalHost(), LocalHost(), port, True) - assert test_case.port_string == str(port) + assert test_case.port_string == port def test_portString_shouldConnectFalse_outputsPortWithMinusPrefix(): - port = 80 + port = "TCP/80" test_case = NetworkTestCase(LocalHost(), LocalHost(), port, False) - assert test_case.port_string == "-" + str(port) + assert test_case.port_string == f"-{port}" # Below equality tests def test_NetworkTestCase_eq_differentFromHost_returnsFalse(): - port = 80 + port = "TCP/80" case1 = NetworkTestCase(LocalHost(), LocalHost(), port, True) case2 = NetworkTestCase(test_host1, LocalHost(), port, True) assert case1 != case2 def test_NetworkTestCase_eq_differentToHost_returnsFalse(): - port = 80 + port = "TCP/80" case1 = NetworkTestCase(LocalHost(), LocalHost(), port, True) case2 = NetworkTestCase(LocalHost(), test_host1, port, True) assert case1 != case2 def test_NetworkTestCase_eq_differentPort_returnsFalse(): - port = 80 - case1 = NetworkTestCase(LocalHost(), LocalHost(), port, True) - case2 = NetworkTestCase(LocalHost(), LocalHost(), port + 1, True) + case1 = NetworkTestCase(LocalHost(), LocalHost(), "TCP/80", True) + case2 = NetworkTestCase(LocalHost(), LocalHost(), "TCP/81", True) assert case1 != case2 def test_NetworkTestCase_eq_differentShouldConnect_returnsFalse(): - port = 80 + port = "TCP/80" case1 = NetworkTestCase(LocalHost(), LocalHost(), port, True) case2 = NetworkTestCase(LocalHost(), LocalHost(), port, False) assert case1 != case2 def test_NetworkTestCase_eq_sameFieldsLocalHostsOnly_returnsTrue(): - port = 80 + port = "TCP/80" case1 = NetworkTestCase(LocalHost(), LocalHost(), port, True) case2 = NetworkTestCase(LocalHost(), LocalHost(), port, True) assert case1 == case2 def test_NetworkTestCase_eq_sameFieldsVariousHosts_returnsTrue(): - port = 80 - test_host1 = ClusterHost("a", {"a": "b"}) + port = "TCP/80" + cur_test_host1 = ClusterHost("a", {"a": "b"}) test_host1_copy = ClusterHost("a", {"a": "b"}) - test_host2 = ExternalHost("192.168.0.1") + cur_test_host2 = ExternalHost("192.168.0.1") test_host2_copy = ExternalHost("192.168.0.1") - case1 = NetworkTestCase(test_host1, test_host2, port, True) + case1 = NetworkTestCase(cur_test_host1, cur_test_host2, port, True) case2 = NetworkTestCase(test_host1_copy, test_host2_copy, port, True) assert case1 == case2 def test_NetworkTestCase_in_sameFieldsVariousHosts_returnsTrue(): - port = 80 - test_host1 = ClusterHost("a", {"a": "b"}) + port = "TCP/80" + cur_test_host1 = ClusterHost("a", {"a": "b"}) test_host1_copy = ClusterHost("a", {"a": "b"}) - test_host2 = ExternalHost("192.168.0.1") + cur_test_host2 = ExternalHost("192.168.0.1") test_host2_copy = ExternalHost("192.168.0.1") - case_list = [NetworkTestCase(test_host1, test_host2, port, True)] + case_list = [NetworkTestCase(cur_test_host1, cur_test_host2, port, True)] copy_case = NetworkTestCase(test_host1_copy, test_host2_copy, port, True) assert copy_case in case_list @@ -122,7 +121,7 @@ def test_ClusterHost_matches_podGivenAndFromHostIsClusterHostsWithSubsetLabelsOf def test_TestCase_matches_podGivenAndHostsAreNotClusterHosts_returnsFalse(): - case = NetworkTestCase(LocalHost(), LocalHost(), 80, False) + case = NetworkTestCase(LocalHost(), LocalHost(), "TCP/80", False) meta = k8s.client.V1ObjectMeta( namespace=test_host1.namespace, labels=test_host1.pod_labels ) @@ -131,7 +130,7 @@ def test_TestCase_matches_podGivenAndHostsAreNotClusterHosts_returnsFalse(): def test_TestCase_matches_podGivenAndOnlyFromHostMatches_returnsFalse(): - case = NetworkTestCase(test_host1, LocalHost(), 80, False) + case = NetworkTestCase(test_host1, LocalHost(), "TCP/80", False) meta = k8s.client.V1ObjectMeta( namespace=test_host1.namespace, labels=test_host1.pod_labels ) @@ -140,7 +139,7 @@ def test_TestCase_matches_podGivenAndOnlyFromHostMatches_returnsFalse(): def test_TestCase_matches_podGivenAndOnlyToHostMatches_returnsFalse(): - case = NetworkTestCase(LocalHost(), test_host1, 80, False) + case = NetworkTestCase(LocalHost(), test_host1, "TCP/80", False) meta = k8s.client.V1ObjectMeta( namespace=test_host1.namespace, labels=test_host1.pod_labels ) @@ -149,7 +148,7 @@ def test_TestCase_matches_podGivenAndOnlyToHostMatches_returnsFalse(): def test_TestCase_matches_podGivenAndBothHoststMatch_returnsTrue(): - case = NetworkTestCase(test_host2, test_host1, 80, False) + case = NetworkTestCase(test_host2, test_host1, "TCP/80", False) meta = k8s.client.V1ObjectMeta( namespace=test_host1.namespace, labels=test_host2.pod_labels ) @@ -158,7 +157,7 @@ def test_TestCase_matches_podGivenAndBothHoststMatch_returnsTrue(): def test_TestCase_matches_podGivenLabelsNone_returnsFalse(): - case = NetworkTestCase(test_host2, test_host1, 80, False) + case = NetworkTestCase(test_host2, test_host1, "TCP/80", False) meta = k8s.client.V1ObjectMeta(namespace=test_host1.namespace) non_matching_pod = k8s.client.V1Pod(metadata=meta) assert case.matches([non_matching_pod]) is False @@ -188,38 +187,38 @@ def test_mergeInDict_emptyList_returnsEmptyDict(): def test_mergeInDict_oneCase_returnsDict(): - case = NetworkTestCase(test_host1, test_host2, 80, False) - expected = {test_host1.to_identifier(): {test_host2.to_identifier(): ["-80"]}} + case = NetworkTestCase(test_host1, test_host2, "TCP/80", False) + expected = {test_host1.to_identifier(): {test_host2.to_identifier(): ["-TCP/80"]}} assert merge_in_dict([case]) == expected def test_mergeInDict_twoCasesNoConflicts_returnsDict(): - case1 = NetworkTestCase(test_host1, test_host2, 80, False) - case2 = NetworkTestCase(test_host2, test_host1, 80, False) + case1 = NetworkTestCase(test_host1, test_host2, "TCP/80", False) + case2 = NetworkTestCase(test_host2, test_host1, "TCP/80", False) expected = { - test_host1.to_identifier(): {test_host2.to_identifier(): ["-80"]}, - test_host2.to_identifier(): {test_host1.to_identifier(): ["-80"]}, + test_host1.to_identifier(): {test_host2.to_identifier(): ["-TCP/80"]}, + test_host2.to_identifier(): {test_host1.to_identifier(): ["-TCP/80"]}, } assert merge_in_dict([case1, case2]) == expected def test_mergeInDict_twoCasesSameFromHostDifferentToHost_returnsDict(): - case1 = NetworkTestCase(test_host1, test_host2, 80, False) - case2 = NetworkTestCase(test_host1, test_host1, 80, False) + case1 = NetworkTestCase(test_host1, test_host2, "TCP/80", False) + case2 = NetworkTestCase(test_host1, test_host1, "TCP/80", False) expected = { test_host1.to_identifier(): { - test_host2.to_identifier(): ["-80"], - test_host1.to_identifier(): ["-80"], + test_host2.to_identifier(): ["-TCP/80"], + test_host1.to_identifier(): ["-TCP/80"], } } assert merge_in_dict([case1, case2]) == expected def test_mergeInDict_twoCasesSameFromHostSameToHost_returnsDict(): - case1 = NetworkTestCase(test_host1, test_host2, 80, False) - case2 = NetworkTestCase(test_host1, test_host2, 8080, True) + case1 = NetworkTestCase(test_host1, test_host2, "TCP/80", False) + case2 = NetworkTestCase(test_host1, test_host2, "TCP/8080", True) expected = { - test_host1.to_identifier(): {test_host2.to_identifier(): ["-80", "8080"]} + test_host1.to_identifier(): {test_host2.to_identifier(): ["-TCP/80", "TCP/8080"]} } assert merge_in_dict([case1, case2]) == expected @@ -229,15 +228,15 @@ def test_mergeInDict_twoCasesSameFromHostSameToHost_returnsDict(): def test_toYaml_oneTestCase_returnsExpectedYaml(): testHost = ClusterHost("namespc", {"label": "val"}) - case = NetworkTestCase(LocalHost(), testHost, 80, False) - expected = "localhost:\n namespc:label=val:\n - '-80'\n" + case = NetworkTestCase(LocalHost(), testHost, "TCP/80", False) + expected = "localhost:\n namespc:label=val:\n - -TCP/80\n" assert to_yaml([case]) == expected def test_fromYaml_simpleSampleYaml_returnsExpectedCase(): - testYaml = "localhost:\n namespc:label=val: ['-80']\n" + testYaml = "localhost:\n namespc:label=val: ['-TCP/80']\n" expectedHost = ClusterHost("namespc", {"label": "val"}) - expected = NetworkTestCase(LocalHost(), expectedHost, 80, False) + expected = NetworkTestCase(LocalHost(), expectedHost, "TCP/80", False) actual = from_yaml(testYaml) assert len(actual) == 1 assert actual[0] == expected diff --git a/tests/test_test_generator.py b/tests/test_test_generator.py index 950004c..9a0530d 100644 --- a/tests/test_test_generator.py +++ b/tests/test_test_generator.py @@ -29,7 +29,10 @@ ], [ NetworkTestCase( - GenericClusterHost({}, {}), ClusterHost("default", {}), "*", True + GenericClusterHost({}, {}), ClusterHost("default", {}), "TCP/*", True + ), + NetworkTestCase( + GenericClusterHost({}, {}), ClusterHost("default", {}), "UDP/*", True ) ], id="Allow all traffic in namespace", @@ -49,7 +52,10 @@ ], [ NetworkTestCase( - ClusterHost("default", {}), ClusterHost("default", {}), "*", False + ClusterHost("default", {}), ClusterHost("default", {}), "TCP/*", False + ), + NetworkTestCase( + ClusterHost("default", {}), ClusterHost("default", {}), "UDP/*", False ) ], id="Deny all traffic in namespace", @@ -85,7 +91,7 @@ "default", {"test.io/test-123_XYZ": "test_456-123.ABC"} ), ClusterHost("default", {}), - "*", + "TCP/*", True, ), NetworkTestCase( @@ -97,7 +103,7 @@ }, ), ClusterHost("default", {}), - "*", + "TCP/*", False, ), NetworkTestCase( @@ -106,7 +112,7 @@ {"test.io/test-123_XYZ": "test_456-123.ABC"}, ), ClusterHost("default", {}), - "*", + "TCP/*", False, ), NetworkTestCase( @@ -118,7 +124,7 @@ }, ), ClusterHost("default", {}), - "*", + "TCP/*", False, ), ], @@ -145,9 +151,9 @@ ClusterHost( "default", {"test.io/test-123_XYZ": "test_456-123.ABC"}, ), - "*", + "TCP/*", True, - ) + ), ], id="Allow all Pods to communicate to labelled Pods in the same Namespace", ), @@ -186,7 +192,7 @@ ClusterHost( "default", {"test.io/test-123_XYZ": "test_456-123.ABC"} ), - "*", + "TCP/*", True, ), NetworkTestCase( @@ -200,7 +206,7 @@ ClusterHost( "default", {"test.io/test-123_XYZ": "test_456-123.ABC"} ), - "*", + "TCP/*", False, ), NetworkTestCase( @@ -211,7 +217,7 @@ ClusterHost( "default", {"test.io/test-123_XYZ": "test_456-123.ABC"} ), - "*", + "TCP/*", False, ), NetworkTestCase( @@ -225,7 +231,7 @@ ClusterHost( "default", {"test.io/test-123_XYZ": "test_456-123.ABC"} ), - "*", + "TCP/*", False, ), ], @@ -267,7 +273,7 @@ "default", {"test.io/test-123_XYZ": "test_456-123.ABC"} ), ClusterHost("default", {}), - "*", + "TCP/*", True, ), NetworkTestCase( @@ -279,7 +285,7 @@ }, ), ClusterHost("default", {}), - "*", + "TCP/*", False, ), NetworkTestCase( @@ -288,7 +294,7 @@ {"test.io/test-123_XYZ": "test_456-123.ABC"}, ), ClusterHost("default", {}), - "*", + "TCP/*", False, ), NetworkTestCase( @@ -300,7 +306,7 @@ }, ), ClusterHost("default", {}), - "*", + "TCP/*", False, ), ], @@ -308,6 +314,7 @@ ), ], ) -def test__generate_test_cases(namespaces, networkpolicies, expected_testcases): +# TODO add test case for UDP ! +def test_generate_test_cases(namespaces, networkpolicies, expected_testcases): cases, _ = gen.generate_test_cases(networkpolicies, namespaces) assert sorted(cases) == sorted(expected_testcases)