diff --git a/agents/patchmon-agent.sh b/agents/patchmon-agent.sh index c2fd428..2bbad61 100755 --- a/agents/patchmon-agent.sh +++ b/agents/patchmon-agent.sh @@ -1,12 +1,12 @@ #!/bin/bash -# PatchMon Agent Script v1.2.6 +# PatchMon Agent Script v1.2.7 # This script sends package update information to the PatchMon server using API credentials # Configuration PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}" API_VERSION="v1" -AGENT_VERSION="1.2.6" +AGENT_VERSION="1.2.7" CONFIG_FILE="/etc/patchmon/agent.conf" CREDENTIALS_FILE="/etc/patchmon/credentials" LOG_FILE="/var/log/patchmon-agent.log" @@ -144,7 +144,7 @@ EOF test_credentials() { load_credentials - local response=$(curl -ksv -X POST \ + local response=$(curl -ks -X POST \ -H "Content-Type: application/json" \ -H "X-API-ID: $API_ID" \ -H "X-API-KEY: $API_KEY" \ @@ -809,7 +809,7 @@ EOF local payload=$(echo "$base_payload $merged_json" | jq -s '.[0] * .[1]') - local response=$(curl -ksv -X POST \ + local response=$(curl -ks -X POST \ -H "Content-Type: application/json" \ -H "X-API-ID: $API_ID" \ -H "X-API-KEY: $API_KEY" \ @@ -821,25 +821,18 @@ EOF success "Update sent successfully" echo "$response" | grep -o '"packagesProcessed":[0-9]*' | cut -d':' -f2 | xargs -I {} info "Processed {} packages" - # Check for PatchMon agent update instructions (this updates the agent script, not system packages) - if echo "$response" | grep -q '"autoUpdate":{'; then - local auto_update_section=$(echo "$response" | grep -o '"autoUpdate":{[^}]*}') - local should_update=$(echo "$auto_update_section" | grep -o '"shouldUpdate":true' | cut -d':' -f2) - if [[ "$should_update" == "true" ]]; then - local latest_version=$(echo "$auto_update_section" | grep -o '"latestVersion":"[^"]*' | cut -d'"' -f4) - local current_version=$(echo "$auto_update_section" | grep -o '"currentVersion":"[^"]*' | cut -d'"' -f4) - local update_message=$(echo "$auto_update_section" | grep -o '"message":"[^"]*' | cut -d'"' -f4) - - info "PatchMon agent update detected: $update_message" - info "Current version: $current_version, Latest version: $latest_version" - - # Automatically run update-agent command to update the PatchMon agent script - info "Automatically updating PatchMon agent to latest version..." + # Check if auto-update is enabled and check for agent updates locally + if check_auto_update_enabled; then + info "Auto-update is enabled, checking for agent updates..." + if check_agent_update_needed; then + info "Agent update available, automatically updating..." if "$0" update-agent; then success "PatchMon agent update completed successfully" else warning "PatchMon agent update failed, but data was sent successfully" fi + else + info "Agent is up to date" fi fi @@ -877,7 +870,7 @@ EOF ping_server() { load_credentials - local response=$(curl -ksv -X POST \ + local response=$(curl -ks -X POST \ -H "Content-Type: application/json" \ -H "X-API-ID: $API_ID" \ -H "X-API-KEY: $API_KEY" \ @@ -920,7 +913,7 @@ check_version() { info "Checking for agent updates..." - local response=$(curl -ksv -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/version") + local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/version") if [[ $? -eq 0 ]]; then local current_version=$(echo "$response" | grep -o '"currentVersion":"[^"]*' | cut -d'"' -f4) @@ -949,59 +942,147 @@ check_version() { fi } +# Check if auto-update is enabled (both globally and for this host) +check_auto_update_enabled() { + # Get settings from server using API credentials + local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/settings" 2>/dev/null) + if [[ $? -ne 0 ]]; then + return 1 + fi + + # Check if both global and host auto-update are enabled + local global_auto_update=$(echo "$response" | grep -o '"auto_update":true' | cut -d':' -f2) + local host_auto_update=$(echo "$response" | grep -o '"host_auto_update":true' | cut -d':' -f2) + + if [[ "$global_auto_update" == "true" && "$host_auto_update" == "true" ]]; then + return 0 + else + return 1 + fi +} + +# Check if agent update is needed (internal function for auto-update) +check_agent_update_needed() { + # Get current agent timestamp + local current_timestamp=0 + if [[ -f "$0" ]]; then + current_timestamp=$(stat -c %Y "$0" 2>/dev/null || stat -f %m "$0" 2>/dev/null || echo "0") + fi + + # Get server agent info using API credentials + local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/timestamp" 2>/dev/null) + + if [[ $? -eq 0 ]]; then + local server_version=$(echo "$response" | grep -o '"version":"[^"]*' | cut -d'"' -f4) + local server_timestamp=$(echo "$response" | grep -o '"timestamp":[0-9]*' | cut -d':' -f2) + local server_exists=$(echo "$response" | grep -o '"exists":true' | cut -d':' -f2) + + if [[ "$server_exists" != "true" ]]; then + return 1 + fi + + # Check if update is needed + if [[ "$server_version" != "$AGENT_VERSION" ]]; then + return 0 # Update needed due to version mismatch + elif [[ "$server_timestamp" -gt "$current_timestamp" ]]; then + return 0 # Update needed due to newer timestamp + else + return 1 # No update needed + fi + else + return 1 # Failed to check + fi +} + +# Check for agent updates based on version and timestamp (interactive command) +check_agent_update() { + load_credentials + + info "Checking for agent updates..." + + # Get current agent timestamp + local current_timestamp=0 + if [[ -f "$0" ]]; then + current_timestamp=$(stat -c %Y "$0" 2>/dev/null || stat -f %m "$0" 2>/dev/null || echo "0") + fi + + # Get server agent info using API credentials + local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/timestamp") + + if [[ $? -eq 0 ]]; then + local server_version=$(echo "$response" | grep -o '"version":"[^"]*' | cut -d'"' -f4) + local server_timestamp=$(echo "$response" | grep -o '"timestamp":[0-9]*' | cut -d':' -f2) + local server_exists=$(echo "$response" | grep -o '"exists":true' | cut -d':' -f2) + + if [[ "$server_exists" != "true" ]]; then + warning "No agent script found on server" + return 1 + fi + + info "Current agent version: $AGENT_VERSION (timestamp: $current_timestamp)" + info "Server agent version: $server_version (timestamp: $server_timestamp)" + + # Check if update is needed + if [[ "$server_version" != "$AGENT_VERSION" ]]; then + info "Version mismatch detected - update needed" + return 0 + elif [[ "$server_timestamp" -gt "$current_timestamp" ]]; then + info "Server script is newer - update needed" + return 0 + else + info "Agent is up to date" + return 1 + fi + else + error "Failed to check agent timestamp from server" + return 1 + fi +} + # Update agent script update_agent() { load_credentials info "Updating agent script..." - local response=$(curl -ksv -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/version") + local download_url="$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/download" - if [[ $? -eq 0 ]]; then - local download_url=$(echo "$response" | grep -o '"downloadUrl":"[^"]*' | cut -d'"' -f4) - - if [[ -z "$download_url" ]]; then - download_url="$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/download" - elif [[ "$download_url" =~ ^/ ]]; then - # If download_url is relative, prepend the server URL - download_url="$PATCHMON_SERVER$download_url" - fi - - info "Downloading latest agent from: $download_url" - - # Create backup of current script - cp "$0" "$0.backup.$(date +%Y%m%d_%H%M%S)" - - # Download new version - if curl -ksv -o "/tmp/patchmon-agent-new.sh" "$download_url"; then - # Verify the downloaded script is valid - if bash -n "/tmp/patchmon-agent-new.sh" 2>/dev/null; then - # Replace current script - mv "/tmp/patchmon-agent-new.sh" "$0" - chmod +x "$0" - success "Agent updated successfully" - info "Backup saved as: $0.backup.$(date +%Y%m%d_%H%M%S)" - - # Get the new version number - local new_version=$(grep '^AGENT_VERSION=' "$0" | cut -d'"' -f2) - info "Updated to version: $new_version" - - # Automatically run update to send new information to PatchMon - info "Sending updated information to PatchMon..." - if "$0" update; then - success "Successfully sent updated information to PatchMon" - else - warning "Failed to send updated information to PatchMon (this is not critical)" - fi + info "Downloading latest agent from: $download_url" + + # Clean up old backups (keep only last 3) + ls -t "$0.backup."* 2>/dev/null | tail -n +4 | xargs -r rm -f + + # Create backup of current script + local backup_file="$0.backup.$(date +%Y%m%d_%H%M%S)" + cp "$0" "$backup_file" + + # Download new version using API credentials + if curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -o "/tmp/patchmon-agent-new.sh" "$download_url"; then + # Verify the downloaded script is valid + if bash -n "/tmp/patchmon-agent-new.sh" 2>/dev/null; then + # Replace current script + mv "/tmp/patchmon-agent-new.sh" "$0" + chmod +x "$0" + success "Agent updated successfully" + info "Backup saved as: $backup_file" + + # Get the new version number + local new_version=$(grep '^AGENT_VERSION=' "$0" | cut -d'"' -f2) + info "Updated to version: $new_version" + + # Automatically run update to send new information to PatchMon + info "Sending updated information to PatchMon..." + if "$0" update; then + success "Successfully sent updated information to PatchMon" else - error "Downloaded script is invalid" - rm -f "/tmp/patchmon-agent-new.sh" + warning "Failed to send updated information to PatchMon (this is not critical)" fi else - error "Failed to download new agent script" + error "Downloaded script is invalid" + rm -f "/tmp/patchmon-agent-new.sh" fi else - error "Failed to get update information" + error "Failed to download new agent script" fi } @@ -1009,7 +1090,7 @@ update_agent() { update_crontab() { load_credentials info "Updating crontab with current policy..." - local response=$(curl -ksv -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval") + local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval") if [[ $? -eq 0 ]]; then local update_interval=$(echo "$response" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2) if [[ -n "$update_interval" ]]; then @@ -1024,18 +1105,27 @@ update_crontab() { expected_crontab="*/$update_interval * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" fi - # Get current crontab - local current_crontab=$(crontab -l 2>/dev/null | grep "patchmon-agent.sh update" | head -1) + # Get current crontab (without patchmon entries) + local current_crontab_without_patchmon=$(crontab -l 2>/dev/null | grep -v "/usr/local/bin/patchmon-agent.sh update" || true) + local current_patchmon_entry=$(crontab -l 2>/dev/null | grep "/usr/local/bin/patchmon-agent.sh update" | head -1) # Check if crontab needs updating - if [[ "$current_crontab" == "$expected_crontab" ]]; then + if [[ "$current_patchmon_entry" == "$expected_crontab" ]]; then info "Crontab is already up to date (interval: $update_interval minutes)" return 0 fi info "Setting update interval to $update_interval minutes" - echo "$expected_crontab" | crontab - - success "Crontab updated successfully" + + # Combine existing cron (without patchmon entries) + new patchmon entry + { + if [[ -n "$current_crontab_without_patchmon" ]]; then + echo "$current_crontab_without_patchmon" + fi + echo "$expected_crontab" + } | crontab - + + success "Crontab updated successfully (duplicates removed)" else error "Could not determine update interval from server" fi @@ -1147,7 +1237,7 @@ main() { check_root setup_directories load_config - configure_credentials "$2" "$3" + configure_credentials "$2" "$3" "$4" ;; "test") check_root @@ -1178,6 +1268,11 @@ main() { load_config check_version ;; + "check-agent-update") + setup_directories + load_config + check_agent_update + ;; "update-agent") check_root setup_directories @@ -1195,22 +1290,23 @@ main() { ;; *) echo "PatchMon Agent v$AGENT_VERSION - API Credential Based" - echo "Usage: $0 {configure|test|update|ping|config|check-version|update-agent|update-crontab|diagnostics}" + echo "Usage: $0 {configure|test|update|ping|config|check-version|check-agent-update|update-agent|update-crontab|diagnostics}" echo "" echo "Commands:" - echo " configure - Configure API credentials for this host" + echo " configure [SERVER_URL] - Configure API credentials for this host" echo " test - Test API credentials connectivity" echo " update - Send package update information to server" echo " ping - Test connectivity to server" echo " config - Show current configuration" echo " check-version - Check for agent updates" + echo " check-agent-update - Check for agent updates using timestamp comparison" echo " update-agent - Update agent to latest version" echo " update-crontab - Update crontab with current policy" echo " diagnostics - Show detailed system diagnostics" echo "" echo "Setup Process:" echo " 1. Contact your PatchMon administrator to create a host entry" - echo " 2. Run: $0 configure (provided by admin)" + echo " 2. Run: $0 configure [SERVER_URL] (provided by admin)" echo " 3. Run: $0 test (to verify connection)" echo " 4. Run: $0 update (to send initial package data)" echo "" diff --git a/agents/patchmon_install.sh b/agents/patchmon_install.sh index 4190800..58779ac 100644 --- a/agents/patchmon_install.sh +++ b/agents/patchmon_install.sh @@ -1,7 +1,7 @@ #!/bin/bash # PatchMon Agent Installation Script -# Usage: curl -ksSL {PATCHMON_URL}/api/v1/hosts/install | bash -s -- {PATCHMON_URL} {API_ID} {API_KEY} +# Usage: curl -ks {PATCHMON_URL}/api/v1/hosts/install -H "X-API-ID: {API_ID}" -H "X-API-KEY: {API_KEY}" | bash set -e @@ -35,10 +35,34 @@ if [[ $EUID -ne 0 ]]; then error "This script must be run as root (use sudo)" fi +# Clean up old files (keep only last 3 of each type) +cleanup_old_files() { + # Clean up old credential backups + ls -t /etc/patchmon/credentials.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f + + # Clean up old agent backups + ls -t /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f + + # Clean up old log files + ls -t /var/log/patchmon-agent.log.old.* 2>/dev/null | tail -n +4 | xargs -r rm -f +} + +# Run cleanup at start +cleanup_old_files + +# Parse arguments from environment (passed via HTTP headers) +if [[ -z "$PATCHMON_URL" ]] || [[ -z "$API_ID" ]] || [[ -z "$API_KEY" ]]; then + error "Missing required parameters. This script should be called via the PatchMon web interface." +fi + +info "๐Ÿš€ Starting PatchMon Agent Installation..." +info "๐Ÿ“‹ Server: $PATCHMON_URL" +info "๐Ÿ”‘ API ID: ${API_ID:0:16}..." + # Install required dependencies info "๐Ÿ“ฆ Installing required dependencies..." -# Detect package manager and install jq +# Detect package manager and install jq and curl if command -v apt-get >/dev/null 2>&1; then # Debian/Ubuntu apt-get update >/dev/null 2>&1 @@ -62,76 +86,40 @@ else warning "Could not detect package manager. Please ensure 'jq' and 'curl' are installed manually." fi -# Verify jq installation -if ! command -v jq >/dev/null 2>&1; then - error "Failed to install 'jq'. Please install it manually: https://stedolan.github.io/jq/download/" +# Step 1: Handle existing configuration directory +info "๐Ÿ“ Setting up configuration directory..." + +# Check if configuration directory already exists +if [[ -d "/etc/patchmon" ]]; then + warning "โš ๏ธ Configuration directory already exists at /etc/patchmon" + warning "โš ๏ธ Preserving existing configuration files" + + # List existing files for user awareness + info "๐Ÿ“‹ Existing files in /etc/patchmon:" + ls -la /etc/patchmon/ 2>/dev/null | grep -v "^total" | while read -r line; do + echo " $line" + done +else + info "๐Ÿ“ Creating new configuration directory..." + mkdir -p /etc/patchmon fi -success "Dependencies installed successfully!" +# Step 2: Create credentials file +info "๐Ÿ” Creating API credentials file..." -# Default server URL (will be replaced by backend with configured URL) -PATCHMON_URL="http://localhost:3001" - -# Parse arguments -if [[ $# -ne 3 ]]; then - echo "Usage: curl -ksSL {PATCHMON_URL}/api/v1/hosts/install | bash -s -- {PATCHMON_URL} {API_ID} {API_KEY}" - echo "" - echo "Example:" - echo "curl -ksSL http://patchmon.example.com/api/v1/hosts/install | bash -s -- http://patchmon.example.com patchmon_1a2b3c4d abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890" - echo "" - echo "Contact your PatchMon administrator to get your API credentials." - exit 1 +# Check if credentials file already exists +if [[ -f "/etc/patchmon/credentials" ]]; then + warning "โš ๏ธ Credentials file already exists at /etc/patchmon/credentials" + warning "โš ๏ธ Moving existing file out of the way for fresh installation" + + # Clean up old credential backups (keep only last 3) + ls -t /etc/patchmon/credentials.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f + + # Move existing file out of the way + mv /etc/patchmon/credentials /etc/patchmon/credentials.backup.$(date +%Y%m%d_%H%M%S) + info "๐Ÿ“‹ Moved existing credentials to: /etc/patchmon/credentials.backup.$(date +%Y%m%d_%H%M%S)" fi -PATCHMON_URL="$1" -API_ID="$2" -API_KEY="$3" - -# Validate inputs -if [[ ! "$PATCHMON_URL" =~ ^https?:// ]]; then - error "Invalid URL format. Must start with http:// or https://" -fi - -if [[ ! "$API_ID" =~ ^patchmon_[a-f0-9]{16}$ ]]; then - error "Invalid API ID format. API ID should be in format: patchmon_xxxxxxxxxxxxxxxx" -fi - -if [[ ! "$API_KEY" =~ ^[a-f0-9]{64}$ ]]; then - error "Invalid API Key format. API Key should be 64 hexadecimal characters." -fi - -info "๐Ÿš€ Installing PatchMon Agent..." -info " Server: $PATCHMON_URL" -info " API ID: $API_ID" - -# Create patchmon directory -info "๐Ÿ“ Creating configuration directory..." -mkdir -p /etc/patchmon - -# Download the agent script -info "๐Ÿ“ฅ Downloading PatchMon agent script..." -curl -ksSL "$PATCHMON_URL/api/v1/hosts/agent/download" -o /usr/local/bin/patchmon-agent.sh -chmod +x /usr/local/bin/patchmon-agent.sh - -# Get the agent version from the downloaded script -AGENT_VERSION=$(grep '^AGENT_VERSION=' /usr/local/bin/patchmon-agent.sh | cut -d'"' -f2) -info "๐Ÿ“‹ Agent version: $AGENT_VERSION" - -# Get expected agent version from server -EXPECTED_VERSION=$(curl -ksv "$PATCHMON_URL/api/v1/hosts/agent/version" | grep -o '"currentVersion":"[^"]*' | cut -d'"' -f4 2>/dev/null || echo "Unknown") -if [[ "$EXPECTED_VERSION" != "Unknown" ]]; then - info "๐Ÿ“‹ Expected version: $EXPECTED_VERSION" - if [[ "$AGENT_VERSION" != "$EXPECTED_VERSION" ]]; then - warning "โš ๏ธ Agent version mismatch! Installed: $AGENT_VERSION, Expected: $EXPECTED_VERSION" - fi -fi - -# Get update interval policy from server -UPDATE_INTERVAL=$(curl -ksv "$PATCHMON_URL/api/v1/settings/update-interval" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2 2>/dev/null || echo "60") -info "๐Ÿ“‹ Update interval: $UPDATE_INTERVAL minutes" - -# Create credentials file -info "๐Ÿ” Setting up API credentials..." cat > /etc/patchmon/credentials << EOF # PatchMon API Credentials # Generated on $(date) @@ -139,63 +127,141 @@ PATCHMON_URL="$PATCHMON_URL" API_ID="$API_ID" API_KEY="$API_KEY" EOF - chmod 600 /etc/patchmon/credentials -# Test the configuration -info "๐Ÿงช Testing configuration..." +# Step 3: Download the agent script using API credentials +info "๐Ÿ“ฅ Downloading PatchMon agent script..." + +# Check if agent script already exists +if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then + warning "โš ๏ธ Agent script already exists at /usr/local/bin/patchmon-agent.sh" + warning "โš ๏ธ Moving existing file out of the way for fresh installation" + + # Clean up old agent backups (keep only last 3) + ls -t /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | tail -n +4 | xargs -r rm -f + + # Move existing file out of the way + mv /usr/local/bin/patchmon-agent.sh /usr/local/bin/patchmon-agent.sh.backup.$(date +%Y%m%d_%H%M%S) + info "๐Ÿ“‹ Moved existing agent to: /usr/local/bin/patchmon-agent.sh.backup.$(date +%Y%m%d_%H%M%S)" +fi + +curl -ks \ + -H "X-API-ID: $API_ID" \ + -H "X-API-KEY: $API_KEY" \ + "$PATCHMON_URL/api/v1/hosts/agent/download" \ + -o /usr/local/bin/patchmon-agent.sh + +chmod +x /usr/local/bin/patchmon-agent.sh + +# Get the agent version from the downloaded script +AGENT_VERSION=$(grep '^AGENT_VERSION=' /usr/local/bin/patchmon-agent.sh | cut -d'"' -f2 2>/dev/null || echo "Unknown") +info "๐Ÿ“‹ Agent version: $AGENT_VERSION" + +# Handle existing log files +if [[ -f "/var/log/patchmon-agent.log" ]]; then + warning "โš ๏ธ Existing log file found at /var/log/patchmon-agent.log" + warning "โš ๏ธ Rotating log file for fresh start" + + # Rotate the log file + mv /var/log/patchmon-agent.log /var/log/patchmon-agent.log.old.$(date +%Y%m%d_%H%M%S) + info "๐Ÿ“‹ Log file rotated to: /var/log/patchmon-agent.log.old.$(date +%Y%m%d_%H%M%S)" +fi + +# Step 4: Test the configuration +info "๐Ÿงช Testing API credentials and connectivity..." if /usr/local/bin/patchmon-agent.sh test; then - success "Configuration test passed!" + success "โœ… API credentials are valid and server is reachable" else - error "Configuration test failed. Please check your credentials." + error "โŒ Failed to validate API credentials or reach server" fi -# Send initial update -info "๐Ÿ“Š Sending initial package data..." +# Step 5: Send initial data +info "๐Ÿ“Š Sending initial package data to server..." if /usr/local/bin/patchmon-agent.sh update; then - success "Initial package data sent successfully!" + success "โœ… Initial package data sent successfully" else - warning "Initial package data failed, but agent is configured. You can run 'patchmon-agent.sh update' manually." + warning "โš ๏ธ Failed to send initial data. You can retry later with: /usr/local/bin/patchmon-agent.sh update" fi -# Setup crontab for automatic package status updates -info "โฐ Setting up automatic package status update every $UPDATE_INTERVAL minutes..." +# Step 6: Get update interval policy from server and setup crontab +info "โฐ Getting update interval policy from server..." +UPDATE_INTERVAL=$(curl -ks \ + -H "X-API-ID: $API_ID" \ + -H "X-API-KEY: $API_KEY" \ + "$PATCHMON_URL/api/v1/settings/update-interval" | \ + grep -o '"updateInterval":[0-9]*' | cut -d':' -f2 2>/dev/null || echo "60") -# Check if patchmon cron job already exists -PATCHMON_CRON_EXISTS=$(crontab -l 2>/dev/null | grep -c "patchmon-agent.sh update" || true) +info "๐Ÿ“‹ Update interval: $UPDATE_INTERVAL minutes" -if [[ $PATCHMON_CRON_EXISTS -gt 0 ]]; then - info " Existing PatchMon cron job found, removing old entry..." - # Remove existing patchmon cron entries and preserve other entries - (crontab -l 2>/dev/null | grep -v "patchmon-agent.sh update") | crontab - +# Setup crontab (smart duplicate detection) +info "๐Ÿ“… Setting up automated updates..." + +# Check if PatchMon cron entries already exist +if crontab -l 2>/dev/null | grep -q "/usr/local/bin/patchmon-agent.sh update"; then + warning "โš ๏ธ Existing PatchMon cron entries found" + warning "โš ๏ธ These will be replaced with new schedule" fi -if [[ $UPDATE_INTERVAL -eq 60 ]]; then - # Hourly updates - safely append to existing crontab - (crontab -l 2>/dev/null; echo "0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1") | crontab - -else - # Custom interval updates - safely append to existing crontab - (crontab -l 2>/dev/null; echo "*/$UPDATE_INTERVAL * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1") | crontab - -fi +# Function to setup crontab without duplicates +setup_crontab() { + local update_interval="$1" + local patchmon_pattern="/usr/local/bin/patchmon-agent.sh update" + + # Get current crontab, remove any existing patchmon entries + local current_cron=$(crontab -l 2>/dev/null | grep -v "$patchmon_pattern" || true) + + # Determine new cron entry + local new_entry + if [[ "$update_interval" -eq 60 ]]; then + # Hourly updates - use a random minute to spread load + local current_minute=$(date +%M) + new_entry="$current_minute * * * * $patchmon_pattern >/dev/null 2>&1" + info "๐Ÿ“‹ Configuring hourly updates at minute $current_minute" + else + # Custom interval updates + new_entry="*/$update_interval * * * * $patchmon_pattern >/dev/null 2>&1" + info "๐Ÿ“‹ Configuring updates every $update_interval minutes" + fi + + # Combine existing cron (without patchmon entries) + new entry + { + if [[ -n "$current_cron" ]]; then + echo "$current_cron" + fi + echo "$new_entry" + } | crontab - + + success "โœ… Crontab configured successfully (duplicates removed)" +} -success "๐ŸŽ‰ PatchMon Agent installation complete!" +setup_crontab "$UPDATE_INTERVAL" + +# Installation complete +success "๐ŸŽ‰ PatchMon Agent installation completed successfully!" echo "" -echo "๐Ÿ“‹ Installation Summary:" -echo " โ€ข Dependencies installed: jq, curl" +echo -e "${GREEN}๐Ÿ“‹ Installation Summary:${NC}" +echo " โ€ข Configuration directory: /etc/patchmon" echo " โ€ข Agent installed: /usr/local/bin/patchmon-agent.sh" -echo " โ€ข Agent version: $AGENT_VERSION" -if [[ "$EXPECTED_VERSION" != "Unknown" ]]; then - echo " โ€ข Expected version: $EXPECTED_VERSION" -fi -echo " โ€ข Config directory: /etc/patchmon/" -echo " โ€ข Credentials file: /etc/patchmon/credentials" -echo " โ€ข Status updates: Every $UPDATE_INTERVAL minutes via crontab" -echo " โ€ข View logs: tail -f /var/log/patchmon-agent.log" -echo "" -echo "๐Ÿ”ง Manual commands:" -echo " โ€ข Test connection: patchmon-agent.sh test" -echo " โ€ข Send update: patchmon-agent.sh update" -echo " โ€ข Check status: patchmon-agent.sh ping" -echo "" -success "Your host is now connected to PatchMon!" +echo " โ€ข Dependencies installed: jq, curl" +echo " โ€ข Crontab configured for automatic updates" +echo " โ€ข API credentials configured and tested" +# Check for moved files and show them +MOVED_FILES=$(ls /etc/patchmon/credentials.backup.* /usr/local/bin/patchmon-agent.sh.backup.* /var/log/patchmon-agent.log.old.* 2>/dev/null || true) +if [[ -n "$MOVED_FILES" ]]; then + echo "" + echo -e "${YELLOW}๐Ÿ“‹ Files Moved for Fresh Installation:${NC}" + echo "$MOVED_FILES" | while read -r moved_file; do + echo " โ€ข $moved_file" + done + echo "" + echo -e "${BLUE}๐Ÿ’ก Note: Old files are automatically cleaned up (keeping last 3)${NC}" +fi + +echo "" +echo -e "${BLUE}๐Ÿ”ง Management Commands:${NC}" +echo " โ€ข Test connection: /usr/local/bin/patchmon-agent.sh test" +echo " โ€ข Manual update: /usr/local/bin/patchmon-agent.sh update" +echo " โ€ข Check status: /usr/local/bin/patchmon-agent.sh diagnostics" +echo "" +success "โœ… Your system is now being monitored by PatchMon!" diff --git a/agents/patchmon_remove.sh b/agents/patchmon_remove.sh new file mode 100755 index 0000000..c6328e4 --- /dev/null +++ b/agents/patchmon_remove.sh @@ -0,0 +1,217 @@ +#!/bin/bash + +# PatchMon Agent Removal Script +# Usage: curl -ks {PATCHMON_URL}/api/v1/hosts/remove | bash +# This script completely removes PatchMon from the system + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Functions +error() { + echo -e "${RED}โŒ ERROR: $1${NC}" >&2 + exit 1 +} + +info() { + echo -e "${BLUE}โ„น๏ธ $1${NC}" +} + +success() { + echo -e "${GREEN}โœ… $1${NC}" +} + +warning() { + echo -e "${YELLOW}โš ๏ธ $1${NC}" +} + +# Check if running as root +if [[ $EUID -ne 0 ]]; then + error "This script must be run as root (use sudo)" +fi + +info "๐Ÿ—‘๏ธ Starting PatchMon Agent Removal..." +echo "" + +# Step 1: Stop any running PatchMon processes +info "๐Ÿ›‘ Stopping PatchMon processes..." +if pgrep -f "patchmon-agent.sh" >/dev/null; then + warning "Found running PatchMon processes, stopping them..." + pkill -f "patchmon-agent.sh" || true + sleep 2 + success "PatchMon processes stopped" +else + info "No running PatchMon processes found" +fi + +# Step 2: Remove crontab entries +info "๐Ÿ“… Removing PatchMon crontab entries..." +if crontab -l 2>/dev/null | grep -q "patchmon-agent.sh"; then + warning "Found PatchMon crontab entries, removing them..." + crontab -l 2>/dev/null | grep -v "patchmon-agent.sh" | crontab - + success "Crontab entries removed" +else + info "No PatchMon crontab entries found" +fi + +# Step 3: Remove agent script +info "๐Ÿ“„ Removing agent script..." +if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then + warning "Removing agent script: /usr/local/bin/patchmon-agent.sh" + rm -f /usr/local/bin/patchmon-agent.sh + success "Agent script removed" +else + info "Agent script not found" +fi + +# Step 4: Remove configuration directory and files +info "๐Ÿ“ Removing configuration files..." +if [[ -d "/etc/patchmon" ]]; then + warning "Removing configuration directory: /etc/patchmon" + + # Show what's being removed + info "๐Ÿ“‹ Files in /etc/patchmon:" + ls -la /etc/patchmon/ 2>/dev/null | grep -v "^total" | while read -r line; do + echo " $line" + done + + # Remove the directory + rm -rf /etc/patchmon + success "Configuration directory removed" +else + info "Configuration directory not found" +fi + +# Step 5: Remove log files +info "๐Ÿ“ Removing log files..." +if [[ -f "/var/log/patchmon-agent.log" ]]; then + warning "Removing log file: /var/log/patchmon-agent.log" + rm -f /var/log/patchmon-agent.log + success "Log file removed" +else + info "Log file not found" +fi + +# Step 6: Clean up backup files (optional) +info "๐Ÿงน Cleaning up backup files..." +BACKUP_COUNT=0 + +# Count credential backups +CRED_BACKUPS=$(ls /etc/patchmon/credentials.backup.* 2>/dev/null | wc -l || echo "0") +if [[ $CRED_BACKUPS -gt 0 ]]; then + BACKUP_COUNT=$((BACKUP_COUNT + CRED_BACKUPS)) +fi + +# Count agent backups +AGENT_BACKUPS=$(ls /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | wc -l || echo "0") +if [[ $AGENT_BACKUPS -gt 0 ]]; then + BACKUP_COUNT=$((BACKUP_COUNT + AGENT_BACKUPS)) +fi + +# Count log backups +LOG_BACKUPS=$(ls /var/log/patchmon-agent.log.old.* 2>/dev/null | wc -l || echo "0") +if [[ $LOG_BACKUPS -gt 0 ]]; then + BACKUP_COUNT=$((BACKUP_COUNT + LOG_BACKUPS)) +fi + +if [[ $BACKUP_COUNT -gt 0 ]]; then + warning "Found $BACKUP_COUNT backup files" + echo "" + echo -e "${YELLOW}๐Ÿ“‹ Backup files found:${NC}" + + # Show credential backups + if [[ $CRED_BACKUPS -gt 0 ]]; then + echo " Credential backups:" + ls /etc/patchmon/credentials.backup.* 2>/dev/null | while read -r file; do + echo " โ€ข $file" + done + fi + + # Show agent backups + if [[ $AGENT_BACKUPS -gt 0 ]]; then + echo " Agent script backups:" + ls /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | while read -r file; do + echo " โ€ข $file" + done + fi + + # Show log backups + if [[ $LOG_BACKUPS -gt 0 ]]; then + echo " Log file backups:" + ls /var/log/patchmon-agent.log.old.* 2>/dev/null | while read -r file; do + echo " โ€ข $file" + done + fi + + echo "" + echo -e "${BLUE}๐Ÿ’ก Note: Backup files are preserved for safety${NC}" + echo -e "${BLUE}๐Ÿ’ก You can remove them manually if not needed${NC}" +else + info "No backup files found" +fi + +# Step 7: Remove dependencies (optional) +info "๐Ÿ“ฆ Checking for PatchMon-specific dependencies..." +if command -v jq >/dev/null 2>&1; then + warning "jq is installed (used by PatchMon)" + echo -e "${BLUE}๐Ÿ’ก Note: jq may be used by other applications${NC}" + echo -e "${BLUE}๐Ÿ’ก Consider keeping it unless you're sure it's not needed${NC}" +else + info "jq not found" +fi + +if command -v curl >/dev/null 2>&1; then + warning "curl is installed (used by PatchMon)" + echo -e "${BLUE}๐Ÿ’ก Note: curl is commonly used by many applications${NC}" + echo -e "${BLUE}๐Ÿ’ก Consider keeping it unless you're sure it's not needed${NC}" +else + info "curl not found" +fi + +# Step 8: Final verification +info "๐Ÿ” Verifying removal..." +REMAINING_FILES=0 + +if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then + REMAINING_FILES=$((REMAINING_FILES + 1)) +fi + +if [[ -d "/etc/patchmon" ]]; then + REMAINING_FILES=$((REMAINING_FILES + 1)) +fi + +if [[ -f "/var/log/patchmon-agent.log" ]]; then + REMAINING_FILES=$((REMAINING_FILES + 1)) +fi + +if crontab -l 2>/dev/null | grep -q "patchmon-agent.sh"; then + REMAINING_FILES=$((REMAINING_FILES + 1)) +fi + +if [[ $REMAINING_FILES -eq 0 ]]; then + success "โœ… PatchMon has been completely removed from the system!" +else + warning "โš ๏ธ Some PatchMon files may still remain ($REMAINING_FILES items)" + echo -e "${BLUE}๐Ÿ’ก You may need to remove them manually${NC}" +fi + +echo "" +echo -e "${GREEN}๐Ÿ“‹ Removal Summary:${NC}" +echo " โ€ข Agent script: Removed" +echo " โ€ข Configuration files: Removed" +echo " โ€ข Log files: Removed" +echo " โ€ข Crontab entries: Removed" +echo " โ€ข Running processes: Stopped" +echo " โ€ข Backup files: Preserved (if any)" +echo "" +echo -e "${BLUE}๐Ÿ”ง Manual cleanup (if needed):${NC}" +echo " โ€ข Remove backup files: rm /etc/patchmon/credentials.backup.* /usr/local/bin/patchmon-agent.sh.backup.* /var/log/patchmon-agent.log.old.*" +echo " โ€ข Remove dependencies: apt remove jq curl (if not needed by other apps)" +echo "" +success "๐ŸŽ‰ PatchMon removal completed!" diff --git a/backend/package.json b/backend/package.json index 17617a7..a12467e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "patchmon-backend", - "version": "1.2.6", + "version": "1.2.7", "description": "Backend API for Linux Patch Monitoring System", "license": "AGPL-3.0", "main": "src/server.js", diff --git a/backend/prisma/migrations/20250929191252_drop_agent_versions_table/migration.sql b/backend/prisma/migrations/20250929191252_drop_agent_versions_table/migration.sql new file mode 100644 index 0000000..57c715f --- /dev/null +++ b/backend/prisma/migrations/20250929191252_drop_agent_versions_table/migration.sql @@ -0,0 +1,2 @@ +-- DropTable +DROP TABLE "agent_versions"; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 4f1b859..563c53f 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -7,18 +7,6 @@ datasource db { url = env("DATABASE_URL") } -model agent_versions { - id String @id - version String @unique - is_current Boolean @default(false) - release_notes String? - download_url String? - min_server_version String? - created_at DateTime @default(now()) - updated_at DateTime - is_default Boolean @default(false) - script_content String? -} model dashboard_preferences { id String @id diff --git a/backend/src/config/database.js b/backend/src/config/database.js index 4427e63..169533d 100644 --- a/backend/src/config/database.js +++ b/backend/src/config/database.js @@ -37,7 +37,7 @@ function createPrismaClient() { }, }, log: - process.env.NODE_ENV === "development" + process.env.PRISMA_LOG_QUERIES === "true" ? ["query", "info", "warn", "error"] : ["warn", "error"], errorFormat: "pretty", @@ -66,17 +66,21 @@ async function waitForDatabase(prisma, options = {}) { parseInt(process.env.PM_DB_CONN_WAIT_INTERVAL, 10) || 2; - console.log( - `Waiting for database connection (max ${maxAttempts} attempts, ${waitInterval}s interval)...`, - ); + if (process.env.ENABLE_LOGGING === "true") { + console.log( + `Waiting for database connection (max ${maxAttempts} attempts, ${waitInterval}s interval)...`, + ); + } for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const isConnected = await checkDatabaseConnection(prisma); if (isConnected) { - console.log( - `Database connected successfully after ${attempt} attempt(s)`, - ); + if (process.env.ENABLE_LOGGING === "true") { + console.log( + `Database connected successfully after ${attempt} attempt(s)`, + ); + } return true; } } catch { @@ -84,9 +88,11 @@ async function waitForDatabase(prisma, options = {}) { } if (attempt < maxAttempts) { - console.log( - `โณ Database not ready (attempt ${attempt}/${maxAttempts}), retrying in ${waitInterval}s...`, - ); + if (process.env.ENABLE_LOGGING === "true") { + console.log( + `โณ Database not ready (attempt ${attempt}/${maxAttempts}), retrying in ${waitInterval}s...`, + ); + } await new Promise((resolve) => setTimeout(resolve, waitInterval * 1000)); } } diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index 0317e88..8c8dca8 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -3,8 +3,8 @@ const { PrismaClient } = require("@prisma/client"); const { body, validationResult } = require("express-validator"); const { v4: uuidv4 } = require("uuid"); const crypto = require("node:crypto"); -const path = require("node:path"); -const fs = require("node:fs"); +const _path = require("node:path"); +const _fs = require("node:fs"); const { authenticateToken, _requireAdmin } = require("../middleware/auth"); const { requireManageHosts, @@ -14,72 +14,48 @@ const { const router = express.Router(); const prisma = new PrismaClient(); -// Public endpoint to download the agent script +// Secure endpoint to download the agent script (requires API authentication) router.get("/agent/download", async (req, res) => { try { - const { version } = req.query; + // Verify API credentials + const apiId = req.headers["x-api-id"]; + const apiKey = req.headers["x-api-key"]; - let agentVersion; - - if (version) { - // Download specific version - agentVersion = await prisma.agent_versions.findUnique({ - where: { version }, - }); - - if (!agentVersion) { - return res.status(404).json({ error: "Agent version not found" }); - } - } else { - // Download current version (latest) - agentVersion = await prisma.agent_versions.findFirst({ - where: { is_current: true }, - orderBy: { created_at: "desc" }, - }); - - if (!agentVersion) { - // Fallback to default version - agentVersion = await prisma.agent_versions.findFirst({ - where: { is_default: true }, - orderBy: { created_at: "desc" }, - }); - } + if (!apiId || !apiKey) { + return res.status(401).json({ error: "API credentials required" }); } - // Use script content from database if available, otherwise fallback to file - if (agentVersion?.script_content) { - // Convert Windows line endings to Unix line endings - const scriptContent = agentVersion.script_content - .replace(/\r\n/g, "\n") - .replace(/\r/g, "\n"); - res.setHeader("Content-Type", "application/x-shellscript"); - res.setHeader( - "Content-Disposition", - `attachment; filename="patchmon-agent-${agentVersion.version}.sh"`, - ); - res.send(scriptContent); - } else { - // Fallback to file system when no database version exists or script has no content - const agentPath = path.join( - __dirname, - "../../../agents/patchmon-agent.sh", - ); - if (!fs.existsSync(agentPath)) { - return res.status(404).json({ error: "Agent script not found" }); - } - // Read file and convert line endings - const scriptContent = fs - .readFileSync(agentPath, "utf8") - .replace(/\r\n/g, "\n") - .replace(/\r/g, "\n"); - res.setHeader("Content-Type", "application/x-shellscript"); - const version = agentVersion ? `-${agentVersion.version}` : ""; - res.setHeader( - "Content-Disposition", - `attachment; filename="patchmon-agent${version}.sh"`, - ); - res.send(scriptContent); + // Validate API credentials + const host = await prisma.hosts.findUnique({ + where: { api_id: apiId }, + }); + + if (!host || host.api_key !== apiKey) { + return res.status(401).json({ error: "Invalid API credentials" }); } + + // Serve agent script directly from file system + const fs = require("node:fs"); + const path = require("node:path"); + + const agentPath = path.join(__dirname, "../../../agents/patchmon-agent.sh"); + + if (!fs.existsSync(agentPath)) { + return res.status(404).json({ error: "Agent script not found" }); + } + + // Read file and convert line endings + const scriptContent = fs + .readFileSync(agentPath, "utf8") + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + + res.setHeader("Content-Type", "application/x-shellscript"); + res.setHeader( + "Content-Disposition", + 'attachment; filename="patchmon-agent.sh"', + ); + res.send(scriptContent); } catch (error) { console.error("Agent download error:", error); res.status(500).json({ error: "Failed to download agent script" }); @@ -89,21 +65,32 @@ router.get("/agent/download", async (req, res) => { // Version check endpoint for agents router.get("/agent/version", async (_req, res) => { try { - const currentVersion = await prisma.agent_versions.findFirst({ - where: { is_current: true }, - orderBy: { created_at: "desc" }, - }); + const fs = require("node:fs"); + const path = require("node:path"); - if (!currentVersion) { - return res.status(404).json({ error: "No current agent version found" }); + // Read version directly from agent script file + const agentPath = path.join(__dirname, "../../../agents/patchmon-agent.sh"); + + if (!fs.existsSync(agentPath)) { + return res.status(404).json({ error: "Agent script not found" }); } + const scriptContent = fs.readFileSync(agentPath, "utf8"); + const versionMatch = scriptContent.match(/AGENT_VERSION="([^"]+)"/); + + if (!versionMatch) { + return res + .status(500) + .json({ error: "Could not extract version from agent script" }); + } + + const currentVersion = versionMatch[1]; + res.json({ - currentVersion: currentVersion.version, - downloadUrl: - currentVersion.download_url || `/api/v1/hosts/agent/download`, - releaseNotes: currentVersion.release_notes, - minServerVersion: currentVersion.min_server_version, + currentVersion: currentVersion, + downloadUrl: `/api/v1/hosts/agent/download`, + releaseNotes: `PatchMon Agent v${currentVersion}`, + minServerVersion: null, }); } catch (error) { console.error("Version check error:", error); @@ -527,42 +514,7 @@ router.post( }); }); - // Check if agent auto-update is enabled and if there's a newer version available - let autoUpdateResponse = null; - try { - const settings = await prisma.settings.findFirst(); - // Check both global agent auto-update setting AND host-specific agent auto-update setting - if (settings?.auto_update && host.auto_update) { - // Get current agent version from the request - const currentAgentVersion = req.body.agentVersion; - - if (currentAgentVersion) { - // Get the latest agent version - const latestAgentVersion = await prisma.agent_versions.findFirst({ - where: { is_current: true }, - orderBy: { created_at: "desc" }, - }); - - if ( - latestAgentVersion && - latestAgentVersion.version !== currentAgentVersion - ) { - // There's a newer version available - autoUpdateResponse = { - shouldUpdate: true, - currentVersion: currentAgentVersion, - latestVersion: latestAgentVersion.version, - message: - "A newer agent version is available. Run: /usr/local/bin/patchmon-agent.sh update-agent", - updateCommand: "update-agent", - }; - } - } - } - } catch (error) { - console.error("Agent auto-update check error:", error); - // Don't fail the update if agent auto-update check fails - } + // Agent auto-update is now handled client-side by the agent itself const response = { message: "Host updated successfully", @@ -571,11 +523,6 @@ router.post( securityUpdates: securityCount, }; - // Add agent auto-update response if available - if (autoUpdateResponse) { - response.autoUpdate = autoUpdateResponse; - } - // Check if crontab update is needed (when update interval changes) // This is a simple check - if the host has auto-update enabled, we'll suggest crontab update if (host.auto_update) { @@ -1103,9 +1050,26 @@ router.patch( }, ); -// Serve the installation script -router.get("/install", async (_req, res) => { +// Serve the installation script (requires API authentication) +router.get("/install", async (req, res) => { try { + // Verify API credentials + const apiId = req.headers["x-api-id"]; + const apiKey = req.headers["x-api-key"]; + + if (!apiId || !apiKey) { + return res.status(401).json({ error: "API credentials required" }); + } + + // Validate API credentials + const host = await prisma.hosts.findUnique({ + where: { api_id: apiId }, + }); + + if (!host || host.api_key !== apiKey) { + return res.status(401).json({ error: "Invalid API credentials" }); + } + const fs = require("node:fs"); const path = require("node:path"); @@ -1124,14 +1088,11 @@ router.get("/install", async (_req, res) => { script = script.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); // Get the configured server URL from settings + let serverUrl = "http://localhost:3001"; try { const settings = await prisma.settings.findFirst(); - if (settings) { - // Replace the default server URL in the script with the configured one - script = script.replace( - /PATCHMON_URL="[^"]*"/g, - `PATCHMON_URL="${settings.server_url}"`, - ); + if (settings?.server_url) { + serverUrl = settings.server_url; } } catch (settingsError) { console.warn( @@ -1140,6 +1101,18 @@ router.get("/install", async (_req, res) => { ); } + // Inject the API credentials and server URL into the script as environment variables + const envVars = `#!/bin/bash +export PATCHMON_URL="${serverUrl}" +export API_ID="${host.api_id}" +export API_KEY="${host.api_key}" + +`; + + // Remove the shebang from the original script and prepend our env vars + script = script.replace(/^#!/, "#"); + script = envVars + script; + res.setHeader("Content-Type", "text/plain"); res.setHeader( "Content-Disposition", @@ -1152,215 +1125,247 @@ router.get("/install", async (_req, res) => { } }); -// ==================== AGENT VERSION MANAGEMENT ==================== +// Serve the removal script (public endpoint - no authentication required) +router.get("/remove", async (_req, res) => { + try { + const fs = require("node:fs"); + const path = require("node:path"); -// Get all agent versions (admin only) + const scriptPath = path.join( + __dirname, + "../../../agents/patchmon_remove.sh", + ); + + if (!fs.existsSync(scriptPath)) { + return res.status(404).json({ error: "Removal script not found" }); + } + + // Read the script content + const script = fs.readFileSync(scriptPath, "utf8"); + + // Set appropriate headers for script download + res.setHeader("Content-Type", "text/plain"); + res.setHeader( + "Content-Disposition", + 'inline; filename="patchmon_remove.sh"', + ); + res.send(script); + } catch (error) { + console.error("Removal script error:", error); + res.status(500).json({ error: "Failed to serve removal script" }); + } +}); + +// ==================== AGENT FILE MANAGEMENT ==================== + +// Get agent file information (admin only) router.get( - "/agent/versions", + "/agent/info", authenticateToken, requireManageSettings, async (_req, res) => { try { - const versions = await prisma.agent_versions.findMany({ - orderBy: { created_at: "desc" }, - }); + const fs = require("node:fs").promises; + const path = require("node:path"); - res.json(versions); - } catch (error) { - console.error("Get agent versions error:", error); - res.status(500).json({ error: "Failed to get agent versions" }); - } - }, -); + const agentPath = path.join( + __dirname, + "../../../agents/patchmon-agent.sh", + ); -// Create new agent version (admin only) -router.post( - "/agent/versions", - authenticateToken, - requireManageSettings, - [ - body("version").isLength({ min: 1 }).withMessage("Version is required"), - body("releaseNotes").optional().isString(), - body("downloadUrl") - .optional() - .isURL() - .withMessage("Download URL must be valid"), - body("minServerVersion").optional().isString(), - body("scriptContent").optional().isString(), - body("isDefault").optional().isBoolean(), - ], - async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } + try { + const stats = await fs.stat(agentPath); + const content = await fs.readFile(agentPath, "utf8"); - const { - version, - releaseNotes, - downloadUrl, - minServerVersion, - scriptContent, - isDefault, - } = req.body; + // Extract version from agent script (look for AGENT_VERSION= line) + const versionMatch = content.match(/^AGENT_VERSION="([^"]+)"/m); + const version = versionMatch ? versionMatch[1] : "unknown"; - // Check if version already exists - const existingVersion = await prisma.agent_versions.findUnique({ - where: { version }, - }); - - if (existingVersion) { - return res.status(400).json({ error: "Version already exists" }); - } - - // If this is being set as default, unset other defaults - if (isDefault) { - await prisma.agent_versions.updateMany({ - where: { is_default: true }, - data: { - is_default: false, - updated_at: new Date(), - }, - }); - } - - const agentVersion = await prisma.agent_versions.create({ - data: { - id: uuidv4(), + res.json({ + exists: true, version, - release_notes: releaseNotes, - download_url: downloadUrl, - min_server_version: minServerVersion, - script_content: scriptContent, - is_default: isDefault || false, - is_current: false, - updated_at: new Date(), - }, - }); - - res.status(201).json(agentVersion); + lastModified: stats.mtime, + size: stats.size, + sizeFormatted: `${(stats.size / 1024).toFixed(1)} KB`, + }); + } catch (error) { + if (error.code === "ENOENT") { + res.json({ + exists: false, + version: null, + lastModified: null, + size: 0, + sizeFormatted: "0 KB", + }); + } else { + throw error; + } + } } catch (error) { - console.error("Create agent version error:", error); - res.status(500).json({ error: "Failed to create agent version" }); + console.error("Get agent info error:", error); + res.status(500).json({ error: "Failed to get agent information" }); } }, ); -// Set current agent version (admin only) -router.patch( - "/agent/versions/:versionId/current", +// Update agent file (admin only) +router.post( + "/agent/upload", authenticateToken, requireManageSettings, async (req, res) => { try { - const { versionId } = req.params; + const { scriptContent } = req.body; - // First, unset all current versions - await prisma.agent_versions.updateMany({ - where: { is_current: true }, - data: { is_current: false, updated_at: new Date() }, - }); + if (!scriptContent || typeof scriptContent !== "string") { + return res.status(400).json({ error: "Script content is required" }); + } - // Set the specified version as current - const agentVersion = await prisma.agent_versions.update({ - where: { id: versionId }, - data: { is_current: true, updated_at: new Date() }, - }); - - res.json(agentVersion); - } catch (error) { - console.error("Set current agent version error:", error); - res.status(500).json({ error: "Failed to set current agent version" }); - } - }, -); - -// Set default agent version (admin only) -router.patch( - "/agent/versions/:versionId/default", - authenticateToken, - requireManageSettings, - async (req, res) => { - try { - const { versionId } = req.params; - - // First, unset all default versions - await prisma.agent_versions.updateMany({ - where: { is_default: true }, - data: { is_default: false, updated_at: new Date() }, - }); - - // Set the specified version as default - const agentVersion = await prisma.agent_versions.update({ - where: { id: versionId }, - data: { is_default: true, updated_at: new Date() }, - }); - - res.json(agentVersion); - } catch (error) { - console.error("Set default agent version error:", error); - res.status(500).json({ error: "Failed to set default agent version" }); - } - }, -); - -// Delete agent version (admin only) -router.delete( - "/agent/versions/:versionId", - authenticateToken, - requireManageSettings, - async (req, res) => { - try { - const { versionId } = req.params; - - // Validate versionId format - if (!versionId || versionId.length < 10) { + // Basic validation - check if it looks like a shell script + if (!scriptContent.trim().startsWith("#!/")) { return res.status(400).json({ - error: "Invalid agent version ID format", - details: "The provided ID does not match expected format", + error: "Invalid script format - must start with shebang (#!/...)", }); } - const agentVersion = await prisma.agent_versions.findUnique({ - where: { id: versionId }, - }); + const fs = require("node:fs").promises; + const path = require("node:path"); - if (!agentVersion) { - return res.status(404).json({ - error: "Agent version not found", - details: `No agent version found with ID: ${versionId}`, - suggestion: - "Please refresh the page to get the latest agent versions", - }); + const agentPath = path.join( + __dirname, + "../../../agents/patchmon-agent.sh", + ); + + // Create backup of existing file + try { + const backupPath = `${agentPath}.backup.${Date.now()}`; + await fs.copyFile(agentPath, backupPath); + console.log(`Created backup: ${backupPath}`); + } catch (error) { + // Ignore if original doesn't exist + if (error.code !== "ENOENT") { + console.warn("Failed to create backup:", error.message); + } } - if (agentVersion.is_current) { - return res.status(400).json({ - error: "Cannot delete current agent version", - details: `Version ${agentVersion.version} is currently active`, - suggestion: "Set another version as current before deleting this one", - }); - } + // Write new agent script + await fs.writeFile(agentPath, scriptContent, { mode: 0o755 }); - await prisma.agent_versions.delete({ - where: { id: versionId }, - }); + // Get updated file info + const stats = await fs.stat(agentPath); + const versionMatch = scriptContent.match(/^AGENT_VERSION="([^"]+)"/m); + const version = versionMatch ? versionMatch[1] : "unknown"; res.json({ - message: "Agent version deleted successfully", - deletedVersion: agentVersion.version, + message: "Agent script updated successfully", + version, + lastModified: stats.mtime, + size: stats.size, + sizeFormatted: `${(stats.size / 1024).toFixed(1)} KB`, }); } catch (error) { - console.error("Delete agent version error:", error); - res.status(500).json({ - error: "Failed to delete agent version", - details: error.message, - }); + console.error("Upload agent error:", error); + res.status(500).json({ error: "Failed to update agent script" }); } }, ); +// Get agent file timestamp for update checking (requires API credentials) +router.get("/agent/timestamp", async (req, res) => { + try { + // Check for API credentials + const apiId = req.headers["x-api-id"]; + const apiKey = req.headers["x-api-key"]; + + if (!apiId || !apiKey) { + return res.status(401).json({ error: "API credentials required" }); + } + + // Verify API credentials + const host = await prisma.hosts.findFirst({ + where: { + api_id: apiId, + api_key: apiKey, + }, + }); + + if (!host) { + return res.status(401).json({ error: "Invalid API credentials" }); + } + + const fs = require("node:fs").promises; + const path = require("node:path"); + + const agentPath = path.join(__dirname, "../../../agents/patchmon-agent.sh"); + + try { + const stats = await fs.stat(agentPath); + const content = await fs.readFile(agentPath, "utf8"); + + // Extract version from agent script + const versionMatch = content.match(/^AGENT_VERSION="([^"]+)"/m); + const version = versionMatch ? versionMatch[1] : "unknown"; + + res.json({ + version, + lastModified: stats.mtime, + timestamp: Math.floor(stats.mtime.getTime() / 1000), // Unix timestamp + exists: true, + }); + } catch (error) { + if (error.code === "ENOENT") { + res.json({ + version: null, + lastModified: null, + timestamp: 0, + exists: false, + }); + } else { + throw error; + } + } + } catch (error) { + console.error("Get agent timestamp error:", error); + res.status(500).json({ error: "Failed to get agent timestamp" }); + } +}); + +// Get settings for agent (requires API credentials) +router.get("/settings", async (req, res) => { + try { + // Check for API credentials + const apiId = req.headers["x-api-id"]; + const apiKey = req.headers["x-api-key"]; + + if (!apiId || !apiKey) { + return res.status(401).json({ error: "API credentials required" }); + } + + // Verify API credentials + const host = await prisma.hosts.findFirst({ + where: { + api_id: apiId, + api_key: apiKey, + }, + }); + + if (!host) { + return res.status(401).json({ error: "Invalid API credentials" }); + } + + const settings = await prisma.settings.findFirst(); + + // Return both global and host-specific auto-update settings + res.json({ + auto_update: settings?.auto_update || false, + host_auto_update: host.auto_update || false, + }); + } catch (error) { + console.error("Get settings error:", error); + res.status(500).json({ error: "Failed to get settings" }); + } +}); + // Update host friendly name (admin only) router.patch( "/:hostId/friendly-name", diff --git a/backend/src/routes/settingsRoutes.js b/backend/src/routes/settingsRoutes.js index 1cf0e33..cbb1fcb 100644 --- a/backend/src/routes/settingsRoutes.js +++ b/backend/src/routes/settingsRoutes.js @@ -109,7 +109,9 @@ async function triggerCrontabUpdates() { router.get("/", authenticateToken, requireManageSettings, async (_req, res) => { try { const settings = await getSettings(); - console.log("Returning settings:", settings); + if (process.env.ENABLE_LOGGING === "true") { + console.log("Returning settings:", settings); + } res.json(settings); } catch (error) { console.error("Settings fetch error:", error); @@ -239,9 +241,26 @@ router.get("/server-url", async (_req, res) => { } }); -// Get update interval policy for agents (public endpoint) -router.get("/update-interval", async (_req, res) => { +// Get update interval policy for agents (requires API authentication) +router.get("/update-interval", async (req, res) => { try { + // Verify API credentials + const apiId = req.headers["x-api-id"]; + const apiKey = req.headers["x-api-key"]; + + if (!apiId || !apiKey) { + return res.status(401).json({ error: "API credentials required" }); + } + + // Validate API credentials + const host = await prisma.hosts.findUnique({ + where: { api_id: apiId }, + }); + + if (!host || host.api_key !== apiKey) { + return res.status(401).json({ error: "Invalid API credentials" }); + } + const settings = await getSettings(); res.json({ updateInterval: settings.update_interval, diff --git a/backend/src/routes/versionRoutes.js b/backend/src/routes/versionRoutes.js index 8dd58b9..1ce0f20 100644 --- a/backend/src/routes/versionRoutes.js +++ b/backend/src/routes/versionRoutes.js @@ -14,7 +14,7 @@ const router = express.Router(); router.get("/current", authenticateToken, async (_req, res) => { try { // Read version from package.json dynamically - let currentVersion = "1.2.6"; // fallback + let currentVersion = "1.2.7"; // fallback try { const packageJson = require("../../package.json"); @@ -174,7 +174,7 @@ router.get( return res.status(400).json({ error: "Settings not found" }); } - const currentVersion = "1.2.6"; + const currentVersion = "1.2.7"; const latestVersion = settings.latest_version || currentVersion; const isUpdateAvailable = settings.update_available || false; const lastUpdateCheck = settings.last_update_check || null; diff --git a/backend/src/server.js b/backend/src/server.js index 953c853..832cefc 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -30,200 +30,6 @@ const { initSettings } = require("./services/settingsService"); // Initialize Prisma client with optimized connection pooling for multiple instances const prisma = createPrismaClient(); -// Simple version comparison function for semantic versioning -function compareVersions(version1, version2) { - const v1Parts = version1.split(".").map(Number); - const v2Parts = version2.split(".").map(Number); - - // Ensure both arrays have the same length - const maxLength = Math.max(v1Parts.length, v2Parts.length); - while (v1Parts.length < maxLength) v1Parts.push(0); - while (v2Parts.length < maxLength) v2Parts.push(0); - - for (let i = 0; i < maxLength; i++) { - if (v1Parts[i] > v2Parts[i]) return true; - if (v1Parts[i] < v2Parts[i]) return false; - } - - return false; // versions are equal -} - -// Function to check and import agent version on startup -async function checkAndImportAgentVersion() { - console.log("๐Ÿ” Starting agent version auto-import check..."); - - // Skip if auto-import is disabled - if (process.env.AUTO_IMPORT_AGENT_VERSION === "false") { - console.log("โŒ Auto-import of agent version is disabled"); - if (process.env.ENABLE_LOGGING === "true") { - logger.info("Auto-import of agent version is disabled"); - } - return; - } - - try { - const fs = require("node:fs"); - const path = require("node:path"); - const crypto = require("node:crypto"); - - // Read and validate agent script - const agentScriptPath = path.join( - __dirname, - "../../agents/patchmon-agent.sh", - ); - console.log("๐Ÿ“ Agent script path:", agentScriptPath); - - // Check if file exists - if (!fs.existsSync(agentScriptPath)) { - console.log("โŒ Agent script file not found, skipping version check"); - if (process.env.ENABLE_LOGGING === "true") { - logger.warn("Agent script file not found, skipping version check"); - } - return; - } - - console.log("โœ… Agent script file found"); - - // Read the file content - const scriptContent = fs.readFileSync(agentScriptPath, "utf8"); - - // Extract version from script content - const versionMatch = scriptContent.match(/AGENT_VERSION="([^"]+)"/); - - if (!versionMatch) { - console.log( - "โŒ Could not extract version from agent script, skipping version check", - ); - if (process.env.ENABLE_LOGGING === "true") { - logger.warn( - "Could not extract version from agent script, skipping version check", - ); - } - return; - } - - const localVersion = versionMatch[1]; - console.log("๐Ÿ“‹ Local version:", localVersion); - - // Check if this version already exists in database - const existingVersion = await prisma.agent_versions.findUnique({ - where: { version: localVersion }, - }); - - if (existingVersion) { - console.log( - `โœ… Agent version ${localVersion} already exists in database`, - ); - if (process.env.ENABLE_LOGGING === "true") { - logger.info(`Agent version ${localVersion} already exists in database`); - } - return; - } - - console.log(`๐Ÿ†• Agent version ${localVersion} not found in database`); - - // Get existing versions for comparison - const allVersions = await prisma.agent_versions.findMany({ - select: { version: true }, - orderBy: { created_at: "desc" }, - }); - - // Determine version flags and whether to proceed - const isFirstVersion = allVersions.length === 0; - const isNewerVersion = - !isFirstVersion && compareVersions(localVersion, allVersions[0].version); - - if (!isFirstVersion && !isNewerVersion) { - console.log( - `โŒ Agent version ${localVersion} is not newer than existing versions, skipping import`, - ); - if (process.env.ENABLE_LOGGING === "true") { - logger.info( - `Agent version ${localVersion} is not newer than existing versions, skipping import`, - ); - } - return; - } - - const shouldSetAsCurrent = isFirstVersion || isNewerVersion; - const shouldSetAsDefault = isFirstVersion; - - console.log( - isFirstVersion - ? `๐Ÿ“Š No existing versions found in database` - : `๐Ÿ“Š Found ${allVersions.length} existing versions in database, latest: ${allVersions[0].version}`, - ); - - if (!isFirstVersion) { - console.log( - `๐Ÿ”„ Version comparison: ${localVersion} > ${allVersions[0].version} = ${isNewerVersion}`, - ); - } - - // Clear existing flags if needed - const updatePromises = []; - if (shouldSetAsCurrent) { - updatePromises.push( - prisma.agent_versions.updateMany({ - where: { is_current: true }, - data: { is_current: false }, - }), - ); - } - if (shouldSetAsDefault) { - updatePromises.push( - prisma.agent_versions.updateMany({ - where: { is_default: true }, - data: { is_default: false }, - }), - ); - } - - if (updatePromises.length > 0) { - await Promise.all(updatePromises); - } - - // Create new version - await prisma.agent_versions.create({ - data: { - id: crypto.randomUUID(), - version: localVersion, - release_notes: `Auto-imported on startup (${new Date().toISOString()})`, - script_content: scriptContent, - is_default: shouldSetAsDefault, - is_current: shouldSetAsCurrent, - updated_at: new Date(), - }, - }); - - console.log( - `๐ŸŽ‰ Successfully auto-imported new agent version ${localVersion} on startup`, - ); - if (shouldSetAsCurrent) { - console.log(`โœ… Set version ${localVersion} as current version`); - } - if (shouldSetAsDefault) { - console.log(`โœ… Set version ${localVersion} as default version`); - } - if (process.env.ENABLE_LOGGING === "true") { - logger.info( - `โœ… Auto-imported new agent version ${localVersion} on startup (current: ${shouldSetAsCurrent}, default: ${shouldSetAsDefault})`, - ); - } - } catch (error) { - console.error( - "โŒ Failed to check/import agent version on startup:", - error.message, - ); - if (process.env.ENABLE_LOGGING === "true") { - logger.error( - "Failed to check/import agent version on startup:", - error.message, - ); - } - } -} - // Function to check and create default role permissions on startup async function checkAndCreateRolePermissions() { console.log("๐Ÿ” Starting role permissions auto-creation check..."); @@ -610,8 +416,6 @@ process.on("SIGTERM", async () => { // Initialize dashboard preferences for all users async function initializeDashboardPreferences() { try { - console.log("๐Ÿ”ง Initializing dashboard preferences for all users..."); - // Get all users const users = await prisma.users.findMany({ select: { @@ -628,12 +432,9 @@ async function initializeDashboardPreferences() { }); if (users.length === 0) { - console.log("โ„น๏ธ No users found in database"); return; } - console.log(`๐Ÿ“Š Found ${users.length} users to initialize`); - let initializedCount = 0; let updatedCount = 0; @@ -648,10 +449,6 @@ async function initializeDashboardPreferences() { if (!hasPreferences) { // User has no preferences - create them - console.log( - `โš™๏ธ Creating preferences for ${user.username} (${user.role})`, - ); - const preferencesData = expectedPreferences.map((pref) => ({ id: require("uuid").v4(), user_id: user.id, @@ -667,18 +464,11 @@ async function initializeDashboardPreferences() { }); initializedCount++; - console.log( - ` โœ… Created ${expectedCardCount} cards based on permissions`, - ); } else { // User already has preferences - check if they need updating const currentCardCount = user.dashboard_preferences.length; if (currentCardCount !== expectedCardCount) { - console.log( - `๐Ÿ”„ Updating preferences for ${user.username} (${user.role}) - ${currentCardCount} โ†’ ${expectedCardCount} cards`, - ); - // Delete existing preferences await prisma.dashboard_preferences.deleteMany({ where: { user_id: user.id }, @@ -700,29 +490,16 @@ async function initializeDashboardPreferences() { }); updatedCount++; - console.log( - ` โœ… Updated to ${expectedCardCount} cards based on permissions`, - ); - } else { - console.log( - `โœ… ${user.username} already has correct preferences (${currentCardCount} cards)`, - ); } } } - console.log(`\n๐Ÿ“‹ Dashboard Preferences Initialization Complete:`); - console.log(` - New users initialized: ${initializedCount}`); - console.log(` - Existing users updated: ${updatedCount}`); - console.log( - ` - Users with correct preferences: ${users.length - initializedCount - updatedCount}`, - ); - console.log(`\n๐ŸŽฏ Permission-based preferences:`); - console.log(` - Cards are now assigned based on actual user permissions`); - console.log( - ` - Each card requires specific permissions (can_view_hosts, can_view_users, etc.)`, - ); - console.log(` - Users only see cards they have permission to access`); + // Only show summary if there were changes + if (initializedCount > 0 || updatedCount > 0) { + console.log( + `โœ… Dashboard preferences: ${initializedCount} initialized, ${updatedCount} updated`, + ); + } } catch (error) { console.error("โŒ Error initializing dashboard preferences:", error); throw error; @@ -889,9 +666,6 @@ async function startServer() { throw initError; // Fail startup if settings can't be initialised } - // Check and import agent version on startup - await checkAndImportAgentVersion(); - // Check and create default role permissions on startup await checkAndCreateRolePermissions(); diff --git a/backend/src/services/settingsService.js b/backend/src/services/settingsService.js index e25c1d9..b81c541 100644 --- a/backend/src/services/settingsService.js +++ b/backend/src/services/settingsService.js @@ -72,9 +72,11 @@ async function syncEnvironmentToSettings(currentSettings) { if (currentValue !== convertedValue) { updates[settingsField] = convertedValue; hasChanges = true; - console.log( - `Environment variable ${envVar} (${envValue}) differs from settings ${settingsField} (${currentValue}), updating...`, - ); + if (process.env.ENABLE_LOGGING === "true") { + console.log( + `Environment variable ${envVar} (${envValue}) differs from settings ${settingsField} (${currentValue}), updating...`, + ); + } } } } @@ -89,7 +91,9 @@ async function syncEnvironmentToSettings(currentSettings) { if (currentSettings.server_url !== constructedServerUrl) { updates.server_url = constructedServerUrl; hasChanges = true; - console.log(`Updating server_url to: ${constructedServerUrl}`); + if (process.env.ENABLE_LOGGING === "true") { + console.log(`Updating server_url to: ${constructedServerUrl}`); + } } // Update settings if there are changes @@ -101,9 +105,11 @@ async function syncEnvironmentToSettings(currentSettings) { updated_at: new Date(), }, }); - console.log( - `Synced ${Object.keys(updates).length} environment variables to settings`, - ); + if (process.env.ENABLE_LOGGING === "true") { + console.log( + `Synced ${Object.keys(updates).length} environment variables to settings`, + ); + } return updatedSettings; } diff --git a/backend/src/services/updateScheduler.js b/backend/src/services/updateScheduler.js index 96c03a4..074cf8e 100644 --- a/backend/src/services/updateScheduler.js +++ b/backend/src/services/updateScheduler.js @@ -109,7 +109,7 @@ class UpdateScheduler { } // Read version from package.json dynamically - let currentVersion = "1.2.6"; // fallback + let currentVersion = "1.2.7"; // fallback try { const packageJson = require("../../package.json"); if (packageJson?.version) { @@ -219,7 +219,7 @@ class UpdateScheduler { const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`; // Get current version for User-Agent - let currentVersion = "1.2.6"; // fallback + let currentVersion = "1.2.7"; // fallback try { const packageJson = require("../../package.json"); if (packageJson?.version) { diff --git a/frontend/package.json b/frontend/package.json index 606034b..3a7bd29 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "patchmon-frontend", "private": true, - "version": "1.2.6", + "version": "1.2.7", "license": "AGPL-3.0", "type": "module", "scripts": { diff --git a/frontend/src/pages/HostDetail.jsx b/frontend/src/pages/HostDetail.jsx index 1e3a601..125411b 100644 --- a/frontend/src/pages/HostDetail.jsx +++ b/frontend/src/pages/HostDetail.jsx @@ -1083,13 +1083,13 @@ const CredentialsModal = ({ host, isOpen, onClose }) => { One-Line Installation

