mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-05-11 11:47:15 +00:00
Merge pull request #20 from ccojocar/yaml_report
Refactor the reporter and add YAML report format
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,4 +4,5 @@ enum34
|
||||
scapy
|
||||
requests
|
||||
PrettyTable
|
||||
urllib3
|
||||
urllib3
|
||||
ruamel.yaml
|
||||
|
||||
88
src/modules/report/collector.py
Normal file
88
src/modules/report/collector.py
Normal 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...")
|
||||
@@ -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)
|
||||
70
src/modules/report/plain.py
Normal file
70
src/modules/report/plain.py
Normal 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)
|
||||
42
src/modules/report/yaml.py
Normal file
42
src/modules/report/yaml.py
Normal 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]
|
||||
Reference in New Issue
Block a user