From 8df666a63f28b449af1f632870438a16388f6fca Mon Sep 17 00:00:00 2001 From: chuck black Date: Mon, 19 Oct 2020 11:04:07 -0700 Subject: [PATCH] started implementation of traceroute --- quokka/__init__.py | 2 + quokka/controller/TracerouteManager.py | 56 +++++++++++++++++++ ...os.standard.config.diff.NXOS-sandbox (SSH) | 1 + quokka/data/traceroutemonitors.yaml | 1 + quokka/models/apis.py | 19 +++++++ quokka/views/ui_views.py | 34 ++++++++++- quokka/workers/TracerouteThread.py | 52 +++++++++++++++++ quokka/workers/traceroute_worker.py | 39 +++++++++++++ quokka/workers/util.py | 21 +++++++ 9 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 quokka/controller/TracerouteManager.py create mode 100644 quokka/data/traceroutemonitors.yaml create mode 100644 quokka/workers/TracerouteThread.py create mode 100644 quokka/workers/traceroute_worker.py diff --git a/quokka/__init__.py b/quokka/__init__.py index 508d823e..1e8877f0 100644 --- a/quokka/__init__.py +++ b/quokka/__init__.py @@ -95,6 +95,8 @@ capture_manager = CaptureManager() from quokka.controller.PortscanManager import PortscanManager portscan_manager = PortscanManager() +from quokka.controller.TracerouteManager import TracerouteManager +traceroute_manager = TracerouteManager() def shutdown(): diff --git a/quokka/controller/TracerouteManager.py b/quokka/controller/TracerouteManager.py new file mode 100644 index 00000000..fd80db0e --- /dev/null +++ b/quokka/controller/TracerouteManager.py @@ -0,0 +1,56 @@ +import pika +import json +import yaml +from quokka.controller.utils import log_console + + +class TracerouteManager: + + try: + with open("quokka/data/" + "traceroutemonitors.yaml", "r") as import_file: + traceroute_monitors = yaml.safe_load(import_file.read()) + except FileNotFoundError as e: + log_console(f"Could not import traceroute monitors file: {repr(e)}") + traceroute_monitors["0.0.0.0/0"] = "localhost" + + @staticmethod + def get_channel(monitor): + + connection = pika.BlockingConnection(pika.ConnectionParameters(host=monitor)) + channel = connection.channel() + channel.queue_declare(queue="traceroute_queue", durable=True) + + return channel + + @staticmethod + def find_monitor(hostname=None): + + if not hostname: + return "localhost" + + if hostname in TracerouteManager.traceroute_monitors: + return TracerouteManager.traceroute_monitors[hostname] + + else: + # If we didn't find a specific monitor, default to localhost + return "localhost" + + @staticmethod + def initiate_traceroute(hostname, token): + + monitor = TracerouteManager.find_monitor(hostname) + channel = TracerouteManager.get_channel(monitor) + + traceroute_info = { + "hostname": hostname, + "token": token, + } + + traceroute_info_json = json.dumps(traceroute_info) + channel.basic_publish( + exchange="", routing_key="traceroute_queue", body=traceroute_info_json + ) + + log_console( + f"Traceroute Manager: starting traceroute: hostname : {hostname}" + ) diff --git a/quokka/data/cisco.nxos.standard.config.diff.NXOS-sandbox (SSH) b/quokka/data/cisco.nxos.standard.config.diff.NXOS-sandbox (SSH) index e191cc39..9b398b33 100644 --- a/quokka/data/cisco.nxos.standard.config.diff.NXOS-sandbox (SSH) +++ b/quokka/data/cisco.nxos.standard.config.diff.NXOS-sandbox (SSH) @@ -1,6 +1,7 @@ cfs eth distribute feature vpc feature telemetry +vlan 1-9,100-105 vpc domain 10 telemetry sensor-group 1 diff --git a/quokka/data/traceroutemonitors.yaml b/quokka/data/traceroutemonitors.yaml new file mode 100644 index 00000000..873d9b8d --- /dev/null +++ b/quokka/data/traceroutemonitors.yaml @@ -0,0 +1 @@ +0.0.0.0/0 : localhost \ No newline at end of file diff --git a/quokka/models/apis.py b/quokka/models/apis.py index 9df176b0..d0c38929 100644 --- a/quokka/models/apis.py +++ b/quokka/models/apis.py @@ -795,6 +795,25 @@ def get_port_scan_extended(host_ip, host_name, token): return "failed", "No scan results in time provided" +def get_traceroute(hostname, token): + + max_wait_time = 300 # extended port scan allowed to take 5 minutes max + start_time = datetime.now() + while (datetime.now() - start_time).total_seconds() < max_wait_time: + + search = {"host_ip": host_ip, "host_name": host_name, "token": token} + portscan_obj = db.session.query(Portscan).filter_by(**search).one_or_none() + + if not portscan_obj: + time.sleep(2) + continue + + portscan = get_model_as_dict(portscan_obj) + return "success", portscan["scan_output"] + + return "failed", "No scan results in time provided" + + def record_device_config(device_id, config): device_config = dict() diff --git a/quokka/views/ui_views.py b/quokka/views/ui_views.py index 189084e6..b2166d6a 100644 --- a/quokka/views/ui_views.py +++ b/quokka/views/ui_views.py @@ -18,17 +18,17 @@ get_all_events, get_service_summary_data, get_host_summary_data, - # get_host_capture, - # get_protocol_capture, get_capture, get_port_scan_extended, get_device_config_diff, + get_traceroute, ) import quokka.models.reset from quokka.controller.ThreadManager import ThreadManager from quokka.controller.CaptureManager import CaptureManager from quokka.controller.PortscanManager import PortscanManager from quokka.controller.host.portscan import get_port_scan_tcp_connection +from quokka.controller.TracerouteManager import TracerouteManager @app.route("/ui/devices", methods=["GET", "POST"]) @@ -306,3 +306,33 @@ def scan_extended(): else: return "Invalid request method" + + +@app.route("/ui/traceroute", methods=["GET", "POST"]) +def traceroute(): + + hostname = request.args.get("hostname") + + if not hostname: + return "Must provide hostname", 400 + + if request.method == "GET": + token = request.args.get("token") + if not token: + return "Must provide token on GET request", 400 + + result, traceroute_image = get_traceroute(hostname, token) + return { + "result": result, + "traceroute_output": str(traceroute_image), + "hostname": hostname, + } + + elif request.method == "POST": + token = str(datetime.now())[:-3] + TracerouteManager.initiate_traceroute(hostname, token) + return {"result": f"Traceroute initiated for hostname: {hostname}", + "token": token} + + else: + return "Invalid request method" diff --git a/quokka/workers/TracerouteThread.py b/quokka/workers/TracerouteThread.py new file mode 100644 index 00000000..11edc1bd --- /dev/null +++ b/quokka/workers/TracerouteThread.py @@ -0,0 +1,52 @@ +import base64 +from datetime import datetime +from socket import gethostname +from threading import Thread + +from scapy.layers.inet import traceroute +from util import send_traceroute + + +class TracerouteThread(Thread): + + def __init__(self, quokka_ip, serial_no, traceroute_info): + super().__init__() + print(f"TracerouteThread: initializing thread object: traceroute_info={traceroute_info}") + + if "hostname" not in traceroute_info: + print(f"TracerouteThread: missing information in traceroute_info: {traceroute_info}") + return + + self.quokka_ip = quokka_ip + self.serial_no = serial_no + self.hostname = traceroute_info["hostname"] + self.token = traceroute_info["token"] + + def process_traceroute(self, traceroute): + + print(f"TracerouteThread: generating traceroute image: {traceroute}") + tmp_png = 'tmp-traceroute-graph.png' + traceroute.graph(format='png', target=tmp_png) + with open(tmp_png, 'rb') as png_file: + traceroute_graph_bytes = base64.b64encode(png_file.read()) + + print(f"TracerouteThread: sending traceroute: {traceroute_graph_bytes}") + status_code = send_traceroute( + gethostname(), + self.quokka_ip, + self.serial_no, + self.hostname, + self.token, + str(datetime.now())[:-1], + traceroute_graph_bytes, + ) + print(f"TracerouteThread: traceroute sent, result={status_code}\n") + + def run(self): + + print(f"TracerouteThread: starting traceroute: hostname = {self.hostname}") + + traceroute_output = traceroute(self.hostname, verbose=0) + self.process_traceroute(traceroute_output[0]) + + print(f"\n\n-----> TracerouteThread: competed traceroute") diff --git a/quokka/workers/traceroute_worker.py b/quokka/workers/traceroute_worker.py new file mode 100644 index 00000000..d59ee43e --- /dev/null +++ b/quokka/workers/traceroute_worker.py @@ -0,0 +1,39 @@ +# ---- Worker application to traceroute packets -------------------------------- + +import pika +import json +from TracerouteThread import TracerouteThread + +quokka_ip = "localhost" +serial_no = "111.111.111" + +connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) +channel = connection.channel() + +channel.queue_declare(queue='traceroute_queue', durable=True) +print('\n\n [*] Traceroute Worker: waiting for messages.') + + +def receive_traceroute_request(traceroute_channel, method, properties, body): + + print(f"traceroute worker: received message") + traceroute_info = json.loads(body) + print(f"traceroute info: {traceroute_info}") + + channel.basic_ack(delivery_tag=method.delivery_tag) + + traceroute_thread = TracerouteThread(quokka_ip, serial_no, traceroute_info) + traceroute_thread.start() + + print('\n\n [*] Traceroute Worker: waiting for messages.') + + +channel.basic_qos(prefetch_count=1) +channel.basic_consume(on_message_callback=receive_traceroute_request, queue='traceroute_queue') + +try: + channel.start_consuming() + +except KeyboardInterrupt as e: + print("\nTraceroute worker shutting down") + connection.close() diff --git a/quokka/workers/util.py b/quokka/workers/util.py index 5bd57a5a..a8a8c344 100644 --- a/quokka/workers/util.py +++ b/quokka/workers/util.py @@ -98,3 +98,24 @@ def send_portscan(source, destination, serial_no, host_ip, host_name, token, tim ) return rsp.status_code + + +def send_traceroute(source, destination, serial_no, target_name, token, timestamp, traceroute_graph_bytes): + + portscan_payload = { + "source": source, + "serial": serial_no, + "target_name": target_name, + "token": token, + "timestamp": timestamp, + "traceroute": traceroute_graph_bytes, + } + rsp = requests.post( + "http://" + destination + ":5000/portscan/store", json=portscan_payload + ) + if rsp.status_code != 200: + print( + f"{str(datetime.now())[:-3]}: Error calling /portscan/store response: {rsp.status_code}, {rsp.content}" + ) + + return rsp.status_code