diff --git a/.env.example b/.env.example index 12009b6..d14be81 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,5 @@ VERKADA_API_KEY=This should be your API Key== ELASTIC_PASSWORD=At least six characters long KIBANA_SYSTEM_PASSWORD=At least six characters long and you will need to set in Kibana refer to README KIBANA_ENCRYPTION_KEY=Create Key using command "openssl rand -base64 32" in linux or wsl== -EMAIL_PASSWORD = Password for the email account you are sending the dashboard from \ No newline at end of file +EMAIL_PASSWORD = Password for the email account you are sending the dashboard from +WINDOWS_IP=This is the IP for your device \ No newline at end of file diff --git a/config.ini.example b/config.ini.example index fadeaa1..d01ecd9 100644 --- a/config.ini.example +++ b/config.ini.example @@ -1,14 +1,14 @@ [Email] -EMAIL_TO=receiver_email@example.com -EMAIL_FROM=your_email@example.com -EMAIL_BODY_PREFIX=Click the link below to access the Kibana dashboard: -EMAIL_SUBJECT=Subject for the Email -EMAIL_SEND_TIME=09:00 +email_to = email@example.com +email_from = email@example.com +email_body_prefix = Click the link below to access link: +email_subject = Subject Line goes here +email_send_time = 14:40 [Verkada] # Default number of days to pull on first run or if index is empty -TIME_DELTA_INSTALLATION=365 +time_delta_installation = 3 -# This is how often you want to update elasticSearch (in 24-hour format) -# i.e. every 15 minutes -> ELASTIC_UPDATE_INTERVAL=00:15 -ELASTIC_UPDATE_INTERVAL = 00:15 +# This is how often you want to update elasticSearch +# i.e. every 15 minutes -> ELASTIC_UPDATE_INTERVAL=15m +elastic_update_interval = 15m diff --git a/scripts/cron_creation.sh b/scripts/cron_creation.sh new file mode 100755 index 0000000..9b122d7 --- /dev/null +++ b/scripts/cron_creation.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Run this from the root of the repo + +# Exit on errors, prevent undefined variables, and fail if any command in a pipeline fails +set -euo pipefail + +# Move into repo root +cd "$(dirname "$0")/.." + +# for reading time fields +declare update_interval +declare cron_command +declare cron_expression + +extract_field() +{ + echo "$(grep -i "$1" config.ini | cut -d '=' -f2 | xargs)" +} + +fail() { + echo + echo "ERROR: $1" + exit 1 +} + +parse_email_time_field() +{ + local h_or_m=$(extract_field "EMAIL_SEND_TIME" | cut -d ':' -f$1) + if [[ "$h_or_m" =~ ^[0-9]{2}$ ]]; then + local stoi=10#$h_or_m + + # handle cases where hour > 23 and minute > 59 + if (( ("$1" == "1" && $stoi > 23) || ("$1" == "2" && $stoi > 59) )); then + echo "-1" + + else + echo $((10#$h_or_m)) + fi + + else + echo "-1" + fi +} + +write_cron_job() +{ + cat <(fgrep -i -v "$cron_command" <(crontab -l)) <(echo "$cron_expression") | crontab - +} + +echo "Creating cron job for ETL..." + +update_interval=$(extract_field "ELASTIC_UPDATE_INTERVAL") + +CRON_EXEC="$(which "docker")" +CRON_TARGET="$(realpath "compose.yaml")" + +cron_command="${CRON_EXEC} compose -f ${CRON_TARGET} up -d" + +case $update_interval in + *m) + minutes=${update_interval%m} + cron_expression="*/$minutes * * * * $cron_command" + ;; + *h) + hours=${update_interval%h} + cron_expression="0 */$hours * * * $cron_command" + ;; + *d) + days=${update_interval%d} + cron_expression="0 0 */$days * * $cron_command" + ;; + *) + fail "Invalid ELASTIC_UPDATE_INTERVAL" + ;; +esac + +write_cron_job +echo "ETL cron job created!" + + +echo "Creating cron job for email sender..." + +CRON_EXEC="$(pwd)/.venv/bin/python" +CRON_TARGET=$(realpath "src/email_sender.py") + +cron_command="${CRON_EXEC} ${CRON_TARGET}" +cron_expression="$(parse_email_time_field "2") $(parse_email_time_field "1") * * * ${cron_command}" + +# check if cron_expression contains "-1", this means we reached an edge case +# so send time format is invalid +if [[ "$cron_expression" == *"-1"* ]]; then + fail "Invalid EMAIL_SEND_TIME, must be in 24-hour format" +fi +write_cron_job +echo "Email cron job created!" + + diff --git a/scripts/get_ip.sh b/scripts/get_ip.sh new file mode 100755 index 0000000..4318dad --- /dev/null +++ b/scripts/get_ip.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Run this from the root of the repo + +set -euo pipefail + +# Move into repo root +cd "$(dirname "$0")/.." + +WINDOWS_IP=$(/mnt/c/Windows/System32/ipconfig.exe | sed -n '/Wireless LAN adapter.*:/,/IPv4 Address/s/.*IPv4 Address[^:]*: //p' | head -n 1 | tr -d '\r' | xargs) + +if [ -z "$WINDOWS_IP" ]; then + WINDOWS_IP=$(/mnt/c/Windows/System32/ipconfig.exe | sed -n '/Ethernet adapter.*:/,/IPv4 Address/s/.*IPv4 Address[^:]*: //p' | head -n 1 | tr -d '\r' | xargs) +fi + +if [ -z "$WINDOWS_IP" ]; then + echo "ERROR: Could not determine Windows IP" + exit 1 +fi + +grep -q "^WINDOWS_IP=" .env && \ + sed -i "s|^WINDOWS_IP=.*|WINDOWS_IP=$WINDOWS_IP|" .env || \ + echo "WINDOWS_IP=$WINDOWS_IP" >> .env \ No newline at end of file diff --git a/scripts/initial_setup.sh b/scripts/initial_setup.sh index fd0218d..dd52451 100755 --- a/scripts/initial_setup.sh +++ b/scripts/initial_setup.sh @@ -7,17 +7,52 @@ set -euo pipefail # Move into repo root cd "$(dirname "$0")/.." +if crontab -l >/dev/null 2>&1; then + echo "Cron jobs exist" + echo "Deleting Cron jobs" + crontab -i -r +else + echo "No cron jobs" +fi + echo echo "Running dependencies check..." sudo apt update sudo apt install -y unzip openssl +sudo apt install python3-venv +sudo apt install python3-tk echo "Dependencies check complete..." +echo +echo "Setting up Python virtual environment and installing dependencies..." +if [ ! -d ".venv" ]; then + python3 -m venv .venv +fi +source .venv/bin/activate +pip install python-dotenv + +echo +echo "Getting Windows IP" +./scripts/get_ip.sh +echo "Windows IP added to .env" + +echo +echo "Ensuring config.ini exists..." +cp -n config.ini.example config.ini + +echo +echo "Ensuring .env exists..." +cp -n .env.example .env + echo echo "Launching setup dialogue box..." python3 ./src/dialogueBox.py echo "Setup dialogue box entry complete..." +echo +echo "building images from compose.yaml..." +docker compose build --no-cache + echo echo "running elastic_setup.sh..." ./scripts/elastic_setup.sh @@ -30,5 +65,9 @@ echo echo "Starting normal secure stack..." docker compose up -d +echo +echo "running cron_creation.sh..." +./scripts/cron_creation.sh + echo echo "Setup complete." diff --git a/src/Application.py b/src/Application.py index e79984d..62b0412 100644 --- a/src/Application.py +++ b/src/Application.py @@ -4,7 +4,7 @@ import os from Verkada import VerkadaContext -from ConfigReader import config +import configparser from datetime import datetime, date, timedelta ELASTIC_PASSWORD = os.environ.get("ELASTIC_PASSWORD") @@ -12,6 +12,9 @@ print("ERROR: ELASTIC_PASSWORD has not been set, exiting...") exit(1) +config = configparser.ConfigParser() +config.read("/config.ini") + def init(): args = CLI.setup_cli() diff --git a/src/ConfigReader.py b/src/ConfigReader.py index 99633a2..4b3f40b 100644 --- a/src/ConfigReader.py +++ b/src/ConfigReader.py @@ -1,4 +1,5 @@ import configparser +import Utils config = configparser.ConfigParser() -config.read("/config.ini") +config.read(Utils.get_project_root() / "config.ini") diff --git a/src/dialogueBox.py b/src/dialogueBox.py index bf8ca0a..51df303 100644 --- a/src/dialogueBox.py +++ b/src/dialogueBox.py @@ -18,15 +18,15 @@ def Save_Button(): newTo = inpTo.get() newFrom = inpFrom.get() newSubject = inpSubject.get() - newMessage = inpMessage.get("1.0", "end-1c") + newMessage = inpMessage.get("1.0", "end").strip() newSendTime = inpSendTime.get() newFreq = freqVar.get() newFreqUnit = timeUnit.get() newEmailPass = inpEmailPass.get() - newVerkAPI = inpApiKey.get("1.0", "end-1c") + newVerkAPI = inpApiKey.get("1.0", "end").strip() newElastPass = inpElastPass.get() newKibPass =inpKibPass.get() - newKibEnc = inpKibEnc.get("1.0", "end-1c") + newKibEnc = inpKibEnc.get("1.0", "end").strip() #Updates config values co["Email"]["EMAIL_TO"] = newTo @@ -40,6 +40,8 @@ def Save_Button(): dotenv.set_key(dotenv_file, "ELASTIC_PASSWORD", newElastPass) dotenv.set_key(dotenv_file,"KIBANA_SYSTEM_PASSWORD", newKibPass) dotenv.set_key(dotenv_file,"KIBANA_ENCRYPTION_KEY", newKibEnc) + + dotenv.load_dotenv(dotenv_file, override=True) #Writes new config values to file with open(os.path.join(BASE_DIR, '..', 'config.ini'), "w") as f: @@ -59,8 +61,8 @@ def generate_kibana_key(): co = configparser.ConfigParser() co.read(os.path.join(BASE_DIR, '..', 'config.ini')) - dotenv.load_dotenv() - dotenv_file = ".env" + dotenv_file = os.path.join(BASE_DIR, '..', '.env') + dotenv.load_dotenv(dotenv_file) root = tk.Tk(screenName=None, baseName='VerityBot', className='VerityBot', useTk=1) frame = tk.Frame(root) diff --git a/src/email_scheduler.py b/src/email_scheduler.py deleted file mode 100644 index 3ec00b0..0000000 --- a/src/email_scheduler.py +++ /dev/null @@ -1,40 +0,0 @@ -import datetime -import logging -import os -import sys - -from crontab import CronTab -from ConfigReader import config - -cron = CronTab(user=True) -logging.basicConfig(level=logging.INFO) - -def _verify_time_format(date_str: str) -> list[str]: - try: - datetime.datetime.strptime(date_str, '%H:%M') - except ValueError: - logging.error("Invalid EMAIL_SEND_TIME, must be 24-hour format") - exit(1) - return date_str.split(':') - -def create_cron_job(): - mail_time = config ['Email'] ['EMAIL_SEND_TIME'] - hours, mins = _verify_time_format(mail_time) - - script_path = os.path.abspath("src/email_sender.py") - # double check to make sure this is the path to the right executable - python_executable = sys.executable - command = f"{python_executable} {script_path}" - - job = cron.new(command=command, comment='Kibana Dashboard Email') - - # schedule the job to run every day at EMAIL_SEND_TIME - job.minute.on(int(mins)) - job.hour.on(int(hours)) - job.day.every(1) # Run every day - # the other fields (month, day of week) default to '*' - - # write the job to the crontab, critical - cron.write() - -create_cron_job() diff --git a/src/email_sender.py b/src/email_sender.py index 47f2df3..1579b16 100644 --- a/src/email_sender.py +++ b/src/email_sender.py @@ -2,6 +2,8 @@ import os import smtplib import socket +import subprocess +import Utils from dotenv import load_dotenv from email.mime.multipart import MIMEMultipart @@ -12,21 +14,40 @@ sender_email = config ['Email'] ['EMAIL_FROM'] email_subject = config ['Email'] ['EMAIL_SUBJECT'] +domain_to_server_mapping: dict = { + "gmail.com": "gmail.com", + "outlook.com": "office365.com", + "louisville.edu": "office365.com", + "shslou.org": "office365.com" +} + message = MIMEMultipart() message["From"] = sender_email message["To"] = recipient_email message["Subject"] = email_subject +def domain_to_server(email: str) -> str | None: + if '@' in email: + domain = email[email.index('@') + 1:] + smtp_server = domain_to_server_mapping.get(domain, None) + return smtp_server + return None + def send_email(): - try: - with smtplib.SMTP("smtp.gmail.com", 587) as s: - s.starttls() - s.login(sender_email, str(email_password)) - s.sendmail(sender_email, recipient_email, message.as_string()) - s.quit() - except Exception as e: - logging.error(f"Unable to send email: error code {e.args.index}") - exit(1) + smtp_server = domain_to_server(sender_email) + if smtp_server: + try: + with smtplib.SMTP(f"smtp.{smtp_server}", 587) as s: + s.set_debuglevel(1) + s.starttls() + s.login(sender_email, str(email_password)) + s.sendmail(sender_email, recipient_email, message.as_string()) + s.quit() + except Exception as e: + logging.error(f"Unable to send email: error code {e.args.index}") + exit(1) + else: + logging.error("Unable to send email. Make sure the sender email is correct!") # for sending the link to Kibana through email # IP address is likely to change due to proxies or DHCP lease termination @@ -44,7 +65,11 @@ def get_ip(): s.close() return IP -IP = get_ip() +script_path = Utils.get_project_root() / "scripts" / "get_ip.sh" +subprocess.run([script_path], check=True) + +load_dotenv() +IP = os.getenv("WINDOWS_IP") or get_ip() # read prefix only from config (no env fallback) body_prefix = config['Email'].get( @@ -55,14 +80,14 @@ def get_ip():

{body_prefix}

- Kibana Dashboard + Kibana Dashboards """ message.attach(MIMEText(html, "html")) -load_dotenv() email_password = os.getenv("EMAIL_PASSWORD") send_email() +