def test_ClusterHost_matches_podGivenAndFromHostIsClusterHostsWithSupersetLabelsOfPod_returnsFalse( ): test_host = ClusterHost("default", {"app": "web", "load": "high"}) meta = k8s.client.V1ObjectMeta(namespace=test_host.namespace, labels={"app": test_host.pod_labels["app"]}) non_matching_pod = k8s.client.V1Pod(metadata=meta) assert test_host.matches(non_matching_pod) is False
def test_NetworkTestCase_in_sameFieldsVariousHosts_returnsTrue(): port = 80 test_host1 = ClusterHost("a", {"a": "b"}) test_host1_copy = ClusterHost("a", {"a": "b"}) test_host2 = ExternalHost("192.168.0.1") test_host2_copy = ExternalHost("192.168.0.1") case_list = [NetworkTestCase(test_host1, test_host2, port, True)] copy_case = NetworkTestCase(test_host1_copy, test_host2_copy, port, True) assert copy_case in case_list
def test_NetworkTestCase_eq_sameFieldsVariousHosts_returnsTrue(): port = 80 test_host1 = ClusterHost("a", {"a": "b"}) test_host1_copy = ClusterHost("a", {"a": "b"}) test_host2 = ExternalHost("192.168.0.1") test_host2_copy = ExternalHost("192.168.0.1") case1 = NetworkTestCase(test_host1, test_host2, port, True) case2 = NetworkTestCase(test_host1_copy, test_host2_copy, port, True) assert case1 == case2
def test__generate_test_cases__deny_all__returns_single_negative_case(): namespaces = [_generate_namespace("default")] networkpolicies = [_generate_deny_all_network_policy("default")] expected = [ NetworkTestCase( ClusterHost("default", {}), ClusterHost("default", {}), "*", False ) ] cases, _ = gen.generate_test_cases(networkpolicies, namespaces) assert len(cases) == 1 assert cases == expected
def invert_cluster_host(host: ClusterHost): if host.pod_labels == {}: return [ClusterHost(INVERTED_ATTRIBUTE_PREFIX + host.namespace, {})] inverted_hosts = [ ClusterHost(INVERTED_ATTRIBUTE_PREFIX + host.namespace, host.pod_labels), ClusterHost(INVERTED_ATTRIBUTE_PREFIX + host.namespace, invert_labels(host.pod_labels)), ClusterHost(host.namespace, invert_labels(host.pod_labels)) ] return inverted_hosts
def test_fromYaml_simpleSampleYaml_returnsExpectedCase(): testYaml = "localhost:\n namespc:label=val: ['-80']\n" expectedHost = ClusterHost("namespc", {"label": "val"}) expected = NetworkTestCase(LocalHost(), expectedHost, 80, False) actual = from_yaml(testYaml) assert len(actual) == 1 assert actual[0] == expected
def _get_other_host_from(connection_targets, rule_namespace): namespace_labels = "namespaceLabels" pod_labels = "podLabels" namespace = "namespace" if namespace_labels in connection_targets and pod_labels in connection_targets: return GenericClusterHost(connection_targets[namespace_labels], connection_targets[pod_labels]) if namespace in connection_targets and pod_labels in connection_targets: return ClusterHost(connection_targets[namespace], connection_targets[pod_labels]) if namespace_labels in connection_targets: # and no podLabels included return GenericClusterHost(connection_targets[namespace_labels], {}) if pod_labels in connection_targets: return ClusterHost(rule_namespace, connection_targets[pod_labels]) if connection_targets == {}: return GenericClusterHost({}, {}) raise ValueError("Unknown combination of field in connection %s" % connection_targets)
def test__generate_test_cases__allow_all__returns_single_positive_case(): namespaces = [_generate_namespace("default")] networkpolicies = [_generate_allow_all_network_policy("default")] expected = [ NetworkTestCase( GenericClusterHost({}, {}), ClusterHost("default", {}), "*", True ) ] cases, _ = gen.generate_test_cases(networkpolicies, namespaces) assert len(cases) == 1 assert cases == expected
def invert_cluster_host(host: ClusterHost): """ Returns a list of ClusterHosts with once inverted pod label selectors, once inverted namespace label selectors and once both """ if host.pod_labels == {}: return [ClusterHost("%s%s" % (INVERTED_ATTRIBUTE_PREFIX, host.namespace), {})] inverted_hosts = [ ClusterHost( "%s%s" % (INVERTED_ATTRIBUTE_PREFIX, host.namespace), host.pod_labels ), ClusterHost( "%s%s" % (INVERTED_ATTRIBUTE_PREFIX, host.namespace), invert_label_selector(host.pod_labels), ), ClusterHost(host.namespace, invert_label_selector(host.pod_labels)), ] return inverted_hosts
def generate_test_cases( self, network_policies: List[k8s.client.V1NetworkPolicy], namespaces: List[k8s.client.V1Namespace], ): """ Generates positive and negative test cases, also returns measured runtimes """ runtimes = {} start_time = time.time() isolated_hosts = [] other_hosts = [] outgoing_test_cases = [] incoming_test_cases = [] self.logger.debug("Generating test cases for %s", network_policies) rules = [Rule.from_network_policy(netPol) for netPol in network_policies] net_pol_parsing_time = time.time() runtimes["parse"] = net_pol_parsing_time - start_time self.logger.debug("Rule: %s", rules) for rule in rules: rule_host = ClusterHost( rule.concerns["namespace"], rule.concerns["podLabels"] ) if rule_host not in isolated_hosts: isolated_hosts.append(rule_host) if rule.allowed: # means it is NOT default deny rule for connection in rule.allowed: for port in connection.ports: on_port = port other_host = _get_other_host_from( connection.targets, rule.concerns["namespace"] ) other_hosts.append(other_host) if connection.direction == "to": case = NetworkTestCase(rule_host, other_host, on_port, True) outgoing_test_cases.append(case) elif connection.direction == "from": case = NetworkTestCase(other_host, rule_host, on_port, True) incoming_test_cases.append(case) else: raise ValueError( "Direction '%s' unknown!" % connection.direction )
def test__generate_test_cases__allow_some_pods__returns_negative_and_positive_case(): allowed_namespace = "default" forbiden_namespace = INVERTED_ATTRIBUTE_PREFIX + allowed_namespace allowed_labels = {"test": "test"} forbidden_labels = {INVERTED_ATTRIBUTE_PREFIX + "test": "test"} namespaces = [ _generate_namespace(allowed_namespace) ] networkpolicies = [ _generate_allow_labelled_pods_network_policy(allowed_namespace, labels=allowed_labels) ] expected = [ NetworkTestCase( ClusterHost(allowed_namespace, allowed_labels), ClusterHost(allowed_namespace, {}), "*", True ), NetworkTestCase( ClusterHost(allowed_namespace, forbidden_labels), ClusterHost(allowed_namespace, {}), "*", False ), NetworkTestCase( ClusterHost(forbiden_namespace, allowed_labels), ClusterHost(allowed_namespace, {}), "*", False ), NetworkTestCase( ClusterHost(forbiden_namespace, forbidden_labels), ClusterHost(allowed_namespace, {}), "*", False ) ] cases, _ = gen.generate_test_cases(networkpolicies, namespaces) assert len(cases) == 4 assert sorted(cases) == sorted(expected)
def _find_or_create_cluster_resources_for_cases(self, cases_dict, api: k8s.client.CoreV1Api): resolved_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) if not isinstance(from_host, (ClusterHost, GenericClusterHost)): raise ValueError( "Only ClusterHost and GenericClusterHost fromHosts are supported by this Orchestrator" ) namespaces_for_host = self._find_or_create_namespace_for_host( from_host, api) from_host = ClusterHost(namespaces_for_host[0].metadata.name, from_host.pod_labels) self.logger.debug("Updated fromHost with found namespace: %s", from_host) pods_for_host = [ pod for pod in self._current_pods if from_host.matches(pod) ] # create pod if none for fromHost is in cluster (and add it to podsForHost) if not pods_for_host: self.logger.debug("Creating dummy pod for host %s", from_host) additional_labels = { 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") dummy = create_pod_manifest(from_host, additional_labels, f"{PROJECT_PREFIX}-dummy-", container) 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) pods_for_host = [resp] self._current_pods.append(resp) else: self.logger.error("Failed to create dummy pod! Resp: %s", resp) else: self.logger.debug("Pods matching %s already exist: ", from_host, pods_for_host) # resolve target names for fromHost and add them to resolved cases dict pod_identifier = "%s:%s" % ( pods_for_host[0].metadata.namespace, pods_for_host[0].metadata.name, ) self.logger.debug("Mapped pod_identifier: %s", pod_identifier) from_host_mappings[from_host_string] = pod_identifier ( names_per_host, port_names_per_host, ) = self._get_target_names_creating_them_if_missing( target_dict, api) to_host_mappings[from_host_string] = names_per_host port_mappings[from_host_string] = port_names_per_host resolved_cases[pod_identifier] = { names_per_host[t]: [port_names_per_host[t][p] for p in target_dict[t]] for t in target_dict } return resolved_cases, from_host_mappings, to_host_mappings, port_mappings
import logging from typing import List from unittest.mock import MagicMock import kubernetes as k8s from illuminatio.host import ClusterHost from illuminatio.test_orchestrator import NetworkTestOrchestrator testHost1 = ClusterHost("default", {"app": "test"}) testHost2 = ClusterHost("default", {"app": "other"}) def createOrchestrator(cases): orch = NetworkTestOrchestrator(cases, logging.getLogger("orchestrator_test")) return orch def test__refreshClusterResourcess_emptyListApiObjectsReturned_extractsEmptyList( ): # setup an api mock that returns an empty pod list api_mock = k8s.client.CoreV1Api() empty_pod_list = k8s.client.V1PodList(items=[]) api_mock.list_pod_for_all_namespaces = MagicMock( return_value=empty_pod_list) api_mock.list_service_for_all_namespaces = MagicMock( return_value=k8s.client.V1ServiceList(items=[])) api_mock.list_namespace = MagicMock( return_value=k8s.client.V1NamespaceList(items=[])) # test that this results in an empty list orch = createOrchestrator([])
import kubernetes as k8s from illuminatio.host import ClusterHost, ExternalHost, LocalHost from illuminatio.test_case import NetworkTestCase, merge_in_dict, to_yaml, from_yaml test_host1 = ClusterHost("default", {"app": "test"}) test_host2 = ClusterHost("default", {"app": "test", "label2": "value"}) def test_portString_shouldConnectTrue_outputsPortOnly(): port = 80 test_case = NetworkTestCase(LocalHost(), LocalHost(), port, True) assert test_case.port_string == str(port) def test_portString_shouldConnectFalse_outputsPortWithMinusPrefix(): port = 80 test_case = NetworkTestCase(LocalHost(), LocalHost(), port, False) assert test_case.port_string == "-" + str(port) # Below equality tests def test_NetworkTestCase_eq_differentFromHost_returnsFalse(): port = 80 case1 = NetworkTestCase(LocalHost(), LocalHost(), port, True) case2 = NetworkTestCase(test_host1, LocalHost(), port, True) assert case1 != case2 def test_NetworkTestCase_eq_differentToHost_returnsFalse():
def _get_target_names_creating_them_if_missing(self, target_dict, api: k8s.client.CoreV1Api): service_names_per_host = {} port_dict_per_host = {} for host_string in target_dict.keys(): host = Host.from_identifier(host_string) if isinstance(host, GenericClusterHost): self.logger.debug( "Found GenericClusterHost %s," "Rewriting it to a ClusterHost in default namespace now.", host, ) host = ClusterHost("default", host.pod_labels) if not isinstance(host, ClusterHost): raise ValueError( "Only ClusterHost targets are supported by this Orchestrator." " Host: %s, hostString: %s" % (host, host_string)) self.logger.debug("Searching service for host %s", host) services_for_host = [ svc for svc in self._current_services if host.matches(svc) ] self.logger.debug( "Found services %s for host %s ", [svc.metadata for svc in services_for_host], host, ) rewritten_ports = self._rewrite_ports_for_host( target_dict[host_string], services_for_host) self.logger.debug("Rewritten ports: %s", rewritten_ports) port_dict_per_host[host_string] = rewritten_ports if not services_for_host: gen_name = "%s-test-target-pod-" % PROJECT_PREFIX target_container = k8s.client.V1Container( image=self.oci_images["target"], name="runner") pod_labels_tuple = (ROLE_LABEL, "test_target_pod") target_pod = create_pod_manifest( host=host, additional_labels={ pod_labels_tuple[0]: pod_labels_tuple[1], CLEANUP_LABEL: CLEANUP_ALWAYS, }, generate_name=gen_name, container=target_container, ) target_ports = [ int(port.replace("-", "")) for port in port_dict_per_host[host_string].values() ] svc = create_service_manifest( host, {pod_labels_tuple[0]: pod_labels_tuple[1]}, { ROLE_LABEL: "test_target_svc", CLEANUP_LABEL: CLEANUP_ALWAYS }, target_ports, ) target_pod_namespace = host.namespace resp = api.create_namespaced_pod( namespace=target_pod_namespace, body=target_pod) if isinstance(resp, k8s.client.V1Pod): self.logger.debug("Target pod %s created succesfully", resp.metadata.name) self._current_pods.append(resp) else: self.logger.error("Failed to create pod! Resp: %s", resp) resp = api.create_namespaced_service(namespace=host.namespace, body=svc) if isinstance(resp, k8s.client.V1Service): service_names_per_host[host_string] = resp.spec.cluster_ip self.logger.debug("Target svc %s created succesfully", resp.metadata.name) self._current_services.append(resp) else: self.logger.error("Failed to create target svc! Resp: %s", resp) else: service_names_per_host[host_string] = services_for_host[ 0].spec.cluster_ip return service_names_per_host, port_dict_per_host
def test_toYaml_oneTestCase_returnsExpectedYaml(): testHost = ClusterHost("namespc", {"label": "val"}) case = NetworkTestCase(LocalHost(), testHost, 80, False) expected = "localhost:\n namespc:label=val: ['-80']\n" assert to_yaml([case]) == expected
ExternalHost("123.123.123.123"), id="ExternalHost with IPv4", ), pytest.param( "fe80::1ff:fe23:4567:890a", ExternalHost("fe80::1ff:fe23:4567:890a"), id="ExternalHost with IPv6", ), pytest.param( "default:nginx-23429-asdf", ConcreteClusterHost("default", "nginx-23429-asdf"), id="Simple ConcreteClusterHost", ), pytest.param( "default:test=test", ClusterHost("default", {"test": "test"}), id="Simple ClusterHost", ), pytest.param( "illuminatio-inverted-default:illuminatio-inverted-test.io/test-123_XYZ=test_456-123.ABC", ClusterHost( "illuminatio-inverted-default", { "illuminatio-inverted-test.io/test-123_XYZ": "test_456-123.ABC" }, ), id="ClusterHost containing all allowed label characters", ), pytest.param( "test=test:test=test",
[ k8s.client.V1NetworkPolicy( metadata=k8s.client.V1ObjectMeta(name="allow-all", namespace="default"), spec=k8s.client.V1NetworkPolicySpec( pod_selector=k8s.client.V1LabelSelector( match_labels=None), ingress=[ k8s.client.V1NetworkPolicyIngressRule(_from=None) ], ), ) ], [ NetworkTestCase(GenericClusterHost({}, {}), ClusterHost("default", {}), "*", True) ], id="Allow all traffic in namespace", ), pytest.param( [ k8s.client.V1Namespace(metadata=k8s.client.V1ObjectMeta( name="default")) ], [ k8s.client.V1NetworkPolicy( metadata=k8s.client.V1ObjectMeta(name="deny-all", namespace="default"), spec=k8s.client.V1NetworkPolicySpec( pod_selector=k8s.client.V1LabelSelector( match_labels=None),
def _find_or_create_cluster_resources_for_cases(self, cases_dict, api: k8s.client.CoreV1Api): resolved_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) logger.debug("Searching pod for host " + str(from_host)) if not (isinstance(from_host, ClusterHost) or isinstance(from_host, GenericClusterHost)): raise ValueError( "Only ClusterHost and GenericClusterHost fromHosts are supported by this Orchestrator" ) namespaces_for_host = self._find_or_create_namespace_for_host( from_host, api) from_host = ClusterHost(namespaces_for_host[0].metadata.name, from_host.pod_labels) logger.debug("Updated fromHost with found namespace: " + str(from_host)) pods_for_host = [ pod for pod in self._current_pods if from_host.matches(pod) ] # create pod if none for fromHost is in cluster (and add it to podsForHost) if not pods_for_host: logger.debug("Creating dummy pod for host " + str(from_host)) additional_labels = { ROLE_LABEL: "from_host_dummy", CLEANUP_LABEL: CLEANUP_ALWAYS } container = k8s.client.V1Container(image="nginx:stable", name="dummy") dummy = init_pod(from_host, additional_labels, PROJECT_PREFIX + "-dummy-", container) resp = api.create_namespaced_pod(dummy.metadata.namespace, dummy) if isinstance(resp, k8s.client.V1Pod): logger.debug("Dummy pod " + resp.metadata.name + " created succesfully") pods_for_host = [resp] self._current_pods.append(resp) else: logger.error("Failed to create dummy pod! Resp: " + str(resp)) else: logger.debug("Pods matching " + str(from_host) + " already exist: " + str(pods_for_host)) # resolve target names for fromHost and add them to resolved cases dict pod_identifier = pods_for_host[ 0].metadata.namespace + ":" + pods_for_host[0].metadata.name logger.debug("Mapped pod_identifier: " + str(pod_identifier)) from_host_mappings[from_host_string] = pod_identifier names_per_host, port_names_per_host = self._get_target_names_creating_them_if_missing( target_dict, api) to_host_mappings[from_host_string] = names_per_host port_mappings[from_host_string] = port_names_per_host resolved_cases[pod_identifier] = { names_per_host[t]: [port_names_per_host[t][p] for p in target_dict[t]] for t in target_dict } return resolved_cases, from_host_mappings, to_host_mappings, port_mappings
def _get_target_names_creating_them_if_missing(self, target_dict, api: k8s.client.CoreV1Api): svc_names_per_host = {} port_dict_per_host = {} for host_string in target_dict.keys(): host = Host.from_identifier(host_string) if isinstance(host, GenericClusterHost): logger.debug( "Found GenericClusterHost " + str(host) + ". Rewriting it to a ClusterHost in default namespace now." ) host = ClusterHost("default", host.pod_labels) if not isinstance(host, ClusterHost): raise ValueError( "Only ClusterHost targets are supported by this Orchestrator. Host: " + str(host) + ", hostString: " + host_string) logger.debug("Searching service for host " + str(host)) services_for_host = [ svc for svc in self._current_services if host.matches(svc) ] logger.debug("Found services {} for host {} ".format( [svc.metadata for svc in services_for_host], host)) rewritten_ports = self._rewrite_ports_for_host( target_dict[host_string], services_for_host) logger.debug("Rewritten ports: " + str(rewritten_ports)) port_dict_per_host[host_string] = rewritten_ports if not services_for_host: gen_name = PROJECT_PREFIX + "-test-target-pod-" target_container = k8s.client.V1Container( image=self.target_image, name="runner") pod_labels_tuple = (ROLE_LABEL, "test_target_pod") target_pod = init_pod(host=host, additional_labels={ pod_labels_tuple[0]: pod_labels_tuple[1], CLEANUP_LABEL: CLEANUP_ALWAYS }, generate_name=gen_name, container=target_container) target_ports = [ int(port.replace("-", "")) for port in port_dict_per_host[host_string].values() ] # ToDo we should use the cluser ip instead of the DNS names # so we don't need the lookups svc_name = "svc-" + convert_to_resource_name( host.to_identifier()) svc = init_svc(host, {pod_labels_tuple[0]: pod_labels_tuple[1]}, { ROLE_LABEL: "test_target_svc", CLEANUP_LABEL: CLEANUP_ALWAYS }, svc_name, target_ports) target_pod_namespace = host.namespace svc_names_per_host[ host_string] = target_pod_namespace + ":" + svc_name resp = api.create_namespaced_pod( namespace=target_pod_namespace, body=target_pod) if isinstance(resp, k8s.client.V1Pod): logger.debug("Target pod " + resp.metadata.name + " created succesfully") self._current_pods.append(resp) else: logger.error("Failed to create pod! Resp: " + str(resp)) resp = api.create_namespaced_service(namespace=host.namespace, body=svc) if isinstance(resp, k8s.client.V1Service): logger.debug("Target svc " + resp.metadata.name + " created succesfully") self._current_services.append(resp) else: logger.error("Failed to create target svc! Resp: " + str(resp)) else: svc_names_per_host[host_string] = services_for_host[ 0].metadata.namespace + ":" + services_for_host[ 0].metadata.name return svc_names_per_host, port_dict_per_host