diff --git a/agents/patchmon-agent.sh.backup.20250920_000936 b/agents/patchmon-agent.sh.backup.20250920_000936 deleted file mode 100755 index f6ecdb3..0000000 --- a/agents/patchmon-agent.sh.backup.20250920_000936 +++ /dev/null @@ -1,1219 +0,0 @@ -#!/bin/bash - -# PatchMon Agent Script v1.2.5 -# 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.5" -CONFIG_FILE="/etc/patchmon/agent.conf" -CREDENTIALS_FILE="/etc/patchmon/credentials" -LOG_FILE="/var/log/patchmon-agent.log" - -# 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() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" -} - -# Error handling -error() { - echo -e "${RED}ERROR: $1${NC}" >&2 - log "ERROR: $1" - exit 1 -} - -# Info logging -info() { - echo -e "${BLUE}INFO: $1${NC}" - log "INFO: $1" -} - -# Success logging -success() { - echo -e "${GREEN}SUCCESS: $1${NC}" - log "SUCCESS: $1" -} - -# Warning logging -warning() { - echo -e "${YELLOW}WARNING: $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 -} - -# 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 -s -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 - apt-get update -qq - - # Get upgradable packages - local upgradable=$(apt list --upgradable 2>/dev/null | grep -v "WARNING") - - while IFS= read -r line; do - if [[ "$line" =~ ^([^/]+)/([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+.*[[:space:]]([^[:space:]]+)[[:space:]]*(\[.*\])? ]]; then - local package_name="${BASH_REMATCH[1]}" - local current_version="${BASH_REMATCH[4]}" - local available_version="${BASH_REMATCH[3]}" - local is_security_update=false - - # Check if it's a security update - if echo "$line" | 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 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" | tail -n +2) - - while IFS= read -r line; do - 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]}" - - # Get current version - local current_version=$($package_manager list installed "$package_name" 2>/dev/null | grep "^$package_name" | awk '{print $2}') - - 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 - if [[ "$line" =~ ^([^[:space:]]+)[[:space:]]+([^[:space:]]+) ]]; then - local package_name="${BASH_REMATCH[1]}" - local version="${BASH_REMATCH[2]}" - - # 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 - ram_installed=$(free -g | grep "^Mem:" | awk '{print $2}') - swap_size=$(free -g | grep "^Swap:" | awk '{print $2}') - elif [[ -f /proc/meminfo ]]; then - ram_installed=$(grep "MemTotal" /proc/meminfo | awk '{print int($2/1024/1024)}') - swap_size=$(grep "SwapTotal" /proc/meminfo | awk '{print int($2/1024/1024)}') - 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:]') - elif [[ -f /etc/selinux/config ]]; then - selinux_status=$(grep "^SELINUX=" /etc/selinux/config | cut -d'=' -f2 | tr '[:upper:]' '[:lower:]') - 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 - - info "Collecting package information..." - local packages_json=$(get_package_info) - - info "Collecting repository information..." - local repositories_json=$(get_repository_info) - - info "Collecting hardware information..." - local hardware_json=$(get_hardware_info) - - info "Collecting network information..." - local network_json=$(get_network_info) - - info "Collecting system information..." - 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]') - - local payload=$(cat </dev/null; then - # Replace current script - mv "/tmp/patchmon-agent-new.sh" "$0" - chmod +x "$0" - success "Agent updated successfully" - info "Backup saved as: $0.backup.$(date +%Y%m%d_%H%M%S)" - - # Get the new version number - local new_version=$(grep '^AGENT_VERSION=' "$0" | cut -d'"' -f2) - info "Updated to version: $new_version" - - # Automatically run update to send new information to PatchMon - info "Sending updated information to PatchMon..." - if "$0" update; then - success "Successfully sent updated information to PatchMon" - else - warning "Failed to send updated information to PatchMon (this is not critical)" - fi - else - error "Downloaded script is invalid" - rm -f "/tmp/patchmon-agent-new.sh" - fi - else - error "Failed to download new agent script" - fi - else - error "Failed to get update information" - fi -} - -# Update crontab with current policy -update_crontab() { - load_credentials - info "Updating crontab with current policy..." - local response=$(curl -s -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval") - if [[ $? -eq 0 ]]; then - local update_interval=$(echo "$response" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2) - if [[ -n "$update_interval" ]]; then - # Generate the expected crontab entry - local expected_crontab="" - if [[ $update_interval -eq 60 ]]; then - # Hourly updates - expected_crontab="0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" - else - # Custom interval updates - expected_crontab="*/$update_interval * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" - fi - - # Get current crontab - local current_crontab=$(crontab -l 2>/dev/null | grep "patchmon-agent.sh update" | head -1) - - # Check if crontab needs updating - if [[ "$current_crontab" == "$expected_crontab" ]]; then - info "Crontab is already up to date (interval: $update_interval minutes)" - return 0 - fi - - info "Setting update interval to $update_interval minutes" - echo "$expected_crontab" | crontab - - success "Crontab updated successfully" - 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" - ;; - "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 - ;; - "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|update-agent|update-crontab|diagnostics}" - echo "" - echo "Commands:" - echo " configure - 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 " update-agent - Update agent to latest version" - echo " update-crontab - Update crontab with current policy" - echo " diagnostics - Show detailed system diagnostics" - echo "" - echo "Setup Process:" - echo " 1. Contact your PatchMon administrator to create a host entry" - echo " 2. Run: $0 configure (provided by admin)" - echo " 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 "$@" \ No newline at end of file diff --git a/agents/patchmon-agent.sh.backup.20250920_001319 b/agents/patchmon-agent.sh.backup.20250920_001319 deleted file mode 100755 index f6ecdb3..0000000 --- a/agents/patchmon-agent.sh.backup.20250920_001319 +++ /dev/null @@ -1,1219 +0,0 @@ -#!/bin/bash - -# PatchMon Agent Script v1.2.5 -# 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.5" -CONFIG_FILE="/etc/patchmon/agent.conf" -CREDENTIALS_FILE="/etc/patchmon/credentials" -LOG_FILE="/var/log/patchmon-agent.log" - -# 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() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" -} - -# Error handling -error() { - echo -e "${RED}ERROR: $1${NC}" >&2 - log "ERROR: $1" - exit 1 -} - -# Info logging -info() { - echo -e "${BLUE}INFO: $1${NC}" - log "INFO: $1" -} - -# Success logging -success() { - echo -e "${GREEN}SUCCESS: $1${NC}" - log "SUCCESS: $1" -} - -# Warning logging -warning() { - echo -e "${YELLOW}WARNING: $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 -} - -# 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 -s -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 - apt-get update -qq - - # Get upgradable packages - local upgradable=$(apt list --upgradable 2>/dev/null | grep -v "WARNING") - - while IFS= read -r line; do - if [[ "$line" =~ ^([^/]+)/([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+.*[[:space:]]([^[:space:]]+)[[:space:]]*(\[.*\])? ]]; then - local package_name="${BASH_REMATCH[1]}" - local current_version="${BASH_REMATCH[4]}" - local available_version="${BASH_REMATCH[3]}" - local is_security_update=false - - # Check if it's a security update - if echo "$line" | 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 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" | tail -n +2) - - while IFS= read -r line; do - 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]}" - - # Get current version - local current_version=$($package_manager list installed "$package_name" 2>/dev/null | grep "^$package_name" | awk '{print $2}') - - 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 - if [[ "$line" =~ ^([^[:space:]]+)[[:space:]]+([^[:space:]]+) ]]; then - local package_name="${BASH_REMATCH[1]}" - local version="${BASH_REMATCH[2]}" - - # 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 - ram_installed=$(free -g | grep "^Mem:" | awk '{print $2}') - swap_size=$(free -g | grep "^Swap:" | awk '{print $2}') - elif [[ -f /proc/meminfo ]]; then - ram_installed=$(grep "MemTotal" /proc/meminfo | awk '{print int($2/1024/1024)}') - swap_size=$(grep "SwapTotal" /proc/meminfo | awk '{print int($2/1024/1024)}') - 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:]') - elif [[ -f /etc/selinux/config ]]; then - selinux_status=$(grep "^SELINUX=" /etc/selinux/config | cut -d'=' -f2 | tr '[:upper:]' '[:lower:]') - 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 - - info "Collecting package information..." - local packages_json=$(get_package_info) - - info "Collecting repository information..." - local repositories_json=$(get_repository_info) - - info "Collecting hardware information..." - local hardware_json=$(get_hardware_info) - - info "Collecting network information..." - local network_json=$(get_network_info) - - info "Collecting system information..." - 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]') - - local payload=$(cat </dev/null; then - # Replace current script - mv "/tmp/patchmon-agent-new.sh" "$0" - chmod +x "$0" - success "Agent updated successfully" - info "Backup saved as: $0.backup.$(date +%Y%m%d_%H%M%S)" - - # Get the new version number - local new_version=$(grep '^AGENT_VERSION=' "$0" | cut -d'"' -f2) - info "Updated to version: $new_version" - - # Automatically run update to send new information to PatchMon - info "Sending updated information to PatchMon..." - if "$0" update; then - success "Successfully sent updated information to PatchMon" - else - warning "Failed to send updated information to PatchMon (this is not critical)" - fi - else - error "Downloaded script is invalid" - rm -f "/tmp/patchmon-agent-new.sh" - fi - else - error "Failed to download new agent script" - fi - else - error "Failed to get update information" - fi -} - -# Update crontab with current policy -update_crontab() { - load_credentials - info "Updating crontab with current policy..." - local response=$(curl -s -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval") - if [[ $? -eq 0 ]]; then - local update_interval=$(echo "$response" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2) - if [[ -n "$update_interval" ]]; then - # Generate the expected crontab entry - local expected_crontab="" - if [[ $update_interval -eq 60 ]]; then - # Hourly updates - expected_crontab="0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" - else - # Custom interval updates - expected_crontab="*/$update_interval * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" - fi - - # Get current crontab - local current_crontab=$(crontab -l 2>/dev/null | grep "patchmon-agent.sh update" | head -1) - - # Check if crontab needs updating - if [[ "$current_crontab" == "$expected_crontab" ]]; then - info "Crontab is already up to date (interval: $update_interval minutes)" - return 0 - fi - - info "Setting update interval to $update_interval minutes" - echo "$expected_crontab" | crontab - - success "Crontab updated successfully" - 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" - ;; - "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 - ;; - "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|update-agent|update-crontab|diagnostics}" - echo "" - echo "Commands:" - echo " configure - 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 " update-agent - Update agent to latest version" - echo " update-crontab - Update crontab with current policy" - echo " diagnostics - Show detailed system diagnostics" - echo "" - echo "Setup Process:" - echo " 1. Contact your PatchMon administrator to create a host entry" - echo " 2. Run: $0 configure (provided by admin)" - echo " 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 "$@" \ No newline at end of file diff --git a/agents/patchmon-agent.sh.backup.20250920_002529 b/agents/patchmon-agent.sh.backup.20250920_002529 deleted file mode 100755 index f6ecdb3..0000000 --- a/agents/patchmon-agent.sh.backup.20250920_002529 +++ /dev/null @@ -1,1219 +0,0 @@ -#!/bin/bash - -# PatchMon Agent Script v1.2.5 -# 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.5" -CONFIG_FILE="/etc/patchmon/agent.conf" -CREDENTIALS_FILE="/etc/patchmon/credentials" -LOG_FILE="/var/log/patchmon-agent.log" - -# 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() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" -} - -# Error handling -error() { - echo -e "${RED}ERROR: $1${NC}" >&2 - log "ERROR: $1" - exit 1 -} - -# Info logging -info() { - echo -e "${BLUE}INFO: $1${NC}" - log "INFO: $1" -} - -# Success logging -success() { - echo -e "${GREEN}SUCCESS: $1${NC}" - log "SUCCESS: $1" -} - -# Warning logging -warning() { - echo -e "${YELLOW}WARNING: $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 -} - -# 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 -s -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 - apt-get update -qq - - # Get upgradable packages - local upgradable=$(apt list --upgradable 2>/dev/null | grep -v "WARNING") - - while IFS= read -r line; do - if [[ "$line" =~ ^([^/]+)/([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+.*[[:space:]]([^[:space:]]+)[[:space:]]*(\[.*\])? ]]; then - local package_name="${BASH_REMATCH[1]}" - local current_version="${BASH_REMATCH[4]}" - local available_version="${BASH_REMATCH[3]}" - local is_security_update=false - - # Check if it's a security update - if echo "$line" | 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 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" | tail -n +2) - - while IFS= read -r line; do - 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]}" - - # Get current version - local current_version=$($package_manager list installed "$package_name" 2>/dev/null | grep "^$package_name" | awk '{print $2}') - - 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 - if [[ "$line" =~ ^([^[:space:]]+)[[:space:]]+([^[:space:]]+) ]]; then - local package_name="${BASH_REMATCH[1]}" - local version="${BASH_REMATCH[2]}" - - # 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 - ram_installed=$(free -g | grep "^Mem:" | awk '{print $2}') - swap_size=$(free -g | grep "^Swap:" | awk '{print $2}') - elif [[ -f /proc/meminfo ]]; then - ram_installed=$(grep "MemTotal" /proc/meminfo | awk '{print int($2/1024/1024)}') - swap_size=$(grep "SwapTotal" /proc/meminfo | awk '{print int($2/1024/1024)}') - 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:]') - elif [[ -f /etc/selinux/config ]]; then - selinux_status=$(grep "^SELINUX=" /etc/selinux/config | cut -d'=' -f2 | tr '[:upper:]' '[:lower:]') - 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 - - info "Collecting package information..." - local packages_json=$(get_package_info) - - info "Collecting repository information..." - local repositories_json=$(get_repository_info) - - info "Collecting hardware information..." - local hardware_json=$(get_hardware_info) - - info "Collecting network information..." - local network_json=$(get_network_info) - - info "Collecting system information..." - 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]') - - local payload=$(cat </dev/null; then - # Replace current script - mv "/tmp/patchmon-agent-new.sh" "$0" - chmod +x "$0" - success "Agent updated successfully" - info "Backup saved as: $0.backup.$(date +%Y%m%d_%H%M%S)" - - # Get the new version number - local new_version=$(grep '^AGENT_VERSION=' "$0" | cut -d'"' -f2) - info "Updated to version: $new_version" - - # Automatically run update to send new information to PatchMon - info "Sending updated information to PatchMon..." - if "$0" update; then - success "Successfully sent updated information to PatchMon" - else - warning "Failed to send updated information to PatchMon (this is not critical)" - fi - else - error "Downloaded script is invalid" - rm -f "/tmp/patchmon-agent-new.sh" - fi - else - error "Failed to download new agent script" - fi - else - error "Failed to get update information" - fi -} - -# Update crontab with current policy -update_crontab() { - load_credentials - info "Updating crontab with current policy..." - local response=$(curl -s -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval") - if [[ $? -eq 0 ]]; then - local update_interval=$(echo "$response" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2) - if [[ -n "$update_interval" ]]; then - # Generate the expected crontab entry - local expected_crontab="" - if [[ $update_interval -eq 60 ]]; then - # Hourly updates - expected_crontab="0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" - else - # Custom interval updates - expected_crontab="*/$update_interval * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" - fi - - # Get current crontab - local current_crontab=$(crontab -l 2>/dev/null | grep "patchmon-agent.sh update" | head -1) - - # Check if crontab needs updating - if [[ "$current_crontab" == "$expected_crontab" ]]; then - info "Crontab is already up to date (interval: $update_interval minutes)" - return 0 - fi - - info "Setting update interval to $update_interval minutes" - echo "$expected_crontab" | crontab - - success "Crontab updated successfully" - 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" - ;; - "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 - ;; - "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|update-agent|update-crontab|diagnostics}" - echo "" - echo "Commands:" - echo " configure - 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 " update-agent - Update agent to latest version" - echo " update-crontab - Update crontab with current policy" - echo " diagnostics - Show detailed system diagnostics" - echo "" - echo "Setup Process:" - echo " 1. Contact your PatchMon administrator to create a host entry" - echo " 2. Run: $0 configure (provided by admin)" - echo " 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 "$@" \ No newline at end of file diff --git a/agents/patchmon_install.sh b/agents/patchmon_install.sh index cb18375..bf7eea9 100644 --- a/agents/patchmon_install.sh +++ b/agents/patchmon_install.sh @@ -35,6 +35,40 @@ if [[ $EUID -ne 0 ]]; then error "This script must be run as root (use sudo)" fi +# Install required dependencies +info "📦 Installing required dependencies..." + +# Detect package manager and install jq +if command -v apt-get >/dev/null 2>&1; then + # Debian/Ubuntu + apt-get update >/dev/null 2>&1 + apt-get install -y jq curl >/dev/null 2>&1 +elif command -v yum >/dev/null 2>&1; then + # CentOS/RHEL 7 + yum install -y jq curl >/dev/null 2>&1 +elif command -v dnf >/dev/null 2>&1; then + # CentOS/RHEL 8+/Fedora + dnf install -y jq curl >/dev/null 2>&1 +elif command -v zypper >/dev/null 2>&1; then + # openSUSE + zypper install -y jq curl >/dev/null 2>&1 +elif command -v pacman >/dev/null 2>&1; then + # Arch Linux + pacman -S --noconfirm jq curl >/dev/null 2>&1 +elif command -v apk >/dev/null 2>&1; then + # Alpine Linux + apk add --no-cache jq curl >/dev/null 2>&1 +else + warning "Could not detect package manager. Please ensure 'jq' and 'curl' are installed manually." +fi + +# Verify jq installation +if ! command -v jq >/dev/null 2>&1; then + error "Failed to install 'jq'. Please install it manually: https://stedolan.github.io/jq/download/" +fi + +success "Dependencies installed successfully!" + # Default server URL (will be replaced by backend with configured URL) PATCHMON_URL="http://localhost:3001" @@ -137,6 +171,7 @@ fi success "🎉 PatchMon Agent installation complete!" echo "" echo "📋 Installation Summary:" +echo " • Dependencies installed: jq, curl" echo " • Agent installed: /usr/local/bin/patchmon-agent.sh" echo " • Agent version: $AGENT_VERSION" if [[ "$EXPECTED_VERSION" != "Unknown" ]]; then diff --git a/backend/prisma/migrations/20250921220948_add_signup_enabled_setting/migration.sql b/backend/prisma/migrations/20250921220948_add_signup_enabled_setting/migration.sql new file mode 100644 index 0000000..ece630a --- /dev/null +++ b/backend/prisma/migrations/20250921220948_add_signup_enabled_setting/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "settings" ADD COLUMN "signup_enabled" BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 23c1998..ec1cb2b 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -168,6 +168,7 @@ model settings { last_update_check DateTime? latest_version String? update_available Boolean @default(false) + signup_enabled Boolean @default(false) } model update_history { diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js index ce0d177..2cfced7 100644 --- a/backend/src/routes/authRoutes.js +++ b/backend/src/routes/authRoutes.js @@ -34,7 +34,7 @@ router.get('/check-admin-users', async (req, res) => { router.post('/setup-admin', [ body('username').isLength({ min: 1 }).withMessage('Username is required'), body('email').isEmail().withMessage('Valid email is required'), - body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters') + body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters for security') ], async (req, res) => { try { const errors = validationResult(req); @@ -425,6 +425,17 @@ router.post('/admin/users/:userId/reset-password', authenticateToken, requireMan } }); +// Check if signup is enabled (public endpoint) +router.get('/signup-enabled', async (req, res) => { + try { + const settings = await prisma.settings.findFirst(); + res.json({ signupEnabled: settings?.signup_enabled || false }); + } catch (error) { + console.error('Error checking signup status:', error); + res.status(500).json({ error: 'Failed to check signup status' }); + } +}); + // Public signup endpoint router.post('/signup', [ body('username').isLength({ min: 3 }).withMessage('Username must be at least 3 characters'), @@ -432,6 +443,12 @@ router.post('/signup', [ body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters') ], async (req, res) => { try { + // Check if signup is enabled + const settings = await prisma.settings.findFirst(); + if (!settings?.signup_enabled) { + return res.status(403).json({ error: 'User signup is currently disabled' }); + } + const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); diff --git a/backend/src/routes/dashboardRoutes.js b/backend/src/routes/dashboardRoutes.js index 1bbbb79..11d15f6 100644 --- a/backend/src/routes/dashboardRoutes.js +++ b/backend/src/routes/dashboardRoutes.js @@ -36,15 +36,12 @@ router.get('/stats', authenticateToken, requireViewDashboard, async (req, res) = osDistribution, updateTrends ] = await Promise.all([ - // Total hosts count - prisma.hosts.count({ - where: { status: 'active' } - }), + // Total hosts count (all hosts regardless of status) + prisma.hosts.count(), // Hosts needing updates (distinct hosts with packages needing updates) prisma.hosts.count({ where: { - status: 'active', host_packages: { some: { needs_update: true diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index a9a3f50..f32ce5c 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -755,7 +755,15 @@ router.get('/admin/list', authenticateToken, requireManageHosts, async (req, res api_id: true, agent_version: true, auto_update: true, - created_at: true + created_at: true, + host_group_id: true, + host_groups: { + select: { + id: true, + name: true, + color: true + } + } }, orderBy: { created_at: 'desc' } }); @@ -767,20 +775,124 @@ router.get('/admin/list', authenticateToken, requireManageHosts, async (req, res } }); +// Admin endpoint to delete multiple hosts +router.delete('/bulk', authenticateToken, requireManageHosts, [ + body('hostIds').isArray({ min: 1 }).withMessage('At least one host ID is required'), + body('hostIds.*').isLength({ min: 1 }).withMessage('Each host ID must be provided') +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { hostIds } = req.body; + + // Verify all hosts exist before deletion + const existingHosts = await prisma.hosts.findMany({ + where: { id: { in: hostIds } }, + select: { id: true, friendly_name: true } + }); + + if (existingHosts.length !== hostIds.length) { + const foundIds = existingHosts.map(h => h.id); + const missingIds = hostIds.filter(id => !foundIds.includes(id)); + return res.status(404).json({ + error: 'Some hosts not found', + missingIds + }); + } + + // Delete all hosts (cascade will handle related data) + const deleteResult = await prisma.hosts.deleteMany({ + where: { id: { in: hostIds } } + }); + + // Check if all hosts were actually deleted + if (deleteResult.count !== hostIds.length) { + console.warn(`Expected to delete ${hostIds.length} hosts, but only deleted ${deleteResult.count}`); + } + + res.json({ + message: `${deleteResult.count} host${deleteResult.count !== 1 ? 's' : ''} deleted successfully`, + deletedCount: deleteResult.count, + requestedCount: hostIds.length, + deletedHosts: existingHosts.map(h => ({ id: h.id, friendly_name: h.friendly_name })) + }); + } catch (error) { + console.error('Bulk host deletion error:', error); + + // Handle specific Prisma errors + if (error.code === 'P2025') { + return res.status(404).json({ + error: 'Some hosts were not found or already deleted', + details: 'The hosts may have been deleted by another process or do not exist' + }); + } + + if (error.code === 'P2003') { + return res.status(400).json({ + error: 'Cannot delete hosts due to foreign key constraints', + details: 'Some hosts have related data that prevents deletion' + }); + } + + res.status(500).json({ + error: 'Failed to delete hosts', + details: error.message || 'An unexpected error occurred' + }); + } +}); + // Admin endpoint to delete host router.delete('/:hostId', authenticateToken, requireManageHosts, async (req, res) => { try { const { hostId } = req.params; + // Check if host exists first + const existingHost = await prisma.hosts.findUnique({ + where: { id: hostId }, + select: { id: true, friendly_name: true } + }); + + if (!existingHost) { + return res.status(404).json({ + error: 'Host not found', + details: 'The host may have been deleted or does not exist' + }); + } + // Delete host and all related data (cascade) await prisma.hosts.delete({ where: { id: hostId } }); - res.json({ message: 'Host deleted successfully' }); + res.json({ + message: 'Host deleted successfully', + deletedHost: { id: existingHost.id, friendly_name: existingHost.friendly_name } + }); } catch (error) { console.error('Host deletion error:', error); - res.status(500).json({ error: 'Failed to delete host' }); + + // Handle specific Prisma errors + if (error.code === 'P2025') { + return res.status(404).json({ + error: 'Host not found', + details: 'The host may have been deleted or does not exist' + }); + } + + if (error.code === 'P2003') { + return res.status(400).json({ + error: 'Cannot delete host due to foreign key constraints', + details: 'The host has related data that prevents deletion' + }); + } + + res.status(500).json({ + error: 'Failed to delete host', + details: error.message || 'An unexpected error occurred' + }); } }); diff --git a/backend/src/routes/packageRoutes.js b/backend/src/routes/packageRoutes.js index f781505..8bf7e71 100644 --- a/backend/src/routes/packageRoutes.js +++ b/backend/src/routes/packageRoutes.js @@ -112,11 +112,19 @@ router.get('/', async (req, res) => { return { ...pkg, + affectedHostsCount: pkg._count.hostPackages, + affectedHosts: affectedHosts.map(hp => ({ + hostId: hp.host.id, + friendlyName: hp.host.friendly_name, + osType: hp.host.os_type, + currentVersion: hp.current_version, + availableVersion: hp.available_version, + isSecurityUpdate: hp.is_security_update + })), stats: { totalInstalls: pkg._count.hostPackages, updatesNeeded: updatesCount, - securityUpdates: securityCount, - affectedHosts: affectedHosts.map(hp => hp.host) + securityUpdates: securityCount } }; }) diff --git a/backend/src/routes/permissionsRoutes.js b/backend/src/routes/permissionsRoutes.js index fc7c90f..048520a 100644 --- a/backend/src/routes/permissionsRoutes.js +++ b/backend/src/routes/permissionsRoutes.js @@ -47,16 +47,16 @@ router.put('/roles/:role', authenticateToken, requireManageSettings, async (req, try { const { role } = req.params; const { - canViewDashboard, - canViewHosts, - canManageHosts, - canViewPackages, - canManagePackages, - canViewUsers, - canManageUsers, - canViewReports, - canExportData, - canManageSettings + can_view_dashboard, + can_view_hosts, + can_manage_hosts, + can_view_packages, + can_manage_packages, + can_view_users, + can_manage_users, + can_view_reports, + can_export_data, + can_manage_settings } = req.body; // Prevent modifying admin role permissions (admin should always have full access) @@ -67,31 +67,31 @@ router.put('/roles/:role', authenticateToken, requireManageSettings, async (req, const permissions = await prisma.role_permissions.upsert({ where: { role }, update: { - can_view_dashboard: canViewDashboard, - can_view_hosts: canViewHosts, - can_manage_hosts: canManageHosts, - can_view_packages: canViewPackages, - can_manage_packages: canManagePackages, - can_view_users: canViewUsers, - can_manage_users: canManageUsers, - can_view_reports: canViewReports, - can_export_data: canExportData, - can_manage_settings: canManageSettings, + can_view_dashboard: can_view_dashboard, + can_view_hosts: can_view_hosts, + can_manage_hosts: can_manage_hosts, + can_view_packages: can_view_packages, + can_manage_packages: can_manage_packages, + can_view_users: can_view_users, + can_manage_users: can_manage_users, + can_view_reports: can_view_reports, + can_export_data: can_export_data, + can_manage_settings: can_manage_settings, updated_at: new Date() }, create: { id: require('uuid').v4(), role, - can_view_dashboard: canViewDashboard, - can_view_hosts: canViewHosts, - can_manage_hosts: canManageHosts, - can_view_packages: canViewPackages, - can_manage_packages: canManagePackages, - can_view_users: canViewUsers, - can_manage_users: canManageUsers, - can_view_reports: canViewReports, - can_export_data: canExportData, - can_manage_settings: canManageSettings, + can_view_dashboard: can_view_dashboard, + can_view_hosts: can_view_hosts, + can_manage_hosts: can_manage_hosts, + can_view_packages: can_view_packages, + can_manage_packages: can_manage_packages, + can_view_users: can_view_users, + can_manage_users: can_manage_users, + can_view_reports: can_view_reports, + can_export_data: can_export_data, + can_manage_settings: can_manage_settings, updated_at: new Date() } }); diff --git a/backend/src/routes/settingsRoutes.js b/backend/src/routes/settingsRoutes.js index 166d2cd..08a4f3b 100644 --- a/backend/src/routes/settingsRoutes.js +++ b/backend/src/routes/settingsRoutes.js @@ -40,7 +40,9 @@ async function triggerCrontabUpdates() { const http = require('http'); const https = require('https'); - const settings = await prisma.settings.findFirst(); + const settings = await prisma.settings.findFirst({ + orderBy: { updated_at: 'desc' } + }); const serverUrl = settings?.server_url || process.env.SERVER_URL || 'http://localhost:3001'; const url = new URL(`${serverUrl}/api/v1/hosts/ping`); const isHttps = url.protocol === 'https:'; @@ -92,7 +94,9 @@ async function triggerCrontabUpdates() { // Get current settings router.get('/', authenticateToken, requireManageSettings, async (req, res) => { try { - let settings = await prisma.settings.findFirst(); + let settings = await prisma.settings.findFirst({ + orderBy: { updated_at: 'desc' } + }); // If no settings exist, create default settings if (!settings) { @@ -106,12 +110,12 @@ router.get('/', authenticateToken, requireManageSettings, async (req, res) => { frontend_url: 'http://localhost:3000', update_interval: 60, auto_update: false, + signup_enabled: false, updated_at: new Date() } }); } - console.log('Returning settings:', settings); res.json(settings); } catch (error) { console.error('Settings fetch error:', error); @@ -127,6 +131,7 @@ router.put('/', authenticateToken, requireManageSettings, [ body('frontendUrl').isLength({ min: 1 }).withMessage('Frontend URL is required'), body('updateInterval').isInt({ min: 5, max: 1440 }).withMessage('Update interval must be between 5 and 1440 minutes'), body('autoUpdate').isBoolean().withMessage('Auto update must be a boolean'), + body('signupEnabled').isBoolean().withMessage('Signup enabled must be a boolean'), body('githubRepoUrl').optional().isLength({ min: 1 }).withMessage('GitHub repo URL must be a non-empty string'), body('repositoryType').optional().isIn(['public', 'private']).withMessage('Repository type must be public or private'), body('sshKeyPath').optional().custom((value) => { @@ -140,36 +145,23 @@ router.put('/', authenticateToken, requireManageSettings, [ }) ], async (req, res) => { try { - console.log('Settings update request body:', req.body); const errors = validationResult(req); if (!errors.isEmpty()) { console.log('Validation errors:', errors.array()); return res.status(400).json({ errors: errors.array() }); } - const { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl, repositoryType, sshKeyPath } = req.body; - console.log('Extracted values:', { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl, repositoryType, sshKeyPath }); - console.log('GitHub repo URL received:', githubRepoUrl, 'Type:', typeof githubRepoUrl); + const { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, signupEnabled, githubRepoUrl, repositoryType, sshKeyPath } = req.body; // Construct server URL from components const serverUrl = `${serverProtocol}://${serverHost}:${serverPort}`; - let settings = await prisma.settings.findFirst(); + let settings = await prisma.settings.findFirst({ + orderBy: { updated_at: 'desc' } + }); if (settings) { // Update existing settings - console.log('Updating existing settings with data:', { - serverUrl, - serverProtocol, - serverHost, - serverPort, - frontendUrl, - updateInterval: updateInterval || 60, - autoUpdate: autoUpdate || false, - githubRepoUrl: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git', - repositoryType: repositoryType || 'public' - }); - console.log('Final githubRepoUrl value being saved:', githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git'); const oldUpdateInterval = settings.update_interval; settings = await prisma.settings.update({ @@ -182,13 +174,13 @@ router.put('/', authenticateToken, requireManageSettings, [ frontend_url: frontendUrl, update_interval: updateInterval || 60, auto_update: autoUpdate || false, + signup_enabled: signupEnabled || false, github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git', repository_type: repositoryType || 'public', ssh_key_path: sshKeyPath || null, updated_at: new Date() } }); - console.log('Settings updated successfully:', settings); // If update interval changed, trigger crontab updates on all hosts with auto-update enabled if (oldUpdateInterval !== (updateInterval || 60)) { @@ -207,6 +199,7 @@ router.put('/', authenticateToken, requireManageSettings, [ frontend_url: frontendUrl, update_interval: updateInterval || 60, auto_update: autoUpdate || false, + signup_enabled: signupEnabled || false, github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git', repository_type: repositoryType || 'public', ssh_key_path: sshKeyPath || null, @@ -228,7 +221,9 @@ router.put('/', authenticateToken, requireManageSettings, [ // Get server URL for public use (used by installation scripts) router.get('/server-url', async (req, res) => { try { - const settings = await prisma.settings.findFirst(); + const settings = await prisma.settings.findFirst({ + orderBy: { updated_at: 'desc' } + }); if (!settings) { return res.json({ server_url: 'http://localhost:3001' }); @@ -244,7 +239,9 @@ router.get('/server-url', async (req, res) => { // Get update interval policy for agents (public endpoint) router.get('/update-interval', async (req, res) => { try { - const settings = await prisma.settings.findFirst(); + const settings = await prisma.settings.findFirst({ + orderBy: { updated_at: 'desc' } + }); if (!settings) { return res.json({ updateInterval: 60 }); @@ -263,7 +260,9 @@ router.get('/update-interval', async (req, res) => { // Get auto-update policy for agents (public endpoint) router.get('/auto-update', async (req, res) => { try { - const settings = await prisma.settings.findFirst(); + const settings = await prisma.settings.findFirst({ + orderBy: { updated_at: 'desc' } + }); if (!settings) { return res.json({ autoUpdate: false }); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7cca227..d07ed06 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -24,12 +24,8 @@ function AppRoutes() { const { needsFirstTimeSetup, checkingSetup, isAuthenticated } = useAuth() const isAuth = isAuthenticated() // Call the function to get boolean value - // Debug logging - console.log('AppRoutes state:', { needsFirstTimeSetup, checkingSetup, isAuthenticated: isAuth }) - // Show loading while checking if setup is needed if (checkingSetup) { - console.log('Showing loading screen...') return (
@@ -42,12 +38,9 @@ function AppRoutes() { // Show first-time setup if no admin users exist if (needsFirstTimeSetup && !isAuth) { - console.log('Showing FirstTimeAdminSetup component...') return } - console.log('Showing normal routes (Login/Dashboard)...') - return ( } /> diff --git a/frontend/src/components/DashboardSettingsModal.jsx b/frontend/src/components/DashboardSettingsModal.jsx index 330b855..e6bf49d 100644 --- a/frontend/src/components/DashboardSettingsModal.jsx +++ b/frontend/src/components/DashboardSettingsModal.jsx @@ -28,9 +28,11 @@ import { Settings as SettingsIcon } from 'lucide-react'; import { dashboardPreferencesAPI } from '../utils/api'; +import { useTheme } from '../contexts/ThemeContext'; // Sortable Card Item Component const SortableCardItem = ({ card, onToggle }) => { + const { isDark } = useTheme(); const { attributes, listeners, @@ -50,7 +52,7 @@ const SortableCardItem = ({ card, onToggle }) => {
@@ -58,12 +60,12 @@ const SortableCardItem = ({ card, onToggle }) => {
-
+
{card.title}
@@ -73,8 +75,8 @@ const SortableCardItem = ({ card, onToggle }) => { onClick={() => onToggle(card.cardId)} className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-colors ${ card.enabled - ? 'bg-green-100 text-green-800 hover:bg-green-200' - : 'bg-secondary-100 text-secondary-600 hover:bg-secondary-200' + ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800' + : 'bg-secondary-100 dark:bg-secondary-700 text-secondary-600 dark:text-secondary-300 hover:bg-secondary-200 dark:hover:bg-secondary-600' }`} > {card.enabled ? ( @@ -97,6 +99,7 @@ const DashboardSettingsModal = ({ isOpen, onClose }) => { const [cards, setCards] = useState([]); const [hasChanges, setHasChanges] = useState(false); const queryClient = useQueryClient(); + const { isDark } = useTheme(); const sensors = useSensors( useSensor(PointerSensor), @@ -212,24 +215,24 @@ const DashboardSettingsModal = ({ isOpen, onClose }) => {
-
-
+
+
-

+

Dashboard Settings

-

+

Customize your dashboard by reordering cards and toggling their visibility. Drag cards to reorder them, and click the visibility toggle to show/hide cards.

@@ -259,7 +262,7 @@ const DashboardSettingsModal = ({ isOpen, onClose }) => { )}
-
+
diff --git a/frontend/src/components/FirstTimeAdminSetup.jsx b/frontend/src/components/FirstTimeAdminSetup.jsx index c7cb7da..435b317 100644 --- a/frontend/src/components/FirstTimeAdminSetup.jsx +++ b/frontend/src/components/FirstTimeAdminSetup.jsx @@ -30,15 +30,19 @@ const FirstTimeAdminSetup = () => { return false } if (!formData.email.trim()) { - setError('Email is required') + setError('Email address is required') return false } - if (!formData.email.includes('@')) { - setError('Please enter a valid email address') + + // Enhanced email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(formData.email.trim())) { + setError('Please enter a valid email address (e.g., user@example.com)') return false } - if (formData.password.length < 6) { - setError('Password must be at least 6 characters') + + if (formData.password.length < 8) { + setError('Password must be at least 8 characters for security') return false } if (formData.password !== formData.confirmPassword) { @@ -186,7 +190,7 @@ const FirstTimeAdminSetup = () => { value={formData.password} onChange={handleInputChange} className="input w-full" - placeholder="Enter your password (min 6 characters)" + placeholder="Enter your password (min 8 characters)" required disabled={isLoading} /> diff --git a/frontend/src/components/InlineGroupEdit.jsx b/frontend/src/components/InlineGroupEdit.jsx index d126d84..fda76bb 100644 --- a/frontend/src/components/InlineGroupEdit.jsx +++ b/frontend/src/components/InlineGroupEdit.jsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useMemo } from 'react'; import { Edit2, Check, X, ChevronDown } from 'lucide-react'; const InlineGroupEdit = ({ @@ -88,11 +88,8 @@ const InlineGroupEdit = ({ const handleSave = async () => { if (disabled || isLoading) return; - console.log('handleSave called:', { selectedValue, originalValue: value, changed: selectedValue !== value }); - // Check if value actually changed if (selectedValue === value) { - console.log('No change detected, closing edit mode'); setIsEditing(false); setIsOpen(false); return; @@ -102,15 +99,12 @@ const InlineGroupEdit = ({ setError(''); try { - console.log('Calling onSave with:', selectedValue); await onSave(selectedValue); - console.log('Save successful'); // Update the local value to match the saved value setSelectedValue(selectedValue); setIsEditing(false); setIsOpen(false); } catch (err) { - console.error('Save failed:', err); setError(err.message || 'Failed to save'); } finally { setIsLoading(false); @@ -127,22 +121,23 @@ const InlineGroupEdit = ({ } }; - const getDisplayValue = () => { - console.log('getDisplayValue called with:', { value, options }); + const displayValue = useMemo(() => { if (!value) { - console.log('No value, returning Ungrouped'); return 'Ungrouped'; } const option = options.find(opt => opt.id === value); - console.log('Found option:', option); return option ? option.name : 'Unknown Group'; - }; + }, [value, options]); - const getDisplayColor = () => { + const displayColor = useMemo(() => { if (!value) return 'bg-secondary-100 text-secondary-800'; const option = options.find(opt => opt.id === value); return option ? `text-white` : 'bg-secondary-100 text-secondary-800'; - }; + }, [value, options]); + + const selectedOption = useMemo(() => { + return options.find(opt => opt.id === value); + }, [value, options]); if (isEditing) { return ( @@ -241,10 +236,10 @@ const InlineGroupEdit = ({ return (
opt.id === value)?.color } : {}} + className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${displayColor}`} + style={value ? { backgroundColor: selectedOption?.color } : {}} > - {getDisplayValue()} + {displayValue} {!disabled && ( - )} + + + +
diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index 2a976ff..e0f3a3a 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -18,10 +18,6 @@ export const AuthProvider = ({ children }) => { const [permissionsLoading, setPermissionsLoading] = useState(false) const [needsFirstTimeSetup, setNeedsFirstTimeSetup] = useState(false) - // Debug: Log when needsFirstTimeSetup changes - useEffect(() => { - console.log('needsFirstTimeSetup changed to:', needsFirstTimeSetup) - }, [needsFirstTimeSetup]) const [checkingSetup, setCheckingSetup] = useState(true) // Initialize auth state from localStorage @@ -227,21 +223,17 @@ export const AuthProvider = ({ children }) => { // Check if any admin users exist (for first-time setup) const checkAdminUsersExist = useCallback(async () => { try { - console.log('Making API call to check admin users...') const response = await fetch('/api/v1/auth/check-admin-users', { method: 'GET', headers: { 'Content-Type': 'application/json' } }) - + if (response.ok) { const data = await response.json() - console.log('Admin check response:', data) // Debug log - console.log('hasAdminUsers:', data.hasAdminUsers, 'Setting needsFirstTimeSetup to:', !data.hasAdminUsers) setNeedsFirstTimeSetup(!data.hasAdminUsers) } else { - console.log('Admin check failed:', response.status, response.statusText) // Debug log // If endpoint doesn't exist or fails, assume setup is needed setNeedsFirstTimeSetup(true) } @@ -256,12 +248,9 @@ export const AuthProvider = ({ children }) => { // Check for admin users on initial load useEffect(() => { - console.log('AuthContext useEffect triggered:', { token: !!token, user: !!user }) if (!token && !user) { - console.log('Calling checkAdminUsersExist...') checkAdminUsersExist() } else { - console.log('Skipping admin check - user already authenticated') setCheckingSetup(false) } }, [token, user, checkAdminUsersExist]) diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 5cb3de4..9d18331 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -9,13 +9,15 @@ import { TrendingUp, RefreshCw, Clock, - WifiOff + WifiOff, + Settings } from 'lucide-react' import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title } from 'chart.js' import { Pie, Bar } from 'react-chartjs-2' import { dashboardAPI, dashboardPreferencesAPI, settingsAPI, formatRelativeTime } from '../utils/api' import DashboardSettingsModal from '../components/DashboardSettingsModal' import { useTheme } from '../contexts/ThemeContext' +import { useAuth } from '../contexts/AuthContext' // Register Chart.js components ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title) @@ -25,6 +27,7 @@ const Dashboard = () => { const [cardPreferences, setCardPreferences] = useState([]) const navigate = useNavigate() const { isDark } = useTheme() + const { user } = useAuth() // Navigation handlers const handleTotalHostsClick = () => { @@ -52,17 +55,61 @@ const Dashboard = () => { } const handleOSDistributionClick = () => { - navigate('/hosts', { replace: true }) + navigate('/hosts?showFilters=true', { replace: true }) } const handleUpdateStatusClick = () => { - navigate('/hosts', { replace: true }) + navigate('/hosts?filter=needsUpdates', { replace: true }) } const handlePackagePriorityClick = () => { navigate('/packages?filter=security') } + // Chart click handlers + const handleOSChartClick = (event, elements) => { + if (elements.length > 0) { + const elementIndex = elements[0].index + const osName = stats.charts.osDistribution[elementIndex].name.toLowerCase() + navigate(`/hosts?osFilter=${osName}&showFilters=true`, { replace: true }) + } + } + + const handleUpdateStatusChartClick = (event, elements) => { + if (elements.length > 0) { + const elementIndex = elements[0].index + const statusName = stats.charts.updateStatusDistribution[elementIndex].name + + // Map status names to filter parameters + let filter = '' + if (statusName.toLowerCase().includes('needs updates')) { + filter = 'needsUpdates' + } else if (statusName.toLowerCase().includes('up to date')) { + filter = 'upToDate' + } else if (statusName.toLowerCase().includes('stale')) { + filter = 'stale' + } + + if (filter) { + navigate(`/hosts?filter=${filter}`, { replace: true }) + } + } + } + + const handlePackagePriorityChartClick = (event, elements) => { + if (elements.length > 0) { + const elementIndex = elements[0].index + const priorityName = stats.charts.packageUpdateDistribution[elementIndex].name + + // Map priority names to filter parameters + if (priorityName.toLowerCase().includes('security')) { + navigate('/packages?filter=security', { replace: true }) + } else if (priorityName.toLowerCase().includes('outdated')) { + navigate('/packages?filter=outdated', { replace: true }) + } + } + } + // Helper function to format the update interval threshold const formatUpdateIntervalThreshold = () => { if (!settings?.updateInterval) return '24 hours' @@ -373,7 +420,7 @@ const Dashboard = () => { >

Update Status

- +
); @@ -386,7 +433,7 @@ const Dashboard = () => { >

Package Priority

- +
); @@ -469,6 +516,39 @@ const Dashboard = () => { } }, }, + onClick: handleOSChartClick, + } + + const updateStatusChartOptions = { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: isDark ? '#ffffff' : '#374151', + font: { + size: 12 + } + } + }, + }, + onClick: handleUpdateStatusChartClick, + } + + const packagePriorityChartOptions = { + responsive: true, + plugins: { + legend: { + position: 'bottom', + labels: { + color: isDark ? '#ffffff' : '#374151', + font: { + size: 12 + } + } + }, + }, + onClick: handlePackagePriorityChartClick, } const barChartOptions = { @@ -582,12 +662,22 @@ const Dashboard = () => { {/* Page Header */}
-

Dashboard

+

+ Welcome back, {user?.first_name || user?.username || 'User'} 👋 +

Overview of your PatchMon infrastructure

+
-
0)}`}> - {getStatusIcon(isStale, host.stats.outdatedPackages > 0)} - {getStatusText(isStale, host.stats.outdatedPackages > 0)} +
0)}`}> + {getStatusIcon(isStale, host.stats.outdated_packages > 0)} + {getStatusText(isStale, host.stats.outdated_packages > 0)}
@@ -333,12 +333,12 @@ const HostDetail = () => {

Host Group

- {host.hostGroup ? ( + {host.host_groups ? ( - {host.hostGroup.name} + {host.host_groups.name} ) : ( @@ -355,18 +355,6 @@ const HostDetail = () => {
- {host.ip && ( -
-

IP Address

-

{host.ip}

-
- )} - - -
-

Last Update

-

{formatRelativeTime(host.last_update)}

-
{host.agent_version && (
@@ -720,7 +708,7 @@ const HostDetail = () => { {/* Package Statistics */}
-

Package Statistics

+

Package Statistics

@@ -728,7 +716,7 @@ const HostDetail = () => {
-

{host.stats.totalPackages}

+

{host.stats.total_packages}

Total Packages

@@ -740,7 +728,7 @@ const HostDetail = () => {
-

{host.stats.outdatedPackages}

+

{host.stats.outdated_packages}

Outdated Packages

@@ -752,7 +740,7 @@ const HostDetail = () => {
-

{host.stats.securityUpdates}

+

{host.stats.security_updates}

Security Updates

@@ -797,8 +785,40 @@ const CredentialsModal = ({ host, isOpen, onClose }) => { const serverUrl = serverUrlData?.server_url || 'http://localhost:3001' - const copyToClipboard = (text) => { - navigator.clipboard.writeText(text) + const copyToClipboard = async (text) => { + try { + // Try modern clipboard API first + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + return + } + + // Fallback for older browsers or non-secure contexts + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-999999px' + textArea.style.top = '-999999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + + try { + const successful = document.execCommand('copy') + if (!successful) { + throw new Error('Copy command failed') + } + } catch (err) { + // If all else fails, show the text in a prompt + prompt('Copy this command:', text) + } finally { + document.body.removeChild(textArea) + } + } catch (err) { + console.error('Failed to copy to clipboard:', err) + // Show the text in a prompt as last resort + prompt('Copy this command:', text) + } } const getSetupCommands = () => { @@ -1035,12 +1055,12 @@ echo " - View logs: tail -f /var/log/patchmon-agent.log"`
Group
- {formData.hostGroupId === group.id && ( + {formData.host_group_id === group.id && (
@@ -214,9 +214,43 @@ const CredentialsModal = ({ host, isOpen, onClose }) => { } }, [host?.isNewHost]) - const copyToClipboard = (text, label) => { - navigator.clipboard.writeText(text) - alert(`${label} copied to clipboard!`) + const copyToClipboard = async (text, label) => { + try { + // Try modern clipboard API first + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + alert(`${label} copied to clipboard!`) + return + } + + // Fallback for older browsers or non-secure contexts + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-999999px' + textArea.style.top = '-999999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + + try { + const successful = document.execCommand('copy') + if (successful) { + alert(`${label} copied to clipboard!`) + } else { + throw new Error('Copy command failed') + } + } catch (err) { + // If all else fails, show the text in a prompt + prompt(`Copy this ${label.toLowerCase()}:`, text) + } finally { + document.body.removeChild(textArea) + } + } catch (err) { + console.error('Failed to copy to clipboard:', err) + // Show the text in a prompt as last resort + prompt(`Copy this ${label.toLowerCase()}:`, text) + } } // Fetch server URL from settings @@ -604,6 +638,7 @@ const Hosts = () => { const [showAddModal, setShowAddModal] = useState(false) const [selectedHosts, setSelectedHosts] = useState([]) const [showBulkAssignModal, setShowBulkAssignModal] = useState(false) + const [showBulkDeleteModal, setShowBulkDeleteModal] = useState(false) const [searchParams] = useSearchParams() const navigate = useNavigate() @@ -622,6 +657,9 @@ const Hosts = () => { // Handle URL filter parameters useEffect(() => { const filter = searchParams.get('filter') + const showFiltersParam = searchParams.get('showFilters') + const osFilterParam = searchParams.get('osFilter') + if (filter === 'needsUpdates') { setShowFilters(true) setStatusFilter('all') @@ -634,6 +672,18 @@ const Hosts = () => { setShowFilters(true) setStatusFilter('active') // We'll filter hosts that are up to date in the filtering logic + } else if (filter === 'stale') { + setShowFilters(true) + setStatusFilter('all') + // We'll filter hosts that are stale in the filtering logic + } else if (showFiltersParam === 'true') { + setShowFilters(true) + } + + // Handle OS filter parameter + if (osFilterParam) { + setShowFilters(true) + setOsFilter(osFilterParam) } // Handle add host action from navigation @@ -732,7 +782,7 @@ const Hosts = () => { // Ensure hostGroupId is set correctly return { ...updatedHost, - hostGroupId: updatedHost.hostGroup?.id || null + hostGroupId: updatedHost.host_groups?.id || null }; } return host; @@ -771,9 +821,6 @@ const Hosts = () => { }); }, onSuccess: (data) => { - console.log('updateHostGroupMutation success:', data); - console.log('Updated host data:', data.host); - console.log('Host group in response:', data.host.hostGroup); // Update the cache with the new host data queryClient.setQueryData(['hosts'], (oldData) => { @@ -785,7 +832,7 @@ const Hosts = () => { // Ensure hostGroupId is set correctly const updatedHost = { ...data.host, - hostGroupId: data.host.hostGroup?.id || null + hostGroupId: data.host.host_groups?.id || null }; console.log('Updated host with hostGroupId:', updatedHost); return updatedHost; @@ -804,6 +851,19 @@ const Hosts = () => { } }) + const bulkDeleteMutation = useMutation({ + mutationFn: (hostIds) => adminHostsAPI.deleteBulk(hostIds), + onSuccess: (data) => { + console.log('Bulk delete success:', data); + queryClient.invalidateQueries(['hosts']); + setSelectedHosts([]); + setShowBulkDeleteModal(false); + }, + onError: (error) => { + console.error('Bulk delete error:', error); + } + }); + // Helper functions for bulk selection const handleSelectHost = (hostId) => { setSelectedHosts(prev => @@ -825,6 +885,10 @@ const Hosts = () => { bulkUpdateGroupMutation.mutate({ hostIds: selectedHosts, hostGroupId }) } + const handleBulkDelete = () => { + bulkDeleteMutation.mutate(selectedHosts) + } + // Table filtering and sorting logic const filteredAndSortedHosts = React.useMemo(() => { if (!hosts) return [] @@ -838,8 +902,8 @@ const Hosts = () => { // Group filter const matchesGroup = groupFilter === 'all' || - (groupFilter === 'ungrouped' && !host.hostGroup) || - (groupFilter !== 'ungrouped' && host.hostGroup?.id === groupFilter) + (groupFilter === 'ungrouped' && !host.host_groups) || + (groupFilter !== 'ungrouped' && host.host_groups?.id === groupFilter) // Status filter const matchesStatus = statusFilter === 'all' || (host.effectiveStatus || host.status) === statusFilter @@ -847,12 +911,13 @@ const Hosts = () => { // OS filter const matchesOs = osFilter === 'all' || host.os_type?.toLowerCase() === osFilter.toLowerCase() - // URL filter for hosts needing updates, inactive hosts, or up-to-date hosts + // URL filter for hosts needing updates, inactive hosts, up-to-date hosts, or stale hosts const filter = searchParams.get('filter') const matchesUrlFilter = (filter !== 'needsUpdates' || (host.updatesCount && host.updatesCount > 0)) && (filter !== 'inactive' || (host.effectiveStatus || host.status) === 'inactive') && - (filter !== 'upToDate' || (!host.isStale && host.updatesCount === 0)) + (filter !== 'upToDate' || (!host.isStale && host.updatesCount === 0)) && + (filter !== 'stale' || host.isStale) // Hide stale filter const matchesHideStale = !hideStale || !host.isStale @@ -878,8 +943,8 @@ const Hosts = () => { bValue = b.ip?.toLowerCase() || 'zzz_no_ip' break case 'group': - aValue = a.hostGroup?.name || 'zzz_ungrouped' - bValue = b.hostGroup?.name || 'zzz_ungrouped' + aValue = a.host_groups?.name || 'zzz_ungrouped' + bValue = b.host_groups?.name || 'zzz_ungrouped' break case 'os': aValue = a.os_type?.toLowerCase() || 'zzz_unknown' @@ -929,7 +994,7 @@ const Hosts = () => { let groupKey switch (groupBy) { case 'group': - groupKey = host.hostGroup?.name || 'Ungrouped' + groupKey = host.host_groups?.name || 'Ungrouped' break case 'status': groupKey = (host.effectiveStatus || host.status).charAt(0).toUpperCase() + (host.effectiveStatus || host.status).slice(1) @@ -1055,16 +1120,10 @@ const Hosts = () => {
) case 'group': - console.log('Rendering group for host:', { - hostId: host.id, - hostGroupId: host.hostGroupId, - hostGroup: host.hostGroup, - availableGroups: hostGroups - }); return ( updateHostGroupMutation.mutate({ hostId: host.id, hostGroupId: newGroupId })} options={hostGroups || []} placeholder="Select group..." @@ -1232,9 +1291,9 @@ const Hosts = () => { } return ( -
+
{/* Page Header */} -
+

Hosts

@@ -1262,7 +1321,7 @@ const Hosts = () => {

{/* Stats Summary */} -
+
{
{/* Hosts List */} -
-
+
+
{selectedHosts.length > 0 && (
@@ -1335,6 +1394,13 @@ const Hosts = () => { Assign to Group +
- {(!hosts || hosts.length === 0) ? ( -
- -

No hosts registered yet

-

- Click "Add Host" to manually register a new host and get API credentials -

-
- ) : ( -
- {Object.entries(groupedHosts).map(([groupName, groupHosts]) => ( +
+ {(!hosts || hosts.length === 0) ? ( +
+ +

No hosts registered yet

+

+ Click "Add Host" to manually register a new host and get API credentials +

+
+ ) : filteredAndSortedHosts.length === 0 ? ( +
+ +

No hosts match your current filters

+

+ Try adjusting your search terms or filters to see more results +

+
+ ) : ( +
+
+ {Object.entries(groupedHosts).map(([groupName, groupHosts]) => (
{/* Group Header */} {groupBy !== 'none' && ( @@ -1628,11 +1704,13 @@ const Hosts = () => {
- ))} -
- )} -
-
+ ))} +
+
+ )} +
+
+
{/* Modals */} { /> )} + {/* Bulk Delete Modal */} + {showBulkDeleteModal && ( + setShowBulkDeleteModal(false)} + onDelete={handleBulkDelete} + isLoading={bulkDeleteMutation.isPending} + /> + )} + {/* Column Settings Modal */} {showColumnSettings && ( { + const selectedHostNames = hosts + .filter(host => selectedHosts.includes(host.id)) + .map(host => host.friendly_name || host.hostname || host.id) + + const handleSubmit = (e) => { + e.preventDefault() + onDelete() + } + + return ( +
+
+
+
+

Delete Hosts

+ +
+
+ +
+
+
+ +

+ Warning: This action cannot be undone +

+
+

+ You are about to permanently delete {selectedHosts.length} host{selectedHosts.length !== 1 ? 's' : ''}. + This will remove all host data, including package information, update history, and API credentials. +

+
+ +
+

+ Hosts to be deleted: +

+
+ {selectedHostNames.map((friendlyName, index) => ( +
+ • {friendlyName} +
+ ))} +
+
+ +
+
+ + +
+
+
+
+
+ ) +} + // Column Settings Modal Component const ColumnSettingsModal = ({ columnConfig, onClose, onToggleVisibility, onReorder, onReset }) => { const [draggedIndex, setDraggedIndex] = useState(null) diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index ebffa4a..50e2d97 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { Eye, EyeOff, Lock, User, AlertCircle, Smartphone, ArrowLeft, Mail } from 'lucide-react' import { useAuth } from '../contexts/AuthContext' @@ -19,10 +19,29 @@ const Login = () => { const [error, setError] = useState('') const [requiresTfa, setRequiresTfa] = useState(false) const [tfaUsername, setTfaUsername] = useState('') + const [signupEnabled, setSignupEnabled] = useState(false) const navigate = useNavigate() const { login } = useAuth() + // Check if signup is enabled + useEffect(() => { + const checkSignupEnabled = async () => { + try { + const response = await fetch('/api/v1/auth/signup-enabled') + if (response.ok) { + const data = await response.json() + setSignupEnabled(data.signupEnabled) + } + } catch (error) { + console.error('Failed to check signup status:', error) + // Default to disabled on error for security + setSignupEnabled(false) + } + } + checkSignupEnabled() + }, []) + const handleSubmit = async (e) => { e.preventDefault() setIsLoading(true) @@ -135,6 +154,10 @@ const Login = () => { } const toggleMode = () => { + // Only allow signup mode if signup is enabled + if (!signupEnabled && !isSignupMode) { + return // Don't allow switching to signup if disabled + } setIsSignupMode(!isSignupMode) setFormData({ username: '', @@ -269,18 +292,20 @@ const Login = () => {
-
-

- {isSignupMode ? 'Already have an account?' : "Don't have an account?"}{' '} - -

-
+ {signupEnabled && ( +
+

+ {isSignupMode ? 'Already have an account?' : "Don't have an account?"}{' '} + +

+
+ )} ) : (
diff --git a/frontend/src/pages/Packages.jsx b/frontend/src/pages/Packages.jsx index d112ec1..6b72265 100644 --- a/frontend/src/pages/Packages.jsx +++ b/frontend/src/pages/Packages.jsx @@ -71,8 +71,9 @@ const Packages = () => { // Handle affected hosts click const handleAffectedHostsClick = (pkg) => { - const hostIds = pkg.affectedHosts.map(host => host.hostId) - const hostNames = pkg.affectedHosts.map(host => host.friendlyName) + const affectedHosts = pkg.affectedHosts || [] + const hostIds = affectedHosts.map(host => host.hostId) + const hostNames = affectedHosts.map(host => host.friendlyName) // Create URL with selected hosts and filter const params = new URLSearchParams() @@ -128,8 +129,9 @@ const Packages = () => { (securityFilter === 'security' && pkg.isSecurityUpdate) || (securityFilter === 'regular' && !pkg.isSecurityUpdate) + const affectedHosts = pkg.affectedHosts || [] const matchesHost = hostFilter === 'all' || - pkg.affectedHosts.some(host => host.hostId === hostFilter) + affectedHosts.some(host => host.hostId === hostFilter) return matchesSearch && matchesCategory && matchesSecurity && matchesHost }) @@ -148,8 +150,8 @@ const Packages = () => { bValue = b.latestVersion?.toLowerCase() || '' break case 'affectedHosts': - aValue = a.affectedHostsCount || 0 - bValue = b.affectedHostsCount || 0 + aValue = a.affectedHostsCount || a.affectedHosts?.length || 0 + bValue = b.affectedHostsCount || b.affectedHosts?.length || 0 break case 'priority': aValue = a.isSecurityUpdate ? 0 : 1 // Security updates first @@ -241,14 +243,15 @@ const Packages = () => {
) case 'affectedHosts': + const affectedHostsCount = pkg.affectedHostsCount || pkg.affectedHosts?.length || 0 return ( ) @@ -278,7 +281,8 @@ const Packages = () => { // Calculate unique affected hosts const uniqueAffectedHosts = new Set() packages?.forEach(pkg => { - pkg.affectedHosts.forEach(host => { + const affectedHosts = pkg.affectedHosts || [] + affectedHosts.forEach(host => { uniqueAffectedHosts.add(host.hostId) }) }) @@ -458,7 +462,7 @@ const Packages = () => { > {hosts?.map(host => ( - + ))}
diff --git a/frontend/src/pages/Permissions.jsx b/frontend/src/pages/Permissions.jsx index ac24b91..c3c0400 100644 --- a/frontend/src/pages/Permissions.jsx +++ b/frontend/src/pages/Permissions.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Shield, @@ -145,17 +145,22 @@ const Permissions = () => { const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDelete }) => { const [permissions, setPermissions] = useState(role) + // Sync permissions state with role prop when it changes + useEffect(() => { + setPermissions(role) + }, [role]) + const permissionFields = [ - { key: 'canViewDashboard', label: 'View Dashboard', icon: BarChart3, description: 'Access to the main dashboard' }, - { key: 'canViewHosts', label: 'View Hosts', icon: Server, description: 'See host information and status' }, - { key: 'canManageHosts', label: 'Manage Hosts', icon: Edit, description: 'Add, edit, and delete hosts' }, - { key: 'canViewPackages', label: 'View Packages', icon: Package, description: 'See package information' }, - { key: 'canManagePackages', label: 'Manage Packages', icon: Settings, description: 'Edit package details' }, - { key: 'canViewUsers', label: 'View Users', icon: Users, description: 'See user list and details' }, - { key: 'canManageUsers', label: 'Manage Users', icon: Shield, description: 'Add, edit, and delete users' }, - { key: 'canViewReports', label: 'View Reports', icon: BarChart3, description: 'Access to reports and analytics' }, - { key: 'canExportData', label: 'Export Data', icon: Download, description: 'Download data and reports' }, - { key: 'canManageSettings', label: 'Manage Settings', icon: Settings, description: 'System configuration access' } + { key: 'can_view_dashboard', label: 'View Dashboard', icon: BarChart3, description: 'Access to the main dashboard' }, + { key: 'can_view_hosts', label: 'View Hosts', icon: Server, description: 'See host information and status' }, + { key: 'can_manage_hosts', label: 'Manage Hosts', icon: Edit, description: 'Add, edit, and delete hosts' }, + { key: 'can_view_packages', label: 'View Packages', icon: Package, description: 'See package information' }, + { key: 'can_manage_packages', label: 'Manage Packages', icon: Settings, description: 'Edit package details' }, + { key: 'can_view_users', label: 'View Users', icon: Users, description: 'See user list and details' }, + { key: 'can_manage_users', label: 'Manage Users', icon: Shield, description: 'Add, edit, and delete users' }, + { key: 'can_view_reports', label: 'View Reports', icon: BarChart3, description: 'Access to reports and analytics' }, + { key: 'can_export_data', label: 'Export Data', icon: Download, description: 'Download data and reports' }, + { key: 'can_manage_settings', label: 'Manage Settings', icon: Settings, description: 'System configuration access' } ] const handlePermissionChange = (key, value) => { @@ -196,7 +201,7 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele
@@ -268,16 +273,16 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele const AddRoleModal = ({ isOpen, onClose, onSuccess }) => { const [formData, setFormData] = useState({ role: '', - canViewDashboard: true, - canViewHosts: true, - canManageHosts: false, - canViewPackages: true, - canManagePackages: false, - canViewUsers: false, - canManageUsers: false, - canViewReports: true, - canExportData: false, - canManageSettings: false + can_view_dashboard: true, + can_view_hosts: true, + can_manage_hosts: false, + can_view_packages: true, + can_manage_packages: false, + can_view_users: false, + can_manage_users: false, + can_view_reports: true, + can_export_data: false, + can_manage_settings: false }) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState('') @@ -309,12 +314,12 @@ const AddRoleModal = ({ isOpen, onClose, onSuccess }) => { return (
-
-

Add New Role

+
+

Add New Role

-

Permissions

{[ - { key: 'canViewDashboard', label: 'View Dashboard' }, - { key: 'canViewHosts', label: 'View Hosts' }, - { key: 'canManageHosts', label: 'Manage Hosts' }, - { key: 'canViewPackages', label: 'View Packages' }, - { key: 'canManagePackages', label: 'Manage Packages' }, - { key: 'canViewUsers', label: 'View Users' }, - { key: 'canManageUsers', label: 'Manage Users' }, - { key: 'canViewReports', label: 'View Reports' }, - { key: 'canExportData', label: 'Export Data' }, - { key: 'canManageSettings', label: 'Manage Settings' } + { key: 'can_view_dashboard', label: 'View Dashboard' }, + { key: 'can_view_hosts', label: 'View Hosts' }, + { key: 'can_manage_hosts', label: 'Manage Hosts' }, + { key: 'can_view_packages', label: 'View Packages' }, + { key: 'can_manage_packages', label: 'Manage Packages' }, + { key: 'can_view_users', label: 'View Users' }, + { key: 'can_manage_users', label: 'Manage Users' }, + { key: 'can_view_reports', label: 'View Reports' }, + { key: 'can_export_data', label: 'Export Data' }, + { key: 'can_manage_settings', label: 'Manage Settings' } ].map((permission) => (
{ onChange={handleInputChange} className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded" /> -
@@ -359,8 +364,8 @@ const AddRoleModal = ({ isOpen, onClose, onSuccess }) => {
{error && ( -
-

{error}

+
+

{error}

)} @@ -368,7 +373,7 @@ const AddRoleModal = ({ isOpen, onClose, onSuccess }) => { diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx index 5c239a9..ca035c7 100644 --- a/frontend/src/pages/Profile.jsx +++ b/frontend/src/pages/Profile.jsx @@ -517,9 +517,45 @@ const TfaTab = () => { regenerateBackupCodesMutation.mutate() } - const copyToClipboard = (text) => { - navigator.clipboard.writeText(text) - setMessage({ type: 'success', text: 'Copied to clipboard!' }) + const copyToClipboard = async (text) => { + try { + // Try modern clipboard API first + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + setMessage({ type: 'success', text: 'Copied to clipboard!' }) + return + } + + // Fallback for older browsers or non-secure contexts + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-999999px' + textArea.style.top = '-999999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + + try { + const successful = document.execCommand('copy') + if (successful) { + setMessage({ type: 'success', text: 'Copied to clipboard!' }) + } else { + throw new Error('Copy command failed') + } + } catch (err) { + // If all else fails, show the text in a prompt + prompt('Copy this text:', text) + setMessage({ type: 'info', text: 'Text shown in prompt for manual copying' }) + } finally { + document.body.removeChild(textArea) + } + } catch (err) { + console.error('Failed to copy to clipboard:', err) + // Show the text in a prompt as last resort + prompt('Copy this text:', text) + setMessage({ type: 'info', text: 'Text shown in prompt for manual copying' }) + } } const downloadBackupCodes = () => { diff --git a/frontend/src/pages/Repositories.jsx b/frontend/src/pages/Repositories.jsx index cede749..4add269 100644 --- a/frontend/src/pages/Repositories.jsx +++ b/frontend/src/pages/Repositories.jsx @@ -19,7 +19,8 @@ import { ArrowDown, X, GripVertical, - Check + Check, + RefreshCw } from 'lucide-react'; import { repositoryAPI } from '../utils/api'; @@ -60,7 +61,7 @@ const Repositories = () => { }; // Fetch repositories - const { data: repositories = [], isLoading, error } = useQuery({ + const { data: repositories = [], isLoading, error, refetch, isFetching } = useQuery({ queryKey: ['repositories'], queryFn: () => repositoryAPI.list().then(res => res.data) }); @@ -132,14 +133,6 @@ const Repositories = () => { repo.url.toLowerCase().includes(searchTerm.toLowerCase()) || repo.distribution.toLowerCase().includes(searchTerm.toLowerCase()); - // Debug logging - console.log('Filtering repo:', { - name: repo.name, - isSecure: repo.isSecure, - filterType, - url: repo.url - }); - // Check security based on URL if isSecure property doesn't exist const isSecure = repo.isSecure !== undefined ? repo.isSecure : repo.url.startsWith('https://'); @@ -151,13 +144,6 @@ const Repositories = () => { (filterStatus === 'active' && repo.is_active === true) || (filterStatus === 'inactive' && repo.is_active === false); - console.log('Filter results:', { - matchesSearch, - matchesType, - matchesStatus, - final: matchesSearch && matchesType && matchesStatus - }); - return matchesSearch && matchesType && matchesStatus; }); @@ -211,6 +197,26 @@ const Repositories = () => { return (
+ {/* Page Header */} +
+
+

Repositories

+

+ Manage and monitor your package repositories +

+
+
+ +
+
{/* Summary Stats */}
@@ -437,7 +443,7 @@ const Repositories = () => { return (
- {repo.hostCount} + {repo.host_count}
) case 'actions': diff --git a/frontend/src/pages/RepositoryDetail.jsx b/frontend/src/pages/RepositoryDetail.jsx index 366c8e4..4a257e2 100644 --- a/frontend/src/pages/RepositoryDetail.jsx +++ b/frontend/src/pages/RepositoryDetail.jsx @@ -310,10 +310,10 @@ const RepositoryDetail = () => {

- Hosts Using This Repository ({repository.hostRepositories?.length || 0}) + Hosts Using This Repository ({repository.host_repositories?.length || 0})

- {!repository.hostRepositories || repository.hostRepositories.length === 0 ? ( + {!repository.host_repositories || repository.host_repositories.length === 0 ? (

No hosts using this repository

@@ -323,28 +323,28 @@ const RepositoryDetail = () => {
) : (
- {repository.hostRepositories.map((hostRepo) => ( + {repository.host_repositories.map((hostRepo) => (
- {hostRepo.host.friendly_name} + {hostRepo.hosts.friendly_name}
- IP: {hostRepo.host.ip} - OS: {hostRepo.host.osType} {hostRepo.host.osVersion} - Last Update: {new Date(hostRepo.host.lastUpdate).toLocaleDateString()} + IP: {hostRepo.hosts.ip} + OS: {hostRepo.hosts.os_type} {hostRepo.hosts.os_version} + Last Update: {new Date(hostRepo.hosts.last_update).toLocaleDateString()}
@@ -352,7 +352,7 @@ const RepositoryDetail = () => {
Last Checked
- {new Date(hostRepo.lastChecked).toLocaleDateString()} + {new Date(hostRepo.last_checked).toLocaleDateString()}
diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 3de813b..e85340f 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -13,6 +13,7 @@ const Settings = () => { frontendUrl: 'http://localhost:3000', updateInterval: 60, autoUpdate: false, + signupEnabled: false, githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git', repositoryType: 'public', sshKeyPath: '', @@ -72,8 +73,6 @@ const Settings = () => { // Update form data when settings are loaded useEffect(() => { if (settings) { - console.log('Settings loaded:', settings); - console.log('updateInterval from settings:', settings.update_interval); const newFormData = { serverProtocol: settings.server_protocol || 'http', serverHost: settings.server_host || 'localhost', @@ -81,12 +80,12 @@ const Settings = () => { frontendUrl: settings.frontend_url || 'http://localhost:3000', updateInterval: settings.update_interval || 60, autoUpdate: settings.auto_update || false, + signupEnabled: settings.signup_enabled === true ? true : false, // Explicit boolean conversion githubRepoUrl: settings.github_repo_url || 'git@github.com:9technologygroup/patchmon.net.git', repositoryType: settings.repository_type || 'public', sshKeyPath: settings.ssh_key_path || '', useCustomSshKey: !!settings.ssh_key_path }; - console.log('Setting form data to:', newFormData); setFormData(newFormData); setIsDirty(false); } @@ -95,34 +94,14 @@ const Settings = () => { // Update settings mutation const updateSettingsMutation = useMutation({ mutationFn: (data) => { - console.log('Mutation called with data:', data); - return settingsAPI.update(data).then(res => { - console.log('API response:', res); - return res.data; - }); + return settingsAPI.update(data).then(res => res.data); }, onSuccess: (data) => { - console.log('Mutation success:', data); - console.log('Invalidating queries and updating form data'); queryClient.invalidateQueries(['settings']); - // Update form data with the returned data - setFormData({ - serverProtocol: data.settings?.server_protocol || data.server_protocol || 'http', - serverHost: data.settings?.server_host || data.server_host || 'localhost', - serverPort: data.settings?.server_port || data.server_port || 3001, - frontendUrl: data.settings?.frontend_url || data.frontend_url || 'http://localhost:3000', - updateInterval: data.settings?.update_interval || data.update_interval || 60, - autoUpdate: data.settings?.auto_update || data.auto_update || false, - githubRepoUrl: data.settings?.github_repo_url || data.github_repo_url || 'git@github.com:9technologygroup/patchmon.net.git', - repositoryType: data.settings?.repository_type || data.repository_type || 'public', - sshKeyPath: data.settings?.ssh_key_path || data.ssh_key_path || '', - useCustomSshKey: !!(data.settings?.ssh_key_path || data.ssh_key_path) - }); setIsDirty(false); setErrors({}); }, onError: (error) => { - console.log('Mutation error:', error); if (error.response?.data?.errors) { setErrors(error.response.data.errors.reduce((acc, err) => { acc[err.path] = err.msg; @@ -138,20 +117,12 @@ const Settings = () => { const { data: agentVersions, isLoading: agentVersionsLoading, error: agentVersionsError } = useQuery({ queryKey: ['agentVersions'], queryFn: () => { - console.log('Fetching agent versions...'); return agentVersionAPI.list().then(res => { - console.log('Agent versions API response:', res); return res.data; }); } }); - // Debug agent versions - useEffect(() => { - console.log('Agent versions data:', agentVersions); - console.log('Agent versions loading:', agentVersionsLoading); - console.log('Agent versions error:', agentVersionsError); - }, [agentVersions, agentVersionsLoading, agentVersionsError]); // Load current version on component mount useEffect(() => { @@ -213,7 +184,7 @@ const Settings = () => { currentVersion: data.currentVersion, latestVersion: data.latestVersion, isUpdateAvailable: data.isUpdateAvailable, - lastUpdateCheck: data.lastUpdateCheck, + last_update_check: data.last_update_check, checking: false, error: null }); @@ -264,10 +235,8 @@ const Settings = () => { }; const handleInputChange = (field, value) => { - console.log(`handleInputChange: ${field} = ${value}`); setFormData(prev => { const newData = { ...prev, [field]: value }; - console.log('New form data:', newData); return newData; }); setIsDirty(true); @@ -316,10 +285,7 @@ const Settings = () => { }; const handleSave = () => { - console.log('Saving settings:', formData); if (validateForm()) { - console.log('Validation passed, calling mutation'); - // Prepare data for submission const dataToSubmit = { ...formData }; if (!dataToSubmit.useCustomSshKey) { @@ -328,10 +294,7 @@ const Settings = () => { // Remove the frontend-only field delete dataToSubmit.useCustomSshKey; - console.log('Submitting data with githubRepoUrl:', dataToSubmit.githubRepoUrl); updateSettingsMutation.mutate(dataToSubmit); - } else { - console.log('Validation failed:', errors); } }; @@ -489,7 +452,6 @@ const Settings = () => { max="1440" value={formData.updateInterval} onChange={(e) => { - console.log('Update interval input changed:', e.target.value); handleInputChange('updateInterval', parseInt(e.target.value) || 60); }} className={`w-full border rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white ${ @@ -523,6 +485,24 @@ const Settings = () => {

+ {/* User Signup Setting */} +
+ +

+ When enabled, users can create their own accounts through the signup page. When disabled, only administrators can create user accounts. +

+
+ {/* Security Notice */}
@@ -970,14 +950,14 @@ const Settings = () => {
{/* Last Checked Time */} - {versionInfo.lastUpdateCheck && ( + {versionInfo.last_update_check && (
Last Checked
- {new Date(versionInfo.lastUpdateCheck).toLocaleString()} + {new Date(versionInfo.last_update_check).toLocaleString()}

Updates are checked automatically every 24 hours diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index d93c388..113370c 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -60,6 +60,7 @@ export const adminHostsAPI = { create: (data) => api.post('/hosts/create', data), list: () => api.get('/hosts/admin/list'), delete: (hostId) => api.delete(`/hosts/${hostId}`), + deleteBulk: (hostIds) => api.delete('/hosts/bulk', { data: { hostIds } }), regenerateCredentials: (hostId) => api.post(`/hosts/${hostId}/regenerate-credentials`), updateGroup: (hostId, hostGroupId) => api.put(`/hosts/${hostId}/group`, { hostGroupId }), bulkUpdateGroup: (hostIds, hostGroupId) => api.put('/hosts/bulk/group', { hostIds, hostGroupId }),