# flake8: noqa: E402 import requests_mock import time from kube_hunter.conf import Config, set_config set_config(Config()) from kube_hunter.modules.hunting.apiserver import ( AccessApiServer, AccessApiServerWithToken, ServerApiAccess, AccessApiServerActive, ) from kube_hunter.modules.hunting.apiserver import ( ListNamespaces, ListPodsAndNamespaces, ListRoles, ListClusterRoles, ) from kube_hunter.modules.hunting.apiserver import ApiServerPassiveHunterFinished from kube_hunter.modules.hunting.apiserver import CreateANamespace, DeleteANamespace from kube_hunter.modules.discovery.apiserver import ApiServer from kube_hunter.core.types import UnauthenticatedAccess, InformationDisclosure from kube_hunter.core.events import handler counter = 0 def test_ApiServerToken(): global counter counter = 0 e = ApiServer() e.host = "1.2.3.4" e.auth_token = "my-secret-token" # Test that the pod's token is passed on through the event h = AccessApiServerWithToken(e) assert h.event.auth_token == "my-secret-token" # This test doesn't generate any events time.sleep(0.01) assert counter == 0 def test_AccessApiServer(): global counter counter = 0 e = ApiServer() e.host = "mockKubernetes" e.port = 443 e.protocol = "https" with requests_mock.Mocker() as m: m.get("https://mockKubernetes:443/api", text="{}") m.get( "https://mockKubernetes:443/api/v1/namespaces", text='{"items":[{"metadata":{"name":"hello"}}]}', ) m.get( "https://mockKubernetes:443/api/v1/pods", text='{"items":[{"metadata":{"name":"podA", "namespace":"namespaceA"}}, \ {"metadata":{"name":"podB", "namespace":"namespaceB"}}]}', ) m.get( "https://mockkubernetes:443/apis/rbac.authorization.k8s.io/v1/roles", status_code=403, ) m.get( "https://mockkubernetes:443/apis/rbac.authorization.k8s.io/v1/clusterroles", text='{"items":[]}', ) m.get( "https://mockkubernetes:443/version", text='{"major": "1","minor": "13+", "gitVersion": "v1.13.6-gke.13", \ "gitCommit": "fcbc1d20b6bca1936c0317743055ac75aef608ce", \ "gitTreeState": "clean", "buildDate": "2019-06-19T20:50:07Z", \ "goVersion": "go1.11.5b4", "compiler": "gc", \ "platform": "linux/amd64"}', ) h = AccessApiServer(e) h.execute() # We should see events for Server API Access, Namespaces, Pods, and the passive hunter finished time.sleep(0.01) assert counter == 4 # Try with an auth token counter = 0 with requests_mock.Mocker() as m: # TODO check that these responses reflect what Kubernetes does m.get("https://mocktoken:443/api", text="{}") m.get( "https://mocktoken:443/api/v1/namespaces", text='{"items":[{"metadata":{"name":"hello"}}]}', ) m.get( "https://mocktoken:443/api/v1/pods", text='{"items":[{"metadata":{"name":"podA", "namespace":"namespaceA"}}, \ {"metadata":{"name":"podB", "namespace":"namespaceB"}}]}', ) m.get( "https://mocktoken:443/apis/rbac.authorization.k8s.io/v1/roles", status_code=403, ) m.get( "https://mocktoken:443/apis/rbac.authorization.k8s.io/v1/clusterroles", text='{"items":[{"metadata":{"name":"my-role"}}]}', ) e.auth_token = "so-secret" e.host = "mocktoken" h = AccessApiServerWithToken(e) h.execute() # We should see the same set of events but with the addition of Cluster Roles time.sleep(0.01) assert counter == 5 @handler.subscribe(ListNamespaces) class test_ListNamespaces: def __init__(self, event): print("ListNamespaces") assert event.evidence == ["hello"] if event.host == "mocktoken": assert event.auth_token == "so-secret" else: assert event.auth_token is None global counter counter += 1 @handler.subscribe(ListPodsAndNamespaces) class test_ListPodsAndNamespaces: def __init__(self, event): print("ListPodsAndNamespaces") assert len(event.evidence) == 2 for pod in event.evidence: if pod["name"] == "podA": assert pod["namespace"] == "namespaceA" if pod["name"] == "podB": assert pod["namespace"] == "namespaceB" if event.host == "mocktoken": assert event.auth_token == "so-secret" assert "token" in event.name assert "anon" not in event.name else: assert event.auth_token is None assert "token" not in event.name assert "anon" in event.name global counter counter += 1 # Should never see this because the API call in the test returns 403 status code @handler.subscribe(ListRoles) class test_ListRoles: def __init__(self, event): print("ListRoles") assert 0 global counter counter += 1 # Should only see this when we have a token because the API call returns an empty list of items # in the test where we have no token @handler.subscribe(ListClusterRoles) class test_ListClusterRoles: def __init__(self, event): print("ListClusterRoles") assert event.auth_token == "so-secret" global counter counter += 1 @handler.subscribe(ServerApiAccess) class test_ServerApiAccess: def __init__(self, event): print("ServerApiAccess") if event.category == UnauthenticatedAccess: assert event.auth_token is None else: assert event.category == InformationDisclosure assert event.auth_token == "so-secret" global counter counter += 1 @handler.subscribe(ApiServerPassiveHunterFinished) class test_PassiveHunterFinished: def __init__(self, event): print("PassiveHunterFinished") assert event.namespaces == ["hello"] global counter counter += 1 def test_AccessApiServerActive(): e = ApiServerPassiveHunterFinished(namespaces=["hello-namespace"]) e.host = "mockKubernetes" e.port = 443 e.protocol = "https" with requests_mock.Mocker() as m: # TODO more tests here with real responses m.post( "https://mockKubernetes:443/api/v1/namespaces", text=""" { "kind": "Namespace", "apiVersion": "v1", "metadata": { "name": "abcde", "selfLink": "/api/v1/namespaces/abcde", "uid": "4a7aa47c-39ba-11e9-ab46-08002781145e", "resourceVersion": "694180", "creationTimestamp": "2019-02-26T11:33:08Z" }, "spec": { "finalizers": [ "kubernetes" ] }, "status": { "phase": "Active" } } """, ) m.post("https://mockKubernetes:443/api/v1/clusterroles", text="{}") m.post( "https://mockkubernetes:443/apis/rbac.authorization.k8s.io/v1/clusterroles", text="{}", ) m.post( "https://mockkubernetes:443/api/v1/namespaces/hello-namespace/pods", text="{}", ) m.post( "https://mockkubernetes:443" "/apis/rbac.authorization.k8s.io/v1/namespaces/hello-namespace/roles", text="{}", ) m.delete( "https://mockKubernetes:443/api/v1/namespaces/abcde", text=""" { "kind": "Namespace", "apiVersion": "v1", "metadata": { "name": "abcde", "selfLink": "/api/v1/namespaces/abcde", "uid": "4a7aa47c-39ba-11e9-ab46-08002781145e", "resourceVersion": "694780", "creationTimestamp": "2019-02-26T11:33:08Z", "deletionTimestamp": "2019-02-26T11:40:58Z" }, "spec": { "finalizers": [ "kubernetes" ] }, "status": { "phase": "Terminating" } } """, ) h = AccessApiServerActive(e) h.execute() @handler.subscribe(CreateANamespace) class test_CreateANamespace: def __init__(self, event): assert "abcde" in event.evidence @handler.subscribe(DeleteANamespace) class test_DeleteANamespace: def __init__(self, event): assert "2019-02-26" in event.evidence