├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── Sample-Config.json ├── clearEnv.sh ├── communication.py ├── database_access.py ├── database_constants.py ├── encryption.py ├── ensemble_agent ├── ensemble_api.py ├── ensemble_constants.py ├── ensemble_director ├── ensemble_enums.py ├── ensemble_logging.py ├── ensemble_web.py ├── requirements.txt ├── server_monitor.sh ├── static ├── css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap-theme.min.css.map │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── ensemble.css │ └── ol.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── images │ └── ensemble.png └── js │ ├── bingmap.js │ ├── bootstrap.min.js │ ├── chart.min.js │ ├── common.js │ ├── controls.js │ ├── elm-pep.js │ ├── jquery-3.5.1.min.js │ ├── jquery-3.6.0.min.js │ ├── jquery-3.7.0.slim.min.js │ ├── login.js │ ├── message-service.js │ ├── minified.js │ ├── npm.js │ ├── ol.js │ ├── plotly-latest.min.js │ ├── polyfill.min.js │ └── popper.min.js ├── templates ├── agenthealth.html ├── agents.html ├── createadminuser.html ├── dashboard.html ├── events.html ├── jobresults.html ├── jobs.html ├── login.html ├── newjob.html ├── pagetemplate.html ├── scheduledjobresults.html ├── scheduledjobs.html └── settings.html └── useraccess.py /.gitignore: -------------------------------------------------------------------------------- 1 | .logs/ 2 | .temp/ 3 | .jobResults/ 4 | .conf/ 5 | .vscode/ 6 | __pycache__ 7 | 8 | .config.json 9 | .ensemble_database.sqlite3 10 | 11 | cert.pem 12 | key.pem 13 | 14 | cleardata 15 | 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kalilinux/kali-last-release 2 | 3 | RUN apt-get update && apt-get upgrade -y && apt-get install -y apt-transport-https 4 | RUN apt-get install python3 -y && apt-get install python3-pip -y && apt-get install procps -y; 5 | 6 | WORKDIR /root/ 7 | 8 | CMD mkdir Ensemble 9 | 10 | WORKDIR /root/Ensemble 11 | 12 | COPY requirements.txt requirements.txt 13 | RUN pip3 install -r requirements.txt 14 | 15 | COPY ensemble_agent ensemble_agent 16 | RUN chmod +x ensemble_agent 17 | COPY communication.py communication.py 18 | COPY encryption.py encryption.py 19 | COPY ensemble_logging.py ensemble_logging.py 20 | COPY ensemble_constants.py ensemble_constants.py 21 | 22 | RUN /root/Ensemble/ensemble_agent --connection-string '{"ENCRYPTION_KEY":"", "HOST":"", "PORT":""}' 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 DNR 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Ensemble 6 | A Bug Bounty Platform that allows hunters to issue commands over a geo-distributed cluster. The ideal user is someone who is attempting to scan multiple bug bounty programs simultaneously, on a recurring basis 7 | 8 | ## Usage 9 | 10 | For every Ensemble cluster to function you will need to have at least one director and one agent. 11 | 12 | --- 13 | 14 | ### Installing Ensemble 15 | 16 | Ensemble is a cluster of machines. So at a minimum you're going to want at least two. One machine to be the director and web portal that you access and another machine that is a node in your cluster. Ideally though you would have a node for every region in the world. I used Digital Ocean for setting up a global cluster but you can use any VPS provider you'd like. 17 | 18 | 19 | This is the least amount of commands to run to start an ensemble server. The server doesn't require any extra tooling as all commands are run on the agents. Your server can and should be very light weight. 20 | 21 | 22 | ``` 23 | apt-get update 24 | apt install git -y; 25 | apt install python3 26 | apt install pipx 27 | git clone https://github.com/DotNetRussell/Ensemble.git; 28 | python3 -m venv .venv 29 | source .venv/bin/activate 30 | python3 -m pip install -r requirements.txt 31 | ``` 32 | 33 | *NOTE* Some users have experienced an issue where flask is installed but when you run ensemble it says it's not installed. I that happens just use the command below. 34 | 35 | `apt install python3-flask` 36 | 37 | --- 38 | 39 | ### Starting an Ensemble Director Server 40 | 41 | Ensemble Director is the master node of your cluster. It will not only be the web portal that you connect to and control your cluster with but it will also be the node that all other nodes in the cluster communicate with. 42 | 43 | To start a director once fully installed, run the following command 44 | `./ensemble_director --config-file ` 45 | 46 | Next, visit the IP of your director on port 5000 and create your admin account. 47 | _DO THIS IMMEDIATELY AFTER STARTING THE DIRECTOR_ 48 | `https://127.0.0.1:5000` 49 | 50 | *NOTE* You will need to use `https` protocol. It will say it's an insecure connection because the ensemble_director just generated a new unique certificate for you and it's not registered with a certificate authority. 51 | 52 | --- 53 | 54 | ### Creating an Ensemble Agent 55 | 56 | Creating an ensemble agent is relatively easy. The director has generated a new symmetric key for you and the command you need to run your agent. Just visit your Ensemble Director settings page and you will find the command you need to run. 57 | 58 | ``` 59 | apt-get update; 60 | apt install git -y; 61 | apt install python3; 62 | apt install python3-psutil; 63 | git clone https://github.com/DotNetRussell/Ensemble.git; 64 | cd Ensemble; 65 | 66 | ./ensemble_agent --connection-string '{"HOST":"","PORT":"5680","ENCRYPTION_KEY":""}' 67 | 68 | ``` 69 | 70 | 71 | *NOTE* As soon as your agent is running, it will appear on your director web portal. 72 | 73 | --- 74 | 75 | ### Navigating the Application 76 | 77 | #### Dashboard Page 78 | ![Alt text](https://i.imgur.com/eCPupxf.png) 79 | 80 | - The Dashboard is where you can easily see where your active agents are distributed in the world. 81 | - It also displays statistics about the jobs that are running and that have completed. 82 | - This is also where you create and switch workspaces. This is helpful for separating out bug bounties 83 | 84 | 85 | #### Agents Page 86 | ![Alt text](https://i.imgur.com/tLgVn75.png) 87 | 88 | - The agents page shows you statistics about your agents. This includes their health, if they're active or offline, and how many jobs they're running. 89 | 90 | #### Agent Health Page 91 | ![Alt text](https://i.imgur.com/dZMy9mx.png) 92 | 93 | - The agent health page shows you detailed information about each agent. 94 | - This includes memory consumption, remaining storage, CPU usage, log file size, job history and running processes 95 | - You also have some control over the agent on this page 96 | 97 | #### Jobs Page 98 | ![Alt text](https://i.imgur.com/Cw9DBER.png) 99 | 100 | - The jobs page shows you running and completed jobs as well as details about the jobs. 101 | - Clicking the magnified glass will navigate to the Job Results page which is an aggregation of all of the job output from all agents 102 | 103 | #### Job Results Page 104 | ![Alt text](https://i.imgur.com/ktV4Hmo.png) 105 | 106 | - The job results page shows detailed information about the job results from each agent the job ran on 107 | 108 | #### Scheduled Jobs Page 109 | ![Alt text](https://i.imgur.com/Wa5Nhrb.png) 110 | 111 | - The scheduled jobs page shows you the jobs that have been scheduled to run both recurringly as well as at a specific date and time. 112 | - Jobs that run recurringly daily/weekly/monthly, will appear under the completed scheduled jobs section and you'll be able to diff the results between runs. This is nice for finding changes in a attack surface over time. 113 | 114 | #### Create New Job Page 115 | ![Alt text](https://i.imgur.com/XE8edSy.png) 116 | 117 | - Create new job page allows users to create new jobs to run instantly, run on a recurring basis, or schedule the job to run at a specific date and time 118 | - Running a load balanced command is best used when you expect the results will be the same regardless what region you run the command in. This will take your command and distribute it across the cluster evenly based on how many targets you have. For example, if you have 4 nodes, and only one target defined, it will issue the job only to the first node. If you have 4 nodes and two targets defined, then it'll issue one target to the first node and one target to the second node. 119 | - Not running a command with the load balanced flag on means that your command and all targets will be issued to every node equally. For example, if you have 4 nodes, and one target. Then each node will run the same command against that one target. 120 | - Run as a single command will run identical to what is described previously except that it will dump all targets into a temporary file, then put the file into the command where you defined {{target}} 121 | - Templates have been added to the application by default but you can run whatever commands you'd like in the command input. You can also create your own templates for future use. 122 | 123 | #### Event Stream Page 124 | ![Alt text](https://i.imgur.com/eQKJDHD.png) 125 | 126 | - Displays the last 100 events to have taken place on the server 127 | 128 | #### Settings Page 129 | ![Alt text](https://i.imgur.com/jaCAAD2.png) 130 | 131 | - Lets users update their password as well as add and remove command templates 132 | - This is also where you will be able to retrieve the command to run your agent 133 | -------------------------------------------------------------------------------- /Sample-Config.json: -------------------------------------------------------------------------------- 1 | {"HostIp": "127.0.0.1", "AgentRegistrationPort": "5680", "AgentCommunicationPort": "5682" } -------------------------------------------------------------------------------- /clearEnv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm cert.pem 4 | rm key.pem 5 | rm .ensemble_database.sqlite3 6 | rm -rf .jobResults 7 | rm -rf .temp 8 | rm -rf .logs 9 | rm -rf .webLogs 10 | 11 | -------------------------------------------------------------------------------- /communication.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import socket 4 | import threading 5 | import ensemble_logging 6 | 7 | EMPTY_BYTES = bytes("".encode()) 8 | END_MARKER = bytes("%%END_=_MARKER%%".encode()) 9 | 10 | def remove_end_marker(data): 11 | return data.replace(END_MARKER,EMPTY_BYTES) 12 | 13 | def txrx(ip,port,message): 14 | if(ip == None or port == None or message == None): 15 | ensemble_logging.log_message(f"TXRX exception\r\nip:{ip}\r\nport:{port}\r\nmessage: {message}") 16 | return None 17 | 18 | responseBuffer = "" 19 | try: 20 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 21 | sock.settimeout(10) 22 | sock.connect((ip, port)) 23 | 24 | message += END_MARKER 25 | sock.send(message) 26 | 27 | responseBuffer = EMPTY_BYTES 28 | while True: 29 | try: 30 | responseBuffer += sock.recv(1024) 31 | if(END_MARKER in responseBuffer): 32 | responseBuffer = remove_end_marker(responseBuffer) 33 | break 34 | elif len(responseBuffer) == 0: 35 | ensemble_logging.log_message("Empty response recieved from agent") 36 | break 37 | except Exception as error: 38 | ensemble_logging.log_message(f"Incomming message exception\r\nip:{ip} port:{port}\r\n{error}") 39 | break 40 | except Exception as error: 41 | ensemble_logging.log_message(f"Incomming message exception\r\nip:{ip} port:{port}\r\n{error}") 42 | return responseBuffer 43 | 44 | def tx(ip,port,message): 45 | try: 46 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 47 | sock.connect((ip, port)) 48 | 49 | message += END_MARKER 50 | sock.send(message) 51 | except: 52 | ensemble_logging.log_message(f"Connection failed {ip}:{port}") 53 | return 54 | 55 | def start_server(bindIp, bindPort, callback): 56 | ensemble_logging.log_message(f"Starting server binding to {bindIp}:{bindPort}") 57 | serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 58 | serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 59 | serverSocket.bind((bindIp, int(bindPort))) 60 | serverSocket.listen(100) 61 | 62 | while True: 63 | try: 64 | connection, address = serverSocket.accept() 65 | request = connection.recv(1024) 66 | ensemble_logging.log_message(f"Message Received {connection} {address} {request}") 67 | threading.Thread(target=callback,args=(request,address,)) 68 | except: 69 | ensemble_logging.log_message("Issue with incoming message") 70 | 71 | 72 | -------------------------------------------------------------------------------- /encryption.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import ensemble_logging 4 | 5 | from cryptography.fernet import Fernet 6 | 7 | _key = None 8 | _FERNET = None 9 | 10 | def initialize(app_config): 11 | 12 | KEY_EXISTS = "EncryptionKey" in app_config 13 | ensemble_logging.log_message(f"Encryption key exists {KEY_EXISTS}") 14 | if(bool(KEY_EXISTS) == False): 15 | ensemble_logging.log_message("Generating new encryption key") 16 | key = Fernet.generate_key() 17 | decodedKey = key.decode('utf-8') 18 | app_config["EncryptionKey"] = decodedKey 19 | else: 20 | ensemble_logging.log_message("Loading encryption key from config") 21 | key = app_config["EncryptionKey"] 22 | 23 | set_encryption_key(key) 24 | return app_config 25 | 26 | def set_encryption_key(key): 27 | global _key 28 | global _FERNET 29 | 30 | _key = key 31 | _FERNET = Fernet(key) 32 | 33 | def encrypt_string(payload): 34 | return _FERNET.encrypt(bytes(payload.encode('utf-8'))) 35 | 36 | def decrypt_string(payload): 37 | return _FERNET.decrypt(payload).decode('utf-8') 38 | 39 | def get_agent_connection_string(hostIp, port): 40 | 41 | ensemble_logging.log_message("Building connection string for agent") 42 | decodedKey = _key 43 | agent_connection_string = f'"HOST":"{hostIp}","PORT":"{port}","ENCRYPTION_KEY":"{decodedKey}"' 44 | agent_connection_string = "{"+agent_connection_string.strip()+"}" 45 | 46 | #print("AGENT CONNECTION STRING ~~~ DO NOT SHARE") 47 | #print(f"'{agent_connection_string}'") 48 | return agent_connection_string 49 | 50 | -------------------------------------------------------------------------------- /ensemble_agent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import json 5 | import psutil 6 | import socket 7 | import base64 8 | import argparse 9 | import threading 10 | import encryption 11 | import subprocess 12 | import communication 13 | import ensemble_logging 14 | import ensemble_constants 15 | 16 | from subprocess import Popen, PIPE, STDOUT 17 | #from crontab import CronTab 18 | from datetime import datetime 19 | 20 | parser = argparse.ArgumentParser(description="Ensemble Agent") 21 | parser.add_argument("--debug", action=argparse.BooleanOptionalAction, help="Puts the agent in debug mode where all logged events are outputed to console") 22 | parser.add_argument("--connection-string", required=True) 23 | 24 | args = parser.parse_args() 25 | 26 | ensemble_logging.initialize(ensemble_constants.AGENT_LOG_FILENAME) 27 | 28 | if(args.debug != None): 29 | ensemble_logging.logLevel = 2 30 | 31 | ENCRYPTION_KEY = "" 32 | DIRECTOR_IP = "" 33 | DIRECTOR_REGISTRATION_PORT = "" 34 | lastHealthCheck = datetime.min 35 | 36 | def initialize(): 37 | # cron = CronTab(user='root') 38 | # job = cron.new(command=f'cd /root/Ensemble;/root/Ensemble/./ensemble_agent --connection-string \'{args.connection_string}\'') 39 | # job.every_reboot() 40 | # cron.write() 41 | 42 | if(os.path.isdir(ensemble_constants.LOGS_DIR) == False): 43 | run_command(f"{ensemble_constants.MAKE_DIR_COMMAND} ./{ensemble_constants.LOGS_DIR}") 44 | if(os.path.isdir(ensemble_constants.TEMP_DIR) == False): 45 | run_command(f"{ensemble_constants.MAKE_DIR_COMMAND} ./{ensemble_constants.TEMP_DIR}") 46 | if(os.path.isdir(ensemble_constants.JOB_RESULTS_DIR) == False): 47 | run_command(f"{ensemble_constants.MAKE_DIR_COMMAND} ./{ensemble_constants.JOB_RESULTS_DIR}") 48 | 49 | parse_connection_string() 50 | register_agent_with_director() 51 | 52 | threading.Thread(target=start_ensemble_agent_server).start() 53 | 54 | threading.Timer(60, check_if_still_connected).start() 55 | 56 | def check_if_still_connected(): 57 | if (datetime.now() - lastHealthCheck).total_seconds() / 60 > 60: 58 | ensemble_logging.log_message("Agent disconnected from director. Attempting to reregister") 59 | register_agent_with_director() 60 | threading.Timer(60, check_if_still_connected).start() 61 | 62 | def parse_connection_string(): 63 | ensemble_logging.log_message("Decoding connection string") 64 | connectionData = json.loads(args.connection_string) 65 | global ENCRYPTION_KEY 66 | global DIRECTOR_IP 67 | global DIRECTOR_REGISTRATION_PORT 68 | 69 | ENCRYPTION_KEY = connectionData["ENCRYPTION_KEY"].encode('utf-8') 70 | encryption.set_encryption_key(ENCRYPTION_KEY) 71 | 72 | DIRECTOR_IP = connectionData["HOST"] 73 | DIRECTOR_REGISTRATION_PORT = connectionData["PORT"] 74 | 75 | ensemble_logging.log_message("Connection string loaded") 76 | 77 | def register_agent_with_director(): 78 | message = "Agent Checking In" 79 | communication.tx(DIRECTOR_IP, int(DIRECTOR_REGISTRATION_PORT), encryption.encrypt_string(message)) 80 | 81 | def start_ensemble_agent_server(): 82 | ensemble_logging.log_message("Starting Ensemble Agent Server") 83 | bindAddress = "0.0.0.0" 84 | bindPort = 5682 85 | serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 86 | serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 87 | serverSocket.bind((bindAddress, bindPort)) 88 | serverSocket.listen(100) 89 | 90 | while True: 91 | connection, address = serverSocket.accept() 92 | request = communication.EMPTY_BYTES 93 | while True: 94 | request += connection.recv(1024) 95 | if(communication.END_MARKER in request): 96 | request = communication.remove_end_marker(request) 97 | break 98 | 99 | ensemble_logging.log_message(f"Message Received {address}") 100 | ensemble_logging.log_message("Verifying sender IP Belongs to Director") 101 | if(address[0] == DIRECTOR_IP): 102 | threading.Thread(target=handle_api_request,args=(request,connection,)).start() 103 | else: 104 | ensemble_logging.log_message(f"Sender {address} is not director...Dumping message") 105 | 106 | def handle_api_request(request,connection): 107 | ensemble_logging.log_message(f"Processing Request") 108 | decryptedRequest = encryption.decrypt_string(request) 109 | ensemble_logging.log_message(f"Decrypted Message {decryptedRequest}") 110 | lines = decryptedRequest.split("\\n") 111 | 112 | if(lines[0] == ensemble_constants.JOB_REQUEST and len(lines) == 2): 113 | ensemble_logging.log_message("Processing Job Request") 114 | process_job_request(lines[1].strip()) 115 | elif(lines[0] == ensemble_constants.JOB_STATUS and len(lines) == 2): 116 | ensemble_logging.log_message("Processing Job Status Request") 117 | process_job_status_request(lines[1].strip(),connection) 118 | elif(lines[0] == ensemble_constants.JOB_RESULTS and len(lines) == 2): 119 | ensemble_logging.log_message("Processing Job Results Request") 120 | process_job_results_request(lines[1].strip(),connection) 121 | elif(lines[0] == ensemble_constants.JOB_CANCEL and len(lines) == 2): 122 | ensemble_logging.log_message("Processing Job Cancel Request") 123 | process_job_cancel_request(lines[1].strip()) 124 | elif(lines[0] == ensemble_constants.HEALTH_REQUEST and len(lines) == 1): 125 | ensemble_logging.log_message("Processing Health Request") 126 | process_agent_health_request(connection) 127 | elif(lines[0] == ensemble_constants.CLEAR_LOGS and len(lines) == 1): 128 | ensemble_logging.log_message("Processing Clearing Logs Request") 129 | process_clear_agent_log_request() 130 | elif(lines[0] == ensemble_constants.KILL and len(lines) == 1): 131 | ensemble_logging.log_message("Processing Killing Agent Request") 132 | process_kill_agent_request() 133 | elif(lines[0] == ensemble_constants.RESTART and len(lines) == 1): 134 | ensemble_logging.log_message("Processing Restarting Agent Request") 135 | process_restart_agent_request() 136 | elif(lines[0] == ensemble_constants.STOP_JOBS and len(lines) == 1): 137 | ensemble_logging.log_message("Processing Killing Jobs Request") 138 | process_kill_jobs_request() 139 | 140 | ## Global Variables ## 141 | 142 | threadPool={} 143 | runningProcesses={} 144 | 145 | ##################### 146 | 147 | def decode_payload(payload): 148 | ensemble_logging.log_message(f"Decoding JSON Payload {payload}") 149 | return json.loads(payload) 150 | 151 | def run_command(command): 152 | return os.popen(command).read().strip() 153 | 154 | def record_process_start(id,pid): 155 | ensemble_logging.log_message(f"Process {id} started") 156 | runningProcesses[id] = pid 157 | 158 | def record_process_stop(id): 159 | ensemble_logging.log_message(f"Process {id} stopped") 160 | runningProcesses.pop(id) 161 | 162 | def run_job_command(id,command): 163 | ensemble_logging.log_message(runningProcesses) 164 | ensemble_logging.log_message(f"Running Job Command: {command}") 165 | subprocessResult = None 166 | 167 | try: 168 | result = subprocess.Popen(command, stdout=PIPE, shell=True, stderr=STDOUT, bufsize=0, close_fds=True) 169 | record_process_start(id,result.pid) 170 | ensemble_logging.log_message(f"Process started with pid {result.pid}") 171 | resultText = '' 172 | for line in result.stdout.readlines(): 173 | resultText = resultText + str(line.decode('UTF-8')) 174 | return resultText 175 | except Exception as error: 176 | return error 177 | 178 | finally: 179 | record_process_stop(id) 180 | 181 | def execute_single_command(job): 182 | ensemble_logging.log_message(f"Running Job ${job['Id']}") 183 | ensemble_logging.log_message(f"Running Command {job['Cmd']}") 184 | ensemble_logging.log_message(f"Targets {job['Targets']} ") 185 | command = job["Cmd"].replace("{{target}}",f"{ensemble_constants.TEMP_DIR}/{job['Id']}") 186 | jobResult = run_job_command(job["Id"],command) 187 | run_command(f"rm ./{ensemble_constants.TEMP_DIR}/{job['Id']}") 188 | threadPool.pop(job["Id"]) 189 | ensemble_logging.log_job_completed(job) 190 | process_job_results(job,jobResult) 191 | 192 | def execute_multi_command(job): 193 | ensemble_logging.log_message(f"Running Job ${job['Id']}") 194 | jobResult = "" 195 | for target in job["Targets"]: 196 | commandResult = run_job_command(job["Id"],job["Cmd"].replace("{{target}}",target)) 197 | jobResult += str(commandResult) 198 | threadPool.pop(job["Id"]) 199 | ensemble_logging.log_job_completed(job) 200 | process_job_results(job,jobResult) 201 | 202 | def run_job(job): 203 | if(job["IsSingleCmd"]): 204 | ensemble_logging.log_message("Running single command") 205 | ensemble_logging.log_message("Stashing Targets") 206 | for target in job["Targets"]: 207 | ensemble_logging.log_message(f"Target {target}") 208 | run_command(f"echo {target} >> .temp/{job['Id']}") 209 | 210 | newJobThread = threading.Thread(target=execute_single_command,args=(job,)) 211 | threadPool[job["Id"]]=newJobThread 212 | newJobThread.start() 213 | else: 214 | newJobThread = threading.Thread(target=execute_multi_command,args=(job,)) 215 | threadPool[job["Id"]]=newJobThread 216 | newJobThread.start() 217 | 218 | def process_job_request(jobJson): 219 | run_job(decode_payload(jobJson)) 220 | 221 | def process_job_results(job,jobResults): 222 | ensemble_logging.log_message(f"Processing job results for job {job['Id']}") 223 | encodedResults = base64.b64encode(jobResults.encode("utf-8")).decode("utf-8").strip() 224 | results = open(f"{ensemble_constants.JOB_RESULTS_DIR}/{job['Id']}", "a") 225 | results.write(encodedResults) 226 | results.close() 227 | 228 | if(args.debug != None): 229 | ensemble_logging.log_message(f"\r\n~~Job Completed~~\r\nID: {job['Id']}\r\nRESULTS: {jobResults}") 230 | 231 | def process_job_status_request(jobId, connection): 232 | 233 | if(jobId in runningProcesses and jobId in threadPool): 234 | ensemble_logging.log_message(f"Job {jobId} Not Completed Yet") 235 | ensemble_logging.log_message("Encoding Status") 236 | response = encryption.encrypt_string("0") 237 | response += communication.END_MARKER 238 | ensemble_logging.log_message(f"Returning response to connection {connection}") 239 | 240 | connection.send(response) 241 | else: 242 | ensemble_logging.log_message(f"Job {jobId} Completed") 243 | ensemble_logging.log_message("Encoding Status") 244 | response = encryption.encrypt_string("1") 245 | response += communication.END_MARKER 246 | ensemble_logging.log_message(f"Returning response to connection {connection}") 247 | 248 | connection.send(response) 249 | 250 | def process_job_results_request(jobId, connection): 251 | ensemble_logging.log_message(f"Checking for job {jobId} status") 252 | if(jobId not in runningProcesses and jobId not in threadPool): 253 | ensemble_logging.log_message(f"Job {jobId} completed") 254 | ensemble_logging.log_message(f"Checking for job {jobId} results") 255 | if(os.path.exists(f"{ensemble_constants.JOB_RESULTS_DIR}/{jobId}")): 256 | ensemble_logging.log_message(f"Job results found for job {jobId}") 257 | with open(f"{ensemble_constants.JOB_RESULTS_DIR}/{jobId}", "r") as results: 258 | ensemble_logging.log_message(f"Sending job {jobId} results to ensemble director") 259 | result = encryption.encrypt_string(results.read()) 260 | result += communication.END_MARKER 261 | 262 | connection.send(result) 263 | 264 | run_command(f"rm {ensemble_constants.JOB_RESULTS_DIR}/{jobId}") 265 | 266 | def process_job_cancel_request(jobId): 267 | ensemble_logging.log_message(f"Attempting to find job {jobId}") 268 | if(jobId in runningProcesses and jobId in threadPool): 269 | ensemble_logging.log_message(f"Job {jobId} found, attempting to cancel") 270 | threadPool[jobId].stop() 271 | threadPool.pop(jobId) 272 | runningProcesses.pop(jobId) 273 | 274 | jobStillRunning = jobId in threadPool or jobId in runningProcesses 275 | ensemble_logging.log_message(f"Job Canceled: {jobStillRunning}") 276 | 277 | def process_agent_health_request(connection): 278 | global lastHealthCheck 279 | lastHealthCheck = datetime.now() 280 | 281 | meminfo = psutil.virtual_memory() 282 | storageinfo = psutil.disk_usage('/') 283 | cpuinfo = psutil.cpu_percent() 284 | logSize = run_command(ensemble_constants.STAT_COMMAND) 285 | runningProcesses = run_command(ensemble_constants.PROCESS_COMMAND) 286 | healthReport = { 287 | "MemInfo": meminfo, 288 | "StgInfo": storageinfo, 289 | "CpuInfo": cpuinfo, 290 | "LogSize": logSize, 291 | "RunningProcesses" : runningProcesses 292 | } 293 | 294 | ensemble_logging.log_message(f"Health Request Processed") 295 | ensemble_logging.log_message("Encoding Health Request") 296 | response = encryption.encrypt_string(json.dumps(healthReport)) 297 | response += communication.END_MARKER 298 | ensemble_logging.log_message(f"Returning response to connection {connection}") 299 | 300 | connection.send(response) 301 | 302 | def process_clear_agent_log_request(): 303 | run_command(f"true > {ensemble_constants.LOGS_DIR}/{ensemble_constants.AGENT_LOG_FILENAME}") 304 | 305 | def process_kill_agent_request(): 306 | run_command(ensemble_constants.SHUTDOWN_COMMAND) 307 | 308 | def process_restart_agent_request(): 309 | run_command(ensemble_constants.REBOOT_COMMAND) 310 | 311 | def process_kill_jobs_request(): 312 | for id in runningProcesses: 313 | ensemble_logging.log_message(f"Killing pid {runningProcesses[id]}") 314 | run_command(f"{ensemble_constants.KILL_COMMAND} {runningProcesses[id]}") 315 | 316 | initialize() 317 | -------------------------------------------------------------------------------- /ensemble_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import time 4 | import json 5 | import base64 6 | import useraccess 7 | import ensemble_enums 8 | import database_access 9 | import ensemble_logging 10 | import ensemble_constants 11 | 12 | from base64 import b64decode 13 | from flask import Blueprint, jsonify, request 14 | 15 | ensemble_logging.initialize(ensemble_constants.WEB_LOG_FILENAME) 16 | ensemble_logging.logLevel = 1 17 | 18 | ensemble_api = Blueprint('ensemble_api', __name__, template_folder='templates') 19 | JSON_CONTENT_TYPE = {ensemble_constants.CONTENT_TYPE_HEADER:ensemble_constants.APPLICATION_JSON} 20 | UNAUTHORIZED = "", ensemble_constants.UNAUTHORIZED, JSON_CONTENT_TYPE 21 | SUCCESS = json.dumps({'success':True}), ensemble_constants.SUCCESS, JSON_CONTENT_TYPE 22 | FAILED = json.dumps({'success':False}), ensemble_constants.SERVER_ERROR, JSON_CONTENT_TYPE 23 | 24 | ### HELPER FUNCTIONS ### 25 | 26 | _session = [] 27 | 28 | def check_logged_in(): 29 | return ensemble_constants.USER_TOKEN in _session 30 | 31 | def set_session(session): 32 | global _session 33 | _session = session 34 | 35 | ##### API CALLS ##### 36 | 37 | @ensemble_api.route(ensemble_constants.API_LOGSTREAM, methods=[ensemble_constants.GET]) 38 | def LogStream(): 39 | try: 40 | if check_logged_in(): 41 | response = jsonify(database_access.get_all_stream_event()) 42 | database_access.clear_all_messages() 43 | return response 44 | else: 45 | return UNAUTHORIZED 46 | except Exception as error: 47 | ensemble_logging.log_message(f"{ensemble_constants.API_LOGSTREAM} failed with error {error}") 48 | return FAILED 49 | 50 | @ensemble_api.route(ensemble_constants.API_JOBS, methods=[ensemble_constants.GET]) 51 | def Jobs(): 52 | try: 53 | if check_logged_in(): 54 | response = jsonify(database_access.get_all_jobs(_session[ensemble_constants.CURRENT_WORKSPACE_ID_TOKEN])) 55 | return response 56 | else: 57 | return UNAUTHORIZED 58 | except Exception as error: 59 | ensemble_logging.log_message(f"{ensemble_constants.API_JOBS} failed with error {error}") 60 | return FAILED 61 | 62 | @ensemble_api.route(ensemble_constants.API_SCHEDULED_JOB_RESULT_INFO, methods=[ensemble_constants.GET]) 63 | def ScheduledJobResultInfo(): 64 | try: 65 | if check_logged_in(): 66 | jobResults = database_access.get_scheduled_job_results_by_scheduled_workspace_id(_session[ensemble_constants.CURRENT_WORKSPACE_ID_TOKEN]) 67 | 68 | responseViewModel = [] 69 | for jobResult in jobResults: 70 | responseViewModel.append({ 71 | 'AgentId':jobResult['agentId'], 72 | 'JobId':jobResult['jobId'], 73 | 'ScheduledJobId':jobResult['scheduledJobId'], 74 | 'JobRunDate':jobResult['jobRunDateTime'] 75 | }) 76 | 77 | return responseViewModel 78 | else: 79 | return UNAUTHORIZED 80 | except Exception as error: 81 | ensemble_logging.log_message(f"{ensemble_constants.API_JOBS} failed with error {error}") 82 | return FAILED 83 | 84 | @ensemble_api.route(ensemble_constants.API_SCHEDULED_JOBS, methods=[ensemble_constants.GET]) 85 | def ScheduledJobs(): 86 | try: 87 | if check_logged_in(): 88 | response = jsonify(database_access.get_scheduled_jobs_by_workspace_id(_session[ensemble_constants.CURRENT_WORKSPACE_ID_TOKEN])) 89 | return response 90 | else: 91 | return UNAUTHORIZED 92 | except Exception as error: 93 | ensemble_logging.log_message(f"{ensemble_constants.API_JOBS} failed with error {error}") 94 | return FAILED 95 | 96 | @ensemble_api.route(ensemble_constants.API_AGENTS, methods=[ensemble_constants.GET]) 97 | def Agents(): 98 | try: 99 | if check_logged_in(): 100 | agents = [] 101 | agentsData = database_access.get_all_agents_and_health_records() 102 | 103 | for agent in agentsData: 104 | memPct = float(agent['MemPct'])/100 105 | memHealthScore = 0 106 | if memPct <= .3: 107 | memHealthScore = .4 108 | elif memPct > .3 and memPct <= .6: 109 | memHealthScore = .2 110 | elif memPct > .6 <= .9: 111 | memHealthScore = .1 112 | else: 113 | memHealthScore = 0 114 | 115 | storagePct = float(agent['StoragePct'])/100 116 | storageHealthScore = 0 117 | if storagePct <= .3: 118 | storageHealthScore = .4 119 | elif storagePct > .3 and storagePct <= .6: 120 | storageHealthScore = .2 121 | elif storagePct > .6 <= .9: 122 | storageHealthScore = .1 123 | else: 124 | storageHealthScore = 0 125 | 126 | cpuPct = float(agent['CpuPct'])/100 127 | cpuHealthScore = 0 128 | if cpuPct <= .7: 129 | cpuHealthScore = .2 130 | elif cpuPct > .7 <= .9: 131 | cpuHealthScore = .1 132 | else: 133 | cpuHealthScore = 0 134 | 135 | agents.append({ 136 | 'Id': agent['Id'], 137 | 'AgentIpAddress': agent['IpAddress'], 138 | 'IsActive': bool(agent['IsActive']), 139 | 'HealthPercent': float(memHealthScore + storageHealthScore + cpuHealthScore), 140 | 'LastCheckinTime': agent['LastReportTime'], 141 | 'ActiveJobCount': agent['ActiveJobCount'] 142 | }) 143 | response = jsonify(agents) 144 | return response 145 | else: 146 | return UNAUTHORIZED 147 | except Exception as error: 148 | ensemble_logging.log_message(f"{ensemble_constants.API_AGENTS} failed with error {error}") 149 | return FAILED 150 | 151 | 152 | @ensemble_api.route(ensemble_constants.API_AGENT_HEALTH, methods=[ensemble_constants.GET]) 153 | def AgentHealth(): 154 | try: 155 | if check_logged_in(): 156 | id = request.args.get(ensemble_constants.ID_ARG, default=1, type=int) 157 | response = jsonify(database_access.get_agent_and_health_record_by_id(id)) 158 | return response 159 | else: 160 | return UNAUTHORIZED 161 | except Exception as error: 162 | ensemble_logging.log_message(f"{ensemble_constants.API_AGENT_HEALTH} failed with error {error}") 163 | return FAILED 164 | 165 | 166 | @ensemble_api.route(ensemble_constants.API_ADD_TARGET, methods=[ensemble_constants.GET, ensemble_constants.POST]) 167 | def AddTarget(): 168 | try: 169 | if check_logged_in(): 170 | 171 | if request.method == ensemble_constants.GET: 172 | target = request.args.get(ensemble_constants.TARGET_ARG, default="", type=str) 173 | database_access.insert_target(target) 174 | database_access.add_message({"MessageType":ensemble_enums.MessageType.NOTIFICATION.value, "Message": f"{target} added to targets list"}) 175 | 176 | elif request.method == ensemble_constants.POST: 177 | data = request.data 178 | targets = json.loads(data) 179 | 180 | for target in targets: 181 | targetExists = database_access.get_target_by_target(target) 182 | if(targetExists is None): 183 | database_access.insert_target(target) 184 | 185 | return SUCCESS 186 | else: 187 | return UNAUTHORIZED 188 | except Exception as error: 189 | ensemble_logging.log_message(f"{ensemble_constants.API_ADD_TARGET} failed with error {error}") 190 | return FAILED 191 | 192 | 193 | @ensemble_api.route(ensemble_constants.API_REMOVE_TARGET, methods=[ensemble_constants.GET, ensemble_constants.POST]) 194 | def RemoveTarget(): 195 | try: 196 | if check_logged_in(): 197 | if request.method == ensemble_constants.GET: 198 | target = request.args.get(ensemble_constants.TARGET_ARG, default="", type=str) 199 | 200 | targetExists = database_access.get_target_by_target(target) 201 | if(targetExists is None): 202 | database_access.insert_target(target) 203 | 204 | database_access.ignore_target_by_target(target) 205 | database_access.add_message({"MessageType":ensemble_enums.MessageType.NOTIFICATION.value, "Message": f"{target} removed from targets list"}) 206 | 207 | elif request.method == ensemble_constants.POST: 208 | data = request.data 209 | targets = json.loads(data) 210 | 211 | for target in targets: 212 | targetExists = database_access.get_target_by_target(target) 213 | if(targetExists is None): 214 | database_access.insert_target(target) 215 | 216 | database_access.batch_ignore_target_by_target(targets) 217 | 218 | return SUCCESS 219 | else: 220 | return UNAUTHORIZED 221 | except Exception as error: 222 | ensemble_logging.log_message(f"{ensemble_constants.API_ADD_TARGET} failed with error {error}") 223 | return FAILED 224 | 225 | @ensemble_api.route(ensemble_constants.API_UPDATE_PASSWORD, methods=[ensemble_constants.POST]) 226 | def UpdatePassword(): 227 | try: 228 | if check_logged_in(): 229 | passwordJson = json.loads(request.data) 230 | oldPass = passwordJson["old"] 231 | newPass = passwordJson["new"] 232 | 233 | sessionUsername = _session[ensemble_constants.USER_TOKEN] 234 | user = database_access.get_user_by_username(sessionUsername) 235 | 236 | hashedPassword = useraccess.salt_password(oldPass, user['Salt']) 237 | 238 | if(hashedPassword == user['PasswordHash']): 239 | database_access.update_user_password(sessionUsername, useraccess.salt_password(newPass, user['Salt'])) 240 | return SUCCESS 241 | else: 242 | return UNAUTHORIZED 243 | else: 244 | return UNAUTHORIZED 245 | 246 | except Exception as error: 247 | ensemble_logging.log_message(f"{ensemble_constants.API_UPDATE_PASSWORD} failed with error {error}") 248 | return FAILED 249 | 250 | @ensemble_api.route(ensemble_constants.API_GET_SERVER_TIME, methods=[ensemble_constants.GET]) 251 | def getTime(): 252 | if check_logged_in(): 253 | return time.strftime('%H:%M:%S') 254 | else: 255 | return UNAUTHORIZED 256 | 257 | @ensemble_api.route(ensemble_constants.API_GET_COMMAND_TEMPLATES, methods=[ensemble_constants.GET]) 258 | def getCommandTemplates(): 259 | try: 260 | if check_logged_in(): 261 | return jsonify(database_access.get_all_command_template()) 262 | else: 263 | return UNAUTHORIZED 264 | except Exception as error: 265 | return UNAUTHORIZED 266 | 267 | @ensemble_api.route(ensemble_constants.API_ADD_COMMAND_TEMPLATES, methods=[ensemble_constants.POST]) 268 | def addCommandTemplates(): 269 | try: 270 | if check_logged_in() and request.method == ensemble_constants.POST: 271 | commandTemplate = json.loads(request.data) 272 | database_access.add_command_template(commandTemplate) 273 | return SUCCESS 274 | else: 275 | return UNAUTHORIZED 276 | except Exception as error: 277 | return UNAUTHORIZED 278 | 279 | 280 | @ensemble_api.route(ensemble_constants.API_DELETE_COMMAND_TEMPLATES, methods=[ensemble_constants.GET]) 281 | def deleteCommandTemplates(): 282 | try: 283 | if check_logged_in(): 284 | commandTemplateId = request.args.get(ensemble_constants.ID_ARG, default="", type=int) 285 | database_access.delete_command_template_by_id(commandTemplateId) 286 | return SUCCESS 287 | else: 288 | return UNAUTHORIZED 289 | except Exception as error: 290 | return UNAUTHORIZED 291 | 292 | @ensemble_api.route(ensemble_constants.API_DELETE_SCHEDULED_JOB, methods=[ensemble_constants.GET]) 293 | def deleteScheduledJob(): 294 | if check_logged_in(): 295 | if request.method == ensemble_constants.GET: 296 | target = request.args.get(ensemble_constants.ID_ARG, default="", type=str) 297 | if(target != ''): 298 | database_access.remove_scheduled_job(target) 299 | return SUCCESS 300 | else: 301 | return UNAUTHORIZED -------------------------------------------------------------------------------- /ensemble_constants.py: -------------------------------------------------------------------------------- 1 | MESSAGE_BREAK = "\\n" 2 | JOB_REQUEST = "JOBREQUEST" 3 | JOB_STATUS = "JOBSTATUS" 4 | JOB_RESULTS = "JOBRESULTS" 5 | JOB_CANCEL = "JOBCANCEL" 6 | HEALTH_REQUEST = "HEALTHREQUEST" 7 | CLEAR_LOGS = "CLEARLOGS" 8 | KILL = "KILL" 9 | RESTART = "RESTART" 10 | STOP_JOBS = "STOPJOBS" 11 | 12 | 13 | TEMP_DIR = ".temp" 14 | LOGS_DIR = ".logs" 15 | JOB_RESULTS_DIR = ".jobResults" 16 | WEB_LOGS_DIR = ".web_logs" 17 | 18 | WEB_LOG_FILENAME = "web_log" 19 | AGENT_LOG_FILENAME = "agent_log" 20 | DIRECTOR_LOG_FILENAME = "director_log" 21 | CERT_PEM_FILENAME = "cert.pem" 22 | KEY_PEM_FILENAME = "key.pem" 23 | 24 | ENSEMBLE_WEB_FILE = "ensemble_web.py" 25 | 26 | ECHO_COMMAND = "echo" 27 | STAT_COMMAND = "stat --printf='%s' .logs/agent_log" 28 | PROCESS_COMMAND = "ps -aux" 29 | SHUTDOWN_COMMAND = "shutdown" 30 | REBOOT_COMMAND = "reboot" 31 | KILL_COMMAND = "kill -9" 32 | MAKE_DIR_COMMAND = "mkdir" 33 | OPENSSL_GENERATE_CERT_AND_KEY_COMMAND = """openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365 -subj "/C=UA/ST=Kharkov/L=Kharkov/O=NSA/OU=IT Department/CN=ensemble_director.com" """ 34 | 35 | 36 | CONFIG_FILE_HOST_IP = "HostIp" 37 | CONFIG_FILE_AGENT_REG_PORT = "AgentRegistrationPort" 38 | CONFIG_FILE_AGENT_COM_PORT = "AgentCommunicationPort" 39 | 40 | NA = "n/a" 41 | UTF8 = "utf-8" 42 | 43 | CLEAR_COMPLETE_JOBS_COMMAND = "Clear" 44 | KILL_ALL_JOBS_COMMAND = "Kill" 45 | STOP_ALL_JOBS = "StopJobs" 46 | RESTART_AGENT = "Restart" 47 | KILL_AGENT = "Kill" 48 | 49 | ID_ARG = "id" 50 | SET_WORKSPACE_ARG = "setWorkspace" 51 | CMD_ARG = "cmd" 52 | AGENT_ID_ARG = "agentId" 53 | DUPLICATE_ARG = "duplicate" 54 | DISSMISS_ARG = "dismiss" 55 | TARGET_ARG = "target" 56 | TARGETS_ARG = "targets" 57 | SINGLE_COMMAND_ARG = "isSingleCmd" 58 | IS_LOADBALANCED_ARG = "isLoadBalanced" 59 | RUN_TIME_ARG = "runTime" 60 | RUN_DATE_TIME_ARG = "runDateTime" 61 | RUN_TYPE_ARG = "runType" 62 | SCHEDULED_JOB_ARG = "scheduledJob" 63 | JOB_DATA_ARG = "jobData" 64 | 65 | USER_TOKEN = "user" 66 | CURRENT_WORKSPACE_ID_TOKEN = "currentWorkspaceId" 67 | LOCATION_HEADER = "location" 68 | 69 | GET = "GET" 70 | POST = "POST" 71 | PUT = "PUT" 72 | DELETE = "DELETE" 73 | CONTENT_TYPE_HEADER = "ContentType" 74 | APPLICATION_JSON = "application/json" 75 | SUCCESS = 200 76 | UNAUTHORIZED = 401 77 | SERVER_ERROR = 500 78 | 79 | ROOT_WEB_DIR = "/" 80 | API_PATH = "/api" 81 | API_LOGSTREAM = API_PATH + "/logstream" 82 | API_JOBS = API_PATH + "/jobs" 83 | API_SCHEDULED_JOBS = API_PATH + "/scheduled_jobs" 84 | API_SCHEDULED_JOB_RESULT_INFO = API_PATH + "/scheduledjobresultinfo" 85 | API_SCHEDULED_JOB_RESULTS = API_PATH + "/scheduledjobresults" 86 | API_AGENTS = API_PATH + "/agents" 87 | API_AGENT_HEALTH = API_PATH + "/agent_health" 88 | API_ADD_TARGET = API_PATH + "/addTarget" 89 | API_REMOVE_TARGET = API_PATH + "/removeTarget" 90 | API_UPDATE_PASSWORD = API_PATH + "/updatePassword" 91 | API_GET_SERVER_TIME = API_PATH + "/getTime" 92 | API_DELETE_SCHEDULED_JOB = API_PATH + "/deletescheduledjob" 93 | API_GET_COMMAND_TEMPLATES = API_PATH + "/getCommandTemplates" 94 | API_ADD_COMMAND_TEMPLATES = API_PATH + "/addCommandTemplates" 95 | API_DELETE_COMMAND_TEMPLATES = API_PATH + "/deleteCommandTemplates" 96 | 97 | DASHBOARD_PATH = "/dashboard" 98 | WORKSPACE_PATH = "/workspace" 99 | AGENT_HEALTH_PATH = "/agenthealth" 100 | AGENTS_PATH = "/agents" 101 | JOB_RESULTS_PATH = "/jobresults" 102 | JOBS_PATH = "/jobs" 103 | SCHEDULED_JOB_RESULTS_PATH = "/scheduledjobresults" 104 | SCHEDULED_JOBS_PATH = "/scheduledjobs" 105 | AGENT_COMMANDS_PATH = "/agentCommands" 106 | NEW_JOB_PATH = "/newjob" 107 | STREAM_EVENTS_PATH = "/streamevents" 108 | MESSAGES_PATH = "/messages" 109 | SETTINGS_PATH = "/settings" 110 | LOGOUT_PATH = "/logout" 111 | 112 | LOGIN_PAGE = "login.html" 113 | CREATE_ADMIN_PAGE = "createadminuser.html" 114 | DASHBOARD_PAGE = "dashboard.html" 115 | AGENT_HEALTH_PAGE = "agenthealth.html" 116 | AGENTS_PAGE = "agents.html" 117 | JOBS_PAGE = "jobs.html" 118 | SCHEDULED_JOBS_PAGE = "scheduledjobs.html" 119 | SCHEDULED_JOB_RESULTS_PAGE = "scheduledjobresults.html" 120 | JOB_RESULTS_PAGE = "jobresults.html" 121 | NEW_JOB_PAGE = "newjob.html" 122 | STREAM_EVENTS_PAGE = "events.html" 123 | SETTINGS_PAGE = "settings.html" -------------------------------------------------------------------------------- /ensemble_director: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import json 5 | import uuid 6 | import copy 7 | import time 8 | import socket 9 | import datetime 10 | import argparse 11 | import threading 12 | import encryption 13 | import communication 14 | import ensemble_enums 15 | import database_access 16 | import ensemble_logging 17 | import ensemble_constants 18 | import dateutil.parser as parser 19 | 20 | from time import sleep 21 | from ast import literal_eval 22 | 23 | 24 | parser = argparse.ArgumentParser(description="Ensemble Agent") 25 | parser.add_argument("--debug", action=argparse.BooleanOptionalAction, help="Puts the agent in debug mode where all logged events are outputed to console") 26 | parser.add_argument("--config-file", required=True, help="Ensemble config file. Required to run the application. Please see github readme for more information") 27 | args = parser.parse_args() 28 | 29 | if(args.debug != None): 30 | ensemble_logging.logLevel = 2 31 | 32 | #Config file loaded from disk 33 | APP_CONFIG = {} 34 | 35 | #The ip address that the director will listen on 36 | HOST_IP = '' 37 | 38 | #Port that agents call in on to register 39 | AGENT_REGISTRATION_PORT = 0 40 | 41 | KEY_EXISTS = False 42 | agents={} 43 | agent_jobs = {} 44 | 45 | def run_command(command): 46 | ensemble_logging.log_message(f"Running Command: {command}") 47 | process = os.popen(command) 48 | ensemble_logging.log_message(f"Process started with pid {process}") 49 | database_access.insert_stream_event({"EventType":ensemble_enums.StreamEvent.Director.value, "Event":f"Running Command: {command}", "EventId":process}) 50 | 51 | result = process.read() 52 | return result.strip() 53 | 54 | def initialize(): 55 | global APP_CONFIG 56 | 57 | if(os.path.exists(ensemble_constants.LOGS_DIR) == False): 58 | os.popen(f"{ensemble_constants.MAKE_DIR_COMMAND} ./{ensemble_constants.LOGS_DIR}") 59 | ensemble_logging.log_message("Creating log directory") 60 | 61 | if(os.path.exists(f"./{ensemble_constants.CERT_PEM_FILENAME}") == False or os.path.exists(f"./{ensemble_constants.KEY_PEM_FILENAME}") == False): 62 | #generate cert and key for web portal: 63 | run_command(ensemble_constants.OPENSSL_GENERATE_CERT_AND_KEY_COMMAND) 64 | 65 | # if(ensemble_logging.logLevel == 1): 66 | # ensemble_logging.log_message("Starting Ensemble Web Server") 67 | # threading.Thread(target=run_command,args=(f"./{ensemble_constants.ENSEMBLE_WEB_FILE}",)).start() 68 | # else: 69 | # ensemble_logging.log_message("Starting Ensemble Web Server with Debugging Enabled") 70 | # threading.Thread(target=run_command, args=(f"./{ensemble_constants.ENSEMBLE_WEB_FILE} --debug",), daemon=True).start() 71 | 72 | 73 | ensemble_logging.log_message("PEMs created") 74 | ensemble_logging.log_message("Importing config file") 75 | import_config() 76 | 77 | #initialize the encryption lib with the key from the config or a new key 78 | APP_CONFIG = encryption.initialize(APP_CONFIG) 79 | 80 | database_access.initialize() 81 | 82 | #write updated config to disk 83 | write_config_to_disk() 84 | 85 | check_for_new_jobs() 86 | #register_api_callbacks() 87 | threading.Timer(5.0,check_job_status).start() 88 | threading.Thread(target=send_agent_health_check).start() 89 | threading.Thread(target=start_agent_listener_server).start() 90 | threading.Thread(target=check_for_agent_commands).start() 91 | threading.Thread(target=check_for_scheduled_jobs).start() 92 | 93 | #Writes the config file to disk at the location provided on startup 94 | def write_config_to_disk(): 95 | config = open(args.config_file,"w") 96 | config.write(json.dumps(APP_CONFIG)) 97 | config.close() 98 | 99 | #Imports the config file from the path provided on startup 100 | def import_config(): 101 | 102 | global APP_CONFIG 103 | global HOST_IP 104 | global AGENT_REGISTRATION_PORT 105 | 106 | if(args.config_file == None): 107 | ensemble_logging.log_message(f"Unable to find {args.config_file} in the working directory. Cannot continue without a config file") 108 | exit() 109 | 110 | APP_CONFIG = json.loads(open(args.config_file,"r").read()) 111 | 112 | if(ensemble_constants.CONFIG_FILE_HOST_IP not in APP_CONFIG 113 | or ensemble_constants.CONFIG_FILE_AGENT_REG_PORT not in APP_CONFIG 114 | or ensemble_constants.CONFIG_FILE_AGENT_COM_PORT not in APP_CONFIG): 115 | ensemble_logging.log_message(f"Unable to find all required fields in the config file. Config must contain '{ensemble_constants.CONFIG_FILE_HOST_IP}', '{ensemble_constants.CONFIG_FILE_AGENT_REG_PORT}', and '{ensemble_constants.CONFIG_FILE_AGENT_COM_PORT}' at a minimum") 116 | exit() 117 | 118 | HOST_IP = APP_CONFIG[ensemble_constants.CONFIG_FILE_HOST_IP] 119 | AGENT_REGISTRATION_PORT = APP_CONFIG[ensemble_constants.CONFIG_FILE_AGENT_REG_PORT] 120 | 121 | #Starts the agent registration listening server 122 | def start_agent_listener_server(): 123 | 124 | ensemble_logging.log_message("Starting Ensemble Agent Listening Server") 125 | bindAddress = HOST_IP 126 | bindPort = int(AGENT_REGISTRATION_PORT) 127 | serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 128 | serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 129 | ensemble_logging.log_message(f"Agent Listener Server Binding to {bindAddress} on port {bindPort}") 130 | serverSocket.bind((bindAddress, bindPort)) 131 | serverSocket.listen(100) 132 | database_access.insert_stream_event({"EventType":ensemble_enums.StreamEvent.Director.value, "Event":f"Agent Listener Server Binding to {bindAddress} on port {bindPort}", "EventId":""}) 133 | 134 | while True: 135 | connection, address = serverSocket.accept() 136 | request = connection.recv(1024) 137 | ensemble_logging.log_message(f"Message Received {connection} {address} {request}") 138 | threading.Thread(target=handle_agent_registration_request,args=(request,address,)).start() 139 | 140 | #Handles an agent registration request 141 | def handle_agent_registration_request(request, address): 142 | try: 143 | database_access.register_agent(address[0]) 144 | ensemble_logging.log_message(encryption.decrypt_string(request)) 145 | ensemble_logging.log_message(f"Agent at {address[0]} registered") 146 | ensemble_logging.log_message(f"{database_access.get_agent_count()} Total Agents Online") 147 | database_access.add_message({"MessageType":ensemble_enums.MessageType.NOTIFICATION.value, "Message": f"Agent Online:{address[0]}"}) 148 | database_access.insert_stream_event({"EventType":ensemble_enums.StreamEvent.Agent.value, "Event":f"Agent at {address[0]} registered", "EventId":address[0]}) 149 | except: 150 | ensemble_logging.log_message("Bad agent request received") 151 | 152 | #Converts a job to string 153 | def job_to_string(job): 154 | return ensemble_constants.JOB_REQUEST + ensemble_constants.MESSAGE_BREAK + json.dumps(job) 155 | 156 | #Handles a job request from the api POSSIBLY DEPRICATED 157 | def handle_job_result_api_request(request): 158 | 159 | results = {} 160 | if(request in agent_jobs): 161 | 162 | for agentIp in agent_jobs[request]: 163 | result = send_job_result_request(agentIp,request) 164 | results[agentIp] = result 165 | 166 | return str(results) 167 | 168 | #Sends a admin command to the agent 169 | def send_agent_command_log_request(agentIp,command): 170 | commandPayload = "" 171 | if(ensemble_constants.CLEAR_LOGS in command): 172 | commandPayload = ensemble_constants.CLEAR_LOGS 173 | database_access.add_message({"MessageType":ensemble_enums.MessageType.INFORMATION.value, "Message": f"Clear log command sent to {agentIp}"}) 174 | elif(ensemble_constants.RESTART in command): 175 | commandPayload = ensemble_constants.RESTART 176 | database_access.add_message({"MessageType":ensemble_enums.MessageType.INFORMATION.value, "Message": f"Restart command sent to {agentIp}"}) 177 | elif(ensemble_constants.STOP_JOBS in command): 178 | commandPayload = ensemble_constants.STOP_JOBS 179 | database_access.add_message({"MessageType":ensemble_enums.MessageType.INFORMATION.value, "Message": f"Stop jobs command sent to {agentIp}"}) 180 | elif(ensemble_constants.KILL in command): 181 | commandPayload = ensemble_constants.KILL 182 | database_access.add_message({"MessageType":ensemble_enums.MessageType.INFORMATION.value, "Message": f"Kill command sent to {agentIp}"}) 183 | 184 | encryptedPayload = encryption.encrypt_string(commandPayload) 185 | communication.tx(agentIp,int(5682),encryptedPayload) 186 | 187 | #Sends a request for a job result to the agent ip passed in 188 | def send_job_result_request(agentIp,id): 189 | payload = ensemble_constants.JOB_RESULTS + ensemble_constants.MESSAGE_BREAK + str(id) 190 | ensemble_logging.log_message(f"Payload {payload}") 191 | 192 | encryptedPayload = encryption.encrypt_string(payload) 193 | ensemble_logging.log_message(f"Sending Job Result Request {encryptedPayload}") 194 | 195 | response = communication.txrx(agentIp,int(5682),encryptedPayload) 196 | decryptedResponse = encryption.decrypt_string(response) 197 | 198 | ensemble_logging.log_message(f"Result for job {id} is {decryptedResponse}") 199 | 200 | return decryptedResponse 201 | 202 | JOB_STATUS_CHECKER_RUNNING = False 203 | def check_job_status(): 204 | try: 205 | global JOB_STATUS_CHECKER_RUNNING 206 | JOB_STATUS_CHECKER_RUNNING = True 207 | 208 | runningJobs = database_access.get_all_running_jobs() 209 | 210 | for runningJob in runningJobs: 211 | agentIp = runningJob["AgentIp"] 212 | jobId = runningJob["JobId"] 213 | 214 | status = send_job_status_request(agentIp, jobId) 215 | 216 | if(int(status) == 1): 217 | ensemble_logging.log_message(f"Job completed for {agentIp}") 218 | database_access.insert_stream_event({"EventType":ensemble_enums.StreamEvent.Job.value, "Event":f"Job completed for {agentIp}", "EventId":jobId}) 219 | 220 | result = send_job_result_request(agentIp, jobId) 221 | 222 | database_access.update_job_result(agentIp,jobId,result) 223 | database_access.add_message({"MessageType":ensemble_enums.MessageType.NOTIFICATION.value, "Message": f"Job Completed for Job ID:{jobId}"}) 224 | runningJobs.remove(runningJob) 225 | else: 226 | ensemble_logging.log_message(f"Job not completed yet for {agentIp}") 227 | except Exception as error: 228 | ensemble_logging.log_message(f"Error while checking job status {error}") 229 | database_access.add_message({"MessageType":ensemble_enums.MessageType.WARNING.value, "Message": f"Job Failed for Job ID:{jobId}"}) 230 | #runningJobs.remove(runningJob) 231 | #retry_job(agentIp, jobId) 232 | finally: 233 | threading.Timer(5.0,check_job_status).start() 234 | 235 | def retry_job(agentIp,jobId): 236 | database_access.add_message({"MessageType":ensemble_enums.MessageType.INFORMATION.value, "Message": f"Retrying Job ID:{jobId}"}) 237 | database_access.update_job_reset_job_status(jobId) 238 | 239 | def send_job_status_request(agentIp,id): 240 | payload = ensemble_constants.JOB_STATUS + ensemble_constants.MESSAGE_BREAK + str(id) 241 | payload = encryption.encrypt_string(payload) 242 | ensemble_logging.log_message(f"Sending Job Status Request {payload}") 243 | 244 | response = encryption.decrypt_string(communication.txrx(agentIp,int(5682), payload)) 245 | 246 | ensemble_logging.log_message(f"Status for job {id} is {response}") 247 | return response 248 | 249 | def handle_start_job_request_api_request(request): 250 | job = json.loads(request) 251 | database_access.queue_job_request(job) 252 | return 253 | 254 | def check_for_new_jobs(): 255 | try: 256 | jobs = database_access.check_for_new_jobs() 257 | if(len(jobs) > 0): 258 | ensemble_logging.log_message(f"Jobs found {jobs}") 259 | database_access.insert_stream_event({"EventType":ensemble_enums.StreamEvent.Director.value, "Event":f"Jobs found {jobs}", "EventId":""}) 260 | 261 | for j in jobs: 262 | if(len(j) >= 5): 263 | job = { 264 | "Id": j[1], 265 | "Cmd": j[2], 266 | "Targets": literal_eval(j[3]), 267 | "IsSingleCmd": j[4] 268 | } 269 | 270 | isLoadBalanced = bool(j[7]) 271 | if(isLoadBalanced): 272 | start_job_request_request_load_balanced(job) 273 | else: 274 | start_job_request_all_agents(job) 275 | else: 276 | ensemble_logging.log_message(f'Bad row in job results {j}') 277 | except Exception as error: 278 | ensemble_logging.log_message(f'Error while checking for new jobs {error}') 279 | finally: 280 | threading.Timer(5.0,check_for_new_jobs).start() 281 | 282 | def start_job_request_all_agents(job): 283 | agents = database_access.get_all_agents() 284 | 285 | for agent in agents: 286 | send_job_request(agent["IpAddress"], job) 287 | 288 | def start_job_request_request_load_balanced(job): 289 | batches = {} 290 | agents = database_access.get_all_agents() 291 | agentCount = len(agents) 292 | if agentCount == 0: 293 | return 294 | 295 | #ROUND ROBIN LOAD BALANCE 296 | agentIndex = 0 297 | if job["Targets"] is not None and len(job["Targets"]) > 0: 298 | for target in job["Targets"]: 299 | 300 | #For every agent, assign one target then increment to the next agent 301 | #if we've reached the last agent, start batching at the begining again 302 | if(agentIndex >= agentCount): 303 | agentIndex = 0 304 | 305 | #If this agent doesn't have any batches yet, create one 306 | if(len(batches) == 0 or agents[agentIndex]["IpAddress"] not in batches): 307 | batches[agents[agentIndex]["IpAddress"]] = [] 308 | 309 | #get the batch for this agent (Batch) 310 | batch = batches[agents[agentIndex]["IpAddress"]] 311 | 312 | #If this is the first batch item create a new batch array and add it 313 | if(len(batch) == 0): 314 | batches[agents[agentIndex]["IpAddress"]] = [target] 315 | 316 | #otherwise append our new batch item 317 | else: 318 | batches[agents[agentIndex]["IpAddress"]].append(target) 319 | 320 | agentIndex += 1 321 | else: 322 | if(agentIndex >= agentCount): 323 | agentIndex = 0 324 | if(len(batches) == 0 or agents[agentIndex]["IpAddress"] not in batches): 325 | batches[agents[agentIndex]["IpAddress"]] = [] 326 | batch = batches[agents[agentIndex]["IpAddress"]] 327 | if(len(batch) == 0): 328 | batches[agents[agentIndex]["IpAddress"]] = [ensemble_constants.NA] 329 | else: 330 | batches[agents[agentIndex]["IpAddress"]].append(ensemble_constants.NA) 331 | 332 | agentIndex += 1 333 | 334 | #for every batch of work, clone the job, assign the targets from the batch 335 | #and send the job to the specificed agent for that batch 336 | for key in batches: 337 | clonedJob = copy.deepcopy(job) 338 | clonedJob["Targets"] = batches[key] 339 | send_job_request(key, clonedJob) 340 | 341 | def send_job_request(agentIp,job): 342 | if(job["Id"] not in agent_jobs): 343 | agent_jobs[job["Id"]] = [agentIp] 344 | else: 345 | agent_jobs[job["Id"]].append(agentIp) 346 | 347 | payloadBuilder = job_to_string(job) 348 | payload=encryption.encrypt_string(payloadBuilder) 349 | database_access.update_job_started(job["Id"]) 350 | 351 | for target in job["Targets"]: 352 | if ensemble_constants.NA in target: 353 | continue 354 | database_access.insert_target(target) 355 | _target = database_access.get_target_by_target(target) 356 | database_access.insert_target_job(_target["Id"],job["Id"]) 357 | 358 | database_access.insert_new_agent_job_record(agentIp, job["Id"]) 359 | database_access.add_message({"MessageType":ensemble_enums.MessageType.NOTIFICATION.value, "Message": f"Job Started for Job ID:{job['Id']}"}) 360 | 361 | ensemble_logging.log_message(f'Sending Job Request: {agentIp}:{APP_CONFIG[ensemble_constants.CONFIG_FILE_AGENT_COM_PORT]} {payload}') 362 | communication.tx(agentIp,int(APP_CONFIG[ensemble_constants.CONFIG_FILE_AGENT_COM_PORT]),payload) 363 | 364 | def check_for_scheduled_jobs(): 365 | try: 366 | #get all scheduled jobs that are recurring or scheduled for today 367 | jobs = database_access.get_scheduled_jobs() 368 | for scheduledJob in jobs: 369 | 370 | scheduledJobExists = database_access.check_if_scheduled_job_exists(scheduledJob["id"]) 371 | 372 | if(scheduledJobExists == False): 373 | scheduledRunTime = None if scheduledJob['runTime'] == '' else datetime.datetime.strptime(f"{datetime.datetime.now().date()} {scheduledJob['runTime']}",f"%Y-%m-%d %H:%M:%S.%f") 374 | 375 | runWindowMargin = datetime.timedelta(minutes=1) 376 | recurringJobThatIsReadyToRun = scheduledJob["runDateTime"] == '' and scheduledRunTime >= (datetime.datetime.now() - runWindowMargin) and scheduledRunTime <= datetime.datetime.now() 377 | 378 | scheduledJobRunDateTime = None if scheduledJob["runDateTime"] == '' else datetime.datetime.strptime(scheduledJob["runDateTime"],'%Y-%m-%d %H:%M:%S') 379 | timeNow = datetime.datetime.now() 380 | scheduledJobIsReadyToRun = scheduledRunTime == None and scheduledJobRunDateTime <= timeNow 381 | 382 | if(recurringJobThatIsReadyToRun or scheduledJobIsReadyToRun): 383 | job = { 384 | "Id": uuid.uuid4(), 385 | "Cmd": scheduledJob['cmd'], 386 | "Targets": scheduledJob['targets'], 387 | "IsSingleCmd": scheduledJob['isSingleCmd'], 388 | "IsLoadBalanced": scheduledJob['isLoadBalanced'], 389 | "WorkspaceId": scheduledJob['workspaceId'] 390 | } 391 | 392 | database_access.queue_job_request(job) 393 | database_access.insert_workspace_job(job["WorkspaceId"], str(job["Id"])) 394 | 395 | if(scheduledJobRunDateTime): 396 | database_access.remove_scheduled_job(scheduledJob["id"]) 397 | 398 | database_access.insert_scheduled_job_mapping({"JobId":job["Id"],"ScheduledJobId":scheduledJob["id"]}) 399 | 400 | #add to scheduled job run mapping table 401 | 402 | except Exception as error: 403 | ensemble_logging.log_message(f'Error while checking for scheduled job {error}') 404 | finally: 405 | threading.Timer(30,check_for_scheduled_jobs).start() 406 | 407 | supportedCommands = [ensemble_constants.CLEAR_LOGS, ensemble_constants.STOP_JOBS, ensemble_constants.KILL, ensemble_constants.RESTART] 408 | def check_for_agent_commands(): 409 | try: 410 | agentCommands = database_access.get_pending_agent_commands() 411 | 412 | for agentCommand in agentCommands: 413 | agent = database_access.get_agent_by_id(agentCommand["AgentId"]) 414 | if(agentCommand["Command"] in supportedCommands): 415 | ensemble_logging.log_message(f'Sending {agentCommand["Command"]} Request to {agent["IpAddress"]}') 416 | database_access.insert_stream_event({"EventType":ensemble_enums.StreamEvent.Agent.value, "Event":f'Sending {agentCommand["Command"]} Request to {agent["IpAddress"]}', "EventId":f"{agentCommand['AgentId']}"}) 417 | 418 | send_agent_command_log_request(agent["IpAddress"],agentCommand["Command"]) 419 | database_access.complete_all_agent_commands(agentCommand["Id"]) 420 | except Exception as error: 421 | ensemble_logging.log_message(f'Error while checking agent commands {error}') 422 | finally: 423 | threading.Timer(2.0,check_for_agent_commands).start() 424 | 425 | def send_agent_health_check(): 426 | try: 427 | agents = database_access.get_all_agents() 428 | for agent in agents: 429 | payload = encryption.encrypt_string(ensemble_constants.HEALTH_REQUEST) 430 | try: 431 | response = communication.txrx(agent["IpAddress"],int(APP_CONFIG[ensemble_constants.CONFIG_FILE_AGENT_COM_PORT]),payload) 432 | 433 | if len(response) == 0: 434 | database_access.set_agent_inactive(agent["Id"]) 435 | database_access.add_message({"MessageType":ensemble_enums.MessageType.WARNING.value, "Message": f"Agent has gone offline {agent['IpAddress']}"}) 436 | database_access.insert_stream_event({"EventType":ensemble_enums.StreamEvent.Agent.value, "Event":f"Agent has gone offline {agent['IpAddress']}", "EventId":f"{agent['Id']}"}) 437 | 438 | else: 439 | healthJsonObj = json.loads(encryption.decrypt_string(response)) 440 | agentHealth = { 441 | "Id":agent["Id"], 442 | "MemPct":healthJsonObj["MemInfo"][2], 443 | "StgPct":healthJsonObj["StgInfo"][3], 444 | "CpuPct":healthJsonObj["CpuInfo"], 445 | "LogSize": 0 if len(str(healthJsonObj["LogSize"])) == 0 or healthJsonObj["LogSize"] == None else healthJsonObj["LogSize"], 446 | "RunningProcesses":healthJsonObj["RunningProcesses"] 447 | } 448 | database_access.update_agent_health_status(agentHealth) 449 | except Exception as error: 450 | ensemble_logging.log_message(f'Error while sending agent health request {error}') 451 | continue 452 | except Exception as error: 453 | ensemble_logging.log_message(f'Error while checking agent health {error}') 454 | finally: 455 | threading.Timer(10.0,send_agent_health_check).start() 456 | 457 | initialize() 458 | -------------------------------------------------------------------------------- /ensemble_enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class MessageType(Enum): 4 | INFORMATION = 0 5 | WARNING = 1 6 | NOTIFICATION = 2 7 | 8 | class StreamEvent(Enum): 9 | Agent = 0 10 | Director = 1 11 | Command = 2 12 | Job = 3 -------------------------------------------------------------------------------- /ensemble_logging.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | import ensemble_constants 6 | 7 | from datetime import datetime 8 | 9 | logLevel = 1 10 | logConsumer = ensemble_constants.DIRECTOR_LOG_FILENAME 11 | 12 | def initialize(consumer): 13 | global logConsumer 14 | logConsumer = consumer 15 | 16 | def log_message(text): 17 | timeStamp = get_date_time_now() 18 | 19 | if(logLevel == 1): 20 | os.popen(f"{ensemble_constants.ECHO_COMMAND} '{timeStamp}: {text}' >> {ensemble_constants.LOGS_DIR}/{logConsumer}") 21 | elif(logLevel == 2): 22 | print(f"{timeStamp}: {text}", file=sys.stdout) 23 | 24 | def log_job(job): 25 | jobString = "" 26 | jobString += f"Job ID: {job['Id']}" 27 | jobString += f"Job CMD: {job['Cmd']}" 28 | jobString += f"Job Targets: {job['Targets']}" 29 | jobString += f"Job Run In Single Command: {job['IsSingleCmd']}" 30 | log_message(jobString) 31 | 32 | def log_job_completed(job): 33 | log_message(f"Job Completed: {job['Id']}") 34 | 35 | def get_date_time_now(): 36 | return datetime.fromtimestamp(datetime.now().timestamp()) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | cryptography==2.5 3 | psutil 4 | python-crontab 5 | waitress 6 | 7 | -------------------------------------------------------------------------------- /server_monitor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to start the server 4 | start_server() { 5 | python3 ./ensemble_web.py --debug & 6 | SERVER_PID=$! 7 | echo "Started ensemble_web.py with PID $SERVER_PID" 8 | sleep 3 9 | } 10 | 11 | # Function to stop the server 12 | stop_server() { 13 | if kill -0 $SERVER_PID 2>/dev/null; then 14 | echo "Stopping server with PID $SERVER_PID" 15 | kill $SERVER_PID 16 | wait $SERVER_PID 2>/dev/null 17 | fi 18 | } 19 | 20 | # Start the server 21 | start_server 22 | 23 | # Give the server a moment to start 24 | sleep 2 25 | 26 | while true; do 27 | 28 | # Try to ping the server 29 | curl --insecure --connect-timeout 5 https://127.0.0.1:5000/ping 30 | 31 | # Check if the curl command was successful 32 | if [ $? -ne 0 ]; then 33 | echo "Server did not respond within 5 seconds. Restarting..." 34 | stop_server 35 | # Wait before restarting 36 | sleep 5 37 | start_server 38 | else 39 | echo "Server is responding." 40 | sleep 5 41 | fi 42 | 43 | done 44 | -------------------------------------------------------------------------------- /static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x;background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x;background-color:#2e6da4}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} 6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */ -------------------------------------------------------------------------------- /static/css/ensemble.css: -------------------------------------------------------------------------------- 1 | .header { 2 | font-weight: bold; 3 | color: black; 4 | } 5 | .label { 6 | margin-left: 5px; 7 | font-size: larger; 8 | color: black; 9 | } 10 | .centered { 11 | position: fixed; 12 | top: 50%; 13 | left: 50%; 14 | transform: translate(-50%, -50%); 15 | } 16 | .logo-header { 17 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serifHelvetica, sans-serif; 18 | } 19 | .map { 20 | width: 100%; 21 | height:500px; 22 | } 23 | 24 | .overlay { 25 | position: fixed; /* Sit on top of the page content */ 26 | width: 50%; /* Full width (cover the whole page) */ 27 | height: 6%; /* Full height (cover the whole page) */ 28 | top: 0; 29 | right: 0; 30 | z-index: 9; /* Specify a stack order in case you're using a different order for other elements */ 31 | } -------------------------------------------------------------------------------- /static/css/ol.css: -------------------------------------------------------------------------------- 1 | :host,:root{--ol-background-color:white;--ol-accent-background-color:#F5F5F5;--ol-subtle-background-color:rgba(128, 128, 128, 0.25);--ol-partial-background-color:rgba(255, 255, 255, 0.75);--ol-foreground-color:#333333;--ol-subtle-foreground-color:#666666;--ol-brand-color:#00AAFF}.ol-box{box-sizing:border-box;border-radius:2px;border:1.5px solid var(--ol-background-color);background-color:var(--ol-partial-background-color)}.ol-mouse-position{top:8px;right:8px;position:absolute}.ol-scale-line{background:var(--ol-partial-background-color);border-radius:4px;bottom:8px;left:8px;padding:2px;position:absolute}.ol-scale-line-inner{border:1px solid var(--ol-subtle-foreground-color);border-top:none;color:var(--ol-foreground-color);font-size:10px;text-align:center;margin:1px;will-change:contents,width;transition:all .25s}.ol-scale-bar{position:absolute;bottom:8px;left:8px}.ol-scale-bar-inner{display:flex}.ol-scale-step-marker{width:1px;height:15px;background-color:var(--ol-foreground-color);float:right;z-index:10}.ol-scale-step-text{position:absolute;bottom:-5px;font-size:10px;z-index:11;color:var(--ol-foreground-color);text-shadow:-1.5px 0 var(--ol-partial-background-color),0 1.5px var(--ol-partial-background-color),1.5px 0 var(--ol-partial-background-color),0 -1.5px var(--ol-partial-background-color)}.ol-scale-text{position:absolute;font-size:12px;text-align:center;bottom:25px;color:var(--ol-foreground-color);text-shadow:-1.5px 0 var(--ol-partial-background-color),0 1.5px var(--ol-partial-background-color),1.5px 0 var(--ol-partial-background-color),0 -1.5px var(--ol-partial-background-color)}.ol-scale-singlebar{position:relative;height:10px;z-index:9;box-sizing:border-box;border:1px solid var(--ol-foreground-color)}.ol-scale-singlebar-even{background-color:var(--ol-subtle-foreground-color)}.ol-scale-singlebar-odd{background-color:var(--ol-background-color)}.ol-unsupported{display:none}.ol-unselectable,.ol-viewport{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.ol-viewport canvas{all:unset}.ol-selectable{-webkit-touch-callout:default;-webkit-user-select:text;-moz-user-select:text;user-select:text}.ol-grabbing{cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.ol-grab{cursor:move;cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.ol-control{position:absolute;background-color:var(--ol-subtle-background-color);border-radius:4px}.ol-zoom{top:.5em;left:.5em}.ol-rotate{top:.5em;right:.5em;transition:opacity .25s linear,visibility 0s linear}.ol-rotate.ol-hidden{opacity:0;visibility:hidden;transition:opacity .25s linear,visibility 0s linear .25s}.ol-zoom-extent{top:4.643em;left:.5em}.ol-full-screen{right:.5em;top:.5em}.ol-control button{display:block;margin:1px;padding:0;color:var(--ol-subtle-foreground-color);font-weight:700;text-decoration:none;font-size:inherit;text-align:center;height:1.375em;width:1.375em;line-height:.4em;background-color:var(--ol-background-color);border:none;border-radius:2px}.ol-control button::-moz-focus-inner{border:none;padding:0}.ol-zoom-extent button{line-height:1.4em}.ol-compass{display:block;font-weight:400;will-change:transform}.ol-touch .ol-control button{font-size:1.5em}.ol-touch .ol-zoom-extent{top:5.5em}.ol-control button:focus,.ol-control button:hover{text-decoration:none;outline:1px solid var(--ol-subtle-foreground-color);color:var(--ol-foreground-color)}.ol-zoom .ol-zoom-in{border-radius:2px 2px 0 0}.ol-zoom .ol-zoom-out{border-radius:0 0 2px 2px}.ol-attribution{text-align:right;bottom:.5em;right:.5em;max-width:calc(100% - 1.3em);display:flex;flex-flow:row-reverse;align-items:center}.ol-attribution a{color:var(--ol-subtle-foreground-color);text-decoration:none}.ol-attribution ul{margin:0;padding:1px .5em;color:var(--ol-foreground-color);text-shadow:0 0 2px var(--ol-background-color);font-size:12px}.ol-attribution li{display:inline;list-style:none}.ol-attribution li:not(:last-child):after{content:" "}.ol-attribution img{max-height:2em;max-width:inherit;vertical-align:middle}.ol-attribution button{flex-shrink:0}.ol-attribution.ol-collapsed ul{display:none}.ol-attribution:not(.ol-collapsed){background:var(--ol-partial-background-color)}.ol-attribution.ol-uncollapsible{bottom:0;right:0;border-radius:4px 0 0}.ol-attribution.ol-uncollapsible img{margin-top:-.2em;max-height:1.6em}.ol-attribution.ol-uncollapsible button{display:none}.ol-zoomslider{top:4.5em;left:.5em;height:200px}.ol-zoomslider button{position:relative;height:10px}.ol-touch .ol-zoomslider{top:5.5em}.ol-overviewmap{left:.5em;bottom:.5em}.ol-overviewmap.ol-uncollapsible{bottom:0;left:0;border-radius:0 4px 0 0}.ol-overviewmap .ol-overviewmap-map,.ol-overviewmap button{display:block}.ol-overviewmap .ol-overviewmap-map{border:1px solid var(--ol-subtle-foreground-color);height:150px;width:150px}.ol-overviewmap:not(.ol-collapsed) button{bottom:0;left:0;position:absolute}.ol-overviewmap.ol-collapsed .ol-overviewmap-map,.ol-overviewmap.ol-uncollapsible button{display:none}.ol-overviewmap:not(.ol-collapsed){background:var(--ol-subtle-background-color)}.ol-overviewmap-box{border:1.5px dotted var(--ol-subtle-foreground-color)}.ol-overviewmap .ol-overviewmap-box:hover{cursor:move} 2 | /*# sourceMappingURL=ol.css.map */ -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotNetRussell/Ensemble/7f62df10aa48aadb8e9bfd2edd9a2fa55ee823c2/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotNetRussell/Ensemble/7f62df10aa48aadb8e9bfd2edd9a2fa55ee823c2/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotNetRussell/Ensemble/7f62df10aa48aadb8e9bfd2edd9a2fa55ee823c2/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotNetRussell/Ensemble/7f62df10aa48aadb8e9bfd2edd9a2fa55ee823c2/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /static/images/ensemble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotNetRussell/Ensemble/7f62df10aa48aadb8e9bfd2edd9a2fa55ee823c2/static/images/ensemble.png -------------------------------------------------------------------------------- /static/js/bingmap.js: -------------------------------------------------------------------------------- 1 | import BingMaps from 'ol/source/BingMaps'; 2 | import Map from 'ol/Map'; 3 | import TileLayer from 'ol/layer/Tile'; 4 | import View from 'ol/View'; 5 | 6 | const styles = [ 7 | 'RoadOnDemand', 8 | 'Aerial', 9 | 'AerialWithLabelsOnDemand', 10 | 'CanvasDark', 11 | 'OrdnanceSurvey', 12 | ]; 13 | const layers = []; 14 | let i, ii; 15 | for (i = 0, ii = styles.length; i < ii; ++i) { 16 | layers.push( 17 | new TileLayer({ 18 | visible: false, 19 | preload: Infinity, 20 | source: new BingMaps({ 21 | key: 'Your Bing Maps Key from https://www.bingmapsportal.com/ here', 22 | imagerySet: styles[i], 23 | // use maxZoom 19 to see stretched tiles instead of the BingMaps 24 | // "no photos at this zoom level" tiles 25 | // maxZoom: 19 26 | }), 27 | }) 28 | ); 29 | } 30 | const map = new Map({ 31 | layers: layers, 32 | target: 'map', 33 | view: new View({ 34 | center: [-6655.5402445057125, 6709968.258934638], 35 | zoom: 13, 36 | }), 37 | }); 38 | 39 | const select = document.getElementById('layer-select'); 40 | function onChange() { 41 | const style = select.value; 42 | for (let i = 0, ii = layers.length; i < ii; ++i) { 43 | layers[i].setVisible(styles[i] === style); 44 | } 45 | } 46 | select.addEventListener('change', onChange); 47 | onChange(); 48 | -------------------------------------------------------------------------------- /static/js/common.js: -------------------------------------------------------------------------------- 1 | 2 | $.getScript('static/js/controls.js'); 3 | $.getScript('static/js/message-service.js'); 4 | 5 | let notificationPollActive = true; 6 | 7 | function GetWarningAlert(id, text) { 8 | return ` 9 | 19 | ` 20 | } 21 | 22 | function GetNotificationAlert(id, text) { 23 | return ` 24 | 34 | ` 35 | } 36 | 37 | function GetInformationAlert(id, text) { 38 | return ` 39 | 49 | ` 50 | } 51 | 52 | function CheckMessages() { 53 | if (!notificationPollActive) 54 | return; 55 | 56 | $.ajax({ 57 | url: '/messages', 58 | method: 'GET', 59 | success: function (res, textStatus, data) { 60 | JSON.parse(data.responseText).forEach(element => { 61 | let existingAlert = $(`#toast_${element.Id}`)[0]; 62 | if (existingAlert == undefined) { 63 | 64 | if (element.MessageType === 0) { 65 | $("#notification-container").append(GetInformationAlert(element.Id, element.Message)); 66 | } 67 | else if (element.MessageType === 1) { 68 | $("#notification-container").append(GetWarningAlert(element.Id, element.Message)); 69 | } 70 | else if (element.MessageType === 2) { 71 | $("#notification-container").append(GetNotificationAlert(element.Id, element.Message)); 72 | } 73 | 74 | $(`#toast_${element.Id}`).on('hidden.bs.toast', function () { 75 | DissmissAlert(element.Id); 76 | $(`#toast_${element.Id}`).remove(); 77 | }); 78 | $(`#toast_${element.Id}`).toast('show'); 79 | } 80 | }); 81 | } 82 | }) 83 | setTimeout(CheckMessages, 3000); 84 | } 85 | 86 | function DissmissAlert(id) { 87 | $.ajax({ 88 | url: `/messages?dismiss=${id}`, 89 | method: 'GET', 90 | success: function (res, textStatus, data) { } 91 | } 92 | ); 93 | } 94 | 95 | function AddWorkspace() { 96 | $("#addworkspaceModal").modal('toggle'); 97 | 98 | $.ajax({ 99 | url: `/workspace?id=${encodeURIComponent($("#WorkspaceName").val())}`, 100 | method: 'GET', 101 | success: function (res, textStatus, data) { 102 | window.location.href = "/"; 103 | } 104 | }); 105 | } 106 | 107 | function DisableNotificationPolling() { 108 | notificationPollActive = false; 109 | } 110 | 111 | function MakeApiEndpointRequest(endpoint, method, callback) { 112 | $.ajax({ 113 | url: endpoint, 114 | type: method, 115 | success: function (res, textStatus, data) { 116 | callback(data); 117 | } 118 | }); 119 | } 120 | 121 | function MakeApiJsonPostRequest(endpoint, callback, postData, errorCallback){ 122 | $.ajax({ 123 | url: endpoint, 124 | type: 'POST', 125 | data: postData, 126 | dataType: 'json', 127 | contentType: "application/json", 128 | success: function (res, textStatus, data) { 129 | callback(data); 130 | }, 131 | error: function (res, textStatus, data){ 132 | errorCallback(); 133 | } 134 | }); 135 | } 136 | 137 | function RegisterApiEndpointPoll(endpoint, method, callback, frequency) { 138 | setTimeout(() => { 139 | MakeApiEndpointRequest(endpoint, method, callback); 140 | RegisterApiEndpointPoll(endpoint, method, callback, frequency); 141 | }, frequency); 142 | } 143 | 144 | function UpdateServerTime(){ 145 | MakeApiEndpointRequest("/api/getTime","GET",(res,status,data)=>{ 146 | $("#serverTime").text(`SERVER TIME: ${res.responseText}`); 147 | setTimeout(UpdateServerTime, 3000) 148 | }); 149 | } 150 | 151 | setTimeout(CheckMessages, 3000); 152 | setTimeout(UpdateServerTime, 3000) -------------------------------------------------------------------------------- /static/js/controls.js: -------------------------------------------------------------------------------- 1 | 2 | const version = "Beta 1.0.0"; 3 | 4 | const navbar = ` 5 | `; 64 | 65 | 66 | const banner = ` 67 |
68 | 69 | ${version} 70 |
`; 71 | 72 | const workspaceModal = ` 73 | 94 | `; 95 | 96 | let navBar = document.getElementById("nav_element"); 97 | if(navBar != undefined) 98 | navBar.innerHTML = navbar; 99 | 100 | let bannerElement = document.getElementById("banner_element"); 101 | if(bannerElement != undefined) 102 | bannerElement.innerHTML = banner; 103 | 104 | let workspaceModalElement = document.getElementById("workspace_modal_element"); 105 | if(workspaceModalElement != undefined) 106 | workspaceModalElement.innerHTML = workspaceModal; 107 | -------------------------------------------------------------------------------- /static/js/elm-pep.js: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/ 4 | // Variable to hold current primary touch event identifier. 5 | // iOS needs this since it does not attribute 6 | // identifier 0 to primary touch event. 7 | var primaryTouchId = null; 8 | // Variable to hold mouse pointer captures. 9 | var mouseCaptureTarget = null; 10 | if (!("PointerEvent" in window)) { 11 | // Define {set,release}PointerCapture 12 | definePointerCapture(); 13 | // Create Pointer polyfill from mouse events only on non-touch device 14 | if (!("TouchEvent" in window)) { 15 | addMouseToPointerListener(document, "mousedown", "pointerdown"); 16 | addMouseToPointerListener(document, "mousemove", "pointermove"); 17 | addMouseToPointerListener(document, "mouseup", "pointerup"); 18 | } 19 | // Define Pointer polyfill from touch events 20 | addTouchToPointerListener(document, "touchstart", "pointerdown"); 21 | addTouchToPointerListener(document, "touchmove", "pointermove"); 22 | addTouchToPointerListener(document, "touchend", "pointerup"); 23 | } 24 | // Function defining {set,release}PointerCapture from {set,releas}Capture 25 | function definePointerCapture() { 26 | Element.prototype.setPointerCapture = Element.prototype.setCapture; 27 | Element.prototype.releasePointerCapture = Element.prototype.releaseCapture; 28 | } 29 | // Function converting a Mouse event to a Pointer event. 30 | function addMouseToPointerListener(target, mouseType, pointerType) { 31 | target.addEventListener(mouseType, function (mouseEvent) { 32 | var pointerEvent = new MouseEvent(pointerType, mouseEvent); 33 | pointerEvent.pointerId = 1; 34 | pointerEvent.isPrimary = true; 35 | pointerEvent.pointerType = "mouse"; 36 | pointerEvent.width = 1; 37 | pointerEvent.height = 1; 38 | pointerEvent.tiltX = 0; 39 | pointerEvent.tiltY = 0; 40 | // pressure is 0.5 if a button is holded 41 | "buttons" in mouseEvent && mouseEvent.buttons !== 0 42 | ? (pointerEvent.pressure = 0.5) 43 | : (pointerEvent.pressure = 0); 44 | // if already capturing mouse event, transfer target 45 | // and don't forget implicit release on mouseup. 46 | var target = mouseEvent.target; 47 | if (mouseCaptureTarget !== null) { 48 | target = mouseCaptureTarget; 49 | if (mouseType === "mouseup") { 50 | mouseCaptureTarget = null; 51 | } 52 | } 53 | target.dispatchEvent(pointerEvent); 54 | if (pointerEvent.defaultPrevented) { 55 | mouseEvent.preventDefault(); 56 | } 57 | }); 58 | } 59 | // Function converting a Touch event to a Pointer event. 60 | function addTouchToPointerListener(target, touchType, pointerType) { 61 | target.addEventListener(touchType, function (touchEvent) { 62 | var changedTouches = touchEvent.changedTouches; 63 | var nbTouches = changedTouches.length; 64 | for (var t = 0; t < nbTouches; t++) { 65 | var pointerEvent = new CustomEvent(pointerType, { 66 | bubbles: true, 67 | cancelable: true 68 | }); 69 | pointerEvent.ctrlKey = touchEvent.ctrlKey; 70 | pointerEvent.shiftKey = touchEvent.shiftKey; 71 | pointerEvent.altKey = touchEvent.altKey; 72 | pointerEvent.metaKey = touchEvent.metaKey; 73 | var touch = changedTouches.item(t); 74 | pointerEvent.clientX = touch.clientX; 75 | pointerEvent.clientY = touch.clientY; 76 | pointerEvent.screenX = touch.screenX; 77 | pointerEvent.screenY = touch.screenY; 78 | pointerEvent.pageX = touch.pageX; 79 | pointerEvent.pageY = touch.pageY; 80 | var rect = touch.target.getBoundingClientRect(); 81 | pointerEvent.offsetX = touch.clientX - rect.left; 82 | pointerEvent.offsetY = touch.clientY - rect.top; 83 | pointerEvent.pointerId = 1 + touch.identifier; 84 | // Default values for standard MouseEvent fields. 85 | pointerEvent.button = 0; 86 | pointerEvent.buttons = 1; 87 | pointerEvent.movementX = 0; 88 | pointerEvent.movementY = 0; 89 | pointerEvent.region = null; 90 | pointerEvent.relatedTarget = null; 91 | pointerEvent.x = pointerEvent.clientX; 92 | pointerEvent.y = pointerEvent.clientY; 93 | // Pointer event details 94 | pointerEvent.pointerType = "touch"; 95 | pointerEvent.width = 1; 96 | pointerEvent.height = 1; 97 | pointerEvent.tiltX = 0; 98 | pointerEvent.tiltY = 0; 99 | pointerEvent.pressure = 1; 100 | // First touch is the primary pointer event. 101 | if (touchType === "touchstart" && primaryTouchId === null) { 102 | primaryTouchId = touch.identifier; 103 | } 104 | pointerEvent.isPrimary = touch.identifier === primaryTouchId; 105 | // If first touch ends, reset primary touch id. 106 | if (touchType === "touchend" && pointerEvent.isPrimary) { 107 | primaryTouchId = null; 108 | } 109 | touchEvent.target.dispatchEvent(pointerEvent); 110 | if (pointerEvent.defaultPrevented) { 111 | touchEvent.preventDefault(); 112 | } 113 | } 114 | }); 115 | } 116 | //# sourceMappingURL=elm-pep.js.map -------------------------------------------------------------------------------- /static/js/login.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | function login(){ 4 | $.ajax({ 5 | url: '/', 6 | method:'POST', 7 | headers:{ 8 | "authorization": "Basic " + btoa($("#usernameinput").val() + ":" + $("#password").val()) 9 | }, 10 | success: function(res, textStatus, data) { 11 | redirect = data.getResponseHeader('location') 12 | window.location.href = "/"; 13 | } 14 | }) 15 | } 16 | 17 | $(document).keypress(function(event){ 18 | if (event.key === "Enter") { 19 | login(); 20 | } 21 | }); 22 | 23 | $('#submitButton').click(function() { 24 | login(); 25 | }); 26 | }); -------------------------------------------------------------------------------- /static/js/message-service.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotNetRussell/Ensemble/7f62df10aa48aadb8e9bfd2edd9a2fa55ee823c2/static/js/message-service.js -------------------------------------------------------------------------------- /static/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /static/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @popperjs/core v2.11.6 - MIT License 3 | */ 4 | 5 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Popper={})}(this,(function(e){"use strict";function t(e){if(null==e)return window;if("[object Window]"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function n(e){return e instanceof t(e).Element||e instanceof Element}function r(e){return e instanceof t(e).HTMLElement||e instanceof HTMLElement}function o(e){return"undefined"!=typeof ShadowRoot&&(e instanceof t(e).ShadowRoot||e instanceof ShadowRoot)}var i=Math.max,a=Math.min,s=Math.round;function f(){var e=navigator.userAgentData;return null!=e&&e.brands?e.brands.map((function(e){return e.brand+"/"+e.version})).join(" "):navigator.userAgent}function c(){return!/^((?!chrome|android).)*safari/i.test(f())}function p(e,o,i){void 0===o&&(o=!1),void 0===i&&(i=!1);var a=e.getBoundingClientRect(),f=1,p=1;o&&r(e)&&(f=e.offsetWidth>0&&s(a.width)/e.offsetWidth||1,p=e.offsetHeight>0&&s(a.height)/e.offsetHeight||1);var u=(n(e)?t(e):window).visualViewport,l=!c()&&i,d=(a.left+(l&&u?u.offsetLeft:0))/f,h=(a.top+(l&&u?u.offsetTop:0))/p,m=a.width/f,v=a.height/p;return{width:m,height:v,top:h,right:d+m,bottom:h+v,left:d,x:d,y:h}}function u(e){var n=t(e);return{scrollLeft:n.pageXOffset,scrollTop:n.pageYOffset}}function l(e){return e?(e.nodeName||"").toLowerCase():null}function d(e){return((n(e)?e.ownerDocument:e.document)||window.document).documentElement}function h(e){return p(d(e)).left+u(e).scrollLeft}function m(e){return t(e).getComputedStyle(e)}function v(e){var t=m(e),n=t.overflow,r=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+r)}function y(e,n,o){void 0===o&&(o=!1);var i,a,f=r(n),c=r(n)&&function(e){var t=e.getBoundingClientRect(),n=s(t.width)/e.offsetWidth||1,r=s(t.height)/e.offsetHeight||1;return 1!==n||1!==r}(n),m=d(n),y=p(e,c,o),g={scrollLeft:0,scrollTop:0},b={x:0,y:0};return(f||!f&&!o)&&(("body"!==l(n)||v(m))&&(g=(i=n)!==t(i)&&r(i)?{scrollLeft:(a=i).scrollLeft,scrollTop:a.scrollTop}:u(i)),r(n)?((b=p(n,!0)).x+=n.clientLeft,b.y+=n.clientTop):m&&(b.x=h(m))),{x:y.left+g.scrollLeft-b.x,y:y.top+g.scrollTop-b.y,width:y.width,height:y.height}}function g(e){var t=p(e),n=e.offsetWidth,r=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-r)<=1&&(r=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:r}}function b(e){return"html"===l(e)?e:e.assignedSlot||e.parentNode||(o(e)?e.host:null)||d(e)}function w(e){return["html","body","#document"].indexOf(l(e))>=0?e.ownerDocument.body:r(e)&&v(e)?e:w(b(e))}function x(e,n){var r;void 0===n&&(n=[]);var o=w(e),i=o===(null==(r=e.ownerDocument)?void 0:r.body),a=t(o),s=i?[a].concat(a.visualViewport||[],v(o)?o:[]):o,f=n.concat(s);return i?f:f.concat(x(b(s)))}function O(e){return["table","td","th"].indexOf(l(e))>=0}function j(e){return r(e)&&"fixed"!==m(e).position?e.offsetParent:null}function E(e){for(var n=t(e),i=j(e);i&&O(i)&&"static"===m(i).position;)i=j(i);return i&&("html"===l(i)||"body"===l(i)&&"static"===m(i).position)?n:i||function(e){var t=/firefox/i.test(f());if(/Trident/i.test(f())&&r(e)&&"fixed"===m(e).position)return null;var n=b(e);for(o(n)&&(n=n.host);r(n)&&["html","body"].indexOf(l(n))<0;){var i=m(n);if("none"!==i.transform||"none"!==i.perspective||"paint"===i.contain||-1!==["transform","perspective"].indexOf(i.willChange)||t&&"filter"===i.willChange||t&&i.filter&&"none"!==i.filter)return n;n=n.parentNode}return null}(e)||n}var D="top",A="bottom",L="right",P="left",M="auto",k=[D,A,L,P],W="start",B="end",H="viewport",T="popper",R=k.reduce((function(e,t){return e.concat([t+"-"+W,t+"-"+B])}),[]),S=[].concat(k,[M]).reduce((function(e,t){return e.concat([t,t+"-"+W,t+"-"+B])}),[]),V=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function q(e){var t=new Map,n=new Set,r=[];function o(e){n.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){if(!n.has(e)){var r=t.get(e);r&&o(r)}})),r.push(e)}return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||o(e)})),r}function C(e){return e.split("-")[0]}function N(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&o(n)){var r=t;do{if(r&&e.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function I(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function _(e,r,o){return r===H?I(function(e,n){var r=t(e),o=d(e),i=r.visualViewport,a=o.clientWidth,s=o.clientHeight,f=0,p=0;if(i){a=i.width,s=i.height;var u=c();(u||!u&&"fixed"===n)&&(f=i.offsetLeft,p=i.offsetTop)}return{width:a,height:s,x:f+h(e),y:p}}(e,o)):n(r)?function(e,t){var n=p(e,!1,"fixed"===t);return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}(r,o):I(function(e){var t,n=d(e),r=u(e),o=null==(t=e.ownerDocument)?void 0:t.body,a=i(n.scrollWidth,n.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=i(n.scrollHeight,n.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),f=-r.scrollLeft+h(e),c=-r.scrollTop;return"rtl"===m(o||n).direction&&(f+=i(n.clientWidth,o?o.clientWidth:0)-a),{width:a,height:s,x:f,y:c}}(d(e)))}function F(e,t,o,s){var f="clippingParents"===t?function(e){var t=x(b(e)),o=["absolute","fixed"].indexOf(m(e).position)>=0&&r(e)?E(e):e;return n(o)?t.filter((function(e){return n(e)&&N(e,o)&&"body"!==l(e)})):[]}(e):[].concat(t),c=[].concat(f,[o]),p=c[0],u=c.reduce((function(t,n){var r=_(e,n,s);return t.top=i(r.top,t.top),t.right=a(r.right,t.right),t.bottom=a(r.bottom,t.bottom),t.left=i(r.left,t.left),t}),_(e,p,s));return u.width=u.right-u.left,u.height=u.bottom-u.top,u.x=u.left,u.y=u.top,u}function U(e){return e.split("-")[1]}function z(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function X(e){var t,n=e.reference,r=e.element,o=e.placement,i=o?C(o):null,a=o?U(o):null,s=n.x+n.width/2-r.width/2,f=n.y+n.height/2-r.height/2;switch(i){case D:t={x:s,y:n.y-r.height};break;case A:t={x:s,y:n.y+n.height};break;case L:t={x:n.x+n.width,y:f};break;case P:t={x:n.x-r.width,y:f};break;default:t={x:n.x,y:n.y}}var c=i?z(i):null;if(null!=c){var p="y"===c?"height":"width";switch(a){case W:t[c]=t[c]-(n[p]/2-r[p]/2);break;case B:t[c]=t[c]+(n[p]/2-r[p]/2)}}return t}function Y(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function G(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function J(e,t){void 0===t&&(t={});var r=t,o=r.placement,i=void 0===o?e.placement:o,a=r.strategy,s=void 0===a?e.strategy:a,f=r.boundary,c=void 0===f?"clippingParents":f,u=r.rootBoundary,l=void 0===u?H:u,h=r.elementContext,m=void 0===h?T:h,v=r.altBoundary,y=void 0!==v&&v,g=r.padding,b=void 0===g?0:g,w=Y("number"!=typeof b?b:G(b,k)),x=m===T?"reference":T,O=e.rects.popper,j=e.elements[y?x:m],E=F(n(j)?j:j.contextElement||d(e.elements.popper),c,l,s),P=p(e.elements.reference),M=X({reference:P,element:O,strategy:"absolute",placement:i}),W=I(Object.assign({},O,M)),B=m===T?W:P,R={top:E.top-B.top+w.top,bottom:B.bottom-E.bottom+w.bottom,left:E.left-B.left+w.left,right:B.right-E.right+w.right},S=e.modifiersData.offset;if(m===T&&S){var V=S[i];Object.keys(R).forEach((function(e){var t=[L,A].indexOf(e)>=0?1:-1,n=[D,A].indexOf(e)>=0?"y":"x";R[e]+=V[n]*t}))}return R}var K={placement:"bottom",modifiers:[],strategy:"absolute"};function Q(){for(var e=arguments.length,t=new Array(e),n=0;n=0?-1:1,i="function"==typeof n?n(Object.assign({},t,{placement:e})):n,a=i[0],s=i[1];return a=a||0,s=(s||0)*o,[P,L].indexOf(r)>=0?{x:s,y:a}:{x:a,y:s}}(n,t.rects,i),e}),{}),s=a[t.placement],f=s.x,c=s.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=f,t.modifiersData.popperOffsets.y+=c),t.modifiersData[r]=a}},se={left:"right",right:"left",bottom:"top",top:"bottom"};function fe(e){return e.replace(/left|right|bottom|top/g,(function(e){return se[e]}))}var ce={start:"end",end:"start"};function pe(e){return e.replace(/start|end/g,(function(e){return ce[e]}))}function ue(e,t){void 0===t&&(t={});var n=t,r=n.placement,o=n.boundary,i=n.rootBoundary,a=n.padding,s=n.flipVariations,f=n.allowedAutoPlacements,c=void 0===f?S:f,p=U(r),u=p?s?R:R.filter((function(e){return U(e)===p})):k,l=u.filter((function(e){return c.indexOf(e)>=0}));0===l.length&&(l=u);var d=l.reduce((function(t,n){return t[n]=J(e,{placement:n,boundary:o,rootBoundary:i,padding:a})[C(n)],t}),{});return Object.keys(d).sort((function(e,t){return d[e]-d[t]}))}var le={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name;if(!t.modifiersData[r]._skip){for(var o=n.mainAxis,i=void 0===o||o,a=n.altAxis,s=void 0===a||a,f=n.fallbackPlacements,c=n.padding,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.flipVariations,h=void 0===d||d,m=n.allowedAutoPlacements,v=t.options.placement,y=C(v),g=f||(y===v||!h?[fe(v)]:function(e){if(C(e)===M)return[];var t=fe(e);return[pe(e),t,pe(t)]}(v)),b=[v].concat(g).reduce((function(e,n){return e.concat(C(n)===M?ue(t,{placement:n,boundary:p,rootBoundary:u,padding:c,flipVariations:h,allowedAutoPlacements:m}):n)}),[]),w=t.rects.reference,x=t.rects.popper,O=new Map,j=!0,E=b[0],k=0;k=0,S=R?"width":"height",V=J(t,{placement:B,boundary:p,rootBoundary:u,altBoundary:l,padding:c}),q=R?T?L:P:T?A:D;w[S]>x[S]&&(q=fe(q));var N=fe(q),I=[];if(i&&I.push(V[H]<=0),s&&I.push(V[q]<=0,V[N]<=0),I.every((function(e){return e}))){E=B,j=!1;break}O.set(B,I)}if(j)for(var _=function(e){var t=b.find((function(t){var n=O.get(t);if(n)return n.slice(0,e).every((function(e){return e}))}));if(t)return E=t,"break"},F=h?3:1;F>0;F--){if("break"===_(F))break}t.placement!==E&&(t.modifiersData[r]._skip=!0,t.placement=E,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function de(e,t,n){return i(e,a(t,n))}var he={name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name,o=n.mainAxis,s=void 0===o||o,f=n.altAxis,c=void 0!==f&&f,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.padding,h=n.tether,m=void 0===h||h,v=n.tetherOffset,y=void 0===v?0:v,b=J(t,{boundary:p,rootBoundary:u,padding:d,altBoundary:l}),w=C(t.placement),x=U(t.placement),O=!x,j=z(w),M="x"===j?"y":"x",k=t.modifiersData.popperOffsets,B=t.rects.reference,H=t.rects.popper,T="function"==typeof y?y(Object.assign({},t.rects,{placement:t.placement})):y,R="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),S=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,V={x:0,y:0};if(k){if(s){var q,N="y"===j?D:P,I="y"===j?A:L,_="y"===j?"height":"width",F=k[j],X=F+b[N],Y=F-b[I],G=m?-H[_]/2:0,K=x===W?B[_]:H[_],Q=x===W?-H[_]:-B[_],Z=t.elements.arrow,$=m&&Z?g(Z):{width:0,height:0},ee=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},te=ee[N],ne=ee[I],re=de(0,B[_],$[_]),oe=O?B[_]/2-G-re-te-R.mainAxis:K-re-te-R.mainAxis,ie=O?-B[_]/2+G+re+ne+R.mainAxis:Q+re+ne+R.mainAxis,ae=t.elements.arrow&&E(t.elements.arrow),se=ae?"y"===j?ae.clientTop||0:ae.clientLeft||0:0,fe=null!=(q=null==S?void 0:S[j])?q:0,ce=F+ie-fe,pe=de(m?a(X,F+oe-fe-se):X,F,m?i(Y,ce):Y);k[j]=pe,V[j]=pe-F}if(c){var ue,le="x"===j?D:P,he="x"===j?A:L,me=k[M],ve="y"===M?"height":"width",ye=me+b[le],ge=me-b[he],be=-1!==[D,P].indexOf(w),we=null!=(ue=null==S?void 0:S[M])?ue:0,xe=be?ye:me-B[ve]-H[ve]-we+R.altAxis,Oe=be?me+B[ve]+H[ve]-we-R.altAxis:ge,je=m&&be?function(e,t,n){var r=de(e,t,n);return r>n?n:r}(xe,me,Oe):de(m?xe:ye,me,m?Oe:ge);k[M]=je,V[M]=je-me}t.modifiersData[r]=V}},requiresIfExists:["offset"]};var me={name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,n=e.state,r=e.name,o=e.options,i=n.elements.arrow,a=n.modifiersData.popperOffsets,s=C(n.placement),f=z(s),c=[P,L].indexOf(s)>=0?"height":"width";if(i&&a){var p=function(e,t){return Y("number"!=typeof(e="function"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:G(e,k))}(o.padding,n),u=g(i),l="y"===f?D:P,d="y"===f?A:L,h=n.rects.reference[c]+n.rects.reference[f]-a[f]-n.rects.popper[c],m=a[f]-n.rects.reference[f],v=E(i),y=v?"y"===f?v.clientHeight||0:v.clientWidth||0:0,b=h/2-m/2,w=p[l],x=y-u[c]-p[d],O=y/2-u[c]/2+b,j=de(w,O,x),M=f;n.modifiersData[r]=((t={})[M]=j,t.centerOffset=j-O,t)}},effect:function(e){var t=e.state,n=e.options.element,r=void 0===n?"[data-popper-arrow]":n;null!=r&&("string"!=typeof r||(r=t.elements.popper.querySelector(r)))&&N(t.elements.popper,r)&&(t.elements.arrow=r)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ve(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function ye(e){return[D,L,A,P].some((function(t){return e[t]>=0}))}var ge={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var t=e.state,n=e.name,r=t.rects.reference,o=t.rects.popper,i=t.modifiersData.preventOverflow,a=J(t,{elementContext:"reference"}),s=J(t,{altBoundary:!0}),f=ve(a,r),c=ve(s,o,i),p=ye(f),u=ye(c);t.modifiersData[n]={referenceClippingOffsets:f,popperEscapeOffsets:c,isReferenceHidden:p,hasPopperEscaped:u},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":u})}},be=Z({defaultModifiers:[ee,te,oe,ie]}),we=[ee,te,oe,ie,ae,le,he,me,ge],xe=Z({defaultModifiers:we});e.applyStyles=ie,e.arrow=me,e.computeStyles=oe,e.createPopper=xe,e.createPopperLite=be,e.defaultModifiers=we,e.detectOverflow=J,e.eventListeners=ee,e.flip=le,e.hide=ge,e.offset=ae,e.popperGenerator=Z,e.popperOffsets=te,e.preventOverflow=he,Object.defineProperty(e,"__esModule",{value:!0})})); 6 | //# sourceMappingURL=popper.min.js.map 7 | -------------------------------------------------------------------------------- /templates/agenthealth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |

Agent Health {{viewmodel.AgentIpAddress}}

35 |
36 |
37 |
38 |
39 | 42 |
43 |
44 | 47 |
48 |
49 | 51 |
52 |
53 | 56 |
57 |
58 | 59 |
60 |
61 |

ID

62 |
63 |
64 |

Ip Address

65 |
66 |
67 |

Memory %

68 |
69 |
70 |

Processor %

71 |
72 |
73 |

Storage %

74 |
75 |
76 |

Log Size

77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | 97 |
98 |
99 |
100 | 103 | 104 |
105 |
106 |
{{viewmodel.RunningProcesses}}
107 |
108 |
109 |
110 |
111 |
112 | 113 | 114 | 117 | 120 | 121 | 122 | 125 | 126 | 145 | 146 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /templates/agents.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

Agents

23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
IPIs ActiveHealth StatusLask CheckinRunning Jobs
36 |
37 |
38 |
39 | 40 | 41 | 44 | 47 | 48 | 149 | 150 | -------------------------------------------------------------------------------- /templates/createadminuser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 |
27 |

Create Admin User

28 |
29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /templates/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 |

Workspace : {{currentworkspace}}

27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |

Active Agents

36 |
{{viewmodel.AgentCount}}
37 |

Running Jobs

38 |
{{viewmodel.RunningJobs}}
39 | 40 |

Select Workspace

41 | 54 | 58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | 67 | 68 | 71 | 74 | 75 | 76 | 77 | 78 | 80 | 81 | 82 | 83 | 108 | 109 | 163 | 174 | 175 | -------------------------------------------------------------------------------- /templates/events.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Event Stream

24 |
25 |
26 |

Fetching data...

27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 |
IDEVENT TIMEEVENT
35 |
36 |
37 |
38 | 39 | 40 | 43 | 46 | 47 | 116 | 117 | -------------------------------------------------------------------------------- /templates/jobresults.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Job Results

24 |
25 |
26 | {{ viewmodel.JobData[0].Command }} 27 |
28 |
29 | 30 | 31 | 32 | 38 | 39 | 40 | 46 | 47 | {% if (viewmodel|length) > 0 %} 48 | {% for job in viewmodel.JobData %} 49 | 50 | 56 | 57 | 58 | 64 | 65 | 66 | 78 | 79 | 80 | 86 | 87 | 118 | 119 | 170 | 171 | {% endfor %} 172 | {% endif %} 173 |
33 |
34 | Job ID: 35 |
{{ viewmodel.JobData[0].JobId }}
36 |
37 |
41 |
42 | Target: 43 |
{{ viewmodel.JobData[0].Target }}
44 |
45 |
51 |
52 | Agent ID: 53 | {{ job.AgentId }} 54 |
55 |
59 |
60 | Start Time: 61 |
{{ job.StartTime }}
62 |
63 |
67 |
68 | End Time: 69 | 70 | {% if job.EndTime != None %} 71 |
{{ job.EndTime }}
72 | {% else %} 73 |
Pending
74 | {% endif %} 75 |
76 |
77 |
81 |
82 | Canceled: 83 | {{ job.WasCanceled }} 84 |
85 |
120 | 122 |
123 | {% if (viewmodel.JobData|length) == 1 %} 124 |
125 | {% endif %} 126 | 127 | {% if (viewmodel.JobData|length) > 1 %} 128 |
130 | {% endif %} 131 |
{{ job.JobResult }}
132 | 168 |
169 |
174 |
175 |
176 |
177 | 178 | 179 | 182 | 185 | 186 | -------------------------------------------------------------------------------- /templates/jobs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 |

Jobs

27 |
28 |
29 |
30 | 32 | 34 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
IDCOMMANDTARGETSSINGLE CMDSTARTEDFINISHED
50 |
51 |
52 |
53 |
54 | 55 | 56 | 59 | 62 | 63 | 165 | 166 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |

Ensemble Director

22 |
23 |
24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 | 34 |
35 |
36 |
37 |
38 | 39 | 40 | 42 | 43 | 44 | 47 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /templates/newjob.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 82 | 83 | 84 | 85 | 86 | 87 |
88 |
89 |
90 |

Create New Job

91 |
92 | 93 | 94 | 132 | 133 | 134 | 148 | 149 | 150 | 151 | 165 | 166 | 167 | 292 | 293 | 294 | 321 | 322 | 323 | 327 | 328 |
95 |
96 |
97 | Command 98 |
99 |
100 | Use {% raw -%}{{target}}{%- endraw %} where the targets go 101 | 103 |
104 |
105 |
106 |
107 |
The following command will run once per target
108 | nmap -sC -sV -p- {% raw -%}{{target}}{%- endraw %} 110 |
If the command expects the targets to be passed in as a file then 111 | it's handled the same in the command
112 | nmap -sC -sV -p- -iL {% raw -%}{{target}}{%- endraw %} 114 |
115 |
116 | 124 | {% if viewmodel.Command != None %} 125 | 127 | {% else %} 128 | 129 | {% endif %} 130 | 131 |
135 |
136 | {% if viewmodel.IsSingleCommand == 1 %} 137 | 139 | {% else %} 140 | 141 | {% endif %} 142 | 146 |
147 |
152 |
153 | {% if viewmodel.IsLoadBalanced == 1 %} 154 | 156 | {% else %} 157 | 158 | {% endif %} 159 | 163 |
164 |
168 |
169 | 170 | 171 | 174 | 175 |
176 | 177 |
178 | 188 | 189 | 190 | 191 | 192 | 193 | 291 |
295 |
Targets
296 |
297 | (separate with line breaks) 298 | 300 |
301 |
302 |
303 | Seriously, you need a hint? 304 | 305 | 192.168.1.1 306 |
307 | 192.168.1.2 308 |
309 | 192.168.1.3 310 |
311 |
312 |
313 | {% if viewmodel.Targets != None %} 314 | 316 | {% else %} 317 | 319 | {% endif %} 320 |
324 | 326 |
329 |
330 |
331 | 332 | 333 | 334 | 337 | 340 | 341 | 348 | 349 | -------------------------------------------------------------------------------- /templates/pagetemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |

TEMPLATE

25 |
26 |
27 |
28 | 29 | 30 | 31 | 34 | 37 | 38 | -------------------------------------------------------------------------------- /templates/scheduledjobresults.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Scheduled Job Results

24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 37 | 38 | {% if (viewmodel|length) > 0 %} 39 | {% for job in viewmodel %} 40 | 41 | 47 | 48 | 49 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 69 | 70 | {% endfor %} 71 | {% endif %} 72 |
32 |
33 | Scheduled Job Id: 34 |
{{ viewmodel[0].ScheduledJobId }}
35 |
36 |
42 |
43 | Agent ID: 44 | {{ job.AgentId }} 45 |
46 |
50 |
51 | Job ID: 52 |
{{ job.JobId }}
53 |
54 |
58 |
59 | Run Time: 60 |
{{ job.JobRunDate }}
61 |
62 |
67 |
{{ job.JobDiff }}
68 |
73 |
74 |
75 |
76 | 77 | 78 | 81 | 84 | 85 | -------------------------------------------------------------------------------- /templates/scheduledjobs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |

Scheduled Jobs

26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
IDCOMMANDTARGETSSINGLE CMDTYPERUN TIMESCHEDULED DATE
42 |
43 |
44 |
45 |

Completed Scheduled Jobs

46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
Scheduled Job IdJob IdJob Run DateAgent Id
58 |
59 |
60 |
61 |
62 | 63 | 64 | 67 | 70 | 71 | 198 | 199 | -------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |

Settings

25 |
26 | 27 | 35 | 36 |
37 |
38 |

User settings

39 | 46 | 47 | 48 | 53 | 57 | 58 | 59 | 64 | 65 | 66 | 69 | 72 | 73 | 74 | 77 | 80 | 81 |
49 |
50 | Username: 51 |
52 |
54 | 56 |
60 |
61 | Password 62 |
63 |
67 |
Old
68 |
70 | 71 |
75 |
New
76 |
78 | 79 |
82 | 83 | 85 |
86 |
87 |
88 |
89 |
90 |

Command Templates

91 | 92 | 93 | 94 | 95 | 96 | 97 |
DescriptionCommand
98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 110 | 113 | 114 | 115 | 120 | 123 | 124 | 125 |

Add new command template

106 |
107 | Description: 108 |
109 |
111 | 112 |
116 |
117 | Command: 118 |
119 |
121 | 122 |
126 | 127 |
128 | 129 |
130 | 131 |
132 | 133 | 134 | 135 | 138 | 141 | 142 | 189 | 190 | -------------------------------------------------------------------------------- /useraccess.py: -------------------------------------------------------------------------------- 1 | import database_access 2 | import hashlib, uuid 3 | import base64 4 | import crypt 5 | 6 | def get_salt(): 7 | return crypt.mksalt(crypt.METHOD_SHA512) 8 | 9 | def salt_password(password,salt): 10 | return hashlib.sha512(base64.b64encode((password + salt).encode("ascii"))).hexdigest() 11 | 12 | def admin_user_exists(): 13 | return database_access.admin_user_check() 14 | 15 | def log_user_in(username, password): 16 | 17 | if(admin_user_exists()): 18 | 19 | user = database_access.get_user_by_username(username) 20 | 21 | if(user == None): 22 | return 0 23 | else: 24 | testPasswordHash = salt_password(password,user["Salt"]) 25 | passwordsMatch = testPasswordHash == user["PasswordHash"] 26 | if(passwordsMatch == True): 27 | return 1 28 | else: 29 | return 0 30 | else: 31 | salt = get_salt() 32 | passwordHashed = salt_password(password,salt) 33 | database_access.add_admin_user(username,passwordHashed,salt) 34 | return 0 --------------------------------------------------------------------------------