mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-05-11 11:47:15 +00:00
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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from reporter import *
|
||||
@@ -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)
|
||||
123
report/default.py
Normal file
123
report/default.py
Normal file
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
handler = EventQueue(800)
|
||||
|
||||
@@ -79,3 +79,9 @@ class OpenPortEvent(Event):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.port)
|
||||
|
||||
class HuntFinished(Event):
|
||||
pass
|
||||
|
||||
class HuntStarted(Event):
|
||||
pass
|
||||
Reference in New Issue
Block a user