diff --git a/agents/patchmon-agent-legacy1-2-8.sh b/agents/patchmon-agent-legacy1-2-8.sh deleted file mode 100644 index 750ce51..0000000 --- a/agents/patchmon-agent-legacy1-2-8.sh +++ /dev/null @@ -1,1598 +0,0 @@ -#!/bin/bash - -# PatchMon Agent Script v1.2.8 -# 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.8" -CONFIG_FILE="/etc/patchmon/agent.conf" -CREDENTIALS_FILE="/etc/patchmon/credentials" -LOG_FILE="/var/log/patchmon-agent.log" - -# This placeholder will be dynamically replaced by the server when serving this -# script based on the "ignore SSL self-signed" setting. If set to -k, curl will -# ignore certificate validation. Otherwise, it will be empty for secure default. -CURL_FLAGS="" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Logging function -log() { - # Try to write to log file, but don't fail if we can't - if [[ -w "$(dirname "$LOG_FILE")" ]] 2>/dev/null; then - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" 2>/dev/null - fi -} - -# Error handling -error() { - echo -e "${RED}ERROR: $1${NC}" >&2 - log "ERROR: $1" - exit 1 -} - -# Info logging (cleaner output - only stdout, no duplicate logging) -info() { - echo -e "${BLUE}ℹ️ $1${NC}" - log "INFO: $1" -} - -# Success logging (cleaner output - only stdout, no duplicate logging) -success() { - echo -e "${GREEN}✅ $1${NC}" - log "SUCCESS: $1" -} - -# Warning logging (cleaner output - only stdout, no duplicate logging) -warning() { - echo -e "${YELLOW}⚠️ $1${NC}" - log "WARNING: $1" -} - -# Get or generate machine ID -get_machine_id() { - # Try standard locations for machine-id - if [[ -f /etc/machine-id ]]; then - cat /etc/machine-id - elif [[ -f /var/lib/dbus/machine-id ]]; then - cat /var/lib/dbus/machine-id - else - # Fallback: generate from hardware UUID or hostname+MAC - if command -v dmidecode &> /dev/null; then - local uuid=$(dmidecode -s system-uuid 2>/dev/null | tr -d ' -' | tr '[:upper:]' '[:lower:]') - if [[ -n "$uuid" && "$uuid" != "notpresent" ]]; then - echo "$uuid" - return - fi - fi - # Last resort: hash hostname + primary MAC address - local primary_mac=$(ip link show | grep -oP '(?<=link/ether\s)[0-9a-f:]+' | head -1 | tr -d ':') - echo "$HOSTNAME-$primary_mac" | sha256sum | cut -d' ' -f1 | cut -c1-32 - fi -} - -# Check if running as root -check_root() { - if [[ $EUID -ne 0 ]]; then - error "This script must be run as root" - fi -} - -# Verify system datetime and timezone -verify_datetime() { - info "Verifying system datetime and timezone..." - - # Get current system time - local system_time=$(date) - local timezone="Unknown" - - # Try to get timezone with timeout protection - if command -v timedatectl >/dev/null 2>&1; then - timezone=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Unknown") - fi - - # Log datetime info (non-blocking) - log "System datetime check - time: $system_time, timezone: $timezone" 2>/dev/null || true - - # Simple check - just log the info, don't block execution - if [[ "$timezone" == "Unknown" ]] || [[ -z "$timezone" ]]; then - warning "System timezone not configured: $timezone" - log "WARNING: System timezone not configured - timezone: $timezone" 2>/dev/null || true - fi - - return 0 -} - -# Create necessary directories -setup_directories() { - mkdir -p /etc/patchmon - mkdir -p /var/log - touch "$LOG_FILE" - chmod 600 "$LOG_FILE" -} - -# Load configuration -load_config() { - if [[ -f "$CONFIG_FILE" ]]; then - source "$CONFIG_FILE" - fi -} - -# Load API credentials -load_credentials() { - if [[ ! -f "$CREDENTIALS_FILE" ]]; then - error "Credentials file not found at $CREDENTIALS_FILE. Please configure API credentials first." - fi - - source "$CREDENTIALS_FILE" - - if [[ -z "$API_ID" ]] || [[ -z "$API_KEY" ]]; then - error "API_ID and API_KEY must be configured in $CREDENTIALS_FILE" - fi - - # Use PATCHMON_URL from credentials if available, otherwise use default - if [[ -n "$PATCHMON_URL" ]]; then - PATCHMON_SERVER="$PATCHMON_URL" - fi -} - -# Configure API credentials -configure_credentials() { - info "Setting up API credentials..." - - if [[ -z "$1" ]] || [[ -z "$2" ]]; then - echo "Usage: $0 configure [SERVER_URL]" - echo "" - echo "Example:" - echo " $0 configure patchmon_1a2b3c4d abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890" - echo " $0 configure patchmon_1a2b3c4d abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890 http://patchmon.example.com" - echo "" - echo "Contact your PatchMon administrator to get your API credentials." - exit 1 - fi - - local api_id="$1" - local api_key="$2" - local server_url="${3:-$PATCHMON_SERVER}" - - # Validate API ID format - if [[ ! "$api_id" =~ ^patchmon_[a-f0-9]{16}$ ]]; then - error "Invalid API ID format. API ID should be in format: patchmon_xxxxxxxxxxxxxxxx" - fi - - # Validate API Key format (64 hex characters) - if [[ ! "$api_key" =~ ^[a-f0-9]{64}$ ]]; then - error "Invalid API Key format. API Key should be 64 hexadecimal characters." - fi - - # Validate server URL format - if [[ ! "$server_url" =~ ^https?:// ]]; then - error "Invalid server URL format. Must start with http:// or https://" - fi - - # Create credentials file - cat > "$CREDENTIALS_FILE" << EOF -# PatchMon API Credentials -# Generated on $(date) -PATCHMON_URL="$server_url" -API_ID="$api_id" -API_KEY="$api_key" -EOF - - chmod 600 "$CREDENTIALS_FILE" - success "API credentials configured successfully" - info "Credentials saved to: $CREDENTIALS_FILE" - - # Test credentials - info "Testing API credentials..." - test_credentials -} - -# Test API credentials -test_credentials() { - load_credentials - - local response=$(curl $CURL_FLAGS -X POST \ - -H "Content-Type: application/json" \ - -H "X-API-ID: $API_ID" \ - -H "X-API-KEY: $API_KEY" \ - "$PATCHMON_SERVER/api/$API_VERSION/hosts/ping") - - if [[ $? -eq 0 ]] && echo "$response" | grep -q "success"; then - success "API credentials are valid" - local hostname=$(echo "$response" | grep -o '"hostname":"[^"]*' | cut -d'"' -f4) - if [[ -n "$hostname" ]]; then - info "Connected as host: $hostname" - fi - else - error "API credentials test failed: $response" - fi -} - -# Detect OS and version -detect_os() { - if [[ -f /etc/os-release ]]; then - source /etc/os-release - OS_TYPE=$(echo "$ID" | tr '[:upper:]' '[:lower:]') - OS_VERSION="$VERSION_ID" - - # Map OS variations to their appropriate categories - case "$OS_TYPE" in - "pop"|"linuxmint"|"elementary") - OS_TYPE="ubuntu" - ;; - "opensuse"|"opensuse-leap"|"opensuse-tumbleweed") - OS_TYPE="suse" - ;; - "almalinux") - OS_TYPE="rhel" - ;; - "ol") - # Keep Oracle Linux as 'ol' for proper frontend identification - OS_TYPE="ol" - ;; - # Rocky Linux keeps its own identity for proper frontend display - esac - - elif [[ -f /etc/redhat-release ]]; then - if grep -q "CentOS" /etc/redhat-release; then - OS_TYPE="centos" - elif grep -q "Red Hat" /etc/redhat-release; then - OS_TYPE="rhel" - fi - OS_VERSION=$(grep -oE '[0-9]+\.[0-9]+' /etc/redhat-release | head -1) - else - error "Unable to detect OS version" - fi - - ARCHITECTURE=$(uname -m) - HOSTNAME=$(hostname) - IP_ADDRESS=$(hostname -I | awk '{print $1}') -} - -# Get repository information based on OS -get_repository_info() { - local repos_json="[" - local first=true - - case "$OS_TYPE" in - "ubuntu"|"debian") - get_apt_repositories repos_json first - ;; - "centos"|"rhel"|"fedora"|"ol"|"rocky") - get_yum_repositories repos_json first - ;; - *) - # Return empty array for unsupported OS - ;; - esac - - repos_json+="]" - echo "$repos_json" -} - -# Get repository info for APT-based systems -get_apt_repositories() { - local -n repos_ref=$1 - local -n first_ref=$2 - - # Parse traditional .list files - local sources_files="/etc/apt/sources.list" - if [[ -d "/etc/apt/sources.list.d" ]]; then - sources_files="$sources_files $(find /etc/apt/sources.list.d -name '*.list' 2>/dev/null)" - fi - - for file in $sources_files; do - if [[ -f "$file" ]]; then - while IFS= read -r line; do - # Skip comments and empty lines - if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then - continue - fi - - # Parse repository line (deb or deb-src) - if [[ "$line" =~ ^[[:space:]]*(deb|deb-src)[[:space:]]+ ]]; then - # Clean the line and extract components - local clean_line=$(echo "$line" | xargs) - local repo_type=$(echo "$clean_line" | awk '{print $1}') - - # Handle modern APT format with options like [signed-by=...] - local url="" - local distribution="" - local components="" - - if [[ "$clean_line" =~ \[.*\] ]]; then - # Modern format: deb [options] URL distribution components - # Extract URL (first field after the options) - url=$(echo "$clean_line" | sed 's/deb[^[:space:]]* \[[^]]*\] //' | awk '{print $1}') - distribution=$(echo "$clean_line" | sed 's/deb[^[:space:]]* \[[^]]*\] //' | awk '{print $2}') - components=$(echo "$clean_line" | sed 's/deb[^[:space:]]* \[[^]]*\] [^[:space:]]* [^[:space:]]* //') - else - # Traditional format: deb URL distribution components - url=$(echo "$clean_line" | awk '{print $2}') - distribution=$(echo "$clean_line" | awk '{print $3}') - components=$(echo "$clean_line" | cut -d' ' -f4- | xargs) - fi - - # Skip if URL doesn't look like a valid URL - if [[ ! "$url" =~ ^https?:// ]] && [[ ! "$url" =~ ^ftp:// ]]; then - continue - fi - - # Skip if distribution is empty or looks malformed - if [[ -z "$distribution" ]] || [[ "$distribution" =~ \[.*\] ]]; then - continue - fi - - # Determine if repository uses HTTPS - local is_secure=false - if [[ "$url" =~ ^https:// ]]; then - is_secure=true - fi - - # Generate repository name from URL and distribution - local repo_name="$distribution" - - # Extract meaningful name from URL for better identification - if [[ "$url" =~ archive\.ubuntu\.com ]]; then - repo_name="ubuntu-$distribution" - elif [[ "$url" =~ security\.ubuntu\.com ]]; then - repo_name="ubuntu-$distribution-security" - elif [[ "$url" =~ deb\.nodesource\.com ]]; then - repo_name="nodesource-$distribution" - elif [[ "$url" =~ packagecloud\.io ]]; then - repo_name="packagecloud-$(echo "$url" | cut -d'/' -f4-5 | tr '/' '-')" - elif [[ "$url" =~ ppa\.launchpad ]]; then - repo_name="ppa-$(echo "$url" | cut -d'/' -f4-5 | tr '/' '-')" - elif [[ "$url" =~ packages\.microsoft\.com ]]; then - repo_name="microsoft-$(echo "$url" | cut -d'/' -f4-)" - elif [[ "$url" =~ download\.docker\.com ]]; then - repo_name="docker-$distribution" - else - # Fallback: use domain name + distribution - local domain=$(echo "$url" | cut -d'/' -f3 | cut -d':' -f1) - repo_name="$domain-$distribution" - fi - - # Add component suffix if relevant - if [[ "$components" =~ updates ]]; then - repo_name="$repo_name-updates" - elif [[ "$components" =~ security ]]; then - repo_name="$repo_name-security" - elif [[ "$components" =~ backports ]]; then - repo_name="$repo_name-backports" - fi - - if [[ "$first_ref" == true ]]; then - first_ref=false - else - repos_ref+="," - fi - - repos_ref+="{\"name\":\"$repo_name\",\"url\":\"$url\",\"distribution\":\"$distribution\",\"components\":\"$components\",\"repoType\":\"$repo_type\",\"isEnabled\":true,\"isSecure\":$is_secure}" - fi - done < "$file" - fi - done - - # Parse modern DEB822 format (.sources files) - if [[ -d "/etc/apt/sources.list.d" ]]; then - local sources_files_deb822=$(find /etc/apt/sources.list.d -name '*.sources' 2>/dev/null) - for file in $sources_files_deb822; do - if [[ -f "$file" ]]; then - local deb822_result=$(parse_deb822_sources_simple "$file") - if [[ -n "$deb822_result" ]]; then - if [[ "$first_ref" == true ]]; then - first_ref=false - repos_ref+="$deb822_result" - else - repos_ref+=",$deb822_result" - fi - fi - fi - done - fi -} - -# Simple DEB822 parser that returns JSON string -parse_deb822_sources_simple() { - local file=$1 - local result="" - local enabled="" - local types="" - local uris="" - local suites="" - local components="" - local name="" - local first_entry=true - - while IFS= read -r line; do - # Skip empty lines and comments - if [[ -z "$line" ]] || [[ "$line" =~ ^[[:space:]]*# ]]; then - continue - fi - - # Parse key-value pairs - if [[ "$line" =~ ^([^:]+):[[:space:]]*(.*)$ ]]; then - local key="${BASH_REMATCH[1]}" - local value="${BASH_REMATCH[2]}" - - case "$key" in - "Enabled") - enabled="$value" - ;; - "Types") - types="$value" - ;; - "URIs") - uris="$value" - ;; - "Suites") - suites="$value" - ;; - "Components") - components="$value" - ;; - "X-Repolib-Name") - name="$value" - ;; - esac - fi - - # Process repository entry when we hit a blank line - if [[ -z "$line" ]] || [[ "$line" =~ ^[[:space:]]*$ ]]; then - if [[ -n "$uris" && -n "$suites" && "$enabled" == "yes" ]]; then - local entry_result=$(process_deb822_entry_simple "$name" "$types" "$uris" "$suites" "$components") - if [[ -n "$entry_result" ]]; then - if [[ "$first_entry" == true ]]; then - first_entry=false - result="$entry_result" - else - result="$result,$entry_result" - fi - fi - fi - # Reset variables for next entry - enabled="" - types="" - uris="" - suites="" - components="" - name="" - fi - done < "$file" - - # Process the last entry if file doesn't end with blank line - if [[ -n "$uris" && -n "$suites" && "$enabled" == "yes" ]]; then - local entry_result=$(process_deb822_entry_simple "$name" "$types" "$uris" "$suites" "$components") - if [[ -n "$entry_result" ]]; then - if [[ "$first_entry" == true ]]; then - result="$entry_result" - else - result="$result,$entry_result" - fi - fi - fi - - echo "$result" -} - -# Process a DEB822 repository entry and return JSON -process_deb822_entry_simple() { - local name=$1 - local types=$2 - local uris=$3 - local suites=$4 - local components=$5 - local result="" - local first_entry=true - - # Handle multiple URIs - for uri in $uris; do - # Skip if URI doesn't look like a valid URL - if [[ ! "$uri" =~ ^https?:// ]] && [[ ! "$uri" =~ ^ftp:// ]]; then - continue - fi - - # Handle multiple suites - for suite in $suites; do - # Skip if suite looks malformed - if [[ -z "$suite" ]]; then - continue - fi - - # Determine if repository uses HTTPS - local is_secure=false - if [[ "$uri" =~ ^https:// ]]; then - is_secure=true - fi - - # Generate repository name - local repo_name="" - if [[ -n "$name" ]]; then - repo_name=$(echo "$name" | tr ' ' '-' | tr '[:upper:]' '[:lower:]') - else - repo_name="$suite" - fi - - # Extract meaningful name from URI for better identification - if [[ "$uri" =~ apt\.pop-os\.org/ubuntu ]]; then - repo_name="pop-os-ubuntu-$suite" - elif [[ "$uri" =~ apt\.pop-os\.org/release ]]; then - repo_name="pop-os-release-$suite" - elif [[ "$uri" =~ apt\.pop-os\.org/proprietary ]]; then - repo_name="pop-os-apps-$suite" - elif [[ "$uri" =~ archive\.ubuntu\.com ]]; then - repo_name="ubuntu-$suite" - elif [[ "$uri" =~ security\.ubuntu\.com ]]; then - repo_name="ubuntu-$suite-security" - else - # Fallback: use domain name + suite - local domain=$(echo "$uri" | cut -d'/' -f3 | cut -d':' -f1) - repo_name="$domain-$suite" - fi - - # Add component suffix if relevant and not already included - if [[ "$suite" != *"security"* && "$components" =~ security ]]; then - repo_name="$repo_name-security" - elif [[ "$suite" != *"updates"* && "$components" =~ updates ]]; then - repo_name="$repo_name-updates" - elif [[ "$suite" != *"backports"* && "$components" =~ backports ]]; then - repo_name="$repo_name-backports" - fi - - # Determine repo type (prefer deb over deb-src) - local repo_type="deb" - if [[ "$types" =~ deb-src ]] && [[ ! "$types" =~ ^deb[[:space:]] ]]; then - repo_type="deb-src" - fi - - local json_entry="{\"name\":\"$repo_name\",\"url\":\"$uri\",\"distribution\":\"$suite\",\"components\":\"$components\",\"repoType\":\"$repo_type\",\"isEnabled\":true,\"isSecure\":$is_secure}" - - if [[ "$first_entry" == true ]]; then - first_entry=false - result="$json_entry" - else - result="$result,$json_entry" - fi - done - done - - echo "$result" -} - -# Get repository info for YUM-based systems -get_yum_repositories() { - local -n repos_ref=$1 - local -n first_ref=$2 - - # Parse yum/dnf repository configuration - local repo_info="" - if command -v dnf >/dev/null 2>&1; then - repo_info=$(dnf repolist all --verbose 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-mirrors|^Repo-name|^Repo-status") - elif command -v yum >/dev/null 2>&1; then - repo_info=$(yum repolist all -v 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-mirrors|^Repo-name|^Repo-status") - fi - - if [[ -z "$repo_info" ]]; then - return - fi - - # Parse repository information - local current_repo="" - local repo_id="" - local repo_name="" - local repo_url="" - local repo_mirrors="" - local repo_status="" - - while IFS= read -r line; do - if [[ "$line" =~ ^Repo-id[[:space:]]+:[[:space:]]+(.+)$ ]]; then - # Process previous repository if we have one - if [[ -n "$current_repo" ]]; then - process_yum_repo repos_ref first_ref "$repo_id" "$repo_name" "$repo_url" "$repo_mirrors" "$repo_status" - fi - - # Start new repository - repo_id="${BASH_REMATCH[1]}" - repo_name="$repo_id" - repo_url="" - repo_mirrors="" - repo_status="" - current_repo="$repo_id" - - elif [[ "$line" =~ ^Repo-name[[:space:]]+:[[:space:]]+(.+)$ ]]; then - repo_name="${BASH_REMATCH[1]}" - - elif [[ "$line" =~ ^Repo-baseurl[[:space:]]+:[[:space:]]+(.+)$ ]]; then - repo_url="${BASH_REMATCH[1]}" - - elif [[ "$line" =~ ^Repo-mirrors[[:space:]]+:[[:space:]]+(.+)$ ]]; then - repo_mirrors="${BASH_REMATCH[1]}" - - elif [[ "$line" =~ ^Repo-status[[:space:]]+:[[:space:]]+(.+)$ ]]; then - repo_status="${BASH_REMATCH[1]}" - fi - done <<< "$repo_info" - - # Process the last repository - if [[ -n "$current_repo" ]]; then - process_yum_repo repos_ref first_ref "$repo_id" "$repo_name" "$repo_url" "$repo_mirrors" "$repo_status" - fi -} - -# Process a single YUM repository and add it to the JSON -process_yum_repo() { - local -n _repos_ref=$1 - local -n _first_ref=$2 - local repo_id="$3" - local repo_name="$4" - local repo_url="$5" - local repo_mirrors="$6" - local repo_status="$7" - - # Skip if we don't have essential info - if [[ -z "$repo_id" ]]; then - return - fi - - # Determine if repository is enabled - local is_enabled=false - if [[ "$repo_status" == "enabled" ]]; then - is_enabled=true - fi - - # Use baseurl if available, otherwise use mirrors URL - local final_url="" - if [[ -n "$repo_url" ]]; then - # Extract first URL if multiple are listed - final_url=$(echo "$repo_url" | head -n 1 | awk '{print $1}') - elif [[ -n "$repo_mirrors" ]]; then - final_url="$repo_mirrors" - fi - - # Skip if we don't have any URL - if [[ -z "$final_url" ]]; then - return - fi - - # Determine if repository uses HTTPS - local is_secure=false - if [[ "$final_url" =~ ^https:// ]]; then - is_secure=true - fi - - # Generate repository name if not provided - if [[ -z "$repo_name" ]]; then - repo_name="$repo_id" - fi - - # Clean up repository name and URL - escape quotes and backslashes - repo_name=$(echo "$repo_name" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') - final_url=$(echo "$final_url" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') - - # Add to JSON - if [[ "$_first_ref" == true ]]; then - _first_ref=false - else - _repos_ref+="," - fi - - _repos_ref+="{\"name\":\"$repo_name\",\"url\":\"$final_url\",\"distribution\":\"$OS_VERSION\",\"components\":\"main\",\"repoType\":\"rpm\",\"isEnabled\":$is_enabled,\"isSecure\":$is_secure}" -} - -# Get package information based on OS -get_package_info() { - local packages_json="[" - local first=true - - case "$OS_TYPE" in - "ubuntu"|"debian") - get_apt_packages packages_json first - ;; - "centos"|"rhel"|"fedora"|"ol"|"rocky") - get_yum_packages packages_json first - ;; - *) - warning "Unsupported OS type: $OS_TYPE - returning empty package list" - ;; - esac - - packages_json+="]" - echo "$packages_json" -} - -# Get package info for APT-based systems -get_apt_packages() { - local -n packages_ref=$1 - local -n first_ref=$2 - - # Update package lists with retry logic for lock conflicts - local retry_count=0 - local max_retries=3 - local retry_delay=5 - - while [[ $retry_count -lt $max_retries ]]; do - if apt-get update -qq 2>/dev/null; then - break - else - retry_count=$((retry_count + 1)) - if [[ $retry_count -lt $max_retries ]]; then - warning "APT lock detected, retrying in ${retry_delay} seconds... (attempt $retry_count/$max_retries)" - sleep $retry_delay - else - warning "APT lock persists after $max_retries attempts, continuing without update..." - fi - fi - done - - # Determine upgradable packages using apt-get simulation (compatible with Ubuntu 18.04) - # Example line format: - # Inst bash [4.4.18-2ubuntu1] (4.4.18-2ubuntu1.2 Ubuntu:18.04/bionic-updates [amd64]) - local upgradable_sim=$(apt-get -s -o Debug::NoLocking=1 upgrade 2>/dev/null | grep "^Inst ") - - while IFS= read -r line; do - # Extract package name, current version (in brackets), and available version (first token inside parentheses) - if [[ "$line" =~ ^Inst[[:space:]]+([^[:space:]]+)[[:space:]]+\[([^\]]+)\][[:space:]]+\(([^[:space:]]+) ]]; then - local package_name="${BASH_REMATCH[1]}" - local current_version="${BASH_REMATCH[2]}" - local available_version="${BASH_REMATCH[3]}" - local is_security_update=false - - # Mark as security update if the line references a security pocket - if echo "$line" | grep -qiE "(-|/)security"; then - is_security_update=true - fi - - # Escape JSON special characters in package data - package_name=$(echo "$package_name" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g') - current_version=$(echo "$current_version" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g') - available_version=$(echo "$available_version" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g') - - if [[ "$first_ref" == true ]]; then - first_ref=false - else - packages_ref+="," - fi - - packages_ref+="{\"name\":\"$package_name\",\"currentVersion\":\"$current_version\",\"availableVersion\":\"$available_version\",\"needsUpdate\":true,\"isSecurityUpdate\":$is_security_update}" - fi - done <<< "$upgradable_sim" - - # Get installed packages that are up to date - local installed=$(dpkg-query -W -f='${Package} ${Version}\n') - - while IFS=' ' read -r package_name version; do - if [[ -n "$package_name" && -n "$version" ]]; then - # Check if this package is not in the upgrade list - if ! echo "$upgradable_sim" | grep -q "^Inst $package_name "; then - # Escape JSON special characters in package data - package_name=$(echo "$package_name" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g') - version=$(echo "$version" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g') - - if [[ "$first_ref" == true ]]; then - first_ref=false - else - packages_ref+="," - fi - - packages_ref+="{\"name\":\"$package_name\",\"currentVersion\":\"$version\",\"needsUpdate\":false,\"isSecurityUpdate\":false}" - fi - fi - done <<< "$installed" -} - -# Get package info for YUM/DNF-based systems -get_yum_packages() { - local -n packages_ref=$1 - local -n first_ref=$2 - - local package_manager="yum" - if command -v dnf &> /dev/null; then - package_manager="dnf" - fi - - # Get upgradable packages - local upgradable=$($package_manager check-update 2>/dev/null | grep -v "^$" | grep -v "^Loaded" | grep -v "^Last metadata" | grep -v "^Security" | tail -n +2) - - while IFS= read -r line; do - # Skip empty lines and lines with special characters - [[ -z "$line" ]] && continue - [[ "$line" =~ ^[[:space:]]*$ ]] && continue - - if [[ "$line" =~ ^([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+) ]]; then - local package_name="${BASH_REMATCH[1]}" - local available_version="${BASH_REMATCH[2]}" - local repo="${BASH_REMATCH[3]}" - - # Sanitize package name and versions (remove any control characters) - package_name=$(echo "$package_name" | tr -d '[:cntrl:]' | sed 's/[^a-zA-Z0-9._+-]//g') - available_version=$(echo "$available_version" | tr -d '[:cntrl:]' | sed 's/[^a-zA-Z0-9._+-]//g') - repo=$(echo "$repo" | tr -d '[:cntrl:]') - - # Skip if package name is empty after sanitization - [[ -z "$package_name" ]] && continue - - # Get current version - local current_version=$($package_manager list installed "$package_name" 2>/dev/null | grep "^$package_name" | awk '{print $2}' | tr -d '[:cntrl:]' | sed 's/[^a-zA-Z0-9._+-]//g') - - # Skip if we couldn't get current version - [[ -z "$current_version" ]] && current_version="unknown" - - local is_security_update=false - if echo "$repo" | grep -q "security"; then - is_security_update=true - fi - - if [[ "$first_ref" == true ]]; then - first_ref=false - else - packages_ref+="," - fi - - packages_ref+="{\"name\":\"$package_name\",\"currentVersion\":\"$current_version\",\"availableVersion\":\"$available_version\",\"needsUpdate\":true,\"isSecurityUpdate\":$is_security_update}" - fi - done <<< "$upgradable" - - # Get some installed packages that are up to date - local installed=$($package_manager list installed 2>/dev/null | grep -v "^Loaded" | grep -v "^Installed") - - while IFS= read -r line; do - # Skip empty lines - [[ -z "$line" ]] && continue - [[ "$line" =~ ^[[:space:]]*$ ]] && continue - - if [[ "$line" =~ ^([^[:space:]]+)[[:space:]]+([^[:space:]]+) ]]; then - local package_name="${BASH_REMATCH[1]}" - local version="${BASH_REMATCH[2]}" - - # Sanitize package name and version - package_name=$(echo "$package_name" | tr -d '[:cntrl:]' | sed 's/[^a-zA-Z0-9._+-]//g') - version=$(echo "$version" | tr -d '[:cntrl:]' | sed 's/[^a-zA-Z0-9._+-]//g') - - # Skip if package name is empty after sanitization - [[ -z "$package_name" ]] && continue - [[ -z "$version" ]] && version="unknown" - - # Check if this package is not in the upgrade list - if ! echo "$upgradable" | grep -q "^$package_name "; then - if [[ "$first_ref" == true ]]; then - first_ref=false - else - packages_ref+="," - fi - - packages_ref+="{\"name\":\"$package_name\",\"currentVersion\":\"$version\",\"needsUpdate\":false,\"isSecurityUpdate\":false}" - fi - fi - done <<< "$installed" -} - -# Get hardware information -get_hardware_info() { - local cpu_model="" - local cpu_cores=0 - local ram_installed=0 - local swap_size=0 - local disk_details="[]" - - # CPU Information - if command -v lscpu >/dev/null 2>&1; then - cpu_model=$(lscpu | grep "Model name" | cut -d':' -f2 | xargs) - cpu_cores=$(lscpu | grep "^CPU(s):" | cut -d':' -f2 | xargs) - elif [[ -f /proc/cpuinfo ]]; then - cpu_model=$(grep "model name" /proc/cpuinfo | head -1 | cut -d':' -f2 | xargs) - cpu_cores=$(grep -c "^processor" /proc/cpuinfo) - fi - - # Memory Information - if command -v free >/dev/null 2>&1; then - # Use free -m to get MB, then convert to GB with decimal precision - ram_installed=$(free -m | grep "^Mem:" | awk '{printf "%.2f", $2/1024}') - swap_size=$(free -m | grep "^Swap:" | awk '{printf "%.2f", $2/1024}') - elif [[ -f /proc/meminfo ]]; then - # Convert KB to GB with decimal precision - ram_installed=$(grep "MemTotal" /proc/meminfo | awk '{printf "%.2f", $2/1048576}') - swap_size=$(grep "SwapTotal" /proc/meminfo | awk '{printf "%.2f", $2/1048576}') - fi - - # Ensure minimum value of 0.01GB to prevent 0 values - if (( $(echo "$ram_installed < 0.01" | bc -l) )); then - ram_installed="0.01" - fi - if (( $(echo "$swap_size < 0" | bc -l) )); then - swap_size="0" - fi - - # Disk Information - if command -v lsblk >/dev/null 2>&1; then - disk_details=$(lsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT | jq -c '[.blockdevices[] | select(.type == "disk") | {name: .name, size: .size, mountpoint: .mountpoint}]') - elif command -v df >/dev/null 2>&1; then - disk_details=$(df -h | grep -E "^/dev/" | awk '{print "{\"name\":\""$1"\",\"size\":\""$2"\",\"mountpoint\":\""$6"\"}"}' | jq -s .) - fi - - echo "{\"cpuModel\":\"$cpu_model\",\"cpuCores\":$cpu_cores,\"ramInstalled\":$ram_installed,\"swapSize\":$swap_size,\"diskDetails\":$disk_details}" -} - -# Get network information -get_network_info() { - local gateway_ip="" - local dns_servers="[]" - local network_interfaces="[]" - - # Gateway IP - if command -v ip >/dev/null 2>&1; then - gateway_ip=$(ip route | grep default | head -1 | awk '{print $3}') - elif command -v route >/dev/null 2>&1; then - gateway_ip=$(route -n | grep '^0.0.0.0' | head -1 | awk '{print $2}') - fi - - # DNS Servers - if [[ -f /etc/resolv.conf ]]; then - dns_servers=$(grep "nameserver" /etc/resolv.conf | awk '{print $2}' | jq -R . | jq -s .) - fi - - # Network Interfaces - if command -v ip >/dev/null 2>&1; then - network_interfaces=$(ip -j addr show | jq -c '[.[] | {name: .ifname, type: .link_type, addresses: [.addr_info[]? | {address: .local, family: .family}]}]') - elif command -v ifconfig >/dev/null 2>&1; then - network_interfaces=$(ifconfig -a | grep -E "^[a-zA-Z]" | awk '{print $1}' | jq -R . | jq -s .) - fi - - echo "{\"gatewayIp\":\"$gateway_ip\",\"dnsServers\":$dns_servers,\"networkInterfaces\":$network_interfaces}" -} - -# Get system information -get_system_info() { - local kernel_version="" - local selinux_status="" - local system_uptime="" - local load_average="[]" - - # Kernel Version - if [[ -f /proc/version ]]; then - kernel_version=$(cat /proc/version | awk '{print $3}') - elif command -v uname >/dev/null 2>&1; then - kernel_version=$(uname -r) - fi - - # SELinux Status - if command -v getenforce >/dev/null 2>&1; then - selinux_status=$(getenforce 2>/dev/null | tr '[:upper:]' '[:lower:]') - # Map "enforcing" to "enabled" for server validation - if [[ "$selinux_status" == "enforcing" ]]; then - selinux_status="enabled" - fi - elif [[ -f /etc/selinux/config ]]; then - selinux_status=$(grep "^SELINUX=" /etc/selinux/config | cut -d'=' -f2 | tr '[:upper:]' '[:lower:]') - # Map "enforcing" to "enabled" for server validation - if [[ "$selinux_status" == "enforcing" ]]; then - selinux_status="enabled" - fi - else - selinux_status="disabled" - fi - - # System Uptime - if [[ -f /proc/uptime ]]; then - local uptime_seconds=$(cat /proc/uptime | awk '{print int($1)}') - local days=$((uptime_seconds / 86400)) - local hours=$(((uptime_seconds % 86400) / 3600)) - local minutes=$(((uptime_seconds % 3600) / 60)) - system_uptime="${days}d ${hours}h ${minutes}m" - elif command -v uptime >/dev/null 2>&1; then - system_uptime=$(uptime | awk -F'up ' '{print $2}' | awk -F', load' '{print $1}') - fi - - # Load Average - if [[ -f /proc/loadavg ]]; then - load_average=$(cat /proc/loadavg | awk '{print "["$1","$2","$3"]"}') - elif command -v uptime >/dev/null 2>&1; then - load_average=$(uptime | awk -F'load average: ' '{print "["$2"]"}' | tr -d ' ') - fi - - echo "{\"kernelVersion\":\"$kernel_version\",\"selinuxStatus\":\"$selinux_status\",\"systemUptime\":\"$system_uptime\",\"loadAverage\":$load_average}" -} - -# Send package update to server -send_update() { - load_credentials - - # Track execution start time - local start_time=$(date +%s.%N) - - # Verify datetime before proceeding - if ! verify_datetime; then - warning "Datetime verification failed, but continuing with update..." - fi - - info "Collecting system information..." - local packages_json=$(get_package_info) - local repositories_json=$(get_repository_info) - local hardware_json=$(get_hardware_info) - local network_json=$(get_network_info) - local system_json=$(get_system_info) - - # Validate JSON before sending - if ! echo "$packages_json" | jq empty 2>/dev/null; then - error "Invalid packages JSON generated: $packages_json" - fi - - if ! echo "$repositories_json" | jq empty 2>/dev/null; then - error "Invalid repositories JSON generated: $repositories_json" - fi - - info "Sending update to PatchMon server..." - - # Merge all JSON objects into one - local merged_json=$(echo "$hardware_json $network_json $system_json" | jq -s '.[0] * .[1] * .[2]') - # Get machine ID - local machine_id=$(get_machine_id) - - # Calculate execution time (in seconds with decimals) - local end_time=$(date +%s.%N) - local execution_time=$(echo "$end_time - $start_time" | bc) - - # Create the base payload and merge with system info - local base_payload=$(cat < "$temp_payload_file" - - # Debug: Show payload size - local payload_size=$(wc -c < "$temp_payload_file") - echo -e "${BLUE}ℹ️ 📊 Payload size: $payload_size bytes${NC}" - - local response=$(curl $CURL_FLAGS -X POST \ - -H "Content-Type: application/json" \ - -H "X-API-ID: $API_ID" \ - -H "X-API-KEY: $API_KEY" \ - -d @"$temp_payload_file" \ - "$PATCHMON_SERVER/api/$API_VERSION/hosts/update" 2>&1) - - local curl_exit_code=$? - - # Clean up temporary file - rm -f "$temp_payload_file" - - if [[ $curl_exit_code -eq 0 ]]; then - if echo "$response" | grep -q "success"; then - local packages_count=$(echo "$response" | grep -o '"packagesProcessed":[0-9]*' | cut -d':' -f2) - success "Update sent successfully (${packages_count} packages processed)" - - # Check if auto-update is enabled and check for agent updates locally - if check_auto_update_enabled; then - info "Checking for agent updates..." - if check_agent_update_needed; then - info "Agent update available, updating..." - if "$0" update-agent; then - success "Agent updated successfully" - else - warning "Agent update failed, but data was sent successfully" - fi - else - info "Agent is up to date" - fi - fi - - # Automatically check if crontab needs updating based on server settings - info "Checking crontab configuration..." - "$0" update-crontab - local crontab_exit_code=$? - if [[ $crontab_exit_code -eq 0 ]]; then - success "Crontab updated successfully" - elif [[ $crontab_exit_code -eq 2 ]]; then - # Already up to date - no additional message needed - true - else - warning "Crontab update failed, but data was sent successfully" - fi - else - error "Update failed: $response" - fi - else - error "Failed to send update (curl exit code: $curl_exit_code): $response" - fi -} - -# Ping server to check connectivity -ping_server() { - load_credentials - - local response=$(curl $CURL_FLAGS -X POST \ - -H "Content-Type: application/json" \ - -H "X-API-ID: $API_ID" \ - -H "X-API-KEY: $API_KEY" \ - "$PATCHMON_SERVER/api/$API_VERSION/hosts/ping") - - if [[ $? -eq 0 ]] && echo "$response" | grep -q "success"; then - success "Ping successful" - local hostname=$(echo "$response" | grep -o '"hostname":"[^"]*' | cut -d'"' -f4) - if [[ -n "$hostname" ]]; then - info "Connected as host: $hostname" - fi - - # Check for crontab update instructions - local should_update_crontab=$(echo "$response" | grep -o '"shouldUpdate":true' | cut -d':' -f2) - if [[ "$should_update_crontab" == "true" ]]; then - local message=$(echo "$response" | grep -o '"message":"[^"]*' | cut -d'"' -f4) - local command=$(echo "$response" | grep -o '"command":"[^"]*' | cut -d'"' -f4) - - if [[ -n "$message" ]]; then - info "$message" - fi - - if [[ "$command" == "update-crontab" ]]; then - info "Updating crontab with new interval..." - "$0" update-crontab - local crontab_exit_code=$? - if [[ $crontab_exit_code -eq 0 ]]; then - success "Crontab updated successfully" - elif [[ $crontab_exit_code -eq 2 ]]; then - # Already up to date - no additional message needed - true - else - warning "Crontab update failed, but data was sent successfully" - fi - fi - fi - else - error "Ping failed: $response" - fi -} - -# Check for agent updates -check_version() { - load_credentials - - info "Checking for agent updates..." - - local response=$(curl $CURL_FLAGS -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) - local download_url=$(echo "$response" | grep -o '"downloadUrl":"[^"]*' | cut -d'"' -f4) - local release_notes=$(echo "$response" | grep -o '"releaseNotes":"[^"]*' | cut -d'"' -f4) - - if [[ -n "$current_version" ]]; then - if [[ "$current_version" != "$AGENT_VERSION" ]]; then - warning "Agent update available!" - echo " Current version: $AGENT_VERSION" - echo " Latest version: $current_version" - if [[ -n "$release_notes" ]]; then - echo " Release notes: $release_notes" - fi - echo " Download URL: $download_url" - echo "" - echo "To update, run: $0 update-agent" - else - success "Agent is up to date (version $AGENT_VERSION)" - fi - else - warning "Could not determine current version from server" - fi - else - error "Failed to check for updates" - 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 $CURL_FLAGS -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 $CURL_FLAGS -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 $CURL_FLAGS -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 download_url="$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/download" - - 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 $CURL_FLAGS -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 - warning "Failed to send updated information to PatchMon (this is not critical)" - fi - else - error "Downloaded script is invalid" - rm -f "/tmp/patchmon-agent-new.sh" - fi - else - error "Failed to download new agent script" - fi -} - -# Update crontab with current policy -update_crontab() { - load_credentials - info "Updating crontab with current policy..." - local response=$(curl $CURL_FLAGS -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) - # Fallback if not found - if [[ -z "$update_interval" ]]; then - update_interval=60 - fi - # Normalize interval: 5-59 valid, otherwise snap to hour presets - if [[ $update_interval -lt 5 ]]; then - update_interval=5 - elif [[ $update_interval -gt 1440 ]]; then - update_interval=1440 - fi - if [[ -n "$update_interval" ]]; then - # Generate the expected crontab entry - local expected_crontab="" - if [[ $update_interval -lt 60 ]]; then - # Every N minutes (5-59) - expected_crontab="*/$update_interval * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" - else - # Hour-based schedules - if [[ $update_interval -eq 60 ]]; then - # Hourly updates starting at current minute to spread load - local current_minute=$(date +%M) - expected_crontab="$current_minute * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" - else - # For 120, 180, 360, 720, 1440 -> every H hours at minute 0 - local hours=$((update_interval / 60)) - expected_crontab="0 */$hours * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" - fi - fi - - # 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_patchmon_entry" == "$expected_crontab" ]]; then - info "Crontab is already up to date (interval: $update_interval minutes)" - return 2 # Special return code for "already up to date" - fi - - info "Setting update interval to $update_interval minutes" - - # 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 - else - error "Failed to get update interval policy" - fi -} - -# Show detailed system diagnostics -show_diagnostics() { - info "PatchMon Agent Diagnostics v$AGENT_VERSION" - echo "" - - # System information - echo "=== System Information ===" - echo "OS: $(uname -s)" - echo "Architecture: $(uname -m)" - echo "Kernel: $(uname -r)" - echo "Hostname: $(hostname)" - echo "Uptime: $(uptime -p 2>/dev/null || uptime)" - echo "" - - # Agent information - echo "=== Agent Information ===" - echo "Version: $AGENT_VERSION" - echo "Script Path: $0" - echo "Config File: $CONFIG_FILE" - echo "Credentials File: $CREDENTIALS_FILE" - echo "Log File: $LOG_FILE" - echo "Script Size: $(stat -c%s "$0" 2>/dev/null || echo "Unknown") bytes" - echo "Last Modified: $(stat -c%y "$0" 2>/dev/null || echo "Unknown")" - echo "" - - # Configuration - if [[ -f "$CONFIG_FILE" ]]; then - echo "=== Configuration ===" - cat "$CONFIG_FILE" - echo "" - else - echo "=== Configuration ===" - echo "No configuration file found at $CONFIG_FILE" - echo "" - fi - - # Credentials status - echo "=== Credentials Status ===" - if [[ -f "$CREDENTIALS_FILE" ]]; then - echo "Credentials file exists: Yes" - echo "File size: $(stat -c%s "$CREDENTIALS_FILE" 2>/dev/null || echo "Unknown") bytes" - echo "File permissions: $(stat -c%a "$CREDENTIALS_FILE" 2>/dev/null || echo "Unknown")" - else - echo "Credentials file exists: No" - fi - echo "" - - # Crontab status - echo "=== Crontab Status ===" - local crontab_entries=$(crontab -l 2>/dev/null | grep patchmon-agent || echo "None") - if [[ "$crontab_entries" != "None" ]]; then - echo "Crontab entries:" - echo "$crontab_entries" - else - echo "No crontab entries found" - fi - echo "" - - # Network connectivity - echo "=== Network Connectivity ===" - if ping -c 1 -W 3 "$(echo "$PATCHMON_SERVER" | sed 's|http://||' | sed 's|https://||' | cut -d: -f1)" >/dev/null 2>&1; then - echo "Server reachable: Yes" - else - echo "Server reachable: No" - fi - echo "Server URL: $PATCHMON_SERVER" - echo "" - - # Recent logs - echo "=== Recent Logs (last 10 lines) ===" - if [[ -f "$LOG_FILE" ]]; then - tail -10 "$LOG_FILE" 2>/dev/null || echo "Could not read log file" - else - echo "Log file does not exist" - fi -} - -# Show current configuration -show_config() { - info "Current Configuration:" - echo " Server: ${PATCHMON_SERVER}" - echo " API Version: ${API_VERSION}" - echo " Agent Version: ${AGENT_VERSION}" - echo " Config File: ${CONFIG_FILE}" - echo " Credentials File: ${CREDENTIALS_FILE}" - echo " Log File: ${LOG_FILE}" - - if [[ -f "$CREDENTIALS_FILE" ]]; then - source "$CREDENTIALS_FILE" - echo " API ID: ${API_ID}" - echo " API Key: ${API_KEY:0:8}..." # Show only first 8 characters - else - echo " API Credentials: Not configured" - fi -} - -# Main function -main() { - case "$1" in - "configure") - check_root - setup_directories - load_config - configure_credentials "$2" "$3" "$4" - ;; - "test") - check_root - setup_directories - load_config - test_credentials - ;; - "update") - check_root - setup_directories - load_config - detect_os - send_update - ;; - "ping") - check_root - setup_directories - load_config - ping_server - ;; - "config") - load_config - show_config - ;; - "check-version") - check_root - setup_directories - load_config - check_version - ;; - "check-agent-update") - setup_directories - load_config - check_agent_update - ;; - "update-agent") - check_root - setup_directories - load_config - update_agent - ;; - "update-crontab") - check_root - setup_directories - load_config - update_crontab - ;; - "diagnostics") - show_diagnostics - ;; - *) - echo "PatchMon Agent v$AGENT_VERSION - API Credential Based" - echo "Usage: $0 {configure|test|update|ping|config|check-version|check-agent-update|update-agent|update-crontab|diagnostics}" - echo "" - echo "Commands:" - 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 [SERVER_URL] (provided by admin)" - echo " 3. Run: $0 test (to verify connection)" - echo " 4. Run: $0 update (to send initial package data)" - echo "" - echo "Configuration:" - echo " Edit $CONFIG_FILE to customize server settings" - echo " PATCHMON_SERVER=http://your-server:3001" - exit 1 - ;; - esac -} - -# Run main function -main "$@" diff --git a/agents/patchmon-agent-linux-386 b/agents/patchmon-agent-linux-386 index 4f8457f..d09f942 100755 Binary files a/agents/patchmon-agent-linux-386 and b/agents/patchmon-agent-linux-386 differ diff --git a/agents/patchmon-agent-linux-amd64 b/agents/patchmon-agent-linux-amd64 index 17a1878..356b098 100755 Binary files a/agents/patchmon-agent-linux-amd64 and b/agents/patchmon-agent-linux-amd64 differ diff --git a/agents/patchmon-agent-linux-arm b/agents/patchmon-agent-linux-arm index d064a56..345500c 100755 Binary files a/agents/patchmon-agent-linux-arm and b/agents/patchmon-agent-linux-arm differ diff --git a/agents/patchmon-agent-linux-arm64 b/agents/patchmon-agent-linux-arm64 index fb76bc7..a8ceefc 100755 Binary files a/agents/patchmon-agent-linux-arm64 and b/agents/patchmon-agent-linux-arm64 differ diff --git a/backend/package.json b/backend/package.json index abfa1c4..38ffcdb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "patchmon-backend", - "version": "1.3.6", + "version": "1.3.7", "description": "Backend API for Linux Patch Monitoring System", "license": "AGPL-3.0", "main": "src/server.js", diff --git a/backend/prisma/migrations/20251202000000_add_docker_enabled_to_hosts/migration.sql b/backend/prisma/migrations/20251202000000_add_docker_enabled_to_hosts/migration.sql new file mode 100644 index 0000000..8384ff0 --- /dev/null +++ b/backend/prisma/migrations/20251202000000_add_docker_enabled_to_hosts/migration.sql @@ -0,0 +1,6 @@ +-- Add docker_enabled field to hosts table +-- This field persists the Docker integration enabled state across container restarts +-- Fixes GitHub issue #352 + +ALTER TABLE "hosts" ADD COLUMN "docker_enabled" BOOLEAN NOT NULL DEFAULT false; + diff --git a/backend/prisma/migrations/20251203000000_add_reboot_reason_field/migration.sql b/backend/prisma/migrations/20251203000000_add_reboot_reason_field/migration.sql new file mode 100644 index 0000000..cd5d0d6 --- /dev/null +++ b/backend/prisma/migrations/20251203000000_add_reboot_reason_field/migration.sql @@ -0,0 +1,6 @@ +-- Add reboot_reason field to hosts table +-- This field stores detailed technical information about why a reboot is required +-- Includes kernel versions, detection method, and other relevant details + +ALTER TABLE "hosts" ADD COLUMN "reboot_reason" TEXT; + diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 18c13df..a06bcb4 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -112,6 +112,8 @@ model hosts { system_uptime String? notes String? needs_reboot Boolean? @default(false) + reboot_reason String? + docker_enabled Boolean @default(false) host_packages host_packages[] host_repositories host_repositories[] host_group_memberships host_group_memberships[] diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index 0cbbf54..31bb664 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -21,6 +21,35 @@ const prisma = getPrismaClient(); // This stores the last known state from successful toggles const integrationStateCache = new Map(); +// Middleware to validate API credentials +const validateApiCredentials = async (req, res, next) => { + try { + const apiId = req.headers["x-api-id"] || req.body.apiId; + const apiKey = req.headers["x-api-key"] || req.body.apiKey; + + if (!apiId || !apiKey) { + return res.status(401).json({ error: "API ID and Key required" }); + } + + const host = await prisma.hosts.findFirst({ + where: { + api_id: apiId, + api_key: apiKey, + }, + }); + + if (!host) { + return res.status(401).json({ error: "Invalid API credentials" }); + } + + req.hostRecord = host; + next(); + } catch (error) { + console.error("API credential validation error:", error); + res.status(500).json({ error: "API credential validation failed" }); + } +}; + // Secure endpoint to download the agent script/binary (requires API authentication) router.get("/agent/download", async (req, res) => { try { @@ -130,11 +159,32 @@ router.get("/agent/download", async (req, res) => { }); // Version check endpoint for agents -router.get("/agent/version", async (req, res) => { +router.get("/agent/version", validateApiCredentials, async (req, res) => { try { const fs = require("node:fs"); const path = require("node:path"); + // Check general server auto_update setting + const settings = await prisma.settings.findFirst(); + const serverAutoUpdateEnabled = settings?.auto_update; + + // Check per-host auto_update setting (req.hostRecord is set by validateApiCredentials middleware) + const host = req.hostRecord; + const hostAutoUpdateEnabled = host?.auto_update; + + // Determine if auto-update is disabled + const autoUpdateDisabled = + !serverAutoUpdateEnabled || !hostAutoUpdateEnabled; + let autoUpdateDisabledReason = null; + if (!serverAutoUpdateEnabled && !hostAutoUpdateEnabled) { + autoUpdateDisabledReason = + "Auto-update is disabled in server settings and for this host"; + } else if (!serverAutoUpdateEnabled) { + autoUpdateDisabledReason = "Auto-update is disabled in server settings"; + } else if (!hostAutoUpdateEnabled) { + autoUpdateDisabledReason = "Auto-update is disabled for this host"; + } + // Get architecture parameter (default to amd64 for Go agents) const architecture = req.query.arch || "amd64"; const agentType = req.query.type || "go"; // "go" or "legacy" @@ -163,9 +213,16 @@ router.get("/agent/version", async (req, res) => { res.json({ currentVersion: currentVersion, + latestVersion: currentVersion, + hasUpdate: false, + autoUpdateDisabled: autoUpdateDisabled, + autoUpdateDisabledReason: autoUpdateDisabled + ? autoUpdateDisabledReason + : null, downloadUrl: `/api/v1/hosts/agent/download`, releaseNotes: `PatchMon Agent v${currentVersion}`, minServerVersion: null, + agentType: "legacy", }); } else { // Go agent version check @@ -235,10 +292,15 @@ router.get("/agent/version", async (req, res) => { // Proper semantic version comparison: only update if server version is NEWER const hasUpdate = compareVersions(serverVersion, agentVersion) > 0; + // Return update info, but indicate if auto-update is disabled return res.json({ currentVersion: agentVersion, latestVersion: serverVersion, - hasUpdate: hasUpdate, + hasUpdate: hasUpdate && !autoUpdateDisabled, // Only true if update available AND auto-update enabled + autoUpdateDisabled: autoUpdateDisabled, + autoUpdateDisabledReason: autoUpdateDisabled + ? autoUpdateDisabledReason + : null, downloadUrl: `/api/v1/hosts/agent/download?arch=${architecture}`, releaseNotes: `PatchMon Agent v${serverVersion}`, minServerVersion: null, @@ -261,6 +323,10 @@ router.get("/agent/version", async (req, res) => { currentVersion: agentVersion, latestVersion: null, hasUpdate: false, + autoUpdateDisabled: autoUpdateDisabled, + autoUpdateDisabledReason: autoUpdateDisabled + ? autoUpdateDisabledReason + : null, architecture: architecture, agentType: "go", }); @@ -278,35 +344,6 @@ const generateApiCredentials = () => { return { apiId, apiKey }; }; -// Middleware to validate API credentials -const validateApiCredentials = async (req, res, next) => { - try { - const apiId = req.headers["x-api-id"] || req.body.apiId; - const apiKey = req.headers["x-api-key"] || req.body.apiKey; - - if (!apiId || !apiKey) { - return res.status(401).json({ error: "API ID and Key required" }); - } - - const host = await prisma.hosts.findFirst({ - where: { - api_id: apiId, - api_key: apiKey, - }, - }); - - if (!host) { - return res.status(401).json({ error: "Invalid API credentials" }); - } - - req.hostRecord = host; - next(); - } catch (error) { - console.error("API credential validation error:", error); - res.status(500).json({ error: "API credential validation failed" }); - } -}; - // Admin endpoint to create a new host manually (replaces auto-registration) router.post( "/create", @@ -324,6 +361,10 @@ router.post( .optional() .isUUID() .withMessage("Each host group ID must be a valid UUID"), + body("docker_enabled") + .optional() + .isBoolean() + .withMessage("Docker enabled must be a boolean"), ], async (req, res) => { try { @@ -332,7 +373,7 @@ router.post( return res.status(400).json({ errors: errors.array() }); } - const { friendly_name, hostGroupIds } = req.body; + const { friendly_name, hostGroupIds, docker_enabled } = req.body; // Generate unique API credentials for this host const { apiId, apiKey } = generateApiCredentials(); @@ -363,6 +404,7 @@ router.post( api_id: apiId, api_key: apiKey, status: "pending", // Will change to 'active' when agent connects + docker_enabled: docker_enabled ?? false, // Set integration state if provided updated_at: new Date(), // Create host group memberships if hostGroupIds are provided host_group_memberships: @@ -602,6 +644,8 @@ router.post( // Reboot Status if (req.body.needsReboot !== undefined) updateData.needs_reboot = req.body.needsReboot; + if (req.body.rebootReason !== undefined) + updateData.reboot_reason = req.body.rebootReason; // If this is the first update (status is 'pending'), change to 'active' if (host.status === "pending") { @@ -875,6 +919,38 @@ router.get("/info", validateApiCredentials, async (req, res) => { } }); +// Get integration status for agent (uses API credentials) +router.get("/integrations", validateApiCredentials, async (req, res) => { + try { + const host = await prisma.hosts.findUnique({ + where: { id: req.hostRecord.id }, + select: { + id: true, + docker_enabled: true, + // Future: add other integration fields here + }, + }); + + if (!host) { + return res.status(404).json({ error: "Host not found" }); + } + + // Return integration states from database (source of truth) + const integrations = { + docker: host.docker_enabled ?? false, + // Future integrations can be added here + }; + + res.json({ + success: true, + integrations: integrations, + }); + } catch (error) { + console.error("Get integration status error:", error); + res.status(500).json({ error: "Failed to get integration status" }); + } +}); + // Ping endpoint for health checks (now uses API credentials) router.post("/ping", validateApiCredentials, async (req, res) => { try { @@ -915,6 +991,13 @@ router.post("/ping", validateApiCredentials, async (req, res) => { agentStartup: isStartup, }; + // Include integration states in ping response for initial agent configuration + // This allows agent to sync config.yml with database state during setup + response.integrations = { + docker: req.hostRecord.docker_enabled ?? false, + // Future integrations can be added here + }; + // Check if this is a crontab update trigger if (req.body.triggerCrontabUpdate && req.hostRecord.auto_update) { console.log( @@ -1589,12 +1672,14 @@ router.post( }); } - // Add job to queue + // Add job to queue with bypass_settings flag for true force updates + // This allows the force endpoint to bypass auto_update settings const job = await queue.add( "update_agent", { api_id: host.api_id, type: "update_agent", + bypass_settings: true, // Force endpoint bypasses settings }, { attempts: 3, @@ -2149,7 +2234,12 @@ router.get( // Get host to verify it exists const host = await prisma.hosts.findUnique({ where: { id: hostId }, - select: { id: true, api_id: true, friendly_name: true }, + select: { + id: true, + api_id: true, + friendly_name: true, + docker_enabled: true, + }, }); if (!host) { @@ -2159,11 +2249,11 @@ router.get( // Check if agent is connected const connected = isConnected(host.api_id); - // Get integration states from cache (or defaults if not cached) - // Default: all integrations are disabled + // Get integration states from database (persisted) with cache fallback + // Database is source of truth, cache is used for quick WebSocket lookups const cachedState = integrationStateCache.get(host.api_id) || {}; const integrations = { - docker: cachedState.docker || false, // Default: disabled + docker: host.docker_enabled ?? cachedState.docker ?? false, // Future integrations can be added here }; @@ -2214,7 +2304,12 @@ router.post( // Get host to verify it exists const host = await prisma.hosts.findUnique({ where: { id: hostId }, - select: { id: true, api_id: true, friendly_name: true }, + select: { + id: true, + api_id: true, + friendly_name: true, + docker_enabled: true, + }, }); if (!host) { @@ -2244,7 +2339,15 @@ router.post( }); } - // Update cache with new state + // Persist integration state to database + if (integrationName === "docker") { + await prisma.hosts.update({ + where: { id: hostId }, + data: { docker_enabled: enabled }, + }); + } + + // Update cache with new state (for quick WebSocket lookups) if (!integrationStateCache.has(host.api_id)) { integrationStateCache.set(host.api_id, {}); } diff --git a/backend/src/services/agentVersionService.js b/backend/src/services/agentVersionService.js index c6b13c8..c221d4d 100644 --- a/backend/src/services/agentVersionService.js +++ b/backend/src/services/agentVersionService.js @@ -623,6 +623,49 @@ class AgentVersionService { `🔍 Checking update for agent ${agentApiId} (version: ${agentVersion})`, ); + // Check general server auto_update setting + const { getPrismaClient } = require("../config/prisma"); + const prisma = getPrismaClient(); + const settings = await prisma.settings.findFirst(); + if (!settings || !settings.auto_update) { + console.log( + `⚠️ Auto-update is disabled in server settings, skipping update check for agent ${agentApiId}`, + ); + return { + needsUpdate: false, + reason: "auto-update-disabled-server", + message: "Auto-update is disabled in server settings", + }; + } + + // Check per-host auto_update setting + const host = await prisma.hosts.findUnique({ + where: { api_id: agentApiId }, + select: { auto_update: true }, + }); + + if (!host) { + console.log( + `⚠️ Host not found for agent ${agentApiId}, skipping update check`, + ); + return { + needsUpdate: false, + reason: "host-not-found", + message: "Host not found", + }; + } + + if (!host.auto_update) { + console.log( + `⚠️ Auto-update is disabled for host ${agentApiId}, skipping update check`, + ); + return { + needsUpdate: false, + reason: "auto-update-disabled-host", + message: "Auto-update is disabled for this host", + }; + } + // Get current server version info const versionInfo = await this.getVersionInfo(); @@ -712,6 +755,22 @@ class AgentVersionService { `🔍 Checking updates for all connected agents (force: ${force})`, ); + // Check general server auto_update setting + const { getPrismaClient } = require("../config/prisma"); + const prisma = getPrismaClient(); + const settings = await prisma.settings.findFirst(); + if (!settings || !settings.auto_update) { + console.log( + `⚠️ Auto-update is disabled in server settings, skipping bulk update check`, + ); + return { + success: false, + message: "Auto-update is disabled in server settings", + updatedAgents: 0, + totalAgents: 0, + }; + } + // Import agentWs service to get connected agents const { pushUpdateNotificationToAll } = require("./agentWs"); diff --git a/backend/src/services/agentWs.js b/backend/src/services/agentWs.js index 77fb860..e433490 100644 --- a/backend/src/services/agentWs.js +++ b/backend/src/services/agentWs.js @@ -310,9 +310,37 @@ function pushUpdateNotification(apiId, updateInfo) { async function pushUpdateNotificationToAll(updateInfo) { let notifiedCount = 0; let failedCount = 0; + let skippedCount = 0; + + // Get all hosts with their auto_update settings + const hosts = await prisma.hosts.findMany({ + where: { + api_id: { in: Array.from(apiIdToSocket.keys()) }, + }, + select: { + api_id: true, + auto_update: true, + }, + }); + + // Create a map for quick lookup + const hostAutoUpdateMap = new Map(); + for (const host of hosts) { + hostAutoUpdateMap.set(host.api_id, host.auto_update); + } for (const [apiId, ws] of apiIdToSocket) { if (ws && ws.readyState === WebSocket.OPEN) { + // Check per-host auto_update setting + const hostAutoUpdate = hostAutoUpdateMap.get(apiId); + if (hostAutoUpdate === false) { + skippedCount++; + console.log( + `⚠️ Skipping update notification for agent ${apiId} (auto-update disabled for host)`, + ); + continue; + } + try { safeSend( ws, @@ -336,10 +364,11 @@ async function pushUpdateNotificationToAll(updateInfo) { } } + const totalAgents = apiIdToSocket.size; console.log( - `📤 Update notification sent to ${notifiedCount} agents, ${failedCount} failed`, + `📤 Update notification sent to ${notifiedCount} agents, ${failedCount} failed, ${skippedCount} skipped (auto-update disabled)`, ); - return { notifiedCount, failedCount }; + return { notifiedCount, failedCount, skippedCount, totalAgents }; } // Notify all subscribers when connection status changes diff --git a/backend/src/services/automation/index.js b/backend/src/services/automation/index.js index cc81099..dc76b15 100644 --- a/backend/src/services/automation/index.js +++ b/backend/src/services/automation/index.js @@ -250,6 +250,40 @@ class QueueManager { const { update_interval } = job.data; agentWs.pushSettingsUpdate(api_id, update_interval); } else if (type === "update_agent") { + // Check if bypass_settings flag is set (for true force updates) + const bypassSettings = job.data.bypass_settings === true; + + if (!bypassSettings) { + // Check general server auto_update setting + const settings = await prisma.settings.findFirst(); + if (!settings || !settings.auto_update) { + console.log( + `⚠️ Auto-update is disabled in server settings, skipping update_agent command for agent ${api_id}`, + ); + throw new Error("Auto-update is disabled in server settings"); + } + + // Check per-host auto_update setting + const host = await prisma.hosts.findUnique({ + where: { api_id: api_id }, + select: { auto_update: true }, + }); + + if (!host) { + console.log( + `⚠️ Host not found for agent ${api_id}, skipping update_agent command`, + ); + throw new Error("Host not found"); + } + + if (!host.auto_update) { + console.log( + `⚠️ Auto-update is disabled for host ${api_id}, skipping update_agent command`, + ); + throw new Error("Auto-update is disabled for this host"); + } + } + // Force agent to update by sending WebSocket command const ws = agentWs.getConnectionByApiId(api_id); if (ws && ws.readyState === 1) { diff --git a/frontend/package.json b/frontend/package.json index 6deaaac..bbf182e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "patchmon-frontend", "private": true, - "version": "1.3.6", + "version": "1.3.7", "license": "AGPL-3.0", "type": "module", "scripts": { diff --git a/frontend/src/pages/HostDetail.jsx b/frontend/src/pages/HostDetail.jsx index a613e46..6dd2cdc 100644 --- a/frontend/src/pages/HostDetail.jsx +++ b/frontend/src/pages/HostDetail.jsx @@ -496,7 +496,10 @@ const HostDetail = () => { {getStatusText(isStale, host.stats.outdated_packages > 0)} {host.needs_reboot && ( - + Reboot Required @@ -842,53 +845,30 @@ const HostDetail = () => { {/* Network Card */} - {(host.ip || - host.gateway_ip || - host.dns_servers || - host.network_interfaces) && ( + {(host.dns_servers || host.network_interfaces) && (

