diff --git a/kube_hunter/__main__.py b/kube_hunter/__main__.py index 2fc5d42..b17b170 100755 --- a/kube_hunter/__main__.py +++ b/kube_hunter/__main__.py @@ -1,66 +1,40 @@ #!/usr/bin/env python3 -import argparse import logging import threading from kube_hunter.conf import config -from kube_hunter.modules.report.plain import PlainReporter -from kube_hunter.modules.report.yaml import YAMLReporter -from kube_hunter.modules.report.json import JSONReporter -from kube_hunter.modules.report.dispatchers import STDOUTDispatcher, HTTPDispatcher 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 +from kube_hunter.modules.report import get_reporter, get_dispatcher - -loglevel = getattr(logging, config.log.upper(), logging.INFO) - -if config.log.lower() != "none": - logging.basicConfig(level=loglevel, format='%(message)s', datefmt='%H:%M:%S') - -reporters = { - 'yaml': YAMLReporter, - 'json': JSONReporter, - 'plain': PlainReporter -} - -if config.report.lower() in reporters.keys(): - config.reporter = reporters[config.report.lower()]() -else: - logging.warning('Unknown reporter selected, using plain') - config.reporter = reporters['plain']() - -dispatchers = { - 'stdout': STDOUTDispatcher, - 'http': HTTPDispatcher -} - -if config.dispatch.lower() in dispatchers.keys(): - config.dispatcher = dispatchers[config.dispatch.lower()]() -else: - logging.warning('Unknown dispatcher selected, using stdout') - config.dispatcher = dispatchers['stdout']() +config.reporter = get_reporter(config.report) +config.dispatcher = get_dispatcher(config.dispatch) import kube_hunter def interactive_set_config(): """Sets config manually, returns True for success""" - options = [("Remote scanning", "scans one or more specific IPs or DNS names"), - ("Interface scanning","scans subnets on all local network interfaces"), - ("IP range scanning","scans a given IP range")] + options = [("Remote scanning", + "scans one or more specific IPs or DNS names"), + ("Interface scanning", + "scans subnets on all local network interfaces"), + ("IP range scanning", "scans a given IP range")] print("Choose one of the options below:") for i, (option, explanation) in enumerate(options): print("{}. {} ({})".format(i+1, option.ljust(20), explanation)) choice = input("Your choice: ") if choice == '1': - config.remote = input("Remotes (separated by a ','): ").replace(' ', '').split(',') + config.remote = input("Remotes (separated by a ','): ").\ + replace(' ', '').split(',') elif choice == '2': config.interface = True elif choice == '3': - config.cidr = input("CIDR (example - 192.168.1.0/24): ").replace(' ', '') + config.cidr = input("CIDR (example - 192.168.1.0/24): ").\ + replace(' ', '') else: return False return True @@ -83,6 +57,7 @@ global hunt_started_lock hunt_started_lock = threading.Lock() hunt_started = False + def main(): global hunt_started scan_options = [ @@ -127,4 +102,4 @@ def main(): if __name__ == '__main__': - main() + main() diff --git a/kube_hunter/conf/__init__.py b/kube_hunter/conf/__init__.py index 111e060..3ffc6a0 100644 --- a/kube_hunter/conf/__init__.py +++ b/kube_hunter/conf/__init__.py @@ -1,20 +1,18 @@ -from argparse import ArgumentParser +import logging +from kube_hunter.conf.parser import parse_args -parser = ArgumentParser(description='Kube-Hunter - hunts for security weaknesses in Kubernetes clusters') -parser.add_argument('--list', action="store_true", help="displays all tests in kubehunter (add --active flag to see active tests)") -parser.add_argument('--interface', action="store_true", help="set hunting of all network interfaces") -parser.add_argument('--pod', action="store_true", help="set hunter as an insider pod") -parser.add_argument('--quick', action="store_true", help="Prefer quick scan (subnet 24)") -parser.add_argument('--include-patched-versions', action="store_true", help="Don't skip patched versions when scanning") -parser.add_argument('--cidr', type=str, help="set an ip range to scan, example: 192.168.0.0/16") -parser.add_argument('--mapping', action="store_true", help="outputs only a mapping of the cluster's nodes") -parser.add_argument('--remote', nargs='+', metavar="HOST", default=list(), help="one or more remote ip/dns to hunt") -parser.add_argument('--active', action="store_true", help="enables active hunting") -parser.add_argument('--log', type=str, metavar="LOGLEVEL", default='INFO', help="set log level, options are: debug, info, warn, none") -parser.add_argument('--report', type=str, default='plain', help="set report type, options are: plain, yaml, json") -parser.add_argument('--dispatch', type=str, default='stdout', help="where to send the report to, options are: stdout, http (set KUBEHUNTER_HTTP_DISPATCH_URL and KUBEHUNTER_HTTP_DISPATCH_METHOD environment variables to configure)") -parser.add_argument('--statistics', action="store_true", help="set hunting statistics") +config = parse_args() + +loglevel = getattr(logging, config.log.upper(), None) + +if not loglevel: + logging.basicConfig(level=logging.INFO, + format='%(message)s', + datefmt='%H:%M:%S') + logging.warning('Unknown log level selected, using info') +elif config.log.lower() != "none": + logging.basicConfig(level=loglevel, + format='%(message)s', + datefmt='%H:%M:%S') import plugins - -config = parser.parse_args() diff --git a/kube_hunter/conf/parser.py b/kube_hunter/conf/parser.py new file mode 100644 index 0000000..0c46bfc --- /dev/null +++ b/kube_hunter/conf/parser.py @@ -0,0 +1,82 @@ +from argparse import ArgumentParser + + +def parse_args(): + parser = ArgumentParser( + description='Kube-Hunter - hunts for security ' + 'weaknesses in Kubernetes clusters') + + parser.add_argument( + '--list', + action="store_true", + help="Displays all tests in kubehunter " + "(add --active flag to see active tests)") + + parser.add_argument( + '--interface', + action="store_true", + help="Set hunting on all network interfaces") + + parser.add_argument( + '--pod', + action="store_true", + help="Set hunter as an insider pod") + + parser.add_argument( + '--quick', + action="store_true", + help="Prefer quick scan (subnet 24)") + + parser.add_argument( + '--include-patched-versions', + action="store_true", + help="Don't skip patched versions when scanning") + + parser.add_argument( + '--cidr', + type=str, + help="Set an ip range to scan, example: 192.168.0.0/16") + parser.add_argument( + '--mapping', + action="store_true", + help="Outputs only a mapping of the cluster's nodes") + + parser.add_argument( + '--remote', + nargs='+', + metavar="HOST", + default=list(), + help="One or more remote ip/dns to hunt") + + parser.add_argument( + '--active', + action="store_true", + help="Enables active hunting") + + parser.add_argument( + '--log', + type=str, + metavar="LOGLEVEL", + default='INFO', + help="Set log level, options are: debug, info, warn, none") + + parser.add_argument( + '--report', + type=str, + default='plain', + help="Set report type, options are: plain, yaml, json") + + parser.add_argument( + '--dispatch', + type=str, + default='stdout', + help="Where to send the report to, options are: " + "stdout, http (set KUBEHUNTER_HTTP_DISPATCH_URL and " + "KUBEHUNTER_HTTP_DISPATCH_METHOD environment variables to configure)") + + parser.add_argument( + '--statistics', + action="store_true", + help="Show hunting statistics") + + return parser.parse_args() diff --git a/kube_hunter/modules/report/__init__.py b/kube_hunter/modules/report/__init__.py index 6c5952c..a4ba8f0 100644 --- a/kube_hunter/modules/report/__init__.py +++ b/kube_hunter/modules/report/__init__.py @@ -1,7 +1 @@ -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)) +from kube_hunter.modules.report.factory import get_reporter, get_dispatcher diff --git a/kube_hunter/modules/report/base.py b/kube_hunter/modules/report/base.py index 2644044..e4bd313 100644 --- a/kube_hunter/modules/report/base.py +++ b/kube_hunter/modules/report/base.py @@ -1,6 +1,6 @@ -from kube_hunter.conf import config from kube_hunter.core.types import Discovery -from kube_hunter.modules.report.collector import services, vulnerabilities, hunters, services_lock, vulnerabilities_lock +from kube_hunter.modules.report.collector import services, vulnerabilities, \ + hunters, services_lock, vulnerabilities_lock class BaseReporter(object): @@ -11,29 +11,36 @@ class BaseReporter(object): for service in services: node_location = str(service.host) if node_location not in node_locations: - nodes.append({"type": "Node/Master", "location": str(service.host)}) + nodes.append({ + "type": "Node/Master", + "location": str(service.host) + }) node_locations.add(node_location) return nodes def get_services(self): with services_lock: - services_data = [{"service": service.get_name(), - "location": "{}:{}{}".format(service.host, service.port, service.get_path()), - "description": service.explain()} - for service in services] + services_data = [{ + "service": service.get_name(), + "location": f"{service.host}:" + f"{service.port}" + f"{service.get_path()}", + "description": service.explain() + } for service in services] return services_data def get_vulnerabilities(self): with vulnerabilities_lock: - vulnerabilities_data = [{"location": vuln.location(), - "vid": vuln.get_vid(), - "category": vuln.category.name, - "severity": vuln.get_severity(), - "vulnerability": vuln.get_name(), - "description": vuln.explain(), - "evidence": str(vuln.evidence), - "hunter": vuln.hunter.get_name()} - for vuln in vulnerabilities] + vulnerabilities_data = [{ + "location": vuln.location(), + "vid": vuln.get_vid(), + "category": vuln.category.name, + "severity": vuln.get_severity(), + "vulnerability": vuln.get_name(), + "description": vuln.explain(), + "evidence": str(vuln.evidence), + "hunter": vuln.hunter.get_name() + } for vuln in vulnerabilities] return vulnerabilities_data def get_hunter_statistics(self): @@ -41,17 +48,21 @@ class BaseReporter(object): for hunter, docs in hunters.items(): if not Discovery in hunter.__mro__: name, doc = hunter.parse_docs(docs) - hunters_data.append({"name": name, "description": doc, "vulnerabilities": hunter.publishedVulnerabilities}) + hunters_data.append({ + "name": name, + "description": doc, + "vulnerabilities": hunter.publishedVulnerabilities + }) return hunters_data - def get_report(self): + def get_report(self, *, statistics, **kwargs): report = { "nodes": self.get_nodes(), "services": self.get_services(), "vulnerabilities": self.get_vulnerabilities() } - if config.statistics: + if statistics: report["hunter_statistics"] = self.get_hunter_statistics() report["kburl"] = "https://aquasecurity.github.io/kube-hunter/kb/{vid}" diff --git a/kube_hunter/modules/report/collector.py b/kube_hunter/modules/report/collector.py index 152f372..6161bb1 100644 --- a/kube_hunter/modules/report/collector.py +++ b/kube_hunter/modules/report/collector.py @@ -79,7 +79,7 @@ class SendFullReport(object): self.event = event def execute(self): - report = config.reporter.get_report() + report = config.reporter.get_report(statistics=config.statistics, mapping=config.mapping) config.dispatcher.dispatch(report) handler.publish_event(ReportDispatched()) handler.publish_event(TablesPrinted()) diff --git a/kube_hunter/modules/report/factory.py b/kube_hunter/modules/report/factory.py new file mode 100644 index 0000000..fc83e7d --- /dev/null +++ b/kube_hunter/modules/report/factory.py @@ -0,0 +1,34 @@ +from kube_hunter.modules.report.json import JSONReporter +from kube_hunter.modules.report.yaml import YAMLReporter +from kube_hunter.modules.report.plain import PlainReporter +from kube_hunter.modules.report.dispatchers import \ + STDOUTDispatcher, HTTPDispatcher + +import logging + +reporters = { + 'yaml': YAMLReporter, + 'json': JSONReporter, + 'plain': PlainReporter +} + +dispatchers = { + 'stdout': STDOUTDispatcher, + 'http': HTTPDispatcher +} + + +def get_reporter(name): + try: + return reporters[name.lower()] + except KeyError: + logging.warning('Unknown reporter selected, using plain') + return reporters['plain']() + + +def get_dispatcher(name): + try: + return dispatchers[name.lower()] + except KeyError: + logging.warning('Unknown dispatcher selected, using stdout') + return dispatchers['stdout']() diff --git a/kube_hunter/modules/report/json.py b/kube_hunter/modules/report/json.py index 18a7ccd..b0fdc51 100644 --- a/kube_hunter/modules/report/json.py +++ b/kube_hunter/modules/report/json.py @@ -3,6 +3,6 @@ from kube_hunter.modules.report.base import BaseReporter class JSONReporter(BaseReporter): - def get_report(self): - report = super().get_report() + def get_report(self, **kwargs): + report = super().get_report(**kwargs) return json.dumps(report) diff --git a/kube_hunter/modules/report/plain.py b/kube_hunter/modules/report/plain.py index af9f65d..854022f 100644 --- a/kube_hunter/modules/report/plain.py +++ b/kube_hunter/modules/report/plain.py @@ -2,9 +2,9 @@ from __future__ import print_function from prettytable import ALL, PrettyTable -from kube_hunter.conf import config from kube_hunter.modules.report.base import BaseReporter -from kube_hunter.modules.report.collector import services, vulnerabilities, hunters, services_lock, vulnerabilities_lock +from kube_hunter.modules.report.collector import services, vulnerabilities, \ + hunters, services_lock, vulnerabilities_lock EVIDENCE_PREVIEW = 40 MAX_TABLE_WIDTH = 20 @@ -13,7 +13,7 @@ KB_LINK = "https://github.com/aquasecurity/kube-hunter/tree/master/docs/_kb" class PlainReporter(BaseReporter): - def get_report(self): + def get_report(self, *, statistics=None, mapping=None, **kwargs): """generates report tables""" output = "" @@ -27,13 +27,13 @@ class PlainReporter(BaseReporter): if services_len: output += self.nodes_table() - if not config.mapping: + if not mapping: output += self.services_table() if vulnerabilities_len: output += self.vulns_table() else: output += "\nNo vulnerabilities were found" - if config.statistics: + if statistics: if hunters_len: output += self.hunters_table() else: @@ -73,12 +73,20 @@ class PlainReporter(BaseReporter): services_table.header_style = "upper" with services_lock: for service in services: - services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()]) - detected_services_ret = "\nDetected Services\n{}\n".format(services_table) + services_table.add_row( + [service.get_name(), + f"{service.host}:" + f"{service.port}" + f"{service.get_path()}", + service.explain()]) + detected_services_ret = "\nDetected Services\n" \ + f"{services_table}\n" return detected_services_ret def vulns_table(self): - column_names = ["ID", "Location", "Category", "Vulnerability", "Description", "Evidence"] + column_names = ["ID", "Location", + "Category", "Vulnerability", + "Description", "Evidence"] vuln_table = PrettyTable(column_names, hrules=ALL) vuln_table.align = "l" vuln_table.max_width = MAX_TABLE_WIDTH diff --git a/kube_hunter/modules/report/yaml.py b/kube_hunter/modules/report/yaml.py index 785506d..6ddc04e 100644 --- a/kube_hunter/modules/report/yaml.py +++ b/kube_hunter/modules/report/yaml.py @@ -1,13 +1,12 @@ from io import StringIO from ruamel.yaml import YAML -from kube_hunter.conf import config from kube_hunter.modules.report.base import BaseReporter class YAMLReporter(BaseReporter): - def get_report(self): - report = super().get_report() + def get_report(self, **kwargs): + report = super().get_report(**kwargs) output = StringIO() yaml = YAML() yaml.dump(report, output)