diff --git a/.gitignore b/.gitignore index 8c75de4..7e99e36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -.idea/ -__pycache__/ *.pyc \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index f2e146b..0000000 --- a/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Kube Hunter - -Insecure Kubernetes clusters detection tool. - -## Installation - -Run the following commands to clone and install pre-requisites: - -```bash -git clone git@bitbucket.org:scalock/kube-hunter.git -cd kube-hunter -pip install -R requirements.txt -./kube-hunter -h -``` - -## Current Features - -The following action are currently supported: - -### Hunt - -Supplied a host IP, the tool will search for open Kubernetes services, -listening to default ports. -For each service found, it will check if it is insecure and grants -capabilities. - -```bash -./kube-hunter hunt 127.0.0.1 -``` - -### Scan - -Supplied a subnet address (CIDR notation), the tool will scan for -hosts with open Kubernetes services. - -## Supported Kubernetes Services - -The tool currently supports the following services: -* Kubernetes Dashboard diff --git a/discovery.py b/discovery.py deleted file mode 100644 index cf82b82..0000000 --- a/discovery.py +++ /dev/null @@ -1,79 +0,0 @@ -from logging import debug, info -from multiprocessing import Process, Queue -from socket import socket - -from netaddr import IPNetwork - - -KUBE_PROXY_PORT = 8001 -KUBELET_PORT = 10250 -KUBELET_READONLY_PORT = 10255 -DASHBOARD_PORT = 30000 - -DEFAULT_PORTS = [ - KUBE_PROXY_PORT, - KUBELET_PORT, - KUBELET_READONLY_PORT, - DASHBOARD_PORT -] - - -def cidr_to_list(cidr): - host_list = list(IPNetwork(cidr)) - return host_list - - -def test_connection(host, ports): - result = [] - - for port in ports: - s = socket() - s.settimeout(1) - success = s.connect_ex((str(host), port)) - s.close() - if success == 0: - info("{}:{} is open".format(host, port)) - result.append("{}:{}".format(host, port)) - - return result - - -class Worker(Process): - _count = 0 - - def __init__(self, queue): - super(Worker, self).__init__() - self.queue = queue - self.name = "Worker #{}".format(Worker._count) - Worker._count += 1 - - def run(self): - for host, ports, callback in iter(self.queue.get, None): - debug("{}: Checking host {}".format(self.name, host)) - for result in test_connection(host, ports): - callback(result) - - -class HostScanner(object): - def __init__(self, threads=1): - self.threads = threads - - def scan(self, cidr, ports, callback): - queue = Queue() - workers = [] - - debug("Starting workers") - for i in range(self.threads): - workers.append(Worker(queue)) - workers[-1].start() - - for host in cidr_to_list(cidr): - queue.put((host, ports, callback)) - - for i in range(self.threads): - queue.put(None) - - debug("Waiting for workers to finish") - for worker in workers: - worker.join() - debug("Workers finished") diff --git a/discovery/__init__.py b/discovery/__init__.py new file mode 100644 index 0000000..e1e1462 --- /dev/null +++ b/discovery/__init__.py @@ -0,0 +1,7 @@ +from os.path import dirname, basename, isfile +import glob + +# dynamically importing all modules in folder +files = glob.glob(dirname(__file__)+"/*.py") +for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')): + exec('from {} import *'.format(module_name)) \ No newline at end of file diff --git a/discovery/dashboard.py b/discovery/dashboard.py new file mode 100644 index 0000000..beb4389 --- /dev/null +++ b/discovery/dashboard.py @@ -0,0 +1,17 @@ +from events import handler, OpenPortEvent, KubeDashboardEvent +import requests + +@handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 30000) +class KubeDashboard(object): + def __init__(self, event): + self.event = event + self.host = event.host + self.port = event.port + + @property + def secure(self): + # TODO: insert logic for detremining a secure/insecure dashboard is there + return False + + def execute(self): + handler.publish_event(KubeDashboardEvent()) diff --git a/discovery/hosts.py b/discovery/hosts.py new file mode 100644 index 0000000..e151b76 --- /dev/null +++ b/discovery/hosts.py @@ -0,0 +1,35 @@ +import logging +import sys +import time +from enum import Enum + +from netaddr import IPNetwork + +from events import HostScanEvent, NewHostEvent, handler +from netifaces import AF_INET, ifaddresses, interfaces + +# for comparing prefixes +class InterfaceTypes(Enum): + LOCALHOST = "127.0.0" + +@handler.subscribe(HostScanEvent) +class HostDiscovery(object): + def __init__(self, event): + self.event = event + # self.external = event.external + + def execute(self): + logging.info("Discovering Open Kubernetes Services...") + + handler.publish_event(NewHostEvent(host="acs954agent1.westus2.cloudapp.azure.com")) # test cluster + for ifaceName in interfaces(): + for ip in self.generate_addresses(ifaceName): + handler.publish_event(NewHostEvent(host=ip)) + + def generate_addresses(self, ifaceName): + for address in [i['addr'] for i in ifaddresses(ifaceName).setdefault(AF_INET, [])]: + subnet = IPNetwork('{0}/24'.format(address)) + for ip in IPNetwork(subnet): + if not self.event.localhost and InterfaceTypes.LOCALHOST.value in ip.__str__(): + continue + yield ip diff --git a/discovery/kubelet.py b/discovery/kubelet.py new file mode 100644 index 0000000..cb4e91a --- /dev/null +++ b/discovery/kubelet.py @@ -0,0 +1,35 @@ +import json +import logging +import urllib3 +from enum import Enum + +import requests + +from events import ReadOnlyKubeletEvent, SecureKubeletEvent, OpenPortEvent, handler + +class KubeletPorts(Enum): + SECURED = 10250 + READ_ONLY = 10255 + +@handler.subscribe(OpenPortEvent, predicate= lambda x: x.port == 10255 or x.port == 10250) +class KubeletDiscovery(object): + def __init__(self, event): + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + self.event = event + + @property + def read_only_access(self): + r = requests.get("http://{host}:{port}/pods".format(host=self.event.host, port=self.event.port)) + return r.status_code == 200 + + @property + def secure_access(self): + r = requests.get("https://{host}:{port}/pods".format(host=self.event.host, port=self.event.port), verify=False) + return r.status_code == 200 + + def execute(self): + logging.debug("secure port on {}".format(self.event.port)) + if self.event.port == KubeletPorts.SECURED.value and self.secure_access: + handler.publish_event(SecureKubeletEvent()) + elif self.event.port == KubeletPorts.READ_ONLY.value and self.read_only_access: + handler.publish_event(ReadOnlyKubeletEvent()) \ No newline at end of file diff --git a/discovery/ports.py b/discovery/ports.py new file mode 100644 index 0000000..ee50985 --- /dev/null +++ b/discovery/ports.py @@ -0,0 +1,26 @@ +from events import handler, NewHostEvent, OpenPortEvent +from socket import socket + +default_ports = [8001, 10250, 10255, 30000] + +@handler.subscribe(NewHostEvent) +class PortDiscovery(object): + def __init__(self, event): + self.event = event + self.host = event.host + self.port = event.port + + def execute(self): + for single_port in default_ports: + if self.test_connection(self.host, single_port): + handler.publish_event(OpenPortEvent(port=single_port)) + + @staticmethod + def test_connection(host, port): + s = socket() + s.settimeout(1.5) + success = s.connect_ex((str(host), port)) + s.close() + if success == 0: + return True + return False diff --git a/discovery/proxy.py b/discovery/proxy.py new file mode 100644 index 0000000..66fc0f8 --- /dev/null +++ b/discovery/proxy.py @@ -0,0 +1,20 @@ +from events import handler, OpenPortEvent, KubeProxyEvent +from collections import defaultdict +from requests import get +import logging + +@handler.subscribe(OpenPortEvent, predicate=lambda x: x.port == 8001) +class KubeProxy(object): + def __init__(self, event): + self.event = event + self.host = event.host + self.port = event.port or 8001 + + @property + def accesible(self): + return True + + def execute(self): + if self.accesible: + handler.publish_event(KubeProxyEvent()) + \ No newline at end of file diff --git a/events/__init__.py b/events/__init__.py new file mode 100644 index 0000000..498da7f --- /dev/null +++ b/events/__init__.py @@ -0,0 +1,11 @@ +from os.path import dirname, basename, isfile +import glob + +# explicitly importing the event handler +from handler import handler + +# dynamically importing all modules in folder +files = glob.glob(dirname(__file__)+"/*.py") +for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')): + if module_name != "handler": + exec('from {} import *'.format(module_name)) \ No newline at end of file diff --git a/events/default_types.py b/events/default_types.py new file mode 100644 index 0000000..88bdad6 --- /dev/null +++ b/events/default_types.py @@ -0,0 +1,61 @@ +import logging + +class Event(object): + def __init__(self): + self.previous = None + + # newest attribute gets selected first + def __getattr__(self, name): + if name == "previous": + return None + for event in self.history: + if name in event.__dict__: + return event.__dict__[name] + + # returns the event history ordered from newest to oldest + @property + def history(self): + previous, history = self.previous, list() + while previous: + history.append(previous) + previous = previous.previous + return history + + +""" Event Objects """ +class NewHostEvent(Event): + def __init__(self, host): + self.host = host + + def __str__(self): + return str(self.host) + +class OpenPortEvent(Event): + def __init__(self, port): + self.port = port + + def __str__(self): + return str(self.port) + +class HostScanEvent(Event): + def __init__(self, interal=True, localhost=True): + self.internal = interal + self.localhost = localhost + +class KubeDashboardEvent(Event): + def __init__(self, path="/", secure=False): + self.path = path + self.secure + pass + +class ReadOnlyKubeletEvent(Event): + def __init__(self): + pass + +class SecureKubeletEvent(Event): + def __init__(self): + pass + +class KubeProxyEvent(Event): + def __init__(self): + pass diff --git a/events/handler.py b/events/handler.py new file mode 100644 index 0000000..d9275e7 --- /dev/null +++ b/events/handler.py @@ -0,0 +1,65 @@ +import inspect +import logging +from abc import ABCMeta +from collections import defaultdict +from Queue import Queue +from threading import Lock, Thread + +# Inherits Queue object, handles events asynchronously +class EventQueue(Queue, object): + def __init__(self, num_worker=10): + super(EventQueue, self).__init__() + self.hooks = defaultdict(list) + self.running = True + + for i in range(num_worker): + t = Thread(target=self.worker) + t.daemon = True + t.start() + + # decorator wrapping for easy subscription + def subscribe(self, event, hook=None, predicate=None): + def wrapper(hook): + self.subscribe_event(event, hook=hook, predicate=predicate) + return hook + return wrapper + + # getting uninstantiated event object + def subscribe_event(self, event, hook=None, predicate=None): + logging.debug('{} subscribed to {}'.format(event.__name__, hook)) + if hook not in self.hooks[event.__name__]: + self.hooks[event.__name__].append((hook, predicate)) + + # getting instantiated event object + def publish_event(self, event): + logging.debug('Event {} got published with {}'.format(event.__class__.__name__, event)) + event_name = event.__class__.__name__ + if event_name in self.hooks: + for hook, predicate in self.hooks[event_name]: + if predicate and not predicate(event): + continue + + # access to stack frame, can also be implemented by changing the function call to recieve self. + # TODO: decide whether invisibility to the developer is the best approach + last_frame = inspect.stack()[1][0] + if "self" in last_frame.f_locals: + event.previous = last_frame.f_locals["self"].event + + self.put(hook(event)) + + # executes callbacks on dedicated thread as a daemon + def worker(self): + while self.running: + hook = self.get() + hook.execute() + self.task_done() + + # stops execution of all daemons + def free(self): + self.running = False + with self.mutex: + self.queue.clear() + + +handler = EventQueue(800) + diff --git a/hunters/__init__.py b/hunters/__init__.py deleted file mode 100644 index ba7370c..0000000 --- a/hunters/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .dashboard import * -from .kubelet import * -from .proxy import * diff --git a/hunters/dashboard.py b/hunters/dashboard.py deleted file mode 100644 index ec4f509..0000000 --- a/hunters/dashboard.py +++ /dev/null @@ -1,104 +0,0 @@ -from io import BytesIO -from logging import info, warning, debug - -from PIL import Image -from requests import get -from selenium import webdriver -from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.ui import WebDriverWait - -from hunters.hunter import Hunter - - - -DASHBOARD_PATHS = [ - "", - "/ui", - "/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy" -] - -API = { - "overview": "/api/v1/overview/default?itemsPerPage=100", - "nodes": "/api/v1/node?itemsPerPage=100" -} - -XPATH = { - "login_skip": "/html/body/kd-login/form/kd-content-card/div/div/div/kd-content/button[2]" -} - - -def test_url(url): - r = get(url) - if r.status_code == 200: - return r.url - - -class Dashboard(Hunter): - def __init__(self, host): - self.host = host - if "://" not in host: - self.host_url = "http://{}".format(host) - else: - self.host_url = host - self._is_auth_required = None - self._base_path = None - - def format_url(self, path): - return self.base_path + path - - def list_nodes(self): - return [str(n["objectMeta"]["name"]) for n in get(self.format_url(API["nodes"])).json()["nodes"]] - - @property - def base_path(self): - if self._base_path: - return self._base_path - for path in DASHBOARD_PATHS: - path = test_url(self.host_url + path) - if path: - self._base_path = path - return path - raise Exception("User interface URL path was not found") - - @property - def is_auth_required(self): - if not self._is_auth_required: - overview = get(self.format_url(API["overview"])).json() - if "errors" in overview and overview["errors"]: - self._is_auth_required = any([e["ErrStatus"]["code"] == 403 for e in overview["errors"]]) - else: - self._is_auth_required = False - return self._is_auth_required - - def take_screenshot(self): - driver = webdriver.Chrome() - driver.fullscreen_window() - waiter = WebDriverWait(driver, 5) - - driver.get(self.base_path) - waiter.until(lambda d: "Overview" in d.title or "Sign" in d.title) - - skip_buttons = driver.find_elements_by_xpath(XPATH["login_skip"]) - if skip_buttons: - skip_buttons[0].click() - waiter.until(expected_conditions.title_contains("Overview")) - - result = driver.get_screenshot_as_png() - driver.quit() - - return result - - def hunt(self, *args, **kwargs): - debug("Hunting dashboard at {}".format(self.host)) - - debug("Checking authentication...") - if self.is_auth_required: - warning("Authentication is required") - return - - debug("Authentication is not required") - debug("Listing nodes on the cluster...") - debug("Nodes: {}".format(self.list_nodes())) - - debug("Taking a screenshot...") - Image.open(BytesIO(self.take_screenshot())).show() diff --git a/hunters/hunter.py b/hunters/hunter.py deleted file mode 100644 index f4db0a7..0000000 --- a/hunters/hunter.py +++ /dev/null @@ -1,9 +0,0 @@ -from abc import ABCMeta, abstractmethod - - -class Hunter(object): - __metaclass__ = ABCMeta - - @abstractmethod - def hunt(self, *args, **kwargs): - pass diff --git a/hunters/kubelet.py b/hunters/kubelet.py deleted file mode 100644 index c7a9f69..0000000 --- a/hunters/kubelet.py +++ /dev/null @@ -1,9 +0,0 @@ -from hunters.hunter import Hunter - - -class Kubelet(Hunter): - def __init__(self, host): - self.host = host - - def hunt(self, *args, **kwargs): - raise NotImplementedError() diff --git a/hunters/proxy.py b/hunters/proxy.py deleted file mode 100644 index 7784e64..0000000 --- a/hunters/proxy.py +++ /dev/null @@ -1,9 +0,0 @@ -from hunters.hunter import Hunter - - -class Proxy(Hunter): - def __init__(self, host): - self.host = host - - def hunt(self, *args, **kwargs): - raise NotImplementedError() diff --git a/hunting/__init__.py b/hunting/__init__.py new file mode 100644 index 0000000..e1e1462 --- /dev/null +++ b/hunting/__init__.py @@ -0,0 +1,7 @@ +from os.path import dirname, basename, isfile +import glob + +# dynamically importing all modules in folder +files = glob.glob(dirname(__file__)+"/*.py") +for module_name in (basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')): + exec('from {} import *'.format(module_name)) \ No newline at end of file diff --git a/hunting/dashboard.py b/hunting/dashboard.py new file mode 100644 index 0000000..28cb360 --- /dev/null +++ b/hunting/dashboard.py @@ -0,0 +1,23 @@ +from events import handler, KubeDashboardEvent +import logging +import requests + +@handler.subscribe(KubeDashboardEvent) +class KubeDashboard(object): + def __init__(self, event): + self.event = event + + @property + def accessible(self): + protocol = "https" if self.event.secure else "http" + r = requests.get("{protocol}://{host}:{port}{loc}".format(protocol=protocol, host=self.event.host, port=self.event.port, loc=self.event.path)) + return r.status_code == 200 + + def execute(self): + if not self.accessible: + return + + if self.event.secure: + logging.info("[OPEN SERVICE] SECURE DASHBOARD - {}:{}{}".format(self.event.host, self.event.port, self.event.path)) + else: + logging.info("[OPEN SERVICE] INSECURE DASHBOARD - {}:{}{}".format(self.event.host, self.event.port, self.event.path)) diff --git a/hunting/kubelet.py b/hunting/kubelet.py new file mode 100644 index 0000000..c207c4b --- /dev/null +++ b/hunting/kubelet.py @@ -0,0 +1,20 @@ +import logging + +from events import handler, ReadOnlyKubeletEvent, SecureKubeletEvent + +""" dividing ports for seperate hunters """ +@handler.subscribe(ReadOnlyKubeletEvent) +class ReadOnlyKubeletPortHunter(object): + def __init__(self, event): + self.event = event + + def execute(self): + logging.info("[OPEN SERVICE] INSECURED KUBELET API - {}:{}".format(self.event.host, self.event.port)) + +@handler.subscribe(SecureKubeletEvent) +class SecurePortKubeletHunter(object): + def __init__(self, event): + self.event = event + + def execute(self): + logging.info("[OPEN SERVICE] SECURED KUBELET API - {}:{}".format(self.event.host, self.event.port)) \ No newline at end of file diff --git a/hunting/proxy.py b/hunting/proxy.py new file mode 100644 index 0000000..b67d17d --- /dev/null +++ b/hunting/proxy.py @@ -0,0 +1,42 @@ +from enum import Enum +from requests import get +from events import KubeDashboardEvent, KubeProxyEvent, handler + + +class Service(Enum): + DASHBOARD = "kubernetes-dashboard" + +@handler.subscribe(KubeProxyEvent) +class KubeProxy(object): + def __init__(self, event): + self.event = event + self.api_url = "http://{host}:{port}/api/v1".format(host=self.event.host, port=self.event.port) + + def execute(self): + for namespace, services in self.services.items(): + for service in services: + curr_path = "api/v1/namespaces/{ns}/services/{sv}/proxy".format(ns=namespace,sv=service) # TODO: check if /proxy is a convention on other services + if service == Service.DASHBOARD.value: + handler.publish_event(KubeDashboardEvent(path=curr_path, secure=False)) + + @property + def namespaces(self): + resource_json = get(self.api_url + "/namespaces").json() + return self.extract_names(resource_json) + + @property + def services(self): + # map between namespaces and service names + services = dict() + for namespace in self.namespaces: + resource_path = "/namespaces/{ns}/services".format(ns=namespace) + resource_json = get(self.api_url + resource_path).json() + services[namespace] = self.extract_names(resource_json) + return services + + @staticmethod + def extract_names(resource_json): + names = list() + for item in resource_json["items"]: + names.append(item["metadata"]["name"]) + return names diff --git a/kube-hunter.py b/kube-hunter.py old mode 100755 new mode 100644 index f1d9897..6664d3c --- a/kube-hunter.py +++ b/kube-hunter.py @@ -1,79 +1,25 @@ -#! /usr/bin/python +#!/bin/env python +import log -from __future__ import print_function +from events import handler, HostScanEvent +from discovery import HostDiscovery +import hunting +import time +import sys +import logging -from argparse import ArgumentParser -from logging import DEBUG, basicConfig, info, warning +def main(): + logging.info("Started") + try: + handler.publish_event(HostScanEvent(interal=True, localhost=False)) + # Blocking to see discovery output + while(True): + time.sleep(100) + except KeyboardInterrupt: + logging.info("Kube-Hunter Stopped") + finally: + handler.free() + logging.debug("Cleaned Queue") -from discovery import DEFAULT_PORTS, HostScanner -from hunters import Dashboard, Kubelet, Proxy -from services import * -from validation import ip, subnet -import chromedriver_binary - -HUNT_MODE = "hunt" -SCAN_MODE = "scan" - - -def hunt_callback(host): - hunters = { - KUBERNETES_DASHBOARD: Dashboard, - KUBERNETES_KUBELET_HTTPS: Kubelet, - KUBERNETES_KUBELET_HTTP: Kubelet, - KUBERNETES_PROXY: Proxy - } - - service_type = identify_service(host) - if service_type == UNKNOWN: - return - - if service_type not in hunters: - warning("Unsupported service type: {}".format(describe_service_type(service_type))) - else: - try: - hunters[service_type](host).hunt() - except NotImplementedError: - pass - -def scan_callback(host): - print("{} - {}".format(host, describe_service_type(identify_service(host)))) - - -def hunt(*args, **kwargs): - target = args[0] - info("Hunting target {}".format(target)) - scanner = HostScanner(threads=1) - scanner.scan(target, DEFAULT_PORTS, hunt_callback) - - -def scan(*args, **kwargs): - target = args[0] - info("Scanning for targets on {}".format(target)) - scanner = HostScanner(threads=20) - scanner.scan(target, DEFAULT_PORTS, scan_callback) - - -def main(mode, *args, **kwargs): - actions = { - SCAN_MODE: scan, - HUNT_MODE: hunt - } - - actions[mode](*args, **kwargs) - - -if __name__ == "__main__": - basicConfig(level=DEBUG) - parser = ArgumentParser() - - subparsers = parser.add_subparsers(dest="action", description="Available actions") - - hunt_parser = subparsers.add_parser(HUNT_MODE) - hunt_parser.add_argument("host", type=ip, help="host to hunt") - - scan_parser = subparsers.add_parser(SCAN_MODE) - scan_parser.add_argument("subnet", type=subnet, help="subnet to scan (CIDR notation)") - - arguments = parser.parse_args() - - main(arguments.action, *([i[1] for i in arguments._get_kwargs()[1:]])) +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/log/__init__.py b/log/__init__.py new file mode 100644 index 0000000..635cf56 --- /dev/null +++ b/log/__init__.py @@ -0,0 +1 @@ +from config import * \ No newline at end of file diff --git a/log/config.py b/log/config.py new file mode 100644 index 0000000..3c74159 --- /dev/null +++ b/log/config.py @@ -0,0 +1,13 @@ +import logging +import argparse + +parser = argparse.ArgumentParser(description='Kubehunter, hunting weak kubernetes clusters') +parser.add_argument('--log', type=str, metavar="LOGLEVEL", default='INFO', help="set output level, options are:\nDEBUG INFO WARNING") + +args = parser.parse_args() +try: + loglevel = getattr(logging, args.log.upper()) +except: + pass + +logging.basicConfig(level=loglevel, format='%(asctime)s - [%(levelname)s]: %(message)s') diff --git a/requirements.txt b/requirements.txt index 9f2bd78..a0951dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ selenium pillow netaddr -chromedriver_binary \ No newline at end of file +chromedriver_binary +netifaces +enum \ No newline at end of file diff --git a/services.py b/services.py deleted file mode 100644 index c68e57b..0000000 --- a/services.py +++ /dev/null @@ -1,81 +0,0 @@ -from requests import get -from urllib3 import disable_warnings - -UNKNOWN = 0 -KUBERNETES_DASHBOARD = 1 -KUBERNETES_PROXY = 2 -KUBERNETES_KUBELET_HTTPS = 3 -KUBERNETES_KUBELET_HTTP = 4 - -disable_warnings() - - -def describe_service_type(service_type): - service_types = { - KUBERNETES_DASHBOARD: "Kubernetes Dashboard", - KUBERNETES_PROXY: "Kubernetes Proxy", - KUBERNETES_KUBELET_HTTPS: "Kubernetes Kubelet", - KUBERNETES_KUBELET_HTTP: "Kubernetes Kubelet (Read only)", - } - return service_types.get(service_type, "Unknown Service") - - -def is_dashboard(host): - try: - r = get("http://{}/api/v1/login/status".format(host)).json() - return all([ - "tokenPresent" in r, - "headerPresent" in r, - "httpsMode" in r - ]) - except: - return False - - -def is_proxy(host): - try: - r = get("http://{}/".format(host)).json() - return all([ - "paths" in r, - "/api" in r["paths"] - ]) - except: - return False - - -def is_kubelet_https(host): - try: - r = get("https://{}/pods".format(host), verify=False).json() - return all([ - "kind" in r, - "items" in r - ]) - except: - return False - - -def is_kubelet_http(host): - try: - r = get("http://{}/pods".format(host)).json() - return all([ - "kind" in r, - "items" in r - ]) - except: - return False - - -def identify_service(host): - if is_dashboard(host): - return KUBERNETES_DASHBOARD - - if is_proxy(host): - return KUBERNETES_PROXY - - if is_kubelet_https(host): - return KUBERNETES_KUBELET_HTTPS - - if is_kubelet_http(host): - return KUBERNETES_KUBELET_HTTP - - return UNKNOWN diff --git a/validation.py b/validation.py deleted file mode 100644 index 27550b4..0000000 --- a/validation.py +++ /dev/null @@ -1,40 +0,0 @@ -from argparse import ArgumentTypeError - - -def ip(string): - error = ArgumentTypeError("{} is not a valid IP address".format(string)) - octets = string.split(".") - - if len(octets) != 4: - raise error - - try: - for o in octets: - o = int(o) - if o < 0 or o > 255: - raise error - except ValueError: - raise error - - return string - - -def subnet(string): - parts = string.split("/") - - if len(parts) != 2: - raise ArgumentTypeError("{} is not a valid subnet".format(string)) - - host, mask = parts - - ip(host) - - try: - mask = int(mask) - except ValueError: - raise ArgumentTypeError("{} is not an integer".format(mask)) - - if mask < 0 or mask > 32: - raise ArgumentTypeError("{} is not valid host identifier".format(mask)) - - return string