- Copy and run this command on the target host to automatically - install and configure the PatchMon agent: + Copy and run this command on the target host to securely install + and configure the PatchMon agent:

@@ -1097,7 +1097,7 @@ const CredentialsModal = ({ host, isOpen, onClose }) => { type="button" onClick={() => copyToClipboard( - `curl -s ${serverUrl}/api/v1/hosts/install | bash -s -- ${serverUrl} "${host.api_id}" "${host.api_key}"`, + `curl -ks ${serverUrl}/api/v1/hosts/install -H "X-API-ID: ${host.api_id}" -H "X-API-KEY: ${host.api_key}" | bash`, ) } className="btn-primary flex items-center gap-1" @@ -1118,21 +1118,19 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
- 1. Download Agent Script + 1. Create Configuration Directory
+
+ + +
- {/* Version Summary */} - {agentVersions && agentVersions.length > 0 && ( -
-
-
- - - Current Version: - - - {agentVersions.find((v) => v.is_current)?.version || - "None"} - -
-
- - - Default Version: - - - {agentVersions.find((v) => v.is_default)?.version || - "None"} - -
-
-
- )} - - {agentVersionsLoading ? ( + {agentFileLoading ? (
- ) : agentVersionsError ? ( + ) : agentFileError ? (

- Error loading agent versions: {agentVersionsError.message} + Error loading agent file: {agentFileError.message}

- ) : !agentVersions || agentVersions.length === 0 ? ( + ) : !agentFileInfo?.exists ? (
-

- No agent versions found + +

+ No agent script found +

+

+ Upload an agent script to get started

) : ( -
- {agentVersions.map((version) => ( -
-
-
- -
-
-

- Version {version.version} -

- {version.is_default && ( - - - Default - - )} - {version.is_current && ( - - Current - - )} -
- {version.release_notes && ( -
-

- {version.release_notes} -

-
- )} -

- Created:{" "} - {new Date( - version.created_at, - ).toLocaleDateString()} -

-
-
-
- - - - +
+ {/* Agent File Info */} +
+

+ Current Agent Script +

+
+
+ + + Version: + + + {agentFileInfo.version} + +
+
+ + + Size: + + + {agentFileInfo.sizeFormatted} + +
+
+ + + Modified: + + + {new Date( + agentFileInfo.lastModified, + ).toLocaleDateString()} + +
+
+
+ + {/* Usage Instructions */} +
+
+ +
+

+ Agent Script Usage +

+
+

This script is used for:

+
    +
  • + New agent installations via the install script +
  • +
  • + Agent downloads from the + /api/v1/hosts/agent/download endpoint +
  • +
  • Manual agent deployments and updates
  • +
- ))} +
- {agentVersions?.length === 0 && ( -
- -

- No agent versions found -

-

- Add your first agent version to get started -

+ {/* Uninstall Instructions */} +
+
+ +
+

+ Agent Uninstall Command +

+
+

+ To completely remove PatchMon from a host: +

+
+
+ curl -ks {window.location.origin} + /api/v1/hosts/remove | sudo bash +
+ +
+

+ โš ๏ธ This will remove all PatchMon files, + configuration, and crontab entries +

+
+
- )} +
)}
@@ -1330,52 +1263,42 @@ const Settings = () => {
- {/* Agent Version Modal */} - {showAgentVersionModal && ( - { - setShowAgentVersionModal(false); - setAgentVersionForm({ - version: "", - releaseNotes: "", - scriptContent: "", - isDefault: false, - }); - }} - onSubmit={createAgentVersionMutation.mutate} - isLoading={createAgentVersionMutation.isPending} + {/* Agent Upload Modal */} + {showUploadModal && ( + setShowUploadModal(false)} + onSubmit={uploadAgentMutation.mutate} + isLoading={uploadAgentMutation.isPending} + error={uploadAgentMutation.error} /> )}
); }; -// Agent Version Modal Component -const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => { - const [formData, setFormData] = useState({ - version: "", - releaseNotes: "", - scriptContent: "", - isDefault: false, - }); - const [errors, setErrors] = useState({}); +// Agent Upload Modal Component +const AgentUploadModal = ({ isOpen, onClose, onSubmit, isLoading, error }) => { + const [scriptContent, setScriptContent] = useState(""); + const [uploadError, setUploadError] = useState(""); const handleSubmit = (e) => { e.preventDefault(); + setUploadError(""); - // Basic validation - const newErrors = {}; - if (!formData.version.trim()) newErrors.version = "Version is required"; - if (!formData.scriptContent.trim()) - newErrors.scriptContent = "Script content is required"; - - if (Object.keys(newErrors).length > 0) { - setErrors(newErrors); + if (!scriptContent.trim()) { + setUploadError("Script content is required"); return; } - onSubmit(formData); + if (!scriptContent.trim().startsWith("#!/")) { + setUploadError( + "Script must start with a shebang (#!/bin/bash or #!/bin/sh)", + ); + return; + } + + onSubmit(scriptContent); }; const handleFileUpload = (e) => { @@ -1383,10 +1306,8 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => { if (file) { const reader = new FileReader(); reader.onload = (event) => { - setFormData((prev) => ({ - ...prev, - scriptContent: event.target.result, - })); + setScriptContent(event.target.result); + setUploadError(""); }; reader.readAsText(file); } @@ -1396,11 +1317,11 @@ const AgentVersionModal = ({ isOpen, onClose, onSubmit, isLoading }) => { return (
-
+

- Add Agent Version + Replace Agent Script