From 31d7c1e754986f80dc44dce03ec129f49b8a7cfa Mon Sep 17 00:00:00 2001 From: daniel_sagi Date: Tue, 17 Jul 2018 15:44:28 +0300 Subject: [PATCH] sperated default report (tables and logging) from the the report being sent to aqua, to seperate modules. also added two core events: HuntStarted and HuntFinished. for reporters to listen to Also changed default of tables for prinring evidence --- kube-hunter.py | 24 ++++--- report/__init__.py | 1 - report/{reporter.py => aqua.py} | 122 +++++++------------------------ report/default.py | 123 ++++++++++++++++++++++++++++++++ src/core/database.py | 15 ---- src/core/events/handler.py | 7 +- src/core/events/types/common.py | 6 ++ 7 files changed, 173 insertions(+), 125 deletions(-) rename report/{reporter.py => aqua.py} (53%) create mode 100644 report/default.py delete mode 100644 src/core/database.py diff --git a/kube-hunter.py b/kube-hunter.py index c5cdc01..86d028c 100644 --- a/kube-hunter.py +++ b/kube-hunter.py @@ -14,8 +14,8 @@ parser.add_argument('--remote', nargs='+', metavar="HOST", default=list(), help= 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('--token', type=str, metavar="AQUA_TOKEN", help="specify the token retrieved from Aqua, after finished executing, the report will be visible on kube-hunter's site") - config = parser.parse_args() + try: loglevel = getattr(logging, config.log.upper()) except: @@ -23,8 +23,11 @@ except: if config.log.lower() != "none": logging.basicConfig(level=loglevel, format='%(asctime)s - [%(levelname)s]: %(message)s') -from report import reporter +from report import default +from report import aqua + from src.core.events import handler +from src.core.events.types import HuntFinished, HuntStarted from src.modules.discovery import HostDiscovery from src.modules.discovery.hosts import HostScanEvent @@ -51,22 +54,22 @@ def interactive_set_config(): return False return True +hunt_started = False def main(): + global hunt_started scan_options = [ config.pod, config.cidr, config.remote, config.internal ] - hunt_started = False try: if not any(scan_options): if not interactive_set_config(): return - if config.token: - reporter.print_report_url(token=config.token) - - hunt_started = True + logging.info("Started") + hunt_started = True + handler.publish_event(HuntStarted()) handler.publish_event(HostScanEvent()) # Blocking to see discovery output @@ -75,11 +78,10 @@ def main(): logging.debug("Kube-Hunter stopped by user") finally: if hunt_started: + handler.publish_event(HuntFinished()) + handler.join() handler.free() - logging.debug("Cleaned Queue") - reporter.print_tables() - if config.token: - reporter.send_report(token=config.token) + logging.debug("Cleaned Queue") if config.pod: while True: time.sleep(5) diff --git a/report/__init__.py b/report/__init__.py index a9f8ddb..e69de29 100644 --- a/report/__init__.py +++ b/report/__init__.py @@ -1 +0,0 @@ -from reporter import * \ No newline at end of file diff --git a/report/reporter.py b/report/aqua.py similarity index 53% rename from report/reporter.py rename to report/aqua.py index 46dbeb3..7bdf19d 100644 --- a/report/reporter.py +++ b/report/aqua.py @@ -1,14 +1,15 @@ import json import logging -from time import time from collections import defaultdict +import time import requests from prettytable import ALL, PrettyTable -from __main__ import config from src.core.events import handler -from src.core.events.types import Service, Vulnerability +from src.core.events.types import Service, Vulnerability, HuntStarted, HuntFinished + +from __main__ import config # [event, ...] services = list() @@ -16,58 +17,28 @@ services = list() # [(TypeClass, event), ...] insights = list() -vulnerabilities = list() - -EVIDENCE_PREVIEW = 40 -MAX_WIDTH_VULNS = 70 -MAX_WIDTH_SERVICES = 60 - AQUA_PUSH_URL = "https://qlyscbqwl7.execute-api.us-east-1.amazonaws.com/Prod/submit?token={token}" AQUA_RESULTS_URL = "https://kubehunter.aquasec.com/report.html?token={token}" @handler.subscribe(Service) @handler.subscribe(Vulnerability) -class Reporter(object): - """Reportes can be initiated by the event handler, and by regular decaration. for usage on end of runtime""" - def __init__(self, event=None): +class AquaReporter(object): + def __init__(self, event): self.event = event self.insights_by_id = defaultdict(list) self.services_by_id = defaultdict(list) def execute(self): - """function is called only when collecting data""" global services, insights bases = self.event.__class__.__mro__ if Service in bases: services.append(self.event) - logging.info("[OPEN SERVICE - {name}] IP:{host} PORT:{port}".format( - host=self.event.host, - port=self.event.port, - name=self.event.get_name(), - desc=self.event.explain() - )) elif Vulnerability in bases: insights.append((Vulnerability, self.event)) - vulnerabilities.append(self.event) - logging.info("[VULNERABILITY - {name}] {desc}".format( - name=self.event.get_name(), - desc=self.event.explain(), - )) if config.token: self.send_report(token=config.token) - def print_tables(self): - """generates report tables and outputs to stdout""" - if len(services): - print_nodes() - if not config.mapping: - print_services() - print_vulnerabilities() - 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 "") - def build_sub_services(self, services_list): # correlation functions def get_insights_by_service(service): @@ -110,7 +81,7 @@ class Reporter(object): } for insight_type, insight in get_insights_by_service(service)] return current_list - def send_report(self, token): + def send_report(self, token, finished=False): def generate_report(): """function generates a report corresponding to specifications of the frontend of kubehunter""" for service in services: @@ -132,12 +103,11 @@ class Reporter(object): report["services"].append(service_report) return report - finished = (not handler.unfinished_tasks) logging.debug("generating report") report = { 'results': generate_report(), 'metadata': { - 'finished': int(time()*1000) if finished else False + 'finished': int(time.time()*1000) if finished else False } } logging.debug("uploading report") @@ -150,65 +120,25 @@ class Reporter(object): else: logging.debug("Failed sending report with:{}, {}".format(r.status_code, r.text)) if finished: - print "\nCould not send report.\n{}".format(json.loads(r.text).get("status", "")) - - def print_report_url(self, token): - url_table = PrettyTable(["{}".format(AQUA_RESULTS_URL.format(token=token))], hrules=ALL) - print "\nReport will be available at:\n{}\n".format(url_table) - -reporter = Reporter() + print "\nCould not send report.\n{}".format(json.loads(r.text).get("status", "")) +reporter = AquaReporter({}) -""" Tables Generation """ -def print_nodes(): - nodes_table = PrettyTable(["Type", "Location"], hrules=ALL) - nodes_table.align="l" - nodes_table.max_width=MAX_WIDTH_SERVICES - 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) - print "Nodes:" - print nodes_table - print +@handler.subscribe(HuntStarted) +class PrintUrlOnStart(object): + def __init__(self, event): + self.event = event -def print_services(): - services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL) - services_table.align="l" - services_table.max_width=MAX_WIDTH_SERVICES - 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()]) - print "Detected Services:" - print services_table - print + def execute(self): + if config.token: + url_table = PrettyTable(["{}".format(AQUA_RESULTS_URL.format(token=config.token))], hrules=ALL) + print "\nReport will be available at:\n{}\n".format(url_table) -def print_vulnerabilities(): - column_names = ["Location", "Category", "Vulnerability", "Description"] - if config.active: column_names.append("Evidence") - vuln_table = PrettyTable(column_names, hrules=ALL) - vuln_table.align="l" - vuln_table.max_width=MAX_WIDTH_VULNS - 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.component.name, vuln.get_name(), vuln.explain()] - if config.active: - 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) - print "Vulnerabilities:" - print vuln_table - print +@handler.subscribe(HuntFinished) +class SendFullReport(object): + def __init__(self, event): + self.event = event + + def execute(self): + if config.token: + reporter.send_report(token=config.token, finished=True) \ No newline at end of file diff --git a/report/default.py b/report/default.py new file mode 100644 index 0000000..5d66952 --- /dev/null +++ b/report/default.py @@ -0,0 +1,123 @@ +import json +import logging +from time import time +from collections import defaultdict + +import requests +from prettytable import ALL, PrettyTable + +from __main__ import config +from src.core.events import handler +from src.core.events.types import Service, Vulnerability, HuntFinished + +# [event, ...] +services = list() + +# [(TypeClass, event), ...] +insights = list() + +vulnerabilities = list() + +EVIDENCE_PREVIEW = 40 +MAX_WIDTH_VULNS = 70 +MAX_WIDTH_SERVICES = 60 + +@handler.subscribe(Service) +@handler.subscribe(Vulnerability) +class DefaultReporter(object): + """Reportes can be initiated by the event handler, and by regular decaration. for usage on end of runtime""" + def __init__(self, event=None): + self.event = event + + def execute(self): + """function is called only when collecting data""" + global services, insights + bases = self.event.__class__.__mro__ + if Service in bases: + services.append(self.event) + logging.info("[OPEN SERVICE - {name}] IP:{host} PORT:{port}".format( + host=self.event.host, + port=self.event.port, + name=self.event.get_name(), + desc=self.event.explain() + )) + elif Vulnerability in bases: + insights.append((Vulnerability, self.event)) + vulnerabilities.append(self.event) + logging.info("[VULNERABILITY - {name}] {desc}".format( + name=self.event.get_name(), + desc=self.event.explain(), + )) + + def print_tables(self): + """generates report tables and outputs to stdout""" + if len(services): + print_nodes() + if not config.mapping: + print_services() + print_vulnerabilities() + 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 "") + +reporter = DefaultReporter() +@handler.subscribe(HuntFinished) +class SendFullReport(object): + def __init__(self, event): + self.event = event + + def execute(self): + reporter.print_tables() + + +""" Tables Generation """ +def print_nodes(): + nodes_table = PrettyTable(["Type", "Location"], hrules=ALL) + nodes_table.align="l" + nodes_table.max_width=MAX_WIDTH_SERVICES + 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) + print "Nodes:" + print nodes_table + print + +def print_services(): + services_table = PrettyTable(["Service", "Location", "Description"], hrules=ALL) + services_table.align="l" + services_table.max_width=MAX_WIDTH_SERVICES + 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()]) + print "Detected Services:" + print services_table + print + +def print_vulnerabilities(): + column_names = ["Location", "Category", "Vulnerability", "Description", "Evidence"] + vuln_table = PrettyTable(column_names, hrules=ALL) + vuln_table.align="l" + vuln_table.max_width=MAX_WIDTH_VULNS + 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) + print "Vulnerabilities:" + print vuln_table + print diff --git a/src/core/database.py b/src/core/database.py deleted file mode 100644 index 959749d..0000000 --- a/src/core/database.py +++ /dev/null @@ -1,15 +0,0 @@ -from threading import Lock - -class Database(object): - def __init__(self): - self.lock = Lock() - - # def __getattribute__(self, value): - # with self.lock: - # return self.__dict__[value] - - # def __setattr__(self, name, value): - # with self.lock: - # self.__dict__[name] = value - -db = Database() \ No newline at end of file diff --git a/src/core/events/handler.py b/src/core/events/handler.py index 7cb3f20..ed23b7f 100644 --- a/src/core/events/handler.py +++ b/src/core/events/handler.py @@ -1,13 +1,16 @@ import logging +import time from abc import ABCMeta from collections import defaultdict from Queue import Queue from threading import Lock, Thread -import time from __main__ import config + from ..types import ActiveHunter +from ...core.events.types import HuntFinished + working_count = 0 lock = Lock() @@ -80,4 +83,4 @@ class EventQueue(Queue, object): with self.mutex: self.queue.clear() -handler = EventQueue(800) \ No newline at end of file +handler = EventQueue(800) diff --git a/src/core/events/types/common.py b/src/core/events/types/common.py index 5361d4e..f123b71 100644 --- a/src/core/events/types/common.py +++ b/src/core/events/types/common.py @@ -79,3 +79,9 @@ class OpenPortEvent(Event): def __str__(self): return str(self.port) + +class HuntFinished(Event): + pass + +class HuntStarted(Event): + pass \ No newline at end of file