diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1ad1dee --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "idf.pythonInstallPath": "/usr/bin/python3" +} \ No newline at end of file diff --git a/README.md b/README.md index f54bb45..64796ef 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ Vigil is designed for stability and security, avoiding keeping credentials in th - [Prerequisites](#prerequisites) - [Installation and Running](#installation-and-running) - [Installation](#installation) - - [Using the install script (recommended)](#using-the-install-script-recommended) - - [Using Docker](#using-docker) - [Usage](#usage) - [Running Vigil](#running-vigil) - [1. Scheduled Mode (serve)](#1-scheduled-mode-serve) @@ -91,8 +89,6 @@ This will check for updates every hour and send an email to the provided email a ## Installation -### Using the install script (recommended) - The easiest way to install Vigil is by using the interactive install script. This script automatically checks for required dependencies, offers to install any that are missing, and can optionally configure Vigil to start automatically on boot via **systemd**. @@ -126,24 +122,6 @@ sudo bash uninstall.sh rm uninstall.sh ``` -### Using docker - -> [!WARNING] -> The docker run command will not work yet and needs to be updated when released. -> The docker section will also be updated to include compose examples. - -If you prefer to use Docker, you can download the latest Docker image from the [Docker Hub](https://hub.docker.com/r/uthmn/vigil) - -```bash -docker pull uthmn/vigil -``` - -You can now run Vigil using the following command: - -```bash -docker run -it --rm uthmn/vigil -``` - ## Usage Once installed, Vigil can be run in two modes: diff --git a/install.sh b/install.sh index 5c07445..d33cd39 100644 --- a/install.sh +++ b/install.sh @@ -10,75 +10,23 @@ fi ACTUAL_USER=${SUDO_USER:-$USER} USER_HOME=$(eval echo ~$ACTUAL_USER) -# Now check if we already have vigil installed -if [[ -d "/opt/vigil" ]]; then - echo "Vigil is already installed." - echo "Would you like to upgrade? This will preserve your .env and users.json files. (y/n)" - read -r answer - - if [[ "${answer,,}" == "y" ]]; then - echo "Backing up configuration files..." - - # Create temporary backup directory - BACKUP_DIR="/tmp/vigil_backup_$(date +%s)" - mkdir -p "$BACKUP_DIR" - - # Backup .env and users.json if they exist - if [[ -f "/opt/vigil/.env" ]]; then - cp /opt/vigil/.env "$BACKUP_DIR/.env" - echo "Backed up .env" - fi - - if [[ -f "/opt/vigil/users.json" ]]; then - cp /opt/vigil/users.json "$BACKUP_DIR/users.json" - echo "Backed up users.json" - fi - - # Stop the service if it's running - if systemctl is-active --quiet vigil.service; then - echo "Stopping vigil service..." - systemctl stop vigil.service - fi - - # Remove the old installation - echo "Removing old installation..." - rm -rf /opt/vigil - - echo "Upgrade preparation complete. Proceeding with fresh installation..." - echo "" - - # Set flag to restore config later - UPGRADE_MODE=true - else - echo "Installation cancelled. Please remove /opt/vigil manually if you want a fresh install." - exit 1 - fi -fi +# /opt/vigil = static files/code +# /etc/vigil = configurations +# /var/log/vigil = logs -# Now check if we are running on a supported system +# Check if we are running on a supported system if [[ "$OSTYPE" != "linux-gnu"* ]]; then echo "This script is only supported on Linux" 1>&2 exit 1 fi -# Check if we have apt installed +# Now check if we have apt installed if ! command -v apt &> /dev/null; then echo "Apt is not installed. Please install it before running this script." 1>&2 exit 1 fi -# Now check if we have git and python3 installed, if not offer to install them -if ! command -v git &> /dev/null; then - echo "Git is not installed. Do you want to install it now? (y/n)" - read -r answer - if [[ "${answer,,}" == "y" ]]; then - apt install git -y - else - echo "Please install git before running this script." 1>&2 - exit 1 - fi -fi - +# Now check if we have python3 installed, if not offer to install it if ! command -v python3 &> /dev/null; then echo "Python 3 is not installed. Do you want to install it now? (y/n)" read -r answer @@ -90,7 +38,19 @@ if ! command -v python3 &> /dev/null; then fi fi -# Now check if we have python3 -m pip installed, if not offer to install it +# Now check if we have curl installed, if not offer to install it +if ! command -v curl &> /dev/null; then + echo "Curl is not installed. Do you want to install it now? (y/n)" + read -r answer + if [[ "${answer,,}" == "y" ]]; then + apt install curl -y + else + echo "Please install curl before running this script." 1>&2 + exit 1 + fi +fi + +# Now check if we have python3-pip installed, if not offer to install it if ! python3 -m pip --version &> /dev/null; then echo "pip is not installed. Do you want to install it now? (y/n)" read -r answer @@ -102,12 +62,76 @@ if ! python3 -m pip --version &> /dev/null; then fi fi -# Now git clone the repository to /opt/vigil -echo "" -echo "Cloning Vigil repository..." -git clone https://git.uthmn.com/ufatih/vigil.git /opt/vigil +# Now check if we have python3-venv installed, if not offer to install it +if ! python3 -m venv &> /dev/null; then + echo "Python3-venv is not installed. Do you want to install it now? (y/n)" + read -r answer + if [[ "${answer,,}" == "y" ]]; then + apt install python3-venv -y + else + echo "Please install python3-venv before running this script." 1>&2 + exit 1 + fi +fi -# Now create a virtual environment and install the required packages +if [[ -d "/opt/vigil" ]]; then + echo "Vigil is already installed." + echo "Would you like to upgrade? This will preserve your configuration. (y/n)" + read -r answer + + if [[ "${answer,,}" == "y" ]]; then + echo "Stopping vigil service..." + systemctl stop vigil 2>/dev/null + echo "Removing old installation..." + rm -rf /opt/vigil + # Continue with installation + else + echo "Installation cancelled." + exit 0 + fi +fi + +# First create the required directories +echo "" +# Creating the /opt/vigil directory +echo "Creating /opt/vigil directory..." +mkdir -p /opt/vigil +# Create the /opt/vigil/services directory +echo "Creating /opt/vigil/services directory..." +mkdir -p /opt/vigil/services +# Create the /etc/vigil directory +echo "Creating /etc/vigil directory..." +mkdir -p /etc/vigil +# Create the /var/log/vigil directory +echo "Creating /var/log/vigil directory..." +mkdir -p /var/log/vigil + +# Now curl -fsSL each file into the /opt/vigil directory +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/LICENSE -o /opt/vigil/LICENSE || { echo "Failed to download LICENSE file."; exit 1; } +echo " ✓ LICENSE" +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/README.md -o /opt/vigil/README.md || { echo "Failed to download README.md file."; exit 1; } +echo " ✓ README.md" +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/install.sh -o /opt/vigil/install.sh || { echo "Failed to download install.sh file."; exit 1; } +echo " ✓ install.sh" +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/uninstall.sh -o /opt/vigil/uninstall.sh || { echo "Failed to download uninstall.sh file."; exit 1; } +echo " ✓ uninstall.sh" +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/requirements.txt -o /opt/vigil/requirements.txt || { echo "Failed to download requirements.txt file."; exit 1; } +echo " ✓ requirements.txt" + +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/main.py -o /opt/vigil/main.py || { echo "Failed to download main.py file."; exit 1; } +echo " ✓ main.py" +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/services/apt.py -o /opt/vigil/services/apt.py || { echo "Failed to download services/apt.py file."; exit 1; } +echo " ✓ services/apt.py" +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/services/mail.py -o /opt/vigil/services/mail.py || { echo "Failed to download services/mail.py file."; exit 1; } +echo " ✓ services/mail.py" +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/services/logger.py -o /opt/vigil/services/logger.py || { echo "Failed to download services/logger.py file."; exit 1; } +echo " ✓ services/logger.py" +curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/services/__init__.py -o /opt/vigil/services/__init__.py || { echo "Failed to download services/__init__.py file."; exit 1; } +echo " ✓ services/__init__.py" + +echo "✓ All files downloaded successfully" + +# Now create a venv and install the required packages echo "" echo "Setting up virtual environment and installing dependencies..." python3 -m venv /opt/vigil/.venv @@ -115,221 +139,18 @@ source /opt/vigil/.venv/bin/activate pip install -r /opt/vigil/requirements.txt deactivate -# Now create a .env file and request the user to fill in the required values -echo "" -echo "=== Email Configuration ===" -echo "" +# Create a systemd service -# Check if we're in upgrade mode and have a backup -if [[ "$UPGRADE_MODE" == true ]] && [[ -f "$BACKUP_DIR/.env" ]]; then - echo "Found existing .env configuration. Would you like to:" - echo "1) Keep existing configuration" - echo "2) Enter new configuration" - read -r config_choice - - if [[ "$config_choice" == "1" ]]; then - cp "$BACKUP_DIR/.env" /opt/vigil/.env - echo "Restored existing .env configuration" - chmod 600 /opt/vigil/.env - - # Skip to connection testing - SKIP_ENV_INPUT=true - fi -fi - -if [[ "$SKIP_ENV_INPUT" != true ]]; then - touch /opt/vigil/.env - - # SMTP settings - echo "Please enter SMTP server address:" - read -r SMTP_SERVER - - echo "Please enter SMTP port (default: 587 for TLS, 465 for SSL):" - read -r SMTP_PORT - - echo "Please enter SMTP username:" - read -r SMTP_USERNAME - - echo "Please enter SMTP password:" - read -rs SMTP_PASSWORD - echo "" - - # IMAP settings - echo "Please enter IMAP server address:" - read -r IMAP_SERVER - - echo "Please enter IMAP port (default: 993 for SSL):" - read -r IMAP_PORT - - echo "Please enter IMAP username:" - read -r IMAP_USERNAME - - echo "Please enter IMAP password:" - read -rs IMAP_PASSWORD - echo "" - - # Write to .env file - cat > /opt/vigil/.env << EOF -SMTP_SERVER=$SMTP_SERVER -SMTP_PORT=$SMTP_PORT -SMTP_USERNAME=$SMTP_USERNAME -SMTP_PASSWORD=$SMTP_PASSWORD - -IMAP_SERVER=$IMAP_SERVER -IMAP_PORT=$IMAP_PORT -IMAP_USERNAME=$IMAP_USERNAME -IMAP_PASSWORD=$IMAP_PASSWORD -EOF - - echo "Configuration saved to /opt/vigil/.env" - - # Set secure permissions on .env file (contains passwords) - chmod 600 /opt/vigil/.env -fi - -# Test if the inputs work/are valid, else tell the user to fill in later and continue -if [[ "$SKIP_ENV_INPUT" != true ]]; then - echo "" - echo "Testing connections..." - - # Check if required commands are available - if command -v nc >/dev/null 2>&1; then - # Test SMTP connection with netcat (timeout after 5 seconds) - echo "Testing SMTP connection to $SMTP_SERVER:$SMTP_PORT..." - if timeout 5 nc -zv "$SMTP_SERVER" "$SMTP_PORT" 2>&1 | grep -q succeeded; then - echo "[OK] SMTP connection successful" - SMTP_VALID=true - else - echo "[FAILED] SMTP connection failed - could not reach $SMTP_SERVER:$SMTP_PORT" - SMTP_VALID=false - fi - - echo "" - - # Test IMAP connection - echo "Testing IMAP connection to $IMAP_SERVER:$IMAP_PORT..." - if timeout 5 nc -zv "$IMAP_SERVER" "$IMAP_PORT" 2>&1 | grep -q succeeded; then - echo "[OK] IMAP connection successful" - IMAP_VALID=true - else - echo "[FAILED] IMAP connection failed - could not reach $IMAP_SERVER:$IMAP_PORT" - IMAP_VALID=false - fi - else - echo "[WARNING] Cannot test connections (nc not installed). Skipping validation." - SMTP_VALID=unknown - IMAP_VALID=unknown - fi - - # Summary and continue - echo "" - if [ "$SMTP_VALID" = false ] || [ "$IMAP_VALID" = false ]; then - echo "[WARNING] Some connections failed." - echo "Configuration has been saved to /opt/vigil/.env" - echo "Please verify your settings and update the file manually if needed." - echo "" - read -p "Press Enter to continue..." -r - else - echo "[OK] All connections successful!" - echo "Configuration saved to /opt/vigil/.env" - fi -else - echo "" - echo "[OK] Using existing email configuration" -fi - -# Now create a users.json file and request the user to fill in the required values (or leave empty to skip) -echo "" -echo "=== Notification Recipients ===" -echo "" - -# Check if we're in upgrade mode and have a backup -if [[ "$UPGRADE_MODE" == true ]] && [[ -f "$BACKUP_DIR/users.json" ]]; then - echo "Found existing users.json configuration. Would you like to:" - echo "1) Keep existing recipients" - echo "2) Enter new recipients" - read -r users_choice - - if [[ "$users_choice" == "1" ]]; then - cp "$BACKUP_DIR/users.json" /opt/vigil/users.json - echo "Restored existing notification recipients" - - # Clean up backup directory - rm -rf "$BACKUP_DIR" - - # Skip to next section - SKIP_USERS_INPUT=true - fi -fi - -if [[ "$SKIP_USERS_INPUT" != true ]]; then - echo "Please enter the email addresses vigil will send notifications to (comma separated):" - echo "(Leave empty to skip)" - read -r EMAILS - - # Strip whitespace and convert to JSON array - if [ -n "$EMAILS" ]; then - # Remove all whitespace, split by comma, and build JSON array - EMAILS_CLEAN=$(echo "$EMAILS" | tr -d '[:space:]') - - # Convert to JSON array format - EMAILS_JSON="[" - IFS=',' read -ra EMAIL_ARRAY <<< "$EMAILS_CLEAN" - for i in "${!EMAIL_ARRAY[@]}"; do - if [ $i -gt 0 ]; then - EMAILS_JSON="$EMAILS_JSON," - fi - EMAILS_JSON="$EMAILS_JSON\"${EMAIL_ARRAY[$i]}\"" - done - EMAILS_JSON="$EMAILS_JSON]" - - echo "$EMAILS_JSON" > /opt/vigil/users.json - echo "Notification recipients saved to /opt/vigil/users.json" - else - echo "[]" > /opt/vigil/users.json - echo "No recipients configured. You can add them later in /opt/vigil/users.json" - fi - - # Clean up backup directory if it exists - if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then - rm -rf "$BACKUP_DIR" # Don't want to hit a steam - fi -fi - -# Set ownership of vigil directory to actual user -chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/vigil - -# Create the alias to the command -echo "" -echo "Adding 'vigil' command alias..." -ALIAS_LINE="alias vigil='source /opt/vigil/.venv/bin/activate && python3 /opt/vigil/main.py && deactivate'" - -# Add to user's bashrc if not already present -if ! grep -q "alias vigil=" "$USER_HOME/.bashrc" 2>/dev/null; then - echo "$ALIAS_LINE" >> "$USER_HOME/.bashrc" - echo "Alias added to $USER_HOME/.bashrc" -else - echo "Alias already exists in $USER_HOME/.bashrc" -fi - -# Suggest to add vigil to systemctl to autostart (after network is up) -echo "" -echo "Would you like to add vigil to autostart on boot? (y/n)" -read -r answer -if [[ "${answer,,}" == "y" ]]; then - # Create systemd service file - cat > /etc/systemd/system/vigil.service << EOF +echo "Creating systemd service..." +cat > /etc/systemd/system/vigil.service << 'EOF' [Unit] -Description=Vigil Monitoring Service +Description=Vigil - APT Update Monitor After=network-online.target Wants=network-online.target [Service] Type=simple -User=$ACTUAL_USER -WorkingDirectory=/opt/vigil -Environment="PATH=/opt/vigil/.venv/bin" -ExecStart=/opt/vigil/.venv/bin/python3 /opt/vigil/main.py +ExecStart=/opt/vigil/.venv/bin/python /opt/vigil/main.py serve --check-daily 18 Restart=on-failure RestartSec=10 @@ -337,26 +158,53 @@ RestartSec=10 WantedBy=multi-user.target EOF - # Reload systemd, enable and start the service - systemctl daemon-reload - systemctl enable vigil.service - systemctl start vigil.service - - echo "[OK] Vigil service has been created and started" - echo "You can check its status with: systemctl status vigil" -else - echo "Skipping autostart configuration" -fi +systemctl daemon-reload +echo "✓ Systemd service created (not enabled or started)" + +# Create a command to run vigil +echo "Creating vigil command..." +cat > /usr/local/bin/vigil << 'EOF' +#!/bin/bash +exec /opt/vigil/.venv/bin/python /opt/vigil/main.py "$@" +EOF + +chmod +x /usr/local/bin/vigil +echo "✓ Command 'vigil' available system-wide" echo "" -echo "========================================" -echo "Vigil has been installed successfully!" -echo "========================================" +echo "✓ Vigil installed successfully!" echo "" -echo "To use vigil, run: source ~/.bashrc && vigil" -echo "Or simply open a new terminal and run: vigil" -echo "" -if [[ "${answer,,}" == "y" ]]; then - echo "Vigil is now running as a system service." - echo "Use 'systemctl status vigil' to check its status" -fi \ No newline at end of file + +# Check what needs to be configured +NEEDS_CONFIG=false + +if [[ ! -f /etc/vigil/.env ]]; then + echo "⚠️ Missing: /etc/vigil/.env" + echo " Create this file with your SMTP credentials" + echo " See README for template: /opt/vigil/README.md" + echo "" + NEEDS_CONFIG=true +fi + +if [[ ! -f /etc/vigil/users.json ]]; then + echo "⚠️ Missing: /etc/vigil/users.json" + echo " Create this file with recipient email addresses" + echo " Example: [\"admin@example.com\"]" + echo "" + NEEDS_CONFIG=true +fi + +if [[ "$NEEDS_CONFIG" == "true" ]]; then + echo "Next steps:" + echo " 1. Configure the files above" + echo " 2. Test: sudo vigil now" + echo " 3. Enable service: sudo systemctl enable --now vigil" +else + echo "✓ Configuration files found" + echo "" + echo "Next steps:" + echo " 1. Test: sudo vigil now" + echo " 2. Enable service: sudo systemctl enable --now vigil" +fi + +echo "" \ No newline at end of file diff --git a/main.py b/main.py index 1846b6c..795bc42 100644 --- a/main.py +++ b/main.py @@ -11,27 +11,58 @@ from typing import Union import services.apt import services.mail +import html as hypertext +import re from socket import gethostname import json -from os.path import dirname, join, exists +from os.path import exists + +from services.logger import logger import typer +import signal + +# Its beautiful ;) +EMAIL_REGEX = re.compile(r""" +(?:[a-z0-9!#$%&'*+\x2f=?^_`\x7b-\x7d~\x2d]+(?:\.[a-z0-9!#$%&'*+\x2f=?^_`\x7b-\x7d~\x2d]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9\x2d]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9\x2d]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9\x2d]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\]) +""", re.VERBOSE | re.IGNORECASE) + +# Change for version of Vigil +__version__ = "1.0.0" + +shutdown_flag = False + +def handle_shutdown(signum, frame): + global shutdown_flag + logger.info("Shutdown signal received; finishing current cycle and exiting...") + shutdown_flag = True + +signal.signal(signal.SIGINT, handle_shutdown) +signal.signal(signal.SIGTERM, handle_shutdown) + app = typer.Typer() -def wait_until(target_time): - now = datetime.now() - seconds_to_wait = (target_time - now).total_seconds() - if seconds_to_wait > 0: - time.sleep(seconds_to_wait) +def wait_until_interruptible(target_time): + global shutdown_flag + while not shutdown_flag: + now = datetime.now() + remaining = (target_time - now).total_seconds() + if remaining <= 0: + return + # sleep in small chunks so shutdown is responsive + time.sleep(min(remaining, 0.5)) def schedule(mode, hour, minute=0, receiver_email=None): - print(f"Scheduler started at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Mode: {mode}") - while True: + global shutdown_flag + logger.info(f"Scheduler started at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Mode: {mode}") + + while not shutdown_flag: try: now = datetime.now() + if mode == "daily": target = now.replace(hour=hour, minute=minute, second=0, microsecond=0) if target <= now: @@ -43,11 +74,19 @@ def schedule(mode, hour, minute=0, receiver_email=None): else: raise ValueError("Mode must be 'daily' or 'hourly'.") - wait_until(target) + logger.info(f"Next check scheduled for {target.strftime('%Y-%m-%d %H:%M:%S')}") + wait_until_interruptible(target) + if shutdown_flag: + break + generate_email(receiver_email) + except Exception as e: - print(f"Error in scheduled task: {e}") - time.sleep(60) # Wait before retrying + logger.error(f"Error in scheduled task: {e}", exc_info=True) + time.sleep(60) + + logger.info("Scheduler exited cleanly.") + @app.command() def serve(receiver_email: str = None, check_hourly: bool = False, check_daily: str = "18"): @@ -58,7 +97,7 @@ def serve(receiver_email: str = None, check_hourly: bool = False, check_daily: s - check_daily=int → hour of day for daily email """ - if check_hourly and check_daily: + if check_hourly and check_daily not in [False, "false", "False", None]: check_daily = False if check_hourly: @@ -71,11 +110,11 @@ def serve(receiver_email: str = None, check_hourly: bool = False, check_daily: s try: hour = int(check_daily) except ValueError: - print(f"Invalid check_daily value: {check_daily}") - exit(1) + logger.error(f"Invalid check_daily value: {check_daily}") + raise typer.Exit(1) schedule("daily", hour, receiver_email=receiver_email) else: - print("No schedule selected.") + logger.error("No schedule selected.") @app.command() def now(receiver_email: str = None): @@ -87,25 +126,13 @@ def now(receiver_email: str = None): def generate_email(receiver_emails: Union[list, None]): services.apt.require_root() if not services.apt.detect_apt(): - print("Apt not found on this system.") - exit(1) - updates = services.apt.check_updates() - - # For testing - #updates = { - # "vim": { - # "installed_version": "8.2.3400", - # "latest_version": "8.2.3400", - # "repo": "universe", - # "security": True - # }, - # "vim-gtk3": { - # "installed_version": "8.2.3400", - # "latest_version": "8.2.3400", - # "repo": "universe", - # "security": False - # } - #} + logger.error("Apt not found on this system.") + raise typer.Exit(1) + try: + updates = services.apt.check_updates() + except Exception as e: + logger.error(f"Failed to check for updates: {e}", exc_info=True) + raise typer.Exit(1) receiver_email = [] if receiver_emails: @@ -116,13 +143,27 @@ def generate_email(receiver_emails: Union[list, None]): # If no CLI emails are provided if not receiver_email: - users_json_path = join(dirname(__file__), "users.json") + users_json_path = "/etc/vigil/users.json" if exists(users_json_path): - with open(users_json_path, "r") as f: - receiver_email = json.load(f) + try: + with open(users_json_path, "r") as f: + data = json.load(f) + if not isinstance(data, list): + logger.error("users.json must contain a list of email addresses") + raise typer.Exit(1) + receiver_email = [str(email).strip() for email in data] + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in users.json: {e}") + raise typer.Exit(1) else: - print("No email address provided and users.json not found.") - exit(1) + logger.error("No email address provided and /etc/vigil/users.json not found.") + raise typer.Exit(1) + + # Validate email address(es) + for email in receiver_email: + if not EMAIL_REGEX.match(email): + logger.error(f"Invalid email address: {email}") + raise typer.Exit(1) # Get how many security updates are available security_updates = 0 @@ -137,7 +178,7 @@ def generate_email(receiver_emails: Union[list, None]): # Check if there are any updates at all if total_updates == 0: - print("No updates available.") + logger.info("No updates available.") return # Get system hostname @@ -154,10 +195,10 @@ def generate_email(receiver_emails: Union[list, None]): continue chunk = f''' - {package} - {updates[package]["installed_version"]} - {updates[package]["latest_version"]} - {updates[package]["repo"]} + {hypertext.escape(package)} + {hypertext.escape(updates[package]["installed_version"])} + {hypertext.escape(updates[package]["latest_version"])} + {hypertext.escape(updates[package]["repo"])} ''' security_chunks += chunk @@ -185,10 +226,10 @@ def generate_email(receiver_emails: Union[list, None]): continue chunk = f''' - {package} - {updates[package]["installed_version"]} - {updates[package]["latest_version"]} - {updates[package]["repo"]} + {hypertext.escape(package)} + {hypertext.escape(updates[package]["installed_version"])} + {hypertext.escape(updates[package]["latest_version"])} + {hypertext.escape(updates[package]["repo"])} ''' general_chunks += chunk @@ -210,10 +251,29 @@ def generate_email(receiver_emails: Union[list, None]): html = security + general - services.mail.send_email(receiver_email, subject, html) - + try: + services.mail.send_email(receiver_email, subject, html) + logger.info(f"Update notification sent: {security_updates} security, {general_updates} general updates to {len(receiver_email)} recipient(s)") + except Exception as e: + logger.error(f"Failed to send email notification: {e}", exc_info=True) + raise typer.Exit(1) + +def version_callback(value: bool): + if value: + typer.echo(f"Vigil {__version__}") + raise typer.Exit() + @app.callback() -def callback(): +def callback( + version: bool = typer.Option( + None, + "--version", + "-v", + callback=version_callback, + is_eager=True, + help="Show version and exit" + ) +): """ Checks apt for upgrades and emails them """ diff --git a/requirements.txt b/requirements.txt index aef0227..048c7c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,8 @@ dotenv==0.9.9 markdown-it-py==4.0.0 mdurl==0.1.2 Pygments==2.19.2 -python-dotenv==1.1.1 +python-dotenv==1.2.1 rich==14.2.0 shellingham==1.5.4 -typer==0.19.2 +typer==0.20.0 typing_extensions==4.15.0 diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/apt.py b/services/apt.py index 5552f7d..5ec3892 100644 --- a/services/apt.py +++ b/services/apt.py @@ -6,13 +6,15 @@ # © Uthmn 2025 under MIT license import subprocess -import os +from os import geteuid import sys import re +from services.logger import logger + def require_root(): - if os.geteuid() != 0: - print("This script requires root privileges. Please run with sudo.") + if geteuid() != 0: + logger.error("This script requires root privileges. Please run with sudo.") sys.exit(1) def detect_apt(): @@ -74,4 +76,4 @@ if __name__ == "__main__": require_root() if detect_apt(): updates = check_updates() - print(updates) + logger.info(updates) diff --git a/services/logger.py b/services/logger.py new file mode 100644 index 0000000..057cbcb --- /dev/null +++ b/services/logger.py @@ -0,0 +1,46 @@ +# _ __________________ +# | | / / _/ ____/ _/ / +# | | / // // / __ / // / +# | |/ // // /_/ // // /___ +# |___/___/\____/___/_____/ +# © Uthmn 2025 under MIT license + +import logging +from logging.handlers import RotatingFileHandler +from os import makedirs +from os.path import exists + +# Setup logging once, globally +logger = logging.getLogger('vigil') + +# Prevent duplicate setup +if not logger.handlers: + logger.setLevel(logging.INFO) + + # Create log directory if it doesn't exist + log_dir = '/var/log/vigil' + if not exists(log_dir): + makedirs(log_dir, exist_ok=True) + + # File handler with rotation + file_handler = RotatingFileHandler( + '/var/log/vigil/vigil.log', + maxBytes=10*1024*1024, # 10MB + backupCount=3 + ) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + )) + + # Console handler (for systemd journal) + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(logging.Formatter( + '%(asctime)s [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + )) + + logger.addHandler(file_handler) + logger.addHandler(console_handler) diff --git a/services/mail.py b/services/mail.py index c2e46b1..1d57243 100644 --- a/services/mail.py +++ b/services/mail.py @@ -13,16 +13,18 @@ from email import message_from_bytes from dotenv import load_dotenv from os import getenv -from os.path import dirname, join, exists +from os.path import exists import time +from services.logger import logger + # Exit if .env does not exist -if not exists(join(dirname(__file__), "../.env")): - print("Please create a .env file in the root directory.") +if not exists("/etc/vigil/.env"): + logger.error("Please create a .env file in /etc/vigil.") exit(1) # Load environment variables from .env file -dotenv_path = join(dirname(__file__), "../.env") +dotenv_path = "/etc/vigil/.env" load_dotenv(dotenv_path) # SMTP server settings @@ -43,7 +45,7 @@ RETRY_DELAY = 2 # in seconds # Check if all environment variables are set if not SMTP_SERVER or not SMTP_PORT or not SMTP_USERNAME or not SMTP_PASSWORD or not IMAP_SERVER or not IMAP_PORT or not IMAP_USERNAME or not IMAP_PASSWORD: - print("Please set all environment variables in .env file.") + logger.error("Please set all environment variables in .env file.") exit(1) # Make sure SMTP and IMAP ports are integers @@ -60,11 +62,11 @@ def connect_smtp_with_retry(): except Exception as e: if attempt < MAX_RETRIES - 1: wait_time = RETRY_DELAY * (2 ** attempt) # Exponential backoff - print(f"SMTP connection attempt {attempt + 1} failed: {e}") - print(f"Retrying in {wait_time} seconds...") + logger.error(f"SMTP connection attempt {attempt + 1} failed: {e}") + logger.info(f"Retrying in {wait_time} seconds...") time.sleep(wait_time) else: - print(f"Failed to connect to SMTP after {MAX_RETRIES} attempts: {e}") + logger.error(f"Failed to connect to SMTP after {MAX_RETRIES} attempts: {e}") raise def connect_imap_with_retry(): @@ -77,11 +79,11 @@ def connect_imap_with_retry(): except Exception as e: if attempt < MAX_RETRIES - 1: wait_time = RETRY_DELAY * (2 ** attempt) # Exponential backoff - print(f"IMAP connection attempt {attempt + 1} failed: {e}") - print(f"Retrying in {wait_time} seconds...") + logger.error(f"IMAP connection attempt {attempt + 1} failed: {e}") + logger.info(f"Retrying in {wait_time} seconds...") time.sleep(wait_time) else: - print(f"Failed to connect to IMAP after {MAX_RETRIES} attempts: {e}") + logger.error(f"Failed to connect to IMAP after {MAX_RETRIES} attempts: {e}") raise # Test all connections @@ -89,14 +91,14 @@ try: server = connect_smtp_with_retry() server.quit() except Exception as e: - print(f"Initial SMTP connection test failed: {e}") + logger.error(f"Initial SMTP connection test failed: {e}") exit(1) try: M = connect_imap_with_retry() M.logout() except Exception as e: - print(f"Initial IMAP connection test failed: {e}") + logger.error(f"Initial IMAP connection test failed: {e}") exit(1) def send_email(receiver_emails, subject, body): @@ -113,11 +115,12 @@ def send_email(receiver_emails, subject, body): server = connect_smtp_with_retry() try: server.sendmail(sender_email, receiver_emails, message.as_string()) - print("HTML email sent successfully!") + logger.info("HTML email sent successfully!") finally: server.quit() except Exception as e: - print(f"Failed to send email: {e}") + logger.error(f"Failed to send email: {e}") + raise def receive_emails(folder="INBOX", limit=50): emails = {} @@ -131,7 +134,7 @@ def receive_emails(folder="INBOX", limit=50): # Get all message IDs typ, data = M.search(None, 'ALL') if typ != 'OK' or not data[0]: - print("No messages found.") + logger.info("No messages found.") return emails all_ids = data[0].split() @@ -140,7 +143,7 @@ def receive_emails(folder="INBOX", limit=50): for num in last_ids: typ, msg_data = M.fetch(num, '(RFC822)') if typ != 'OK': - print(f"Failed to fetch message {num}") + logger.error(f"Failed to fetch message {num}") continue # Parse the email @@ -185,6 +188,6 @@ def receive_emails(folder="INBOX", limit=50): M.logout() except Exception as e: - print(f"Error receiving emails: {e}") + logger.error(f"Error receiving emails: {e}") return emails diff --git a/uninstall.sh b/uninstall.sh index 89c102f..98c998e 100644 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,76 +1,70 @@ #!/bin/bash -# TODO: Add uninstall to readme - -# Just double check, check /opt/vigil exists, then remove it, also check+remove systemd service, remove aliases from bashrc and also dont offer but notify that dependencies not uninstalled - -# Check if vigil is installed, if not then exit -if [[ ! -d "/opt/vigil" ]]; then - echo "Vigil is not installed. Exiting." 1>&2 - exit 1 -fi - -# Check if we are running as root +# Check if running as root if [[ $EUID -ne 0 ]]; then echo "This script must be run as root" 1>&2 exit 1 fi -# Get the actual user (not root) for later use -ACTUAL_USER=${SUDO_USER:-$USER} -USER_HOME=$(eval echo ~$ACTUAL_USER) - -# Now check if we are running on a supported system -if [[ "$OSTYPE" != "linux-gnu"* ]]; then - echo "This script is only supported on Linux" 1>&2 - exit 1 -fi - -# Warning and countdown -echo "WARNING: This will remove /opt/vigil and any related data." -echo "Press Ctrl+C to cancel..." -echo "" - -for i in 5 4 3 2 1; do - echo -n "$i..." - sleep 1 -done +echo "This will uninstall Vigil from your system." echo "" +echo "What would you like to remove?" +echo " 1) Everything (code, config, and logs)" +echo " 2) Just the code (preserve /etc/vigil and /var/log/vigil)" +echo " 3) Cancel" echo "" +read -p "Enter choice [1-3]: " choice -# Final confirmation -echo "Continue with uninstallation? (y/n)" -read -r answer -if [[ "${answer,,}" != "y" ]]; then - echo "Uninstallation cancelled." - exit 0 -fi +case $choice in + 1) + REMOVE_ALL=true + ;; + 2) + REMOVE_ALL=false + ;; + 3) + echo "Uninstall cancelled." + exit 0 + ;; + *) + echo "Invalid choice. Uninstall cancelled." + exit 1 + ;; +esac -# Now delete the vigil directory +# Stop and disable service echo "" -echo "Removing Vigil installation..." +echo "Stopping vigil service..." +systemctl stop vigil 2>/dev/null +systemctl disable vigil 2>/dev/null + +# Remove systemd service +echo "Removing systemd service..." +rm -f /etc/systemd/system/vigil.service +systemctl daemon-reload + +# Remove command +echo "Removing vigil command..." +rm -f /usr/local/bin/vigil + +# Remove code +echo "Removing code from /opt/vigil..." rm -rf /opt/vigil -# Now delete the systemd service (if it exists) -if [[ -f /etc/systemd/system/vigil.service ]]; then - echo "Removing Vigil systemd service..." - systemctl stop vigil.service 2>/dev/null - systemctl disable vigil.service 2>/dev/null - rm /etc/systemd/system/vigil.service - systemctl daemon-reload +if [[ "$REMOVE_ALL" == "true" ]]; then + echo "Removing configuration from /etc/vigil..." + rm -rf /etc/vigil + echo "Removing logs from /var/log/vigil..." + rm -rf /var/log/vigil + echo "" + echo "✓ Vigil completely removed from your system" else - echo "No systemd service found (skipping)" + echo "" + echo "✓ Vigil code removed" + echo "✓ Configuration preserved in /etc/vigil" + echo "✓ Logs preserved in /var/log/vigil" + echo "" + echo "To reinstall: curl ... | sudo bash" fi -# Now delete the aliases from bashrc -echo "Removing Vigil aliases from bashrc..." -if grep -q "alias vigil=" "$USER_HOME/.bashrc" 2>/dev/null; then - sed -i '/alias vigil=/d' "$USER_HOME/.bashrc" - echo "Aliases removed from $USER_HOME/.bashrc" -else - echo "No aliases found in bashrc" -fi - -echo "" -echo "Dependencies not uninstalled. Please uninstall them manually if needed: python3, git, pip" -echo "Vigil has been uninstalled successfully." +echo "" \ No newline at end of file