mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 07:42:05 +00:00
1410 lines
51 KiB
Bash
Executable File
1410 lines
51 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# 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.7"
|
||
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"
|
||
}
|
||
|
||
# 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 <API_ID> <API_KEY> [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"
|
||
;;
|
||
"rocky"|"almalinux")
|
||
OS_TYPE="rhel"
|
||
;;
|
||
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")
|
||
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
|
||
if command -v dnf >/dev/null 2>&1; then
|
||
local repo_info=$(dnf repolist all --verbose 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-name|^Repo-status")
|
||
elif command -v yum >/dev/null 2>&1; then
|
||
local repo_info=$(yum repolist all -v 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-name|^Repo-status")
|
||
fi
|
||
|
||
# This is a simplified implementation - would need more work for full YUM support
|
||
# For now, return empty for non-APT systems
|
||
}
|
||
|
||
# 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")
|
||
get_yum_packages packages_json first
|
||
;;
|
||
*)
|
||
error "Unsupported OS type: $OS_TYPE"
|
||
;;
|
||
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 (use apt-get for older distros; quieter output)
|
||
apt-get update -qq
|
||
|
||
# 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
|
||
|
||
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' | head -100)
|
||
|
||
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" | 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 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" | head -100)
|
||
|
||
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
|
||
|
||
# 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)
|
||
|
||
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]')
|
||
# Create the base payload and merge with system info
|
||
local base_payload=$(cat <<EOF
|
||
{
|
||
"packages": $packages_json,
|
||
"repositories": $repositories_json,
|
||
"osType": "$OS_TYPE",
|
||
"osVersion": "$OS_VERSION",
|
||
"hostname": "$HOSTNAME",
|
||
"ip": "$IP_ADDRESS",
|
||
"architecture": "$ARCHITECTURE",
|
||
"agentVersion": "$AGENT_VERSION"
|
||
}
|
||
EOF
|
||
)
|
||
|
||
# Merge the base payload with the system information
|
||
local payload=$(echo "$base_payload $merged_json" | jq -s '.[0] * .[1]')
|
||
|
||
|
||
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 "$payload" \
|
||
"$PATCHMON_SERVER/api/$API_VERSION/hosts/update")
|
||
|
||
if [[ $? -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"
|
||
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 <API_ID> <API_KEY> [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 <API_ID> <API_KEY> [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 "$@"
|