mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-02-15 02:20:10 +00:00
Compare commits
23 Commits
main
...
feature/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40ebaa6259 | ||
|
|
c2d0efc6cd | ||
|
|
348a288411 | ||
|
|
12f0d17c3a | ||
|
|
23b048c6d2 | ||
|
|
1aab95085a | ||
|
|
f706802eb9 | ||
|
|
17fd10120e | ||
|
|
2400ab5bb1 | ||
|
|
f6e43e2bbb | ||
|
|
f7e73fe642 | ||
|
|
6a8568173e | ||
|
|
d5efef45e2 | ||
|
|
46b7f9f5d9 | ||
|
|
2f6badd32f | ||
|
|
6a4e6040f7 | ||
|
|
6a3c462fde | ||
|
|
f1d4defcb6 | ||
|
|
7601692d42 | ||
|
|
7fe047e512 | ||
|
|
d6d46527cd | ||
|
|
2fac45e42b | ||
|
|
5b36c9c06a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ venv/
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
!kube_hunter/console/env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
|
||||
@@ -34,6 +34,7 @@ set_config(config)
|
||||
# Running all other registered plugins before execution
|
||||
pm.hook.load_plugin(args=args)
|
||||
|
||||
from kube_hunter.console.manager import start_console
|
||||
from kube_hunter.core.events import handler
|
||||
from kube_hunter.core.events.types import HuntFinished, HuntStarted
|
||||
from kube_hunter.modules.discovery.hosts import RunningAsPodEvent, HostScanEvent
|
||||
@@ -92,11 +93,15 @@ def main():
|
||||
global hunt_started
|
||||
scan_options = [config.pod, config.cidr, config.remote, config.interface, config.k8s_auto_discover_nodes]
|
||||
try:
|
||||
if args.list:
|
||||
if args.console:
|
||||
start_console()
|
||||
return
|
||||
|
||||
elif args.list:
|
||||
list_hunters()
|
||||
return
|
||||
|
||||
if not any(scan_options):
|
||||
elif not any(scan_options):
|
||||
if not interactive_set_config():
|
||||
return
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from argparse import ArgumentParser
|
||||
from kube_hunter.plugins import hookimpl
|
||||
|
||||
from colorama import init, Fore, Style
|
||||
|
||||
@hookimpl
|
||||
def parser_add_arguments(parser):
|
||||
@@ -8,6 +8,15 @@ def parser_add_arguments(parser):
|
||||
This is the default hook implementation for parse_add_argument
|
||||
Contains initialization for all default arguments
|
||||
"""
|
||||
# Initializes colorma
|
||||
init()
|
||||
|
||||
parser.add_argument(
|
||||
"-c", "--console",
|
||||
action="store_true",
|
||||
help=f"Starts kube-hunter's {Fore.GREEN}Immersed Console{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
|
||||
4
kube_hunter/console/__init__.py
Normal file
4
kube_hunter/console/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import general
|
||||
from . import manager
|
||||
from . import env
|
||||
from . import auth
|
||||
1
kube_hunter/console/auth/__init__.py
Normal file
1
kube_hunter/console/auth/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .auth import AuthSubConsole
|
||||
62
kube_hunter/console/auth/auth.py
Normal file
62
kube_hunter/console/auth/auth.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import argparse
|
||||
from cmd2 import with_argparser
|
||||
from kube_hunter.console.general import BaseKubeHunterCmd
|
||||
|
||||
|
||||
class AuthSubConsole(BaseKubeHunterCmd):
|
||||
"""
|
||||
Manages the underlying AuthStore database
|
||||
Implementes 3 methods to manage the db
|
||||
|
||||
delete - Removes an auth entry by an index
|
||||
create - creates a new entry in the db,
|
||||
select - select the auth to use in the environment
|
||||
"""
|
||||
def __init__(self, env):
|
||||
super(AuthSubConsole, self).__init__()
|
||||
self.env = env
|
||||
self.sub_console_name = "env/auth"
|
||||
|
||||
delete_parser = argparse.ArgumentParser()
|
||||
delete_parser.add_argument("index", type=int, help="index of the auth entry for deletion")
|
||||
@with_argparser(delete_parser)
|
||||
def do_delete(self, opts):
|
||||
"""Creates a new entry in the auth database based on a given raw jwt token"""
|
||||
if opts.index:
|
||||
if self.env.current_auth.get_auths_count() > opts.index:
|
||||
self.env.current_auth.delete_auth(opts.jwt_token.strip())
|
||||
else:
|
||||
self.perror("Index too large")
|
||||
|
||||
create_parser = argparse.ArgumentParser()
|
||||
create_parser.add_argument("jwt_token", help="A raw jwt_token of the new auth entry")
|
||||
@with_argparser(create_parser)
|
||||
def do_create(self, opts):
|
||||
"""Creates a new entry in the auth database based on a given raw jwt token"""
|
||||
if opts.jwt_token:
|
||||
self.env.current_auth.new_auth(opts.jwt_token.strip())
|
||||
|
||||
|
||||
show_parser = argparse.ArgumentParser()
|
||||
show_parser.add_argument("-i", "--index", type=int, help="set index to print raw data for a specific auth entry")
|
||||
@with_argparser(show_parser)
|
||||
def do_show(self, opts):
|
||||
"""Show current collected auths"""
|
||||
if opts.index is not None:
|
||||
if self.env.current_auth.get_auths_count() > opts.index:
|
||||
self.poutput(f"Token:\n{self.env.current_auth.get_auth(opts.index).raw_token}")
|
||||
else:
|
||||
self.perror("Index too large")
|
||||
else:
|
||||
self.poutput(self.env.current_auth.get_table())
|
||||
|
||||
select_parser = argparse.ArgumentParser()
|
||||
select_parser.add_argument("index", type=int, help="index of auth entry to set for environment")
|
||||
@with_argparser(select_parser)
|
||||
def do_select(self, opts):
|
||||
"""Sets the auth entry for the environment"""
|
||||
if opts.index is not None:
|
||||
if self.env.current_auth.get_auths_count() > opts.index:
|
||||
self.env.current_auth.set_select_auth(opts.index)
|
||||
else:
|
||||
self.perror("Index too large")
|
||||
76
kube_hunter/console/auth/types.py
Normal file
76
kube_hunter/console/auth/types.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import json
|
||||
import base64
|
||||
|
||||
from prettytable import PrettyTable, ALL
|
||||
|
||||
""" Auth models"""
|
||||
class Auth:
|
||||
def parse_token(self, token):
|
||||
""" Extracting data from token file """
|
||||
# adding maximum base64 padding to parse correctly
|
||||
self.raw_token = token
|
||||
token_json = base64.b64decode(f"{token.split('.')[1]}==")
|
||||
token_data = json.loads(token_json)
|
||||
|
||||
self.iss = token_data.get("iss")
|
||||
self.namespace = token_data.get("kubernetes.io/serviceaccount/namespace")
|
||||
self.name = token_data.get("kubernetes.io/serviceaccount/secret.name")
|
||||
self.service_account_name = token_data.get("kubernetes.io/serviceaccount/service-account.name")
|
||||
self.uid = token_data.get("kubernetes.io/serviceaccount/service-account.uid")
|
||||
self.sub = token_data.get("sub")
|
||||
|
||||
def __init__(self, token=None):
|
||||
if token:
|
||||
self.parse_token(token)
|
||||
|
||||
class AuthStore:
|
||||
auths = []
|
||||
selected_auth = None
|
||||
|
||||
def new_auth(self, token):
|
||||
""" Initializes new Auth object and adds it to the auth db """
|
||||
new_auth = Auth(token)
|
||||
|
||||
if not self.is_exists(new_auth):
|
||||
self.auths.append(new_auth)
|
||||
|
||||
# if it's the only auth, selecting it
|
||||
if not self.selected_auth:
|
||||
self.selected_auth = 0
|
||||
|
||||
def get_auths_count(self):
|
||||
return len(self.auths)
|
||||
|
||||
def delete_auth(self, index):
|
||||
return self.auths.pop(index)
|
||||
|
||||
def is_exists(self, check_auth):
|
||||
""" Checks for uniques auth in auth_store """
|
||||
for auth in self.auths:
|
||||
if auth.sub == check_auth.sub:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_current_auth(self):
|
||||
return self.auths[self.selected_auth]
|
||||
|
||||
def get_auth(self, index):
|
||||
return self.auths[index]
|
||||
|
||||
def set_select_auth(self, index):
|
||||
self.selected_auth = index
|
||||
|
||||
def get_table(self):
|
||||
auth_table = PrettyTable(["index", "Name", "Selected"], hrules=ALL)
|
||||
auth_table.align = "l"
|
||||
auth_table.padding_width = 1
|
||||
auth_table.header_style = "upper"
|
||||
|
||||
# building auth token table, showing selected auths
|
||||
for i, auth in enumerate(self.auths):
|
||||
selected_mark = ""
|
||||
if i == self.selected_auth:
|
||||
selected_mark = "*"
|
||||
auth_table.add_row([i, auth.sub, selected_mark])
|
||||
|
||||
return auth_table
|
||||
0
kube_hunter/console/discover/__init__.py
Normal file
0
kube_hunter/console/discover/__init__.py
Normal file
67
kube_hunter/console/discover/discover.py
Normal file
67
kube_hunter/console/discover/discover.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from kube_hunter.console.auth import AuthSubConsole
|
||||
from kube_hunter.console.general import BaseKubeHunterCmd
|
||||
|
||||
from kube_hunter.conf import Config, set_config
|
||||
from kube_hunter.conf.logging import setup_logger
|
||||
from kube_hunter.core.events import handler
|
||||
from kube_hunter.modules.discovery.hosts import RunningAsPodEvent, HostScanEvent
|
||||
from kube_hunter.modules.report import get_reporter, get_dispatcher
|
||||
from kube_hunter.core.events.types import HuntFinished, HuntStarted
|
||||
|
||||
import time
|
||||
from cmd2 import ansi
|
||||
from progressbar import FormatLabel, RotatingMarker, UnknownLength, ProgressBar, Timer
|
||||
|
||||
class HuntSubConsole(BaseKubeHunterCmd):
|
||||
"""DiscoverSubConsole
|
||||
In charge of managing and running kube-hunter's discover modules
|
||||
"""
|
||||
def __init__(self, env):
|
||||
super(HuntSubConsole, self).__init__()
|
||||
self.env = env
|
||||
self.sub_console_name = "hunt"
|
||||
|
||||
@staticmethod
|
||||
def progress_bar():
|
||||
"""Displays animated progress bar
|
||||
Integrates with handler object, to properly block until hunt finish
|
||||
"""
|
||||
# Logger
|
||||
widgets = ['[', Timer(), ']', ': ', FormatLabel(''), ' ', RotatingMarker()]
|
||||
bar = ProgressBar(max_value=UnknownLength, widgets=widgets)
|
||||
while handler.unfinished_tasks > 0:
|
||||
widgets[4] = FormatLabel(f'Tasks Left To Process: {handler.unfinished_tasks}')
|
||||
bar.update(handler.unfinished_tasks)
|
||||
time.sleep(0.1)
|
||||
bar.finish()
|
||||
|
||||
def do_everything(self, arg):
|
||||
"""Wraps running of kube-hunter's hunting
|
||||
Uses the current environment to specify data to start_event when starting a scan
|
||||
"""
|
||||
# TODO: display output
|
||||
current_auth = self.env.current_auth.get_current_auth()
|
||||
|
||||
start_event = None
|
||||
if self.env.is_inside_pod:
|
||||
self.pfeedback(ansi.style(f"Hunting Started (as {current_auth.sub})", fg="green"))
|
||||
|
||||
start_event = RunningAsPodEvent()
|
||||
start_event.auth_token = current_auth.raw_token
|
||||
|
||||
# setting basic stuff for output methods
|
||||
setup_logger("none", None)
|
||||
config = Config()
|
||||
config.dispatcher = get_dispatcher("stdout")
|
||||
config.reporter = get_reporter("plain")
|
||||
set_config(config)
|
||||
|
||||
# trigger hunting
|
||||
handler.publish_event(start_event)
|
||||
handler.publish_event(HuntStarted())
|
||||
|
||||
self.progress_bar()
|
||||
self.pfeedback(ansi.style(f"Finished hunting. found {0} services and {0} vulnerabilities", fg="green"))
|
||||
handler.join()
|
||||
handler.publish_event(HuntFinished())
|
||||
handler.free()
|
||||
2
kube_hunter/console/env/__init__.py
vendored
Normal file
2
kube_hunter/console/env/__init__.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
from .env import EnvSubConsole
|
||||
from .types import ImmersedEnvironment
|
||||
17
kube_hunter/console/env/env.py
vendored
Normal file
17
kube_hunter/console/env/env.py
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
from kube_hunter.console.auth import AuthSubConsole
|
||||
from kube_hunter.console.general import BaseKubeHunterCmd
|
||||
|
||||
class EnvSubConsole(BaseKubeHunterCmd):
|
||||
"""EnvSubConsole
|
||||
In charge of managing and viewing the entire current environment state
|
||||
Includes: Auth database..
|
||||
"""
|
||||
def __init__(self, env):
|
||||
super(EnvSubConsole, self).__init__()
|
||||
self.env = env
|
||||
self.sub_console_name = "env"
|
||||
# self.prompt = self.env.get_prompt(sub_console="env")
|
||||
|
||||
def do_auth(self, arg):
|
||||
AuthSubConsole(self.env).cmdloop()
|
||||
|
||||
55
kube_hunter/console/env/types.py
vendored
Normal file
55
kube_hunter/console/env/types.py
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
from colorama import init, Fore, Style
|
||||
|
||||
from kube_hunter.console.general import types as GeneralTypes
|
||||
from kube_hunter.console.auth import types as AuthTypes
|
||||
|
||||
# initializes colorma
|
||||
init()
|
||||
|
||||
class ImmersedEnvironment:
|
||||
"""
|
||||
ImmersedEnvironment keeps track of the current console run state.
|
||||
"""
|
||||
auths = []
|
||||
pods = []
|
||||
|
||||
is_inside_cloud = False
|
||||
is_inside_container = False
|
||||
is_inside_pod = False
|
||||
|
||||
current_cloud = GeneralTypes.UnknownCloud()
|
||||
current_pod = GeneralTypes.Pod()
|
||||
current_container = GeneralTypes.Container()
|
||||
|
||||
current_auth = AuthTypes.AuthStore()
|
||||
|
||||
def get_prompt(self, sub_console=""):
|
||||
"""
|
||||
Parses current env state to picture a short description of where we are right now
|
||||
General format is `(cloud) -> (run_unit) kube-hunter $`
|
||||
"""
|
||||
arrow = "->"
|
||||
prompt_prefix = f" kube-hunter{' [' + sub_console + ']' if sub_console else ''} $ "
|
||||
|
||||
# add colores unly
|
||||
cloud = f"({Fore.BLUE}{self.current_cloud}{Style.RESET_ALL})"
|
||||
pod = f"({Fore.MAGENTA}{self.current_pod}{Style.RESET_ALL})"
|
||||
container = f"(container: {Fore.CYAN}{self.current_container}{Style.RESET_ALL})"
|
||||
container_in_pod = f"({Fore.MAGENTA}{self.current_pod}/{{}}{Style.RESET_ALL})"
|
||||
|
||||
env_description = ""
|
||||
if self.current_auth.get_auths_count():
|
||||
auth = self.current_auth.get_current_auth()
|
||||
env_description += f" {Fore.LIGHTRED_EX}[Impersonating {auth.sub}]{Style.RESET_ALL}\n"
|
||||
|
||||
env_description += cloud
|
||||
if self.is_inside_pod:
|
||||
if len(self.current_pod.containers):
|
||||
env_description += f" {arrow} {container_in_pod.format(self.current_pod.containers[0])}"
|
||||
else:
|
||||
env_description += f" {arrow} {pod}"
|
||||
|
||||
elif self.is_inside_container:
|
||||
env_description += f" {arrow} {container}"
|
||||
|
||||
return f"{env_description}{prompt_prefix}"
|
||||
1
kube_hunter/console/general/__init__.py
Normal file
1
kube_hunter/console/general/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .console import BaseKubeHunterCmd
|
||||
16
kube_hunter/console/general/console.py
Normal file
16
kube_hunter/console/general/console.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import cmd2
|
||||
|
||||
class BaseKubeHunterCmd(cmd2.Cmd):
|
||||
sub_console_name = ""
|
||||
|
||||
def postcmd(self, stop, line):
|
||||
self.prompt = self.env.get_prompt(self.sub_console_name)
|
||||
if stop:
|
||||
return True
|
||||
|
||||
def do_exit(self, arg):
|
||||
'exists shell'
|
||||
return True
|
||||
|
||||
def emptyline(self):
|
||||
pass
|
||||
38
kube_hunter/console/general/types.py
Normal file
38
kube_hunter/console/general/types.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import socket
|
||||
|
||||
""" General Types """
|
||||
class Container:
|
||||
""" Basic model for Container objects """
|
||||
name = ""
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Pod:
|
||||
""" Basic model for Pod objects """
|
||||
ip_address = ""
|
||||
name = ""
|
||||
namespace = ""
|
||||
containers = []
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.namespace}/{self.name}"
|
||||
|
||||
def incluster_update(self, pod_event):
|
||||
"""
|
||||
uses pod_event and other techniques to get full data on the incluster pod env data
|
||||
"""
|
||||
self.namespace = pod_event.namespace
|
||||
# hostname will almost always will be the pod's name
|
||||
self.name = socket.gethostname()
|
||||
|
||||
class Cloud:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class UnknownCloud(Cloud):
|
||||
def __init__(self):
|
||||
super(UnknownCloud, self).__init__("Unknown Cloud")
|
||||
59
kube_hunter/console/manager.py
Normal file
59
kube_hunter/console/manager.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from kube_hunter.console.discover.discover import HuntSubConsole
|
||||
from kube_hunter.console.general import BaseKubeHunterCmd
|
||||
from kube_hunter.console.env import EnvSubConsole, ImmersedEnvironment
|
||||
from kube_hunter.modules.discovery.hosts import RunningAsPodEvent
|
||||
|
||||
from colorama import (
|
||||
Back,
|
||||
Fore,
|
||||
Style,
|
||||
)
|
||||
from cmd2 import ansi
|
||||
|
||||
class KubeHunterMainConsole(BaseKubeHunterCmd):
|
||||
def __init__(self, env):
|
||||
super(KubeHunterMainConsole, self).__init__()
|
||||
kube_hunter_logo = r"""
|
||||
_ __ __ __
|
||||
/\ \ /\ \ /\ \ /\ \__
|
||||
\ \ \/'\ __ _\ \ \____ __ \ \ \___ __ __ ___\ \ ,_\ __ _ __
|
||||
\ \ , < /\ \/\ \ \ '__`\ /'__`\ ______\ \ _ `\/\ \/\ \/' _ `\ \ \/ /'__`/\`'__\
|
||||
\ \ \\`\\ \ \_\ \ \ \L\ /\ __//\______\ \ \ \ \ \ \_\ /\ \/\ \ \ \_/\ __\ \ \/
|
||||
\ \_\ \_\ \____/\ \_,__\ \____\/______/\ \_\ \_\ \____\ \_\ \_\ \__\ \____\ \_\
|
||||
\/_/\/_/\/___/ \/___/ \/____/ \/_/\/_/\/___/ \/_/\/_/\/__/\/____/\/_/
|
||||
"""
|
||||
self.intro = f'{kube_hunter_logo}\n\nWelcome to kube-hunter Immeresed Console. Type help or ? to list commands.\n'
|
||||
self.env = env
|
||||
|
||||
def do_hunt(self, arg):
|
||||
'hunt using specified environment'
|
||||
HuntSubConsole(self.env).cmdloop()
|
||||
|
||||
def do_env(self, arg):
|
||||
'Show your environment data collected so far'
|
||||
EnvSubConsole(self.env).cmdloop()
|
||||
|
||||
def do_interactive(self, arg):
|
||||
"""Start interactive ipython session"""
|
||||
environment = self.env
|
||||
self.poutput("\n\tStarted an interactive python session. use `environment` to manage the populated environment object")
|
||||
import ipdb
|
||||
ipdb.set_trace()
|
||||
|
||||
def do_whereami(self, arg):
|
||||
"""Try to determine you are based on local files and mounts"""
|
||||
self.pfeedback("Trying to find out where you are...")
|
||||
pod_event = RunningAsPodEvent()
|
||||
if pod_event.auth_token:
|
||||
self.pfeedback(ansi.style("Found running inside a kubernetes pod", fg="green"))
|
||||
self.env.current_auth.new_auth(pod_event.auth_token)
|
||||
self.pfeedback(ansi.style("Loaded a new auth entry: (hint: env/auth/show)", fg="green"))
|
||||
self.env.current_pod.incluster_update(pod_event)
|
||||
self.env.is_inside_pod = True
|
||||
self.pfeedback("Updated environment with locally found data")
|
||||
|
||||
|
||||
def start_console():
|
||||
environment = ImmersedEnvironment()
|
||||
a = KubeHunterMainConsole(environment)
|
||||
a.cmdloop()
|
||||
Reference in New Issue
Block a user