diff --git a/Dockerfile b/Dockerfile index 68a12ae..5884298 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,9 @@ FROM python:2.7.15-alpine3.7 RUN apk add --update \ linux-headers \ - build-base + build-base \ + tcpdump \ + wireshark RUN mkdir -p /kube-hunter COPY . /kube-hunter diff --git a/kube-hunter.py b/kube-hunter.py index ca757f7..1c2ce4a 100755 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -3,8 +3,6 @@ from __future__ import print_function import argparse import logging -import sys -import time try: raw_input # Python 2 @@ -20,6 +18,7 @@ parser.add_argument('--mapping', action="store_true", help="outputs only a mappi 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") import plugins @@ -32,6 +31,14 @@ except: if config.log.lower() != "none": logging.basicConfig(level=loglevel, format='%(message)s', datefmt='%H:%M:%S') +from src.modules.report.plain import PlainReporter +from src.modules.report.yaml import YAMLReporter + +if config.report.lower() == "yaml": + config.reporter = YAMLReporter() +else: + config.reporter = PlainReporter() + from src.core.events import handler from src.core.events.types import HuntFinished, HuntStarted from src.modules.discovery import HostDiscovery diff --git a/requirements.txt b/requirements.txt index 9563477..7c572ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ enum34 scapy requests PrettyTable -urllib3 \ No newline at end of file +urllib3 +ruamel.yaml diff --git a/src/modules/report/collector.py b/src/modules/report/collector.py new file mode 100644 index 0000000..d316439 --- /dev/null +++ b/src/modules/report/collector.py @@ -0,0 +1,88 @@ +import logging + +from __main__ import config +from src.core.events import handler +from src.core.events.types import Event, Service, Vulnerability, HuntFinished, HuntStarted + +services = list() +vulnerabilities = list() + + +def console_trim(text, prefix=' '): + a = text.split(" ") + b = a[:] + total_length = 0 + count_of_inserts = 0 + for index, value in enumerate(a): + if (total_length + (len(value) + len(prefix))) >= 80: + b.insert(index + count_of_inserts, '\n') + count_of_inserts += 1 + total_length = 0 + else: + total_length += len(value) + len(prefix) + return '\n'.join([prefix + line.strip(' ') for line in ' '.join(b).split('\n')]) + + +def wrap_last_line(text, prefix='| ', suffix='|_'): + lines = text.split('\n') + lines[-1] = lines[-1].replace(prefix, suffix, 1) + return '\n'.join(lines) + + +@handler.subscribe(Service) +@handler.subscribe(Vulnerability) +class Collector(object): + def __init__(self, event=None): + self.event = event + + def execute(self): + """function is called only when collecting data""" + global services, vulnerabilities + bases = self.event.__class__.__mro__ + if Service in bases: + services.append(self.event) + import datetime + logging.info("|\n| {name}:\n| type: open service\n| service: {name}\n|_ host: {host}:{port}".format( + host=self.event.host, + port=self.event.port, + name=self.event.get_name(), + time=datetime.time() + )) + + elif Vulnerability in bases: + vulnerabilities.append(self.event) + logging.info( + "|\n| {name}:\n| type: vulnerability\n| host: {host}:{port}\n| description: \n{desc}".format( + name=self.event.get_name(), + host=self.event.host, + port=self.event.port, + desc=wrap_last_line(console_trim(self.event.explain(), '| ')) + )) + + +class TablesPrinted(Event): + pass + + +@handler.subscribe(HuntFinished) +class SendFullReport(object): + def __init__(self, event): + self.event = event + + def execute(self): + report = config.reporter.get_report() + if config.report == "plain": + logging.info("\n{div}\n{report}".format(div="-" * 10, report=report)) + else: + print(report) + handler.publish_event(TablesPrinted()) + + +@handler.subscribe(HuntStarted) +class StartedInfo(object): + def __init__(self, event): + self.event = event + + def execute(self): + logging.info("~ Started") + logging.info("~ Discovering Open Kubernetes Services...") diff --git a/src/modules/report/default.py b/src/modules/report/default.py deleted file mode 100644 index 7ab41df..0000000 --- a/src/modules/report/default.py +++ /dev/null @@ -1,157 +0,0 @@ -import logging - -from prettytable import ALL, PrettyTable - -from __main__ import config -from src.core.events import handler -from src.core.events.types import Event, Service, Vulnerability, HuntFinished, HuntStarted - -# [event, ...] -services = list() - -class TablesPrinted(Event): - pass - -vulnerabilities = list() - -EVIDENCE_PREVIEW = 40 -MAX_TABLE_WIDTH = 20 - -def console_trim(text, prefix=' '): - a = text.split(" ") - b = a[:] - total_length = 0 - count_of_inserts = 0 - for index, value in enumerate(a): - if (total_length + (len(value) + len(prefix))) >= 80: - b.insert(index + count_of_inserts, '\n') - count_of_inserts += 1 - total_length = 0 - else: - total_length += len(value) + len(prefix) - return '\n'.join([prefix + line.strip(' ') for line in ' '.join(b).split('\n')]) - - -def wrap_last_line(text, prefix='| ', suffix='|_'): - lines = text.split('\n') - lines[-1] = lines[-1].replace(prefix, suffix, 1) - return '\n'.join(lines) - - -@handler.subscribe(Service) -@handler.subscribe(Vulnerability) -class DefaultReporter(object): - def __init__(self, event=None): - self.event = event - - def execute(self): - """function is called only when collecting data""" - global services, vulnerabilities - bases = self.event.__class__.__mro__ - if Service in bases: - services.append(self.event) - import datetime - logging.info("|\n| {name}:\n| type: open service\n| service: {name}\n|_ host: {host}:{port}".format( - host=self.event.host, - port=self.event.port, - name=self.event.get_name(), - time=datetime.time() - )) - - elif Vulnerability in bases: - vulnerabilities.append(self.event) - logging.info( - "|\n| {name}:\n| type: vulnerability\n| host: {host}:{port}\n| description: \n{desc}".format( - name=self.event.get_name(), - host=self.event.host, - port=self.event.port, - desc=wrap_last_line(console_trim(self.event.explain(), '| ')) - )) - - def get_tables(self): - """generates report tables""" - output = "" - if len(services): - output += nodes_table() - if not config.mapping: - output += services_table() - if len(vulnerabilities): - output += vulns_table() - else: - output += "\nNo vulnerabilities were found" - else: - print("\nKube Hunter couldn't find any clusters") - # print("\nKube Hunter couldn't find any clusters. {}".format("Maybe try with --active?" if not config.active else "")) - return output - -reporter = DefaultReporter() - - -@handler.subscribe(HuntFinished) -class SendFullReport(object): - def __init__(self, event): - self.event = event - - def execute(self): - logging.info("\n{div}\n{tables}".format(div="-" * 10, tables=reporter.get_tables())) - handler.publish_event(TablesPrinted()) - - -@handler.subscribe(HuntStarted) -class StartedInfo(object): - def __init__(self, event): - self.event = event - - def execute(self): - logging.info("~ Started") - logging.info("~ Discovering Open Kubernetes Services...") - - -""" Tables Generation """ -def nodes_table(): - nodes_table = PrettyTable(["Type", "Location"], hrules=ALL) - nodes_table.align="l" - nodes_table.max_width=MAX_TABLE_WIDTH - nodes_table.padding_width=1 - nodes_table.sortby="Type" - nodes_table.reversesort=True - nodes_table.header_style="upper" - - # TODO: replace with sets - id_memory = list() - for service in services: - if service.event_id not in id_memory: - nodes_table.add_row(["Node/Master", service.host]) - id_memory.append(service.event_id) - return "\nNodes\n{}\n".format(nodes_table) - - -def services_table(): - services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL) - services_table.align="l" - services_table.max_width=MAX_TABLE_WIDTH - services_table.padding_width=1 - services_table.sortby="Service" - services_table.reversesort=True - services_table.header_style="upper" - for service in services: - services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()]) - - return "\nDetected Services\n{}\n".format(services_table) - - -def vulns_table(): - column_names = ["Location", "Category", "Vulnerability", "Description", "Evidence"] - vuln_table = PrettyTable(column_names, hrules=ALL) - vuln_table.align="l" - vuln_table.max_width=MAX_TABLE_WIDTH - vuln_table.sortby="Category" - vuln_table.reversesort=True - vuln_table.padding_width=1 - vuln_table.header_style="upper" - for vuln in vulnerabilities: - row = ["{}:{}".format(vuln.host, vuln.port) if vuln.host else "", vuln.category.name, vuln.get_name(), vuln.explain()] - evidence = str(vuln.evidence)[:EVIDENCE_PREVIEW] + "..." if len(str(vuln.evidence)) > EVIDENCE_PREVIEW else str(vuln.evidence) - row.append(evidence) - vuln_table.add_row(row) - return "\nVulnerabilities\n{}\n".format(vuln_table) diff --git a/src/modules/report/plain.py b/src/modules/report/plain.py new file mode 100644 index 0000000..85b4145 --- /dev/null +++ b/src/modules/report/plain.py @@ -0,0 +1,70 @@ +from prettytable import ALL, PrettyTable + +from __main__ import config +from collector import services, vulnerabilities + +EVIDENCE_PREVIEW = 40 +MAX_TABLE_WIDTH = 20 + + +class PlainReporter(object): + + def get_report(self): + """generates report tables""" + output = "" + if len(services): + output += self.nodes_table() + if not config.mapping: + output += self.services_table() + if len(vulnerabilities): + output += self.vulns_table() + else: + output += "\nNo vulnerabilities were found" + else: + print "\nKube Hunter couldn't find any clusters" + # print "\nKube Hunter couldn't find any clusters. {}".format("Maybe try with --active?" if not config.active else "") + return output + + def nodes_table(self): + nodes_table = PrettyTable(["Type", "Location"], hrules=ALL) + nodes_table.align = "l" + nodes_table.max_width = MAX_TABLE_WIDTH + nodes_table.padding_width = 1 + nodes_table.sortby = "Type" + nodes_table.reversesort = True + nodes_table.header_style = "upper" + # TODO: replace with sets + id_memory = list() + for service in services: + if service.event_id not in id_memory: + nodes_table.add_row(["Node/Master", service.host]) + id_memory.append(service.event_id) + return "\nNodes\n{}\n".format(nodes_table) + + def services_table(self): + services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL) + services_table.align = "l" + services_table.max_width = MAX_TABLE_WIDTH + services_table.padding_width = 1 + services_table.sortby = "Service" + services_table.reversesort = True + services_table.header_style = "upper" + for service in services: + services_table.add_row([service.get_name(), "{}:{}{}".format(service.host, service.port, service.get_path()), service.explain()]) + return "\nDetected Services\n{}\n".format(services_table) + + def vulns_table(self): + column_names = ["Location", "Category", "Vulnerability", "Description", "Evidence"] + vuln_table = PrettyTable(column_names, hrules=ALL) + vuln_table.align = "l" + vuln_table.max_width = MAX_TABLE_WIDTH + vuln_table.sortby = "Category" + vuln_table.reversesort = True + vuln_table.padding_width = 1 + vuln_table.header_style = "upper" + for vuln in vulnerabilities: + row = ["{}:{}".format(vuln.host, vuln.port) if vuln.host else "", vuln.category.name, vuln.get_name(), vuln.explain()] + evidence = str(vuln.evidence)[:EVIDENCE_PREVIEW] + "..." if len(str(vuln.evidence)) > EVIDENCE_PREVIEW else str(vuln.evidence) + row.append(evidence) + vuln_table.add_row(row) + return "\nVulnerabilities\n{}\n".format(vuln_table) diff --git a/src/modules/report/yaml.py b/src/modules/report/yaml.py new file mode 100644 index 0000000..07418b6 --- /dev/null +++ b/src/modules/report/yaml.py @@ -0,0 +1,42 @@ +import StringIO + +from ruamel.yaml import YAML + +from collector import services, vulnerabilities + + +class YAMLReporter(object): + def get_report(self): + yaml = YAML() + report = { + "nodes": self.get_nodes(), + "services": self.get_services(), + "vulnerabilities": self.get_vulenrabilities() + } + output = StringIO.StringIO() + yaml.dump(report, output) + return output.getvalue() + + def get_nodes(self): + nodes = list() + node_locations = set() + 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)}) + node_locations.add(node_location) + return nodes + + def get_services(self): + return [{"service": service.get_name(), + "location": "{}:{}{}".format(service.host, service.port, service.get_path()), + "description": service.explain()} + for service in services] + + def get_vulenrabilities(self): + return [{"location": "{}:{}".format(vuln.host, vuln.port) if vuln.host else "", + "category": vuln.category.name, + "vulnerability": vuln.get_name(), + "description": vuln.explain(), + "evidence": str(vuln.evidence)} + for vuln in vulnerabilities]