Merge pull request #20 from ccojocar/yaml_report

Refactor the reporter and add YAML report format
This commit is contained in:
Liz Rice
2018-08-20 10:05:37 +01:00
committed by GitHub
7 changed files with 214 additions and 161 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -4,4 +4,5 @@ enum34
scapy
requests
PrettyTable
urllib3
urllib3
ruamel.yaml

View File

@@ -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...")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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]