- Fix migration conflicts and duplicate key errors - Remove duplicate scan logging from agents - Fix AgentHealth UI and Storage page triggers - Prevent scans from appearing on wrong pages Fixes duplicate key violations on fresh installs and storage scans appearing on Updates page.
1843 lines
63 KiB
Go
1843 lines
63 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/acknowledgment"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/cache"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/circuitbreaker"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/config"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/constants"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/crypto"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/display"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/installer"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/migration"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/orchestrator"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/guardian"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/scanner"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/service"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/validator"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/version"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
var (
|
|
lastConfigVersion int64 = 0 // Track last applied config version
|
|
)
|
|
|
|
// reportLogWithAck reports a command log to the server and tracks it for acknowledgment
|
|
func reportLogWithAck(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, logReport client.LogReport) error {
|
|
// Track this command result as pending acknowledgment
|
|
ackTracker.Add(logReport.CommandID)
|
|
|
|
// Save acknowledgment state immediately
|
|
if err := ackTracker.Save(); err != nil {
|
|
log.Printf("Warning: Failed to save acknowledgment for command %s: %v", logReport.CommandID, err)
|
|
}
|
|
|
|
// Report the log to the server (FIX: was calling itself recursively!)
|
|
if err := apiClient.ReportLog(cfg.AgentID, logReport); err != nil {
|
|
// If reporting failed, increment retry count but don't remove from pending
|
|
ackTracker.IncrementRetry(logReport.CommandID)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getCurrentPollingInterval returns the appropriate polling interval based on rapid mode
|
|
func getCurrentPollingInterval(cfg *config.Config) int {
|
|
// Check if rapid polling mode is active and not expired
|
|
if cfg.RapidPollingEnabled && time.Now().Before(cfg.RapidPollingUntil) {
|
|
return 5 // Rapid polling: 5 seconds
|
|
}
|
|
|
|
// Check if rapid polling has expired and clean up
|
|
if cfg.RapidPollingEnabled && time.Now().After(cfg.RapidPollingUntil) {
|
|
cfg.RapidPollingEnabled = false
|
|
cfg.RapidPollingUntil = time.Time{}
|
|
// Save the updated config to clean up expired rapid mode
|
|
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
|
|
log.Printf("Warning: Failed to cleanup expired rapid polling mode: %v", err)
|
|
}
|
|
}
|
|
|
|
return cfg.CheckInInterval // Normal polling: 5 minutes (300 seconds) by default
|
|
}
|
|
|
|
// getDefaultServerURL returns the default server URL with environment variable support
|
|
func getDefaultServerURL() string {
|
|
// Check environment variable first
|
|
if envURL := os.Getenv("REDFLAG_SERVER_URL"); envURL != "" {
|
|
return envURL
|
|
}
|
|
|
|
// Platform-specific defaults
|
|
if runtime.GOOS == "windows" {
|
|
// For Windows, use a placeholder that prompts users to configure
|
|
return "http://REPLACE_WITH_SERVER_IP:8080"
|
|
}
|
|
return "http://localhost:8080"
|
|
}
|
|
|
|
func main() {
|
|
// Define CLI flags
|
|
registerCmd := flag.Bool("register", false, "Register agent with server")
|
|
scanCmd := flag.Bool("scan", false, "Scan for updates and display locally")
|
|
statusCmd := flag.Bool("status", false, "Show agent status")
|
|
listUpdatesCmd := flag.Bool("list-updates", false, "List detailed update information")
|
|
versionCmd := flag.Bool("version", false, "Show version information")
|
|
serverURL := flag.String("server", "", "Server URL")
|
|
registrationToken := flag.String("token", "", "Registration token for secure enrollment")
|
|
proxyHTTP := flag.String("proxy-http", "", "HTTP proxy URL")
|
|
proxyHTTPS := flag.String("proxy-https", "", "HTTPS proxy URL")
|
|
proxyNoProxy := flag.String("proxy-no", "", "Comma-separated hosts to bypass proxy")
|
|
logLevel := flag.String("log-level", "", "Log level (debug, info, warn, error)")
|
|
configFile := flag.String("config", "", "Configuration file path")
|
|
tagsFlag := flag.String("tags", "", "Comma-separated tags for agent")
|
|
organization := flag.String("organization", "", "Organization/group name")
|
|
displayName := flag.String("name", "", "Display name for agent")
|
|
insecureTLS := flag.Bool("insecure-tls", false, "Skip TLS certificate verification")
|
|
exportFormat := flag.String("export", "", "Export format: json, csv")
|
|
|
|
// Windows service management commands
|
|
installServiceCmd := flag.Bool("install-service", false, "Install as Windows service")
|
|
removeServiceCmd := flag.Bool("remove-service", false, "Remove Windows service")
|
|
startServiceCmd := flag.Bool("start-service", false, "Start Windows service")
|
|
stopServiceCmd := flag.Bool("stop-service", false, "Stop Windows service")
|
|
serviceStatusCmd := flag.Bool("service-status", false, "Show Windows service status")
|
|
flag.Parse()
|
|
|
|
// Handle version command
|
|
if *versionCmd {
|
|
fmt.Printf("RedFlag Agent v%s\n", version.Version)
|
|
fmt.Printf("Self-hosted update management platform\n")
|
|
os.Exit(0)
|
|
}
|
|
|
|
// Handle Windows service management commands (only on Windows)
|
|
if runtime.GOOS == "windows" {
|
|
if *installServiceCmd {
|
|
if err := service.InstallService(); err != nil {
|
|
log.Fatalf("Failed to install service: %v", err)
|
|
}
|
|
fmt.Println("RedFlag service installed successfully")
|
|
os.Exit(0)
|
|
}
|
|
|
|
if *removeServiceCmd {
|
|
if err := service.RemoveService(); err != nil {
|
|
log.Fatalf("Failed to remove service: %v", err)
|
|
}
|
|
fmt.Println("RedFlag service removed successfully")
|
|
os.Exit(0)
|
|
}
|
|
|
|
if *startServiceCmd {
|
|
if err := service.StartService(); err != nil {
|
|
log.Fatalf("Failed to start service: %v", err)
|
|
}
|
|
fmt.Println("RedFlag service started successfully")
|
|
os.Exit(0)
|
|
}
|
|
|
|
if *stopServiceCmd {
|
|
if err := service.StopService(); err != nil {
|
|
log.Fatalf("Failed to stop service: %v", err)
|
|
}
|
|
fmt.Println("RedFlag service stopped successfully")
|
|
os.Exit(0)
|
|
}
|
|
|
|
if *serviceStatusCmd {
|
|
if err := service.ServiceStatus(); err != nil {
|
|
log.Fatalf("Failed to get service status: %v", err)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
// Parse tags from comma-separated string
|
|
var tags []string
|
|
if *tagsFlag != "" {
|
|
tags = strings.Split(*tagsFlag, ",")
|
|
for i, tag := range tags {
|
|
tags[i] = strings.TrimSpace(tag)
|
|
}
|
|
}
|
|
|
|
// Create CLI flags structure
|
|
cliFlags := &config.CLIFlags{
|
|
ServerURL: *serverURL,
|
|
RegistrationToken: *registrationToken,
|
|
ProxyHTTP: *proxyHTTP,
|
|
ProxyHTTPS: *proxyHTTPS,
|
|
ProxyNoProxy: *proxyNoProxy,
|
|
LogLevel: *logLevel,
|
|
ConfigFile: *configFile,
|
|
Tags: tags,
|
|
Organization: *organization,
|
|
DisplayName: *displayName,
|
|
InsecureTLS: *insecureTLS,
|
|
}
|
|
|
|
// Determine config path
|
|
configPath := constants.GetAgentConfigPath()
|
|
if *configFile != "" {
|
|
configPath = *configFile
|
|
}
|
|
|
|
// Check for migration requirements before loading configuration
|
|
migrationConfig := migration.NewFileDetectionConfig()
|
|
// Set old paths to detect existing installations
|
|
migrationConfig.OldConfigPath = constants.LegacyConfigPath
|
|
migrationConfig.OldStatePath = constants.LegacyStatePath
|
|
// Set new paths that agent will actually use
|
|
migrationConfig.NewConfigPath = constants.GetAgentConfigDir()
|
|
migrationConfig.NewStatePath = constants.GetAgentStateDir()
|
|
|
|
// Detect migration requirements
|
|
migrationDetection, err := migration.DetectMigrationRequirements(migrationConfig)
|
|
if err != nil {
|
|
log.Printf("Warning: Failed to detect migration requirements: %v", err)
|
|
} else if migrationDetection.RequiresMigration {
|
|
log.Printf("[RedFlag Server Migrator] Migration detected: %s → %s", migrationDetection.CurrentAgentVersion, version.Version)
|
|
log.Printf("[RedFlag Server Migrator] Required migrations: %v", migrationDetection.RequiredMigrations)
|
|
|
|
// Create migration plan
|
|
migrationPlan := &migration.MigrationPlan{
|
|
Detection: migrationDetection,
|
|
TargetVersion: version.Version,
|
|
Config: migrationConfig,
|
|
BackupPath: constants.GetMigrationBackupDir(), // Set backup path within agent's state directory
|
|
}
|
|
|
|
// Execute migration
|
|
executor := migration.NewMigrationExecutor(migrationPlan, configPath)
|
|
result, err := executor.ExecuteMigration()
|
|
if err != nil {
|
|
log.Printf("[RedFlag Server Migrator] Migration failed: %v", err)
|
|
log.Printf("[RedFlag Server Migrator] Backup available at: %s", result.BackupPath)
|
|
log.Printf("[RedFlag Server Migrator] Agent may not function correctly until migration is completed")
|
|
} else {
|
|
log.Printf("[RedFlag Server Migrator] Migration completed successfully")
|
|
if result.RollbackAvailable {
|
|
log.Printf("[RedFlag Server Migrator] Rollback available at: %s", result.BackupPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load configuration with priority: CLI > env > file > defaults
|
|
cfg, err := config.Load(configPath, cliFlags)
|
|
if err != nil {
|
|
log.Fatal("Failed to load configuration:", err)
|
|
}
|
|
|
|
// Always set the current agent version in config
|
|
if cfg.AgentVersion != version.Version {
|
|
if cfg.AgentVersion != "" {
|
|
log.Printf("[RedFlag Server Migrator] Version change detected: %s → %s", cfg.AgentVersion, version.Version)
|
|
log.Printf("[RedFlag Server Migrator] Performing lightweight migration check...")
|
|
}
|
|
|
|
// Update config version to match current agent
|
|
cfg.AgentVersion = version.Version
|
|
|
|
// Save updated config
|
|
if err := cfg.Save(configPath); err != nil {
|
|
log.Printf("Warning: Failed to update agent version in config: %v", err)
|
|
} else {
|
|
if cfg.AgentVersion != "" {
|
|
log.Printf("[RedFlag Server Migrator] Agent version updated in configuration")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle registration
|
|
if *registerCmd {
|
|
// Validate server URL for Windows users
|
|
if runtime.GOOS == "windows" && strings.Contains(*serverURL, "REPLACE_WITH_SERVER_IP") {
|
|
fmt.Println("❌ CONFIGURATION REQUIRED!")
|
|
fmt.Println("==================================================================")
|
|
fmt.Println("Please configure the server URL before registering:")
|
|
fmt.Println("")
|
|
fmt.Println("Option 1 - Use the -server flag:")
|
|
fmt.Printf(" redflag-agent.exe -register -server http://10.10.20.159:8080\n")
|
|
fmt.Println("")
|
|
fmt.Println("Option 2 - Use environment variable:")
|
|
fmt.Println(" set REDFLAG_SERVER_URL=http://10.10.20.159:8080")
|
|
fmt.Println(" redflag-agent.exe -register")
|
|
fmt.Println("")
|
|
fmt.Println("Option 3 - Create a .env file:")
|
|
fmt.Println(" REDFLAG_SERVER_URL=http://10.10.20.159:8080")
|
|
fmt.Println("==================================================================")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err := registerAgent(cfg, *serverURL); err != nil {
|
|
log.Fatal("Registration failed:", err)
|
|
}
|
|
fmt.Println("==================================================================")
|
|
fmt.Println("🎉 AGENT REGISTRATION SUCCESSFUL!")
|
|
fmt.Println("==================================================================")
|
|
fmt.Printf("📋 Agent ID: %s\n", cfg.AgentID)
|
|
fmt.Printf("🌐 Server: %s\n", cfg.ServerURL)
|
|
fmt.Printf("⏱️ Check-in Interval: %ds\n", cfg.CheckInInterval)
|
|
fmt.Println("==================================================================")
|
|
fmt.Println("💡 Save this Agent ID for your records!")
|
|
fmt.Println("🚀 You can now start the agent without flags")
|
|
fmt.Println("")
|
|
return
|
|
}
|
|
|
|
// Handle scan command
|
|
if *scanCmd {
|
|
if err := handleScanCommand(cfg, *exportFormat); err != nil {
|
|
log.Fatal("Scan failed:", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Handle status command
|
|
if *statusCmd {
|
|
if err := handleStatusCommand(cfg); err != nil {
|
|
log.Fatal("Status command failed:", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Handle list-updates command
|
|
if *listUpdatesCmd {
|
|
if err := handleListUpdatesCommand(cfg, *exportFormat); err != nil {
|
|
log.Fatal("List updates failed:", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check if registered
|
|
if !cfg.IsRegistered() {
|
|
log.Fatal("Agent not registered. Run with -register flag first.")
|
|
}
|
|
|
|
// Check if running as Windows service
|
|
if runtime.GOOS == "windows" && service.IsService() {
|
|
// Run as Windows service
|
|
if err := service.RunService(cfg); err != nil {
|
|
log.Fatal("Service failed:", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Start agent service (console mode)
|
|
if err := runAgent(cfg); err != nil {
|
|
log.Fatal("Agent failed:", err)
|
|
}
|
|
}
|
|
|
|
func registerAgent(cfg *config.Config, serverURL string) error {
|
|
// Get detailed system information
|
|
sysInfo, err := system.GetSystemInfo(version.Version)
|
|
if err != nil {
|
|
log.Printf("Warning: Failed to get detailed system info: %v\n", err)
|
|
// Fall back to basic detection
|
|
hostname, _ := os.Hostname()
|
|
osType, osVersion, osArch := client.DetectSystem()
|
|
sysInfo = &system.SystemInfo{
|
|
Hostname: hostname,
|
|
OSType: osType,
|
|
OSVersion: osVersion,
|
|
OSArchitecture: osArch,
|
|
AgentVersion: version.Version,
|
|
Metadata: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// Use registration token from config if available
|
|
apiClient := client.NewClient(serverURL, cfg.RegistrationToken)
|
|
|
|
// Create metadata with system information
|
|
metadata := map[string]string{
|
|
"installation_time": time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
// Add system info to metadata
|
|
if sysInfo.CPUInfo.ModelName != "" {
|
|
metadata["cpu_model"] = sysInfo.CPUInfo.ModelName
|
|
}
|
|
if sysInfo.CPUInfo.Cores > 0 {
|
|
metadata["cpu_cores"] = fmt.Sprintf("%d", sysInfo.CPUInfo.Cores)
|
|
}
|
|
if sysInfo.MemoryInfo.Total > 0 {
|
|
metadata["memory_total"] = fmt.Sprintf("%d", sysInfo.MemoryInfo.Total)
|
|
}
|
|
if sysInfo.RunningProcesses > 0 {
|
|
metadata["processes"] = fmt.Sprintf("%d", sysInfo.RunningProcesses)
|
|
}
|
|
if sysInfo.Uptime != "" {
|
|
metadata["uptime"] = sysInfo.Uptime
|
|
}
|
|
|
|
// Add disk information
|
|
for i, disk := range sysInfo.DiskInfo {
|
|
if i == 0 {
|
|
metadata["disk_mount"] = disk.Mountpoint
|
|
metadata["disk_total"] = fmt.Sprintf("%d", disk.Total)
|
|
metadata["disk_used"] = fmt.Sprintf("%d", disk.Used)
|
|
break // Only add primary disk info
|
|
}
|
|
}
|
|
|
|
// Get machine ID for binding
|
|
machineID, err := system.GetMachineID()
|
|
if err != nil {
|
|
log.Printf("Warning: Failed to get machine ID: %v", err)
|
|
machineID = "unknown-" + sysInfo.Hostname
|
|
}
|
|
|
|
// Get embedded public key fingerprint
|
|
publicKeyFingerprint := system.GetPublicKeyFingerprint()
|
|
if publicKeyFingerprint == "" {
|
|
log.Printf("Warning: No embedded public key fingerprint found")
|
|
}
|
|
|
|
req := client.RegisterRequest{
|
|
Hostname: sysInfo.Hostname,
|
|
OSType: sysInfo.OSType,
|
|
OSVersion: sysInfo.OSVersion,
|
|
OSArchitecture: sysInfo.OSArchitecture,
|
|
AgentVersion: sysInfo.AgentVersion,
|
|
MachineID: machineID,
|
|
PublicKeyFingerprint: publicKeyFingerprint,
|
|
Metadata: metadata,
|
|
}
|
|
|
|
resp, err := apiClient.Register(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update configuration
|
|
cfg.ServerURL = serverURL
|
|
cfg.AgentID = resp.AgentID
|
|
cfg.Token = resp.Token
|
|
cfg.RefreshToken = resp.RefreshToken
|
|
|
|
// Get check-in interval from server config
|
|
if interval, ok := resp.Config["check_in_interval"].(float64); ok {
|
|
cfg.CheckInInterval = int(interval)
|
|
} else {
|
|
cfg.CheckInInterval = 300 // Default 5 minutes
|
|
}
|
|
|
|
// Save configuration
|
|
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
// Fetch and cache server public key for signature verification
|
|
log.Println("Fetching server public key for update signature verification...")
|
|
if err := fetchAndCachePublicKey(cfg.ServerURL); err != nil {
|
|
log.Printf("Warning: Failed to fetch server public key: %v", err)
|
|
log.Printf("Agent will not be able to verify update signatures")
|
|
// Don't fail registration - key can be fetched later
|
|
} else {
|
|
log.Println("✓ Server public key cached successfully")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// fetchAndCachePublicKey fetches the server's Ed25519 public key and caches it locally
|
|
func fetchAndCachePublicKey(serverURL string) error {
|
|
_, err := crypto.FetchAndCacheServerPublicKey(serverURL)
|
|
return err
|
|
}
|
|
|
|
// renewTokenIfNeeded handles 401 errors by renewing the agent token using refresh token
|
|
func renewTokenIfNeeded(apiClient *client.Client, cfg *config.Config, err error) (*client.Client, error) {
|
|
if err != nil && strings.Contains(err.Error(), "401 Unauthorized") {
|
|
log.Printf("🔄 Access token expired - attempting renewal with refresh token...")
|
|
|
|
// Check if we have a refresh token
|
|
if cfg.RefreshToken == "" {
|
|
log.Printf("❌ No refresh token available - re-registration required")
|
|
return nil, fmt.Errorf("refresh token missing - please re-register agent")
|
|
}
|
|
|
|
// Create temporary client without token for renewal
|
|
tempClient := client.NewClient(cfg.ServerURL, "")
|
|
|
|
// Attempt to renew access token using refresh token
|
|
if err := tempClient.RenewToken(cfg.AgentID, cfg.RefreshToken, version.Version); err != nil {
|
|
log.Printf("❌ Refresh token renewal failed: %v", err)
|
|
log.Printf("💡 Refresh token may be expired (>90 days) - re-registration required")
|
|
return nil, fmt.Errorf("refresh token renewal failed: %w - please re-register agent", err)
|
|
}
|
|
|
|
// Update config with new access token (agent ID and refresh token stay the same!)
|
|
cfg.Token = tempClient.GetToken()
|
|
|
|
// Save updated config
|
|
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
|
|
log.Printf("⚠️ Warning: Failed to save renewed access token: %v", err)
|
|
}
|
|
|
|
log.Printf("✅ Access token renewed successfully - agent ID maintained: %s", cfg.AgentID)
|
|
return tempClient, nil
|
|
}
|
|
|
|
// Return original client if no 401 error
|
|
return apiClient, nil
|
|
}
|
|
|
|
// getCurrentSubsystemEnabled returns the current enabled state for a subsystem
|
|
func getCurrentSubsystemEnabled(cfg *config.Config, subsystemName string) bool {
|
|
switch subsystemName {
|
|
case "system":
|
|
return cfg.Subsystems.System.Enabled
|
|
case "updates":
|
|
return cfg.Subsystems.Updates.Enabled
|
|
case "docker":
|
|
return cfg.Subsystems.Docker.Enabled
|
|
case "storage":
|
|
return cfg.Subsystems.Storage.Enabled
|
|
case "apt":
|
|
return cfg.Subsystems.APT.Enabled
|
|
case "dnf":
|
|
return cfg.Subsystems.DNF.Enabled
|
|
case "windows":
|
|
return cfg.Subsystems.Windows.Enabled
|
|
case "winget":
|
|
return cfg.Subsystems.Winget.Enabled
|
|
default:
|
|
// Unknown subsystem, assume disabled
|
|
return false
|
|
}
|
|
}
|
|
|
|
// syncServerConfigProper checks for and applies server configuration updates with validation and protection
|
|
func syncServerConfigProper(apiClient *client.Client, cfg *config.Config) error {
|
|
serverConfig, err := apiClient.GetConfig(cfg.AgentID)
|
|
if err != nil {
|
|
log.Printf("[HISTORY] [agent] [config] sync_failed error=\"%v\" timestamp=%s",
|
|
err, time.Now().Format(time.RFC3339))
|
|
return fmt.Errorf("failed to get server config: %w", err)
|
|
}
|
|
|
|
if serverConfig.Version <= lastConfigVersion {
|
|
return nil // No update needed
|
|
}
|
|
|
|
log.Printf("[INFO] [agent] [config] server config update detected (version: %d)", serverConfig.Version)
|
|
changes := false
|
|
|
|
// Create validator for interval bounds checking
|
|
intervalValidator := validator.NewIntervalValidator()
|
|
|
|
// Create guardian to protect against check-in interval override attempts
|
|
intervalGuardian := guardian.NewIntervalGuardian()
|
|
intervalGuardian.SetBaseline(cfg.CheckInInterval)
|
|
|
|
// Process subsystem configurations
|
|
for subsystemName, subsystemConfig := range serverConfig.Subsystems {
|
|
if configMap, ok := subsystemConfig.(map[string]interface{}); ok {
|
|
|
|
// Parse interval from server config
|
|
intervalFloat := 0.0
|
|
if rawInterval, ok := configMap["interval_minutes"].(float64); ok {
|
|
intervalFloat = rawInterval
|
|
}
|
|
intervalMinutes := int(intervalFloat)
|
|
|
|
// Validate scanner interval
|
|
if intervalMinutes > 0 {
|
|
if err := intervalValidator.ValidateScannerInterval(intervalMinutes); err != nil {
|
|
log.Printf("[ERROR] [agent] [config] [%s] scanner interval validation failed: %v",
|
|
subsystemName, err)
|
|
log.Printf("[HISTORY] [agent] [config] [%s] interval_rejected interval=%d reason=\"%v\" timestamp=%s",
|
|
subsystemName, intervalMinutes, err, time.Now().Format(time.RFC3339))
|
|
continue // Skip invalid interval but don't fail entire sync
|
|
}
|
|
|
|
log.Printf("[INFO] [agent] [config] [%s] interval=%d minutes", subsystemName, intervalMinutes)
|
|
changes = true
|
|
|
|
// Apply validated interval to the appropriate subsystem
|
|
switch subsystemName {
|
|
case "system":
|
|
cfg.Subsystems.System.IntervalMinutes = intervalMinutes
|
|
case "apt":
|
|
cfg.Subsystems.APT.IntervalMinutes = intervalMinutes
|
|
case "dnf":
|
|
cfg.Subsystems.DNF.IntervalMinutes = intervalMinutes
|
|
case "storage":
|
|
cfg.Subsystems.Storage.IntervalMinutes = intervalMinutes
|
|
case "winget":
|
|
cfg.Subsystems.Winget.IntervalMinutes = intervalMinutes
|
|
default:
|
|
log.Printf("[WARNING] [agent] [config] unknown subsystem: %s", subsystemName)
|
|
}
|
|
|
|
// Log to history table
|
|
log.Printf("[HISTORY] [agent] [config] [%s] interval_updated minutes=%d timestamp=%s",
|
|
subsystemName, intervalMinutes, time.Now().Format(time.RFC3339))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verification: Ensure no scanner interval is interfering with check-in frequency
|
|
// This guards against regressions where scanner settings might affect agent polling
|
|
if intervalGuardian.GetViolationCount() > 0 {
|
|
log.Printf("[WARNING] [agent] [config] guardian detected %d previous interval violations",
|
|
intervalGuardian.GetViolationCount())
|
|
}
|
|
|
|
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
|
|
log.Printf("[HISTORY] [agent] [config] save_failed error=\"%v\" timestamp=%s",
|
|
err, time.Now().Format(time.RFC3339))
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
if changes {
|
|
log.Printf("[INFO] [agent] [config] scanner interval updates applied")
|
|
}
|
|
|
|
lastConfigVersion = serverConfig.Version
|
|
log.Printf("[SUCCESS] [agent] [config] config saved successfully")
|
|
|
|
return nil
|
|
}
|
|
|
|
// syncServerConfigWithRetry wraps syncServerConfigProper with retry logic
|
|
func syncServerConfigWithRetry(apiClient *client.Client, cfg *config.Config, maxRetries int) error {
|
|
var lastErr error
|
|
|
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
|
if err := syncServerConfigProper(apiClient, cfg); err != nil {
|
|
lastErr = err
|
|
|
|
log.Printf("[ERROR] [agent] [config] sync attempt %d/%d failed: %v",
|
|
attempt, maxRetries, err)
|
|
|
|
// Log to history table
|
|
log.Printf("[HISTORY] [agent] [config] sync_failed attempt=%d/%d error=\"%v\" timestamp=%s",
|
|
attempt, maxRetries, err, time.Now().Format(time.RFC3339))
|
|
|
|
if attempt < maxRetries {
|
|
// Exponential backoff: 1s, 2s, 4s, 8s...
|
|
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
|
|
log.Printf("[INFO] [agent] [config] retrying in %v...", backoff)
|
|
time.Sleep(backoff)
|
|
}
|
|
continue
|
|
}
|
|
|
|
log.Printf("[SUCCESS] [agent] [config] synced after %d attempts", attempt)
|
|
return nil
|
|
}
|
|
|
|
// After maxRetries, degrade gracefully
|
|
if err := cfg.SetDegradedMode(true); err != nil {
|
|
log.Printf("[ERROR] [agent] [config] failed to enter degraded mode: %v", err)
|
|
log.Printf("[HISTORY] [agent] [config] degraded_mode_failed error=\"%v\" timestamp=%s",
|
|
err, time.Now().Format(time.RFC3339))
|
|
} else {
|
|
log.Printf("[WARNING] [agent] [config] entering degraded mode after %d failed attempts", maxRetries)
|
|
}
|
|
|
|
// Log degraded mode entry to history
|
|
log.Printf("[HISTORY] [agent] [config] degraded_mode_entered failures=%d timestamp=%s",
|
|
maxRetries, time.Now().Format(time.RFC3339))
|
|
|
|
return lastErr
|
|
}
|
|
|
|
func runAgent(cfg *config.Config) error {
|
|
log.Printf("🚩 RedFlag Agent v%s starting...\n", version.Version)
|
|
log.Printf("==================================================================")
|
|
log.Printf("📋 AGENT ID: %s", cfg.AgentID)
|
|
log.Printf("🌐 SERVER: %s", cfg.ServerURL)
|
|
log.Printf("⏱️ CHECK-IN INTERVAL: %ds", cfg.CheckInInterval)
|
|
log.Printf("==================================================================")
|
|
log.Printf("💡 Tip: Use this Agent ID to identify this agent in the web UI")
|
|
log.Printf("")
|
|
|
|
apiClient := client.NewClient(cfg.ServerURL, cfg.Token)
|
|
|
|
// Initialize scanners for package updates (used by update orchestrator)
|
|
aptScanner := scanner.NewAPTScanner()
|
|
dnfScanner := scanner.NewDNFScanner()
|
|
windowsUpdateScanner := scanner.NewWindowsUpdateScanner()
|
|
wingetScanner := scanner.NewWingetScanner()
|
|
|
|
// Docker, Storage, and System scanners are created by individual subsystem handlers
|
|
// dockerScanner is created in handleScanDocker
|
|
// storageScanner and systemScanner are created in main for individual handlers
|
|
|
|
// Initialize circuit breakers for update scanners only
|
|
aptCB := circuitbreaker.New("APT", circuitbreaker.Config{
|
|
FailureThreshold: cfg.Subsystems.APT.CircuitBreaker.FailureThreshold,
|
|
FailureWindow: cfg.Subsystems.APT.CircuitBreaker.FailureWindow,
|
|
OpenDuration: cfg.Subsystems.APT.CircuitBreaker.OpenDuration,
|
|
HalfOpenAttempts: cfg.Subsystems.APT.CircuitBreaker.HalfOpenAttempts,
|
|
})
|
|
dnfCB := circuitbreaker.New("DNF", circuitbreaker.Config{
|
|
FailureThreshold: cfg.Subsystems.DNF.CircuitBreaker.FailureThreshold,
|
|
FailureWindow: cfg.Subsystems.DNF.CircuitBreaker.FailureWindow,
|
|
OpenDuration: cfg.Subsystems.DNF.CircuitBreaker.OpenDuration,
|
|
HalfOpenAttempts: cfg.Subsystems.DNF.CircuitBreaker.HalfOpenAttempts,
|
|
})
|
|
windowsCB := circuitbreaker.New("Windows Update", circuitbreaker.Config{
|
|
FailureThreshold: cfg.Subsystems.Windows.CircuitBreaker.FailureThreshold,
|
|
FailureWindow: cfg.Subsystems.Windows.CircuitBreaker.FailureWindow,
|
|
OpenDuration: cfg.Subsystems.Windows.CircuitBreaker.OpenDuration,
|
|
HalfOpenAttempts: cfg.Subsystems.Windows.CircuitBreaker.HalfOpenAttempts,
|
|
})
|
|
wingetCB := circuitbreaker.New("Winget", circuitbreaker.Config{
|
|
FailureThreshold: cfg.Subsystems.Winget.CircuitBreaker.FailureThreshold,
|
|
FailureWindow: cfg.Subsystems.Winget.CircuitBreaker.FailureWindow,
|
|
OpenDuration: cfg.Subsystems.Winget.CircuitBreaker.OpenDuration,
|
|
HalfOpenAttempts: cfg.Subsystems.Winget.CircuitBreaker.HalfOpenAttempts,
|
|
})
|
|
|
|
// Initialize scanner orchestrator for parallel execution and granular subsystem management
|
|
scanOrchestrator := orchestrator.NewOrchestrator()
|
|
|
|
// Initialize scanners for storage, system, and docker (used by individual subsystem handlers)
|
|
storageScanner := orchestrator.NewStorageScanner(version.Version)
|
|
systemScanner := orchestrator.NewSystemScanner(version.Version)
|
|
dockerScanner, _ := scanner.NewDockerScanner()
|
|
|
|
// Initialize circuit breakers for all subsystems
|
|
storageCB := circuitbreaker.New("Storage", circuitbreaker.Config{
|
|
FailureThreshold: cfg.Subsystems.Storage.CircuitBreaker.FailureThreshold,
|
|
FailureWindow: cfg.Subsystems.Storage.CircuitBreaker.FailureWindow,
|
|
OpenDuration: cfg.Subsystems.Storage.CircuitBreaker.OpenDuration,
|
|
HalfOpenAttempts: cfg.Subsystems.Storage.CircuitBreaker.HalfOpenAttempts,
|
|
})
|
|
systemCB := circuitbreaker.New("System", circuitbreaker.Config{
|
|
FailureThreshold: cfg.Subsystems.System.CircuitBreaker.FailureThreshold,
|
|
FailureWindow: cfg.Subsystems.System.CircuitBreaker.FailureWindow,
|
|
OpenDuration: cfg.Subsystems.System.CircuitBreaker.OpenDuration,
|
|
HalfOpenAttempts: cfg.Subsystems.System.CircuitBreaker.HalfOpenAttempts,
|
|
})
|
|
dockerCB := circuitbreaker.New("Docker", circuitbreaker.Config{
|
|
FailureThreshold: cfg.Subsystems.Docker.CircuitBreaker.FailureThreshold,
|
|
FailureWindow: cfg.Subsystems.Docker.CircuitBreaker.FailureWindow,
|
|
OpenDuration: cfg.Subsystems.Docker.CircuitBreaker.OpenDuration,
|
|
HalfOpenAttempts: cfg.Subsystems.Docker.CircuitBreaker.HalfOpenAttempts,
|
|
})
|
|
|
|
// Register ALL scanners with the orchestrator
|
|
// Update scanners (package management)
|
|
scanOrchestrator.RegisterScanner("apt", orchestrator.NewAPTScannerWrapper(aptScanner), aptCB, cfg.Subsystems.APT.Timeout, cfg.Subsystems.APT.Enabled)
|
|
scanOrchestrator.RegisterScanner("dnf", orchestrator.NewDNFScannerWrapper(dnfScanner), dnfCB, cfg.Subsystems.DNF.Timeout, cfg.Subsystems.DNF.Enabled)
|
|
scanOrchestrator.RegisterScanner("windows", orchestrator.NewWindowsUpdateScannerWrapper(windowsUpdateScanner), windowsCB, cfg.Subsystems.Windows.Timeout, cfg.Subsystems.Windows.Enabled)
|
|
scanOrchestrator.RegisterScanner("winget", orchestrator.NewWingetScannerWrapper(wingetScanner), wingetCB, cfg.Subsystems.Winget.Timeout, cfg.Subsystems.Winget.Enabled)
|
|
|
|
// System scanners (metrics and monitoring)
|
|
scanOrchestrator.RegisterScanner("storage", orchestrator.NewStorageScannerWrapper(storageScanner), storageCB, cfg.Subsystems.Storage.Timeout, cfg.Subsystems.Storage.Enabled)
|
|
scanOrchestrator.RegisterScanner("system", orchestrator.NewSystemScannerWrapper(systemScanner), systemCB, cfg.Subsystems.System.Timeout, cfg.Subsystems.System.Enabled)
|
|
scanOrchestrator.RegisterScanner("docker", orchestrator.NewDockerScannerWrapper(dockerScanner), dockerCB, cfg.Subsystems.Docker.Timeout, cfg.Subsystems.Docker.Enabled)
|
|
|
|
// Initialize acknowledgment tracker for command result reliability
|
|
ackTracker := acknowledgment.NewTracker(constants.GetAgentStateDir())
|
|
if err := ackTracker.Load(); err != nil {
|
|
log.Printf("Warning: Failed to load pending acknowledgments: %v", err)
|
|
} else {
|
|
pendingCount := len(ackTracker.GetPending())
|
|
if pendingCount > 0 {
|
|
log.Printf("Loaded %d pending command acknowledgments from previous session", pendingCount)
|
|
}
|
|
}
|
|
|
|
// Periodic cleanup of old/stale acknowledgments
|
|
go func() {
|
|
cleanupTicker := time.NewTicker(1 * time.Hour)
|
|
defer cleanupTicker.Stop()
|
|
for range cleanupTicker.C {
|
|
removed := ackTracker.Cleanup()
|
|
if removed > 0 {
|
|
log.Printf("Cleaned up %d stale acknowledgments", removed)
|
|
if err := ackTracker.Save(); err != nil {
|
|
log.Printf("Warning: Failed to save acknowledgments after cleanup: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// System info tracking
|
|
var lastSystemInfoUpdate time.Time
|
|
const systemInfoUpdateInterval = 1 * time.Hour // Update detailed system info every hour
|
|
|
|
// Main check-in loop
|
|
for {
|
|
// Add jitter to prevent thundering herd
|
|
jitter := time.Duration(rand.Intn(30)) * time.Second
|
|
time.Sleep(jitter)
|
|
|
|
// Check if we need to send detailed system info update
|
|
if time.Since(lastSystemInfoUpdate) >= systemInfoUpdateInterval {
|
|
log.Printf("Updating detailed system information...")
|
|
if err := reportSystemInfo(apiClient, cfg); err != nil {
|
|
log.Printf("Failed to report system info: %v\n", err)
|
|
} else {
|
|
lastSystemInfoUpdate = time.Now()
|
|
log.Printf("✓ System information updated\n")
|
|
}
|
|
}
|
|
|
|
log.Printf("Checking in with server... (Agent v%s)", version.Version)
|
|
|
|
// Collect lightweight system metrics
|
|
sysMetrics, err := system.GetLightweightMetrics()
|
|
var metrics *client.SystemMetrics
|
|
if err == nil {
|
|
metrics = &client.SystemMetrics{
|
|
CPUPercent: sysMetrics.CPUPercent,
|
|
MemoryPercent: sysMetrics.MemoryPercent,
|
|
MemoryUsedGB: sysMetrics.MemoryUsedGB,
|
|
MemoryTotalGB: sysMetrics.MemoryTotalGB,
|
|
DiskUsedGB: sysMetrics.DiskUsedGB,
|
|
DiskTotalGB: sysMetrics.DiskTotalGB,
|
|
DiskPercent: sysMetrics.DiskPercent,
|
|
Uptime: sysMetrics.Uptime,
|
|
Version: version.Version,
|
|
}
|
|
}
|
|
|
|
// Add heartbeat status to metrics metadata if available
|
|
if metrics != nil && cfg.RapidPollingEnabled {
|
|
// Check if rapid polling is still valid
|
|
if time.Now().Before(cfg.RapidPollingUntil) {
|
|
// Include heartbeat metadata in metrics
|
|
if metrics.Metadata == nil {
|
|
metrics.Metadata = make(map[string]interface{})
|
|
}
|
|
metrics.Metadata["rapid_polling_enabled"] = true
|
|
metrics.Metadata["rapid_polling_until"] = cfg.RapidPollingUntil.Format(time.RFC3339)
|
|
metrics.Metadata["rapid_polling_duration_minutes"] = int(time.Until(cfg.RapidPollingUntil).Minutes())
|
|
} else {
|
|
// Heartbeat expired, disable it
|
|
cfg.RapidPollingEnabled = false
|
|
cfg.RapidPollingUntil = time.Time{}
|
|
}
|
|
}
|
|
|
|
// Add pending acknowledgments to metrics for reliability
|
|
if metrics != nil {
|
|
pendingAcks := ackTracker.GetPending()
|
|
if len(pendingAcks) > 0 {
|
|
metrics.PendingAcknowledgments = pendingAcks
|
|
log.Printf("Including %d pending acknowledgments in check-in: %v", len(pendingAcks), pendingAcks)
|
|
} else {
|
|
log.Printf("No pending acknowledgments to send")
|
|
}
|
|
} else {
|
|
log.Printf("Metrics is nil - not sending system information or acknowledgments")
|
|
}
|
|
|
|
// Get commands from server (with optional metrics)
|
|
response, err := apiClient.GetCommands(cfg.AgentID, metrics)
|
|
if err != nil {
|
|
// Try to renew token if we got a 401 error
|
|
newClient, renewErr := renewTokenIfNeeded(apiClient, cfg, err)
|
|
if renewErr != nil {
|
|
log.Printf("Check-in unsuccessful and token renewal failed: %v\n", renewErr)
|
|
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
|
|
continue
|
|
}
|
|
|
|
// If token was renewed, update client and retry
|
|
if newClient != apiClient {
|
|
log.Printf("🔄 Retrying check-in with renewed token...")
|
|
apiClient = newClient
|
|
response, err = apiClient.GetCommands(cfg.AgentID, metrics)
|
|
if err != nil {
|
|
log.Printf("Check-in unsuccessful even after token renewal: %v\n", err)
|
|
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
|
|
continue
|
|
}
|
|
} else {
|
|
log.Printf("Check-in unsuccessful: %v\n", err)
|
|
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Process acknowledged command results
|
|
if response != nil && len(response.AcknowledgedIDs) > 0 {
|
|
ackTracker.Acknowledge(response.AcknowledgedIDs)
|
|
log.Printf("Server acknowledged %d command result(s)", len(response.AcknowledgedIDs))
|
|
|
|
// Save acknowledgment state
|
|
if err := ackTracker.Save(); err != nil {
|
|
log.Printf("Warning: Failed to save acknowledgment state: %v", err)
|
|
}
|
|
}
|
|
|
|
// Sync configuration from server (non-blocking) with retry logic
|
|
go func() {
|
|
if err := syncServerConfigWithRetry(apiClient, cfg, 5); err != nil {
|
|
log.Printf("Warning: Failed to sync server config after retries: %v", err)
|
|
}
|
|
}()
|
|
|
|
commands := response.Commands
|
|
if len(commands) == 0 {
|
|
log.Printf("Check-in successful - no new commands")
|
|
} else {
|
|
log.Printf("Check-in successful - received %d command(s)", len(commands))
|
|
}
|
|
|
|
// Process each command
|
|
for _, cmd := range commands {
|
|
log.Printf("Processing command: %s (%s)\n", cmd.Type, cmd.ID)
|
|
|
|
switch cmd.Type {
|
|
case "scan_storage":
|
|
if err := handleScanStorage(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID); err != nil {
|
|
log.Printf("Error scanning storage: %v\n", err)
|
|
}
|
|
|
|
case "scan_system":
|
|
if err := handleScanSystem(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID); err != nil {
|
|
log.Printf("Error scanning system: %v\n", err)
|
|
}
|
|
|
|
case "scan_docker":
|
|
if err := handleScanDocker(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID); err != nil {
|
|
log.Printf("Error scanning Docker: %v\n", err)
|
|
}
|
|
|
|
case "scan_apt":
|
|
if err := handleScanAPT(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID); err != nil {
|
|
log.Printf("Error scanning APT: %v\n", err)
|
|
}
|
|
|
|
case "scan_dnf":
|
|
if err := handleScanDNF(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID); err != nil {
|
|
log.Printf("Error scanning DNF: %v\n", err)
|
|
}
|
|
|
|
case "scan_windows":
|
|
if err := handleScanWindows(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID); err != nil {
|
|
log.Printf("Error scanning Windows Updates: %v\n", err)
|
|
}
|
|
|
|
case "scan_winget":
|
|
if err := handleScanWinget(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID); err != nil {
|
|
log.Printf("Error scanning Winget: %v\n", err)
|
|
}
|
|
|
|
case "collect_specs":
|
|
log.Println("Spec collection not yet implemented")
|
|
|
|
case "dry_run_update":
|
|
if err := handleDryRunUpdate(apiClient, cfg, ackTracker, cmd.ID, cmd.Params); err != nil {
|
|
log.Printf("Error dry running update: %v\n", err)
|
|
}
|
|
|
|
case "install_updates":
|
|
if err := handleInstallUpdates(apiClient, cfg, ackTracker, cmd.ID, cmd.Params); err != nil {
|
|
log.Printf("Error installing updates: %v\n", err)
|
|
}
|
|
|
|
case "confirm_dependencies":
|
|
if err := handleConfirmDependencies(apiClient, cfg, ackTracker, cmd.ID, cmd.Params); err != nil {
|
|
log.Printf("Error confirming dependencies: %v\n", err)
|
|
}
|
|
|
|
case "enable_heartbeat":
|
|
if err := handleEnableHeartbeat(apiClient, cfg, ackTracker, cmd.ID, cmd.Params); err != nil {
|
|
log.Printf("[Heartbeat] Error enabling heartbeat: %v\n", err)
|
|
}
|
|
|
|
case "disable_heartbeat":
|
|
if err := handleDisableHeartbeat(apiClient, cfg, ackTracker, cmd.ID); err != nil {
|
|
log.Printf("[Heartbeat] Error disabling heartbeat: %v\n", err)
|
|
}
|
|
|
|
case "reboot":
|
|
if err := handleReboot(apiClient, cfg, ackTracker, cmd.ID, cmd.Params); err != nil {
|
|
log.Printf("[Reboot] Error processing reboot command: %v\n", err)
|
|
}
|
|
|
|
case "update_agent":
|
|
if err := handleUpdateAgent(apiClient, cfg, ackTracker, cmd.Params, cmd.ID); err != nil {
|
|
log.Printf("[Update] Error processing agent update command: %v\n", err)
|
|
}
|
|
|
|
default:
|
|
log.Printf("Unknown command type: %s - reporting as invalid command\n", cmd.Type)
|
|
// Report invalid command back to server
|
|
logReport := client.LogReport{
|
|
CommandID: cmd.ID,
|
|
Action: "process_command",
|
|
Result: "failed",
|
|
Stdout: "",
|
|
Stderr: fmt.Sprintf("Invalid command type: %s", cmd.Type),
|
|
ExitCode: 1,
|
|
DurationSeconds: 0,
|
|
}
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("Failed to report invalid command result: %v", reportErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wait for next check-in
|
|
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
|
|
}
|
|
}
|
|
|
|
// subsystemScan executes a scanner function with circuit breaker and timeout protection
|
|
func subsystemScan(name string, cb *circuitbreaker.CircuitBreaker, timeout time.Duration, scanFn func() ([]client.UpdateReportItem, error)) ([]client.UpdateReportItem, error) {
|
|
var updates []client.UpdateReportItem
|
|
var scanErr error
|
|
|
|
err := cb.Call(func() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
type result struct {
|
|
updates []client.UpdateReportItem
|
|
err error
|
|
}
|
|
resultChan := make(chan result, 1)
|
|
|
|
go func() {
|
|
u, e := scanFn()
|
|
resultChan <- result{u, e}
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("%s scan timeout after %v", name, timeout)
|
|
case res := <-resultChan:
|
|
if res.err != nil {
|
|
return res.err
|
|
}
|
|
updates = res.updates
|
|
return nil
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
scanErr = err
|
|
}
|
|
|
|
return updates, scanErr
|
|
}
|
|
|
|
// handleScanCommand performs a local scan and displays results
|
|
func handleScanCommand(cfg *config.Config, exportFormat string) error {
|
|
// Initialize scanners
|
|
aptScanner := scanner.NewAPTScanner()
|
|
dnfScanner := scanner.NewDNFScanner()
|
|
dockerScanner, _ := scanner.NewDockerScanner()
|
|
windowsUpdateScanner := scanner.NewWindowsUpdateScanner()
|
|
wingetScanner := scanner.NewWingetScanner()
|
|
|
|
fmt.Println("🔍 Scanning for updates...")
|
|
var allUpdates []client.UpdateReportItem
|
|
|
|
// Scan APT updates
|
|
if aptScanner.IsAvailable() {
|
|
fmt.Println(" - Scanning APT packages...")
|
|
updates, err := aptScanner.Scan()
|
|
if err != nil {
|
|
fmt.Printf(" ⚠️ APT scan failed: %v\n", err)
|
|
} else {
|
|
fmt.Printf(" ✓ Found %d APT updates\n", len(updates))
|
|
allUpdates = append(allUpdates, updates...)
|
|
}
|
|
}
|
|
|
|
// Scan DNF updates
|
|
if dnfScanner.IsAvailable() {
|
|
fmt.Println(" - Scanning DNF packages...")
|
|
updates, err := dnfScanner.Scan()
|
|
if err != nil {
|
|
fmt.Printf(" ⚠️ DNF scan failed: %v\n", err)
|
|
} else {
|
|
fmt.Printf(" ✓ Found %d DNF updates\n", len(updates))
|
|
allUpdates = append(allUpdates, updates...)
|
|
}
|
|
}
|
|
|
|
// Scan Docker updates
|
|
if dockerScanner != nil && dockerScanner.IsAvailable() {
|
|
fmt.Println(" - Scanning Docker images...")
|
|
updates, err := dockerScanner.Scan()
|
|
if err != nil {
|
|
fmt.Printf(" ⚠️ Docker scan failed: %v\n", err)
|
|
} else {
|
|
fmt.Printf(" ✓ Found %d Docker image updates\n", len(updates))
|
|
allUpdates = append(allUpdates, updates...)
|
|
}
|
|
}
|
|
|
|
// Scan Windows updates
|
|
if windowsUpdateScanner.IsAvailable() {
|
|
fmt.Println(" - Scanning Windows updates...")
|
|
updates, err := windowsUpdateScanner.Scan()
|
|
if err != nil {
|
|
fmt.Printf(" ⚠️ Windows Update scan failed: %v\n", err)
|
|
} else {
|
|
fmt.Printf(" ✓ Found %d Windows updates\n", len(updates))
|
|
allUpdates = append(allUpdates, updates...)
|
|
}
|
|
}
|
|
|
|
// Scan Winget packages
|
|
if wingetScanner.IsAvailable() {
|
|
fmt.Println(" - Scanning Winget packages...")
|
|
updates, err := wingetScanner.Scan()
|
|
if err != nil {
|
|
fmt.Printf(" ⚠️ Winget scan failed: %v\n", err)
|
|
} else {
|
|
fmt.Printf(" ✓ Found %d Winget package updates\n", len(updates))
|
|
allUpdates = append(allUpdates, updates...)
|
|
}
|
|
}
|
|
|
|
// Load and update cache
|
|
localCache, err := cache.Load()
|
|
if err != nil {
|
|
fmt.Printf("⚠️ Warning: Failed to load cache: %v\n", err)
|
|
localCache = &cache.LocalCache{}
|
|
}
|
|
|
|
// Update cache with scan results
|
|
localCache.UpdateScanResults(allUpdates)
|
|
if cfg.IsRegistered() {
|
|
localCache.SetAgentInfo(cfg.AgentID, cfg.ServerURL)
|
|
localCache.SetAgentStatus("online")
|
|
}
|
|
|
|
// Save cache
|
|
if err := localCache.Save(); err != nil {
|
|
fmt.Printf("⚠️ Warning: Failed to save cache: %v\n", err)
|
|
}
|
|
|
|
// Display results
|
|
fmt.Println()
|
|
return display.PrintScanResults(allUpdates, exportFormat)
|
|
}
|
|
|
|
// handleStatusCommand displays agent status information
|
|
func handleStatusCommand(cfg *config.Config) error {
|
|
// Load cache
|
|
localCache, err := cache.Load()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load cache: %w", err)
|
|
}
|
|
|
|
// Determine status
|
|
agentStatus := "offline"
|
|
if cfg.IsRegistered() {
|
|
agentStatus = "online"
|
|
}
|
|
if localCache.AgentStatus != "" {
|
|
agentStatus = localCache.AgentStatus
|
|
}
|
|
|
|
// Use cached info if available, otherwise use config
|
|
agentID := cfg.AgentID.String()
|
|
if localCache.AgentID != (uuid.UUID{}) {
|
|
agentID = localCache.AgentID.String()
|
|
}
|
|
|
|
serverURL := cfg.ServerURL
|
|
if localCache.ServerURL != "" {
|
|
serverURL = localCache.ServerURL
|
|
}
|
|
|
|
// Display status
|
|
display.PrintAgentStatus(
|
|
agentID,
|
|
serverURL,
|
|
localCache.LastCheckIn,
|
|
localCache.LastScanTime,
|
|
localCache.UpdateCount,
|
|
agentStatus,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleListUpdatesCommand displays detailed update information
|
|
func handleListUpdatesCommand(cfg *config.Config, exportFormat string) error {
|
|
// Load cache
|
|
localCache, err := cache.Load()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load cache: %w", err)
|
|
}
|
|
|
|
// Check if we have cached scan results
|
|
if len(localCache.Updates) == 0 {
|
|
fmt.Println("📋 No cached scan results found.")
|
|
fmt.Println("💡 Run '--scan' first to discover available updates.")
|
|
return nil
|
|
}
|
|
|
|
// Warn if cache is old
|
|
if localCache.IsExpired(24 * time.Hour) {
|
|
fmt.Printf("⚠️ Scan results are %s old. Run '--scan' for latest results.\n\n",
|
|
formatTimeSince(localCache.LastScanTime))
|
|
}
|
|
|
|
// Display detailed results
|
|
return display.PrintDetailedUpdates(localCache.Updates, exportFormat)
|
|
}
|
|
|
|
// handleInstallUpdates handles install_updates command
|
|
func handleInstallUpdates(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, commandID string, params map[string]interface{}) error {
|
|
log.Println("Installing updates...")
|
|
|
|
// Parse parameters
|
|
packageType := ""
|
|
packageName := ""
|
|
|
|
if pt, ok := params["package_type"].(string); ok {
|
|
packageType = pt
|
|
}
|
|
if pn, ok := params["package_name"].(string); ok {
|
|
packageName = pn
|
|
}
|
|
|
|
// Validate package type
|
|
if packageType == "" {
|
|
return fmt.Errorf("package_type parameter is required")
|
|
}
|
|
|
|
// Create installer based on package type
|
|
inst, err := installer.InstallerFactory(packageType)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create installer for package type %s: %w", packageType, err)
|
|
}
|
|
|
|
// Check if installer is available
|
|
if !inst.IsAvailable() {
|
|
return fmt.Errorf("%s installer is not available on this system", packageType)
|
|
}
|
|
|
|
var result *installer.InstallResult
|
|
var action string
|
|
|
|
// Perform installation based on what's specified
|
|
if packageName != "" {
|
|
action = "update"
|
|
log.Printf("Updating package: %s (type: %s)", packageName, packageType)
|
|
result, err = inst.UpdatePackage(packageName)
|
|
} else if len(params) > 1 {
|
|
// Multiple packages might be specified in various ways
|
|
var packageNames []string
|
|
for key, value := range params {
|
|
if key != "package_type" {
|
|
if name, ok := value.(string); ok && name != "" {
|
|
packageNames = append(packageNames, name)
|
|
}
|
|
}
|
|
}
|
|
if len(packageNames) > 0 {
|
|
action = "install_multiple"
|
|
log.Printf("Installing multiple packages: %v (type: %s)", packageNames, packageType)
|
|
result, err = inst.InstallMultiple(packageNames)
|
|
} else {
|
|
// Upgrade all packages if no specific packages named
|
|
action = "upgrade"
|
|
log.Printf("Upgrading all packages (type: %s)", packageType)
|
|
result, err = inst.Upgrade()
|
|
}
|
|
} else {
|
|
// Upgrade all packages if no specific packages named
|
|
action = "upgrade"
|
|
log.Printf("Upgrading all packages (type: %s)", packageType)
|
|
result, err = inst.Upgrade()
|
|
}
|
|
|
|
if err != nil {
|
|
// Report installation failure with actual command output
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: action,
|
|
Result: "failed",
|
|
Stdout: result.Stdout,
|
|
Stderr: result.Stderr,
|
|
ExitCode: result.ExitCode,
|
|
DurationSeconds: result.DurationSeconds,
|
|
}
|
|
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("Failed to report installation failure: %v\n", reportErr)
|
|
}
|
|
|
|
return fmt.Errorf("installation failed: %w", err)
|
|
}
|
|
|
|
// Report installation success
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: result.Action,
|
|
Result: "success",
|
|
Stdout: result.Stdout,
|
|
Stderr: result.Stderr,
|
|
ExitCode: result.ExitCode,
|
|
DurationSeconds: result.DurationSeconds,
|
|
}
|
|
|
|
// Add additional metadata to the log report
|
|
if len(result.PackagesInstalled) > 0 {
|
|
logReport.Stdout += fmt.Sprintf("\nPackages installed: %v", result.PackagesInstalled)
|
|
}
|
|
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("Failed to report installation success: %v\n", reportErr)
|
|
}
|
|
|
|
if result.Success {
|
|
log.Printf("✓ Installation completed successfully in %d seconds\n", result.DurationSeconds)
|
|
if len(result.PackagesInstalled) > 0 {
|
|
log.Printf(" Packages installed: %v\n", result.PackagesInstalled)
|
|
}
|
|
} else {
|
|
log.Printf("✗ Installation failed after %d seconds\n", result.DurationSeconds)
|
|
log.Printf(" Error: %s\n", result.ErrorMessage)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleDryRunUpdate handles dry_run_update command
|
|
func handleDryRunUpdate(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, commandID string, params map[string]interface{}) error {
|
|
log.Println("Performing dry run update...")
|
|
|
|
// Parse parameters
|
|
packageType := ""
|
|
packageName := ""
|
|
|
|
if pt, ok := params["package_type"].(string); ok {
|
|
packageType = pt
|
|
}
|
|
if pn, ok := params["package_name"].(string); ok {
|
|
packageName = pn
|
|
}
|
|
|
|
// Validate parameters
|
|
if packageType == "" || packageName == "" {
|
|
return fmt.Errorf("package_type and package_name parameters are required")
|
|
}
|
|
|
|
// Create installer based on package type
|
|
inst, err := installer.InstallerFactory(packageType)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create installer for package type %s: %w", packageType, err)
|
|
}
|
|
|
|
// Check if installer is available
|
|
if !inst.IsAvailable() {
|
|
return fmt.Errorf("%s installer is not available on this system", packageType)
|
|
}
|
|
|
|
// Perform dry run
|
|
log.Printf("Dry running package: %s (type: %s)", packageName, packageType)
|
|
result, err := inst.DryRun(packageName)
|
|
if err != nil {
|
|
// Report dry run failure
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: "dry_run",
|
|
Result: "failed",
|
|
Stdout: "",
|
|
Stderr: fmt.Sprintf("Dry run error: %v", err),
|
|
ExitCode: 1,
|
|
DurationSeconds: 0,
|
|
}
|
|
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("Failed to report dry run failure: %v\n", reportErr)
|
|
}
|
|
|
|
return fmt.Errorf("dry run failed: %w", err)
|
|
}
|
|
|
|
// Convert installer.InstallResult to client.InstallResult for reporting
|
|
clientResult := &client.InstallResult{
|
|
Success: result.Success,
|
|
ErrorMessage: result.ErrorMessage,
|
|
Stdout: result.Stdout,
|
|
Stderr: result.Stderr,
|
|
ExitCode: result.ExitCode,
|
|
DurationSeconds: result.DurationSeconds,
|
|
Action: result.Action,
|
|
PackagesInstalled: result.PackagesInstalled,
|
|
ContainersUpdated: result.ContainersUpdated,
|
|
Dependencies: result.Dependencies,
|
|
IsDryRun: true,
|
|
}
|
|
|
|
// Report dependencies back to server
|
|
depReport := client.DependencyReport{
|
|
PackageName: packageName,
|
|
PackageType: packageType,
|
|
Dependencies: result.Dependencies,
|
|
UpdateID: params["update_id"].(string),
|
|
DryRunResult: clientResult,
|
|
}
|
|
|
|
if reportErr := apiClient.ReportDependencies(cfg.AgentID, depReport); reportErr != nil {
|
|
log.Printf("Failed to report dependencies: %v\n", reportErr)
|
|
return fmt.Errorf("failed to report dependencies: %w", reportErr)
|
|
}
|
|
|
|
// Report dry run success
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: "dry_run",
|
|
Result: "success",
|
|
Stdout: result.Stdout,
|
|
Stderr: result.Stderr,
|
|
ExitCode: result.ExitCode,
|
|
DurationSeconds: result.DurationSeconds,
|
|
}
|
|
|
|
if len(result.Dependencies) > 0 {
|
|
logReport.Stdout += fmt.Sprintf("\nDependencies found: %v", result.Dependencies)
|
|
}
|
|
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("Failed to report dry run success: %v\n", reportErr)
|
|
}
|
|
|
|
if result.Success {
|
|
log.Printf("✓ Dry run completed successfully in %d seconds\n", result.DurationSeconds)
|
|
if len(result.Dependencies) > 0 {
|
|
log.Printf(" Dependencies found: %v\n", result.Dependencies)
|
|
} else {
|
|
log.Printf(" No additional dependencies found\n")
|
|
}
|
|
} else {
|
|
log.Printf("✗ Dry run failed after %d seconds\n", result.DurationSeconds)
|
|
log.Printf(" Error: %s\n", result.ErrorMessage)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleConfirmDependencies handles confirm_dependencies command
|
|
func handleConfirmDependencies(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, commandID string, params map[string]interface{}) error {
|
|
log.Println("Installing update with confirmed dependencies...")
|
|
|
|
// Parse parameters
|
|
packageType := ""
|
|
packageName := ""
|
|
var dependencies []string
|
|
|
|
if pt, ok := params["package_type"].(string); ok {
|
|
packageType = pt
|
|
}
|
|
if pn, ok := params["package_name"].(string); ok {
|
|
packageName = pn
|
|
}
|
|
if deps, ok := params["dependencies"].([]interface{}); ok {
|
|
for _, dep := range deps {
|
|
if depStr, ok := dep.(string); ok {
|
|
dependencies = append(dependencies, depStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate parameters
|
|
if packageType == "" || packageName == "" {
|
|
return fmt.Errorf("package_type and package_name parameters are required")
|
|
}
|
|
|
|
// Create installer based on package type
|
|
inst, err := installer.InstallerFactory(packageType)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create installer for package type %s: %w", packageType, err)
|
|
}
|
|
|
|
// Check if installer is available
|
|
if !inst.IsAvailable() {
|
|
return fmt.Errorf("%s installer is not available on this system", packageType)
|
|
}
|
|
|
|
var result *installer.InstallResult
|
|
var action string
|
|
|
|
// Perform installation with dependencies
|
|
if len(dependencies) > 0 {
|
|
action = "install_with_dependencies"
|
|
log.Printf("Installing package with dependencies: %s (dependencies: %v)", packageName, dependencies)
|
|
// Install main package + dependencies
|
|
allPackages := append([]string{packageName}, dependencies...)
|
|
result, err = inst.InstallMultiple(allPackages)
|
|
} else {
|
|
action = "upgrade"
|
|
log.Printf("Installing package: %s (no dependencies)", packageName)
|
|
// Use UpdatePackage instead of Install to handle existing packages
|
|
result, err = inst.UpdatePackage(packageName)
|
|
}
|
|
|
|
if err != nil {
|
|
// Report installation failure with actual command output
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: action,
|
|
Result: "failed",
|
|
Stdout: result.Stdout,
|
|
Stderr: result.Stderr,
|
|
ExitCode: result.ExitCode,
|
|
DurationSeconds: result.DurationSeconds,
|
|
}
|
|
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("Failed to report installation failure: %v\n", reportErr)
|
|
}
|
|
|
|
return fmt.Errorf("installation failed: %w", err)
|
|
}
|
|
|
|
// Report installation success
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: result.Action,
|
|
Result: "success",
|
|
Stdout: result.Stdout,
|
|
Stderr: result.Stderr,
|
|
ExitCode: result.ExitCode,
|
|
DurationSeconds: result.DurationSeconds,
|
|
}
|
|
|
|
// Add additional metadata to the log report
|
|
if len(result.PackagesInstalled) > 0 {
|
|
logReport.Stdout += fmt.Sprintf("\nPackages installed: %v", result.PackagesInstalled)
|
|
}
|
|
if len(dependencies) > 0 {
|
|
logReport.Stdout += fmt.Sprintf("\nDependencies included: %v", dependencies)
|
|
}
|
|
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("Failed to report installation success: %v\n", reportErr)
|
|
}
|
|
|
|
if result.Success {
|
|
log.Printf("✓ Installation with dependencies completed successfully in %d seconds\n", result.DurationSeconds)
|
|
if len(result.PackagesInstalled) > 0 {
|
|
log.Printf(" Packages installed: %v\n", result.PackagesInstalled)
|
|
}
|
|
} else {
|
|
log.Printf("✗ Installation with dependencies failed after %d seconds\n", result.DurationSeconds)
|
|
log.Printf(" Error: %s\n", result.ErrorMessage)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleEnableHeartbeat handles enable_heartbeat command
|
|
func handleEnableHeartbeat(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, commandID string, params map[string]interface{}) error {
|
|
// Parse duration parameter (default to 10 minutes)
|
|
durationMinutes := 10
|
|
if duration, ok := params["duration_minutes"]; ok {
|
|
if durationFloat, ok := duration.(float64); ok {
|
|
durationMinutes = int(durationFloat)
|
|
}
|
|
}
|
|
|
|
// Calculate when heartbeat should expire
|
|
expiryTime := time.Now().Add(time.Duration(durationMinutes) * time.Minute)
|
|
|
|
log.Printf("[Heartbeat] Enabling rapid polling for %d minutes (expires: %s)", durationMinutes, expiryTime.Format(time.RFC3339))
|
|
|
|
// Update agent config to enable rapid polling
|
|
cfg.RapidPollingEnabled = true
|
|
cfg.RapidPollingUntil = expiryTime
|
|
|
|
// Save config to persist heartbeat settings
|
|
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
|
|
log.Printf("[Heartbeat] Warning: Failed to save config: %v", err)
|
|
}
|
|
|
|
// Create log report for heartbeat enable
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: "enable_heartbeat",
|
|
Result: "success",
|
|
Stdout: fmt.Sprintf("Heartbeat enabled for %d minutes", durationMinutes),
|
|
Stderr: "",
|
|
ExitCode: 0,
|
|
DurationSeconds: 0,
|
|
}
|
|
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("[Heartbeat] Failed to report heartbeat enable: %v", reportErr)
|
|
}
|
|
|
|
// Send immediate check-in to update heartbeat status in UI
|
|
log.Printf("[Heartbeat] Sending immediate check-in to update status")
|
|
sysMetrics, err := system.GetLightweightMetrics()
|
|
if err == nil {
|
|
metrics := &client.SystemMetrics{
|
|
CPUPercent: sysMetrics.CPUPercent,
|
|
MemoryPercent: sysMetrics.MemoryPercent,
|
|
MemoryUsedGB: sysMetrics.MemoryUsedGB,
|
|
MemoryTotalGB: sysMetrics.MemoryTotalGB,
|
|
DiskUsedGB: sysMetrics.DiskUsedGB,
|
|
DiskTotalGB: sysMetrics.DiskTotalGB,
|
|
DiskPercent: sysMetrics.DiskPercent,
|
|
Uptime: sysMetrics.Uptime,
|
|
Version: version.Version,
|
|
}
|
|
// Include heartbeat metadata to show enabled state
|
|
metrics.Metadata = map[string]interface{}{
|
|
"rapid_polling_enabled": true,
|
|
"rapid_polling_until": expiryTime.Format(time.RFC3339),
|
|
}
|
|
|
|
// Send immediate check-in with updated heartbeat status
|
|
_, checkinErr := apiClient.GetCommands(cfg.AgentID, metrics)
|
|
if checkinErr != nil {
|
|
log.Printf("[Heartbeat] Failed to send immediate check-in: %v", checkinErr)
|
|
} else {
|
|
log.Printf("[Heartbeat] Immediate check-in sent successfully")
|
|
}
|
|
} else {
|
|
log.Printf("[Heartbeat] Failed to get system metrics for immediate check-in: %v", err)
|
|
}
|
|
|
|
log.Printf("[Heartbeat] Rapid polling enabled successfully")
|
|
return nil
|
|
}
|
|
|
|
// handleDisableHeartbeat handles disable_heartbeat command
|
|
func handleDisableHeartbeat(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, commandID string) error {
|
|
log.Printf("[Heartbeat] Disabling rapid polling")
|
|
|
|
// Update agent config to disable rapid polling
|
|
cfg.RapidPollingEnabled = false
|
|
cfg.RapidPollingUntil = time.Time{} // Zero value
|
|
|
|
// Save config to persist heartbeat settings
|
|
if err := cfg.Save(constants.GetAgentConfigPath()); err != nil {
|
|
log.Printf("[Heartbeat] Warning: Failed to save config: %v", err)
|
|
}
|
|
|
|
// Create log report for heartbeat disable
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: "disable_heartbeat",
|
|
Result: "success",
|
|
Stdout: "Heartbeat disabled",
|
|
Stderr: "",
|
|
ExitCode: 0,
|
|
DurationSeconds: 0,
|
|
}
|
|
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("[Heartbeat] Failed to report heartbeat disable: %v", reportErr)
|
|
}
|
|
|
|
// Send immediate check-in to update heartbeat status in UI
|
|
log.Printf("[Heartbeat] Sending immediate check-in to update status")
|
|
sysMetrics, err := system.GetLightweightMetrics()
|
|
if err == nil {
|
|
metrics := &client.SystemMetrics{
|
|
CPUPercent: sysMetrics.CPUPercent,
|
|
MemoryPercent: sysMetrics.MemoryPercent,
|
|
MemoryUsedGB: sysMetrics.MemoryUsedGB,
|
|
MemoryTotalGB: sysMetrics.MemoryTotalGB,
|
|
DiskUsedGB: sysMetrics.DiskUsedGB,
|
|
DiskTotalGB: sysMetrics.DiskTotalGB,
|
|
DiskPercent: sysMetrics.DiskPercent,
|
|
Uptime: sysMetrics.Uptime,
|
|
Version: version.Version,
|
|
}
|
|
// Include empty heartbeat metadata to explicitly show disabled state
|
|
metrics.Metadata = map[string]interface{}{
|
|
"rapid_polling_enabled": false,
|
|
"rapid_polling_until": "",
|
|
}
|
|
|
|
// Send immediate check-in with updated heartbeat status
|
|
_, checkinErr := apiClient.GetCommands(cfg.AgentID, metrics)
|
|
if checkinErr != nil {
|
|
log.Printf("[Heartbeat] Failed to send immediate check-in: %v", checkinErr)
|
|
} else {
|
|
log.Printf("[Heartbeat] Immediate check-in sent successfully")
|
|
}
|
|
} else {
|
|
log.Printf("[Heartbeat] Failed to get system metrics for immediate check-in: %v", err)
|
|
}
|
|
|
|
log.Printf("[Heartbeat] Rapid polling disabled successfully")
|
|
return nil
|
|
}
|
|
|
|
// reportSystemInfo collects and reports detailed system information to the server
|
|
func reportSystemInfo(apiClient *client.Client, cfg *config.Config) error {
|
|
// Collect detailed system information
|
|
sysInfo, err := system.GetSystemInfo(version.Version)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get system info: %w", err)
|
|
}
|
|
|
|
// Create system info report
|
|
report := client.SystemInfoReport{
|
|
Timestamp: time.Now(),
|
|
CPUModel: sysInfo.CPUInfo.ModelName,
|
|
CPUCores: sysInfo.CPUInfo.Cores,
|
|
CPUThreads: sysInfo.CPUInfo.Threads,
|
|
MemoryTotal: sysInfo.MemoryInfo.Total,
|
|
DiskTotal: uint64(0),
|
|
DiskUsed: uint64(0),
|
|
IPAddress: sysInfo.IPAddress,
|
|
Processes: sysInfo.RunningProcesses,
|
|
Uptime: sysInfo.Uptime,
|
|
Metadata: make(map[string]interface{}),
|
|
}
|
|
|
|
// Add primary disk info
|
|
if len(sysInfo.DiskInfo) > 0 {
|
|
primaryDisk := sysInfo.DiskInfo[0]
|
|
report.DiskTotal = primaryDisk.Total
|
|
report.DiskUsed = primaryDisk.Used
|
|
report.Metadata["disk_mount"] = primaryDisk.Mountpoint
|
|
report.Metadata["disk_filesystem"] = primaryDisk.Filesystem
|
|
}
|
|
|
|
// Add collection timestamp and additional metadata
|
|
report.Metadata["collected_at"] = time.Now().Format(time.RFC3339)
|
|
report.Metadata["hostname"] = sysInfo.Hostname
|
|
report.Metadata["os_type"] = sysInfo.OSType
|
|
report.Metadata["os_version"] = sysInfo.OSVersion
|
|
report.Metadata["os_architecture"] = sysInfo.OSArchitecture
|
|
|
|
// Add any existing metadata from system info
|
|
for key, value := range sysInfo.Metadata {
|
|
report.Metadata[key] = value
|
|
}
|
|
|
|
// Report to server
|
|
if err := apiClient.ReportSystemInfo(cfg.AgentID, report); err != nil {
|
|
return fmt.Errorf("failed to report system info: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleReboot handles reboot command
|
|
func handleReboot(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, commandID string, params map[string]interface{}) error {
|
|
log.Println("[Reboot] Processing reboot request...")
|
|
|
|
// Parse parameters
|
|
delayMinutes := 1 // Default to 1 minute
|
|
message := "System reboot requested by RedFlag"
|
|
|
|
if delay, ok := params["delay_minutes"]; ok {
|
|
if delayFloat, ok := delay.(float64); ok {
|
|
delayMinutes = int(delayFloat)
|
|
}
|
|
}
|
|
if msg, ok := params["message"].(string); ok && msg != "" {
|
|
message = msg
|
|
}
|
|
|
|
log.Printf("[Reboot] Scheduling system reboot in %d minute(s): %s", delayMinutes, message)
|
|
|
|
var cmd *exec.Cmd
|
|
|
|
// Execute platform-specific reboot command
|
|
if runtime.GOOS == "linux" {
|
|
// Linux: shutdown -r +MINUTES "message"
|
|
cmd = exec.Command("shutdown", "-r", fmt.Sprintf("+%d", delayMinutes), message)
|
|
} else if runtime.GOOS == "windows" {
|
|
// Windows: shutdown /r /t SECONDS /c "message"
|
|
delaySeconds := delayMinutes * 60
|
|
cmd = exec.Command("shutdown", "/r", "/t", fmt.Sprintf("%d", delaySeconds), "/c", message)
|
|
} else {
|
|
err := fmt.Errorf("reboot not supported on platform: %s", runtime.GOOS)
|
|
log.Printf("[Reboot] Error: %v", err)
|
|
|
|
// Report failure
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: "reboot",
|
|
Result: "failed",
|
|
Stdout: "",
|
|
Stderr: err.Error(),
|
|
ExitCode: 1,
|
|
DurationSeconds: 0,
|
|
}
|
|
reportLogWithAck(apiClient, cfg, ackTracker, logReport)
|
|
return err
|
|
}
|
|
|
|
// Execute reboot command
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("[Reboot] Failed to schedule reboot: %v", err)
|
|
log.Printf("[Reboot] Output: %s", string(output))
|
|
|
|
// Report failure
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: "reboot",
|
|
Result: "failed",
|
|
Stdout: string(output),
|
|
Stderr: err.Error(),
|
|
ExitCode: 1,
|
|
DurationSeconds: 0,
|
|
}
|
|
reportLogWithAck(apiClient, cfg, ackTracker, logReport)
|
|
return err
|
|
}
|
|
|
|
log.Printf("[Reboot] System reboot scheduled successfully")
|
|
log.Printf("[Reboot] The system will reboot in %d minute(s)", delayMinutes)
|
|
|
|
// Report success
|
|
logReport := client.LogReport{
|
|
CommandID: commandID,
|
|
Action: "reboot",
|
|
Result: "success",
|
|
Stdout: fmt.Sprintf("System reboot scheduled for %d minute(s) from now. Message: %s", delayMinutes, message),
|
|
Stderr: "",
|
|
ExitCode: 0,
|
|
DurationSeconds: 0,
|
|
}
|
|
|
|
if reportErr := reportLogWithAck(apiClient, cfg, ackTracker, logReport); reportErr != nil {
|
|
log.Printf("[Reboot] Failed to report reboot command result: %v", reportErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// formatTimeSince formats a duration as "X time ago"
|
|
func formatTimeSince(t time.Time) string {
|
|
duration := time.Since(t)
|
|
if duration < time.Minute {
|
|
return fmt.Sprintf("%d seconds ago", int(duration.Seconds()))
|
|
} else if duration < time.Hour {
|
|
return fmt.Sprintf("%d minutes ago", int(duration.Minutes()))
|
|
} else if duration < 24*time.Hour {
|
|
return fmt.Sprintf("%d hours ago", int(duration.Hours()))
|
|
} else {
|
|
return fmt.Sprintf("%d days ago", int(duration.Hours()/24))
|
|
}
|
|
}
|