Network

-
- {host.ip && ( -
-

- IP Address -

-

- {host.ip} -

-
- )} - - {host.gateway_ip && ( -
-

- Gateway IP -

-

- {host.gateway_ip} -

-
- )} - +
{host.dns_servers && Array.isArray(host.dns_servers) && host.dns_servers.length > 0 && (
-

+

DNS Servers

{host.dns_servers.map((dns) => ( -

- {dns} -

+

+ {dns} +

+
))}
@@ -898,17 +878,125 @@ const HostDetail = () => { Array.isArray(host.network_interfaces) && host.network_interfaces.length > 0 && (
-

+

Network Interfaces

-
+
{host.network_interfaces.map((iface) => ( -

- {iface.name} -

+ {/* Interface Header */} +
+
+

+ {iface.name} +

+ {iface.type && ( + + {iface.type} + + )} + {iface.status && ( + + {iface.status === "up" ? "UP" : "DOWN"} + + )} +
+
+ + {/* Interface Details */} +
+ {iface.macAddress && ( +
+

+ MAC Address +

+

+ {iface.macAddress} +

+
+ )} + {iface.mtu && ( +
+

+ MTU +

+

+ {iface.mtu} +

+
+ )} + {iface.linkSpeed && iface.linkSpeed > 0 && ( +
+

+ Link Speed +

+

+ {iface.linkSpeed} Mbps + {iface.duplex && + ` (${iface.duplex} duplex)`} +

+
+ )} +
+ + {/* Addresses */} + {iface.addresses && + Array.isArray(iface.addresses) && + iface.addresses.length > 0 && ( +
+

+ IP Addresses +

+
+ {iface.addresses.map((addr, idx) => ( +
+
+ + {addr.family === "inet6" + ? "inet6" + : "inet"} + + + {addr.address} + {addr.netmask && ( + + {addr.netmask} + + )} + +
+ {addr.gateway && ( +
+ Gateway:{" "} + + {addr.gateway} + +
+ )} +
+ ))} +
+
+ )} +
))}
@@ -1689,74 +1777,161 @@ const HostDetail = () => { {/* Network Information */} {activeTab === "network" && - (host.ip || - host.gateway_ip || - host.dns_servers || - host.network_interfaces) && ( -
-
- {host.ip && ( + (host.dns_servers || host.network_interfaces) && ( +
+ {/* DNS Servers */} + {host.dns_servers && + Array.isArray(host.dns_servers) && + host.dns_servers.length > 0 && (
-

- IP Address -

-

- {host.ip} -

-
- )} - - {host.gateway_ip && ( -
-

- Gateway IP -

-

- {host.gateway_ip} -

-
- )} - - {host.dns_servers && - Array.isArray(host.dns_servers) && - host.dns_servers.length > 0 && ( -
-

- DNS Servers -

-
- {host.dns_servers.map((dns) => ( -

+

+ + DNS Servers +

+
+ {host.dns_servers.map((dns) => ( +
+

{dns}

- ))} -
+
+ ))}
- )} +
+ )} - {host.network_interfaces && - Array.isArray(host.network_interfaces) && - host.network_interfaces.length > 0 && ( -
-

- Network Interfaces -

-
- {host.network_interfaces.map((iface) => ( -

- {iface.name} -

- ))} -
+ {/* Network Interfaces */} + {host.network_interfaces && + Array.isArray(host.network_interfaces) && + host.network_interfaces.length > 0 && ( +
+

+ + Network Interfaces +

+
+ {host.network_interfaces.map((iface) => ( +
+ {/* Interface Header */} +
+
+

+ {iface.name} +

+ {iface.type && ( + + {iface.type} + + )} + {iface.status && ( + + {iface.status === "up" ? "UP" : "DOWN"} + + )} +
+
+ + {/* Interface Details */} +
+ {iface.macAddress && ( +
+

+ MAC Address +

+

+ {iface.macAddress} +

+
+ )} + {iface.mtu && ( +
+

+ MTU +

+

+ {iface.mtu} +

+
+ )} + {iface.linkSpeed && iface.linkSpeed > 0 && ( +
+

+ Link Speed +

+

+ {iface.linkSpeed} Mbps + {iface.duplex && + ` (${iface.duplex} duplex)`} +

+
+ )} +
+ + {/* Addresses */} + {iface.addresses && + Array.isArray(iface.addresses) && + iface.addresses.length > 0 && ( +
+

+ IP Addresses +

+
+ {iface.addresses.map((addr, idx) => ( +
+
+ + {addr.family === "inet6" + ? "inet6" + : "inet"} + + + {addr.address} + {addr.netmask && ( + + {addr.netmask} + + )} + +
+ {addr.gateway && ( +
+ Gateway:{" "} + + {addr.gateway} + +
+ )} +
+ ))} +
+
+ )} +
+ ))}
- )} -
+
+ )}
)} diff --git a/frontend/src/pages/Hosts.jsx b/frontend/src/pages/Hosts.jsx index fdf9de3..569bf69 100644 --- a/frontend/src/pages/Hosts.jsx +++ b/frontend/src/pages/Hosts.jsx @@ -7,6 +7,7 @@ import { CheckCircle, CheckSquare, ChevronDown, + ChevronUp, Clock, Columns, ExternalLink, @@ -44,9 +45,11 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => { const [formData, setFormData] = useState({ friendly_name: "", hostGroupIds: [], // Changed to array for multiple selection + docker_enabled: false, // Integration states }); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(""); + const [integrationsExpanded, setIntegrationsExpanded] = useState(false); // Fetch host groups for selection const { data: hostGroups } = useQuery({ @@ -66,7 +69,12 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => { const response = await adminHostsAPI.create(formData); console.log("Host created successfully:", formData.friendly_name); onSuccess(response.data); - setFormData({ friendly_name: "", hostGroupIds: [] }); + setFormData({ + friendly_name: "", + hostGroupIds: [], + docker_enabled: false, + }); + setIntegrationsExpanded(false); onClose(); } catch (err) { console.error("Full error object:", err); @@ -189,6 +197,65 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => {

+ {/* Integrations Section */} +
+ + + {integrationsExpanded && ( +
+ {/* Docker Integration */} + +

+ Integration settings will be synced to the agent's config.yml + during installation. +

+
+ )} +
+ {error && (

@@ -1812,7 +1879,13 @@ const Hosts = () => { ) && host.needs_reboot && (

- + Reboot Required diff --git a/frontend/src/pages/PackageDetail.jsx b/frontend/src/pages/PackageDetail.jsx index 9fd0a4d..9b73c87 100644 --- a/frontend/src/pages/PackageDetail.jsx +++ b/frontend/src/pages/PackageDetail.jsx @@ -369,7 +369,10 @@ const PackageDetail = () => {
{host.needsReboot && ( - + Reboot Required @@ -450,7 +453,10 @@ const PackageDetail = () => { {host.needsReboot ? ( - + Required diff --git a/frontend/src/pages/RepositoryDetail.jsx b/frontend/src/pages/RepositoryDetail.jsx index fb525af..8a11d29 100644 --- a/frontend/src/pages/RepositoryDetail.jsx +++ b/frontend/src/pages/RepositoryDetail.jsx @@ -610,7 +610,12 @@ const RepositoryDetail = () => { {hostRepo.hosts.needs_reboot ? ( - + Required diff --git a/package.json b/package.json index 27ef3b5..58dac5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "patchmon", - "version": "1.3.6", + "version": "1.3.7", "description": "Linux Patch Monitoring System", "license": "AGPL-3.0", "private": true,