fix: Complete AgentHealth improvements and build fixes

- Update Update scanner default from 15min to 12 hours (backend)
- Add 1 week and 2 week frequency options (frontend)
- Rename AgentScanners to AgentHealth component
- Add OS-aware package manager badges (APT, DNF, Windows/Winget, Docker)
- Fix all build errors (types, imports, storage metrics)
- Add useMemo optimization for enabled/auto-run counts
This commit is contained in:
Fimeg
2025-12-17 21:08:38 -05:00
parent 9effa967a1
commit a90692f1d8
11 changed files with 207 additions and 827 deletions

View File

@@ -20,14 +20,14 @@ import (
// Client handles API communication with the server
type Client struct {
baseURL string
token string
http *http.Client
RapidPollingEnabled bool
RapidPollingUntil time.Time
machineID string // Cached machine ID for security binding
eventBuffer *event.Buffer
agentID uuid.UUID
baseURL string
token string
http *http.Client
RapidPollingEnabled bool
RapidPollingUntil time.Time
machineID string // Cached machine ID for security binding
eventBuffer *event.Buffer
agentID uuid.UUID
}
// NewClient creates a new API client
@@ -54,13 +54,13 @@ func NewClient(baseURL, token string) *Client {
func NewClientWithEventBuffer(baseURL, token string, statePath string, agentID uuid.UUID) *Client {
client := NewClient(baseURL, token)
client.agentID = agentID
// Initialize event buffer if state path is provided
if statePath != "" {
eventBufferPath := filepath.Join(statePath, "events_buffer.json")
client.eventBuffer = event.NewBuffer(eventBufferPath)
}
return client
}
@@ -121,22 +121,22 @@ func (c *Client) SetToken(token string) {
// RegisterRequest is the payload for agent registration
type RegisterRequest struct {
Hostname string `json:"hostname"`
OSType string `json:"os_type"`
OSVersion string `json:"os_version"`
OSArchitecture string `json:"os_architecture"`
AgentVersion string `json:"agent_version"`
RegistrationToken string `json:"registration_token,omitempty"` // Fallback method
MachineID string `json:"machine_id"`
PublicKeyFingerprint string `json:"public_key_fingerprint"`
Metadata map[string]string `json:"metadata"`
Hostname string `json:"hostname"`
OSType string `json:"os_type"`
OSVersion string `json:"os_version"`
OSArchitecture string `json:"os_architecture"`
AgentVersion string `json:"agent_version"`
RegistrationToken string `json:"registration_token,omitempty"` // Fallback method
MachineID string `json:"machine_id"`
PublicKeyFingerprint string `json:"public_key_fingerprint"`
Metadata map[string]string `json:"metadata"`
}
// RegisterResponse is returned after successful registration
type RegisterResponse struct {
AgentID uuid.UUID `json:"agent_id"`
Token string `json:"token"` // Short-lived access token (24h)
RefreshToken string `json:"refresh_token"` // Long-lived refresh token (90d)
Token string `json:"token"` // Short-lived access token (24h)
RefreshToken string `json:"refresh_token"` // Long-lived refresh token (90d)
Config map[string]interface{} `json:"config"`
}
@@ -156,7 +156,7 @@ func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
c.bufferEvent("registration_failure", "marshal_error", "error", "client",
fmt.Sprintf("Failed to marshal registration request: %v", err),
map[string]interface{}{
"error": err.Error(),
"error": err.Error(),
"hostname": req.Hostname,
})
return nil, err
@@ -168,7 +168,7 @@ func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
c.bufferEvent("registration_failure", "request_creation_error", "error", "client",
fmt.Sprintf("Failed to create registration request: %v", err),
map[string]interface{}{
"error": err.Error(),
"error": err.Error(),
"hostname": req.Hostname,
})
return nil, err
@@ -187,8 +187,8 @@ func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
c.bufferEvent("registration_failure", "network_error", "error", "client",
fmt.Sprintf("Registration request failed: %v", err),
map[string]interface{}{
"error": err.Error(),
"hostname": req.Hostname,
"error": err.Error(),
"hostname": req.Hostname,
"server_url": c.baseURL,
})
return nil, err
@@ -198,15 +198,15 @@ func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
errorMsg := fmt.Sprintf("registration failed: %s - %s", resp.Status, string(bodyBytes))
// Buffer registration failure event
c.bufferEvent("registration_failure", "api_error", "error", "client",
errorMsg,
map[string]interface{}{
"status_code": resp.StatusCode,
"status_code": resp.StatusCode,
"response_body": string(bodyBytes),
"hostname": req.Hostname,
"server_url": c.baseURL,
"hostname": req.Hostname,
"server_url": c.baseURL,
})
return nil, fmt.Errorf(errorMsg)
}
@@ -217,7 +217,7 @@ func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
c.bufferEvent("registration_failure", "decode_error", "error", "client",
fmt.Sprintf("Failed to decode registration response: %v", err),
map[string]interface{}{
"error": err.Error(),
"error": err.Error(),
"hostname": req.Hostname,
})
return nil, err
@@ -239,7 +239,7 @@ type TokenRenewalRequest struct {
// TokenRenewalResponse is returned after successful token renewal
type TokenRenewalResponse struct {
Token string `json:"token"` // New short-lived access token (24h)
Token string `json:"token"` // New short-lived access token (24h)
}
// RenewToken uses refresh token to get a new access token (proper implementation)
@@ -258,7 +258,7 @@ func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string, agentVersion
c.bufferEvent("token_renewal_failure", "marshal_error", "error", "client",
fmt.Sprintf("Failed to marshal token renewal request: %v", err),
map[string]interface{}{
"error": err.Error(),
"error": err.Error(),
"agent_id": agentID.String(),
})
return err
@@ -270,7 +270,7 @@ func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string, agentVersion
c.bufferEvent("token_renewal_failure", "request_creation_error", "error", "client",
fmt.Sprintf("Failed to create token renewal request: %v", err),
map[string]interface{}{
"error": err.Error(),
"error": err.Error(),
"agent_id": agentID.String(),
})
return err
@@ -283,8 +283,8 @@ func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string, agentVersion
c.bufferEvent("token_renewal_failure", "network_error", "error", "client",
fmt.Sprintf("Token renewal request failed: %v", err),
map[string]interface{}{
"error": err.Error(),
"agent_id": agentID.String(),
"error": err.Error(),
"agent_id": agentID.String(),
"server_url": c.baseURL,
})
return err
@@ -294,15 +294,15 @@ func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string, agentVersion
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
errorMsg := fmt.Sprintf("token renewal failed: %s - %s", resp.Status, string(bodyBytes))
// Buffer token renewal failure event
c.bufferEvent("token_renewal_failure", "api_error", "error", "client",
errorMsg,
map[string]interface{}{
"status_code": resp.StatusCode,
"status_code": resp.StatusCode,
"response_body": string(bodyBytes),
"agent_id": agentID.String(),
"server_url": c.baseURL,
"agent_id": agentID.String(),
"server_url": c.baseURL,
})
return fmt.Errorf(errorMsg)
}
@@ -313,7 +313,7 @@ func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string, agentVersion
c.bufferEvent("token_renewal_failure", "decode_error", "error", "client",
fmt.Sprintf("Failed to decode token renewal response: %v", err),
map[string]interface{}{
"error": err.Error(),
"error": err.Error(),
"agent_id": agentID.String(),
})
return err
@@ -327,10 +327,10 @@ func (c *Client) RenewToken(agentID uuid.UUID, refreshToken string, agentVersion
// Command represents a command from the server
type Command struct {
ID string `json:"id"`
Type string `json:"type"`
Params map[string]interface{} `json:"params"`
Signature string `json:"signature,omitempty"` // Ed25519 signature of the command
ID string `json:"id"`
Type string `json:"type"`
Params map[string]interface{} `json:"params"`
Signature string `json:"signature,omitempty"` // Ed25519 signature of the command
}
// CommandItem is an alias for Command for consistency with server models
@@ -338,9 +338,9 @@ type CommandItem = Command
// CommandsResponse contains pending commands
type CommandsResponse struct {
Commands []Command `json:"commands"`
RapidPolling *RapidPollingConfig `json:"rapid_polling,omitempty"`
AcknowledgedIDs []string `json:"acknowledged_ids,omitempty"` // IDs server has received
Commands []Command `json:"commands"`
RapidPolling *RapidPollingConfig `json:"rapid_polling,omitempty"`
AcknowledgedIDs []string `json:"acknowledged_ids,omitempty"` // IDs server has received
}
// RapidPollingConfig contains rapid polling configuration from server
@@ -351,16 +351,16 @@ type RapidPollingConfig struct {
// SystemMetrics represents lightweight system metrics sent with check-ins
type SystemMetrics struct {
CPUPercent float64 `json:"cpu_percent,omitempty"`
MemoryPercent float64 `json:"memory_percent,omitempty"`
MemoryUsedGB float64 `json:"memory_used_gb,omitempty"`
MemoryTotalGB float64 `json:"memory_total_gb,omitempty"`
DiskUsedGB float64 `json:"disk_used_gb,omitempty"`
DiskTotalGB float64 `json:"disk_total_gb,omitempty"`
DiskPercent float64 `json:"disk_percent,omitempty"`
Uptime string `json:"uptime,omitempty"`
Version string `json:"version,omitempty"` // Agent version
Metadata map[string]interface{} `json:"metadata,omitempty"` // Additional metadata
CPUPercent float64 `json:"cpu_percent,omitempty"`
MemoryPercent float64 `json:"memory_percent,omitempty"`
MemoryUsedGB float64 `json:"memory_used_gb,omitempty"`
MemoryTotalGB float64 `json:"memory_total_gb,omitempty"`
DiskUsedGB float64 `json:"disk_used_gb,omitempty"`
DiskTotalGB float64 `json:"disk_total_gb,omitempty"`
DiskPercent float64 `json:"disk_percent,omitempty"`
Uptime string `json:"uptime,omitempty"`
Version string `json:"version,omitempty"` // Agent version
Metadata map[string]interface{} `json:"metadata,omitempty"` // Additional metadata
// Command acknowledgment tracking
PendingAcknowledgments []string `json:"pending_acknowledgments,omitempty"` // Command IDs awaiting ACK
@@ -427,9 +427,9 @@ func (c *Client) GetCommands(agentID uuid.UUID, metrics *SystemMetrics) (*Comman
// UpdateReport represents discovered updates
type UpdateReport struct {
CommandID string `json:"command_id"`
Timestamp time.Time `json:"timestamp"`
Updates []UpdateReportItem `json:"updates"`
CommandID string `json:"command_id"`
Timestamp time.Time `json:"timestamp"`
Updates []UpdateReportItem `json:"updates"`
}
// UpdateReportItem represents a single update
@@ -480,20 +480,20 @@ func (c *Client) ReportUpdates(agentID uuid.UUID, report UpdateReport) error {
// MetricsReport represents metrics data (storage, system, CPU, memory)
type MetricsReport struct {
CommandID string `json:"command_id"`
Timestamp time.Time `json:"timestamp"`
CommandID string `json:"command_id"`
Timestamp time.Time `json:"timestamp"`
Metrics []MetricsReportItem `json:"metrics"`
}
// MetricsReportItem represents a single metric
type MetricsReportItem struct {
PackageType string `json:"package_type"`
PackageName string `json:"package_name"`
CurrentVersion string `json:"current_version"`
AvailableVersion string `json:"available_version"`
Severity string `json:"severity"`
RepositorySource string `json:"repository_source"`
Metadata map[string]interface{} `json:"metadata"`
PackageType string `json:"package_type"`
PackageName string `json:"package_name"`
CurrentVersion string `json:"current_version"`
AvailableVersion string `json:"available_version"`
Severity string `json:"severity"`
RepositorySource string `json:"repository_source"`
Metadata map[string]interface{} `json:"metadata"`
}
// ReportMetrics sends metrics data to the server
@@ -529,20 +529,20 @@ func (c *Client) ReportMetrics(agentID uuid.UUID, report MetricsReport) error {
// DockerReport represents Docker image information
type DockerReport struct {
CommandID string `json:"command_id"`
Timestamp time.Time `json:"timestamp"`
CommandID string `json:"command_id"`
Timestamp time.Time `json:"timestamp"`
Images []DockerReportItem `json:"images"`
}
// DockerReportItem represents a single Docker image
type DockerReportItem struct {
PackageType string `json:"package_type"`
PackageName string `json:"package_name"`
CurrentVersion string `json:"current_version"`
AvailableVersion string `json:"available_version"`
Severity string `json:"severity"`
RepositorySource string `json:"repository_source"`
Metadata map[string]interface{} `json:"metadata"`
PackageType string `json:"package_type"`
PackageName string `json:"package_name"`
CurrentVersion string `json:"current_version"`
AvailableVersion string `json:"available_version"`
Severity string `json:"severity"`
RepositorySource string `json:"repository_source"`
Metadata map[string]interface{} `json:"metadata"`
}
// ReportDockerImages sends Docker image information to the server
@@ -577,7 +577,7 @@ func (c *Client) ReportDockerImages(agentID uuid.UUID, report DockerReport) erro
}
// ReportStorageMetrics sends storage metrics to the server via dedicated endpoint
func (c *Client) ReportStorageMetrics(agentID uuid.UUID, report StorageMetricReport) error {
func (c *Client) ReportStorageMetrics(agentID uuid.UUID, report models.StorageMetricReport) error {
url := fmt.Sprintf("%s/api/v1/agents/%s/storage-metrics", c.baseURL, agentID)
body, err := json.Marshal(report)
@@ -652,26 +652,26 @@ func (c *Client) ReportLog(agentID uuid.UUID, report LogReport) error {
// DependencyReport represents a dependency report after dry run
type DependencyReport struct {
PackageName string `json:"package_name"`
PackageType string `json:"package_type"`
Dependencies []string `json:"dependencies"`
UpdateID string `json:"update_id"`
DryRunResult *InstallResult `json:"dry_run_result,omitempty"`
PackageName string `json:"package_name"`
PackageType string `json:"package_type"`
Dependencies []string `json:"dependencies"`
UpdateID string `json:"update_id"`
DryRunResult *InstallResult `json:"dry_run_result,omitempty"`
}
// InstallResult represents the result of a package installation attempt
type InstallResult struct {
Success bool `json:"success"`
ErrorMessage string `json:"error_message,omitempty"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
ExitCode int `json:"exit_code"`
DurationSeconds int `json:"duration_seconds"`
Action string `json:"action,omitempty"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
ExitCode int `json:"exit_code"`
DurationSeconds int `json:"duration_seconds"`
Action string `json:"action,omitempty"`
PackagesInstalled []string `json:"packages_installed,omitempty"`
ContainersUpdated []string `json:"containers_updated,omitempty"`
Dependencies []string `json:"dependencies,omitempty"`
IsDryRun bool `json:"is_dry_run"`
IsDryRun bool `json:"is_dry_run"`
}
// ReportDependencies sends dependency report to the server
@@ -707,7 +707,7 @@ func (c *Client) ReportDependencies(agentID uuid.UUID, report DependencyReport)
// SystemInfoReport represents system information updates
type SystemInfoReport struct {
Timestamp time.Time `json:"timestamp"`
Timestamp time.Time `json:"timestamp"`
CPUModel string `json:"cpu_model,omitempty"`
CPUCores int `json:"cpu_cores,omitempty"`
CPUThreads int `json:"cpu_threads,omitempty"`

View File

@@ -7,7 +7,7 @@ type SubsystemConfig struct {
// Execution settings
Enabled bool `json:"enabled"`
Timeout time.Duration `json:"timeout"` // Timeout for this subsystem
// Interval for this subsystem (in minutes)
// This controls how often the server schedules scans for this subsystem
IntervalMinutes int `json:"interval_minutes,omitempty"`
@@ -51,16 +51,16 @@ func GetDefaultSubsystemsConfig() SubsystemsConfig {
// Default circuit breaker config
defaultCB := CircuitBreakerConfig{
Enabled: true,
FailureThreshold: 3, // 3 consecutive failures
FailureThreshold: 3, // 3 consecutive failures
FailureWindow: 10 * time.Minute, // within 10 minutes
OpenDuration: 30 * time.Minute, // circuit open for 30 min
HalfOpenAttempts: 2, // 2 successful attempts to close circuit
HalfOpenAttempts: 2, // 2 successful attempts to close circuit
}
// Aggressive circuit breaker for Windows Update (known to be slow/problematic)
windowsCB := CircuitBreakerConfig{
Enabled: true,
FailureThreshold: 2, // Only 2 failures
FailureThreshold: 2, // Only 2 failures
FailureWindow: 15 * time.Minute,
OpenDuration: 60 * time.Minute, // Open for 1 hour
HalfOpenAttempts: 3,
@@ -68,15 +68,15 @@ func GetDefaultSubsystemsConfig() SubsystemsConfig {
return SubsystemsConfig{
System: SubsystemConfig{
Enabled: true, // System scanner always available
Enabled: true, // System scanner always available
Timeout: 10 * time.Second, // System info should be fast
IntervalMinutes: 5, // Default: 5 minutes
IntervalMinutes: 5, // Default: 5 minutes
CircuitBreaker: defaultCB,
},
Updates: SubsystemConfig{
Enabled: true, // Virtual subsystem for package update scheduling
Timeout: 0, // Not used - delegates to individual package scanners
IntervalMinutes: 15, // Default: 15 minutes
Enabled: true, // Virtual subsystem for package update scheduling
Timeout: 0, // Not used - delegates to individual package scanners
IntervalMinutes: 720, // Default: 12 hours (more reasonable for update checks)
CircuitBreaker: CircuitBreakerConfig{Enabled: false}, // No circuit breaker for virtual subsystem
},
APT: SubsystemConfig{
@@ -88,31 +88,31 @@ func GetDefaultSubsystemsConfig() SubsystemsConfig {
DNF: SubsystemConfig{
Enabled: true,
Timeout: 15 * time.Minute, // TODO: Make scanner timeouts user-adjustable via settings. DNF operations can take a long time on large systems
IntervalMinutes: 15, // Default: 15 minutes
IntervalMinutes: 15, // Default: 15 minutes
CircuitBreaker: defaultCB,
},
Docker: SubsystemConfig{
Enabled: true,
Timeout: 60 * time.Second, // Registry queries can be slow
IntervalMinutes: 15, // Default: 15 minutes
IntervalMinutes: 15, // Default: 15 minutes
CircuitBreaker: defaultCB,
},
Windows: SubsystemConfig{
Enabled: true,
Timeout: 10 * time.Minute, // Windows Update can be VERY slow
IntervalMinutes: 15, // Default: 15 minutes
IntervalMinutes: 15, // Default: 15 minutes
CircuitBreaker: windowsCB,
},
Winget: SubsystemConfig{
Enabled: true,
Timeout: 2 * time.Minute, // Winget has multiple retry strategies
IntervalMinutes: 15, // Default: 15 minutes
IntervalMinutes: 15, // Default: 15 minutes
CircuitBreaker: defaultCB,
},
Storage: SubsystemConfig{
Enabled: true,
Timeout: 10 * time.Second, // Disk info should be fast
IntervalMinutes: 5, // Default: 5 minutes
IntervalMinutes: 5, // Default: 5 minutes
CircuitBreaker: defaultCB,
},
}

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"time"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
)