Refactor installation and uninstallation scripts; enhance logging and error handling

- Removed Docker installation instructions from README.md as they are not yet functional.
- Simplified the installation script by removing the upgrade process and directly checking for existing installations.
- Added checks for required dependencies (python3, curl, python3-venv) during installation.
- Improved logging throughout the application, replacing print statements with logger calls.
- Enhanced email validation using regex and added error handling for invalid email addresses.
- Updated the uninstall script to provide options for complete or partial removal of Vigil.
- Created a logger service to handle logging to both console and log files.
- Updated requirements.txt to use newer versions of dependencies.
- Added versioning to the main application and provided a version option in the command line interface.
This commit is contained in:
Uthman Fatih 2025-11-14 22:58:55 +00:00
parent a08228f7ef
commit 312d3e9674
10 changed files with 382 additions and 448 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"idf.pythonInstallPath": "/usr/bin/python3"
}

View File

@ -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:

View File

@ -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
# /opt/vigil = static files/code
# /etc/vigil = configurations
# /var/log/vigil = logs
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
# 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
systemctl daemon-reload
echo "✓ Systemd service created (not enabled or started)"
echo "[OK] Vigil service has been created and started"
echo "You can check its status with: systemctl status vigil"
# 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 "✓ Vigil installed successfully!"
echo ""
# 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 "Skipping autostart configuration"
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 ""
echo "========================================"
echo "Vigil has been installed successfully!"
echo "========================================"
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

160
main.py
View File

@ -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'''
<tr>
<td style="border: 1px solid #ddd; padding: 8px;">{package}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["installed_version"]}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["latest_version"]}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["repo"]}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(package)}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["installed_version"])}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["latest_version"])}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["repo"])}</td>
</tr>
'''
security_chunks += chunk
@ -185,10 +226,10 @@ def generate_email(receiver_emails: Union[list, None]):
continue
chunk = f'''
<tr>
<td style="border: 1px solid #ddd; padding: 8px;">{package}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["installed_version"]}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["latest_version"]}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["repo"]}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(package)}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["installed_version"])}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["latest_version"])}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["repo"])}</td>
</tr>
'''
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
"""

View File

@ -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

0
services/__init__.py Normal file
View File

View File

@ -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)

46
services/logger.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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)"
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"
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
echo ""
echo "Dependencies not uninstalled. Please uninstall them manually if needed: python3, git, pip"
echo "Vigil has been uninstalled successfully."