diff --git a/aggregator-agent/cmd/agent/subsystem_handlers.go b/aggregator-agent/cmd/agent/subsystem_handlers.go index cc724b5..64ea332 100644 --- a/aggregator-agent/cmd/agent/subsystem_handlers.go +++ b/aggregator-agent/cmd/agent/subsystem_handlers.go @@ -19,6 +19,7 @@ import ( "github.com/Fimeg/RedFlag/aggregator-agent/internal/acknowledgment" "github.com/Fimeg/RedFlag/aggregator-agent/internal/client" "github.com/Fimeg/RedFlag/aggregator-agent/internal/config" + "github.com/Fimeg/RedFlag/aggregator-agent/internal/models" "github.com/Fimeg/RedFlag/aggregator-agent/internal/orchestrator" ) @@ -223,13 +224,13 @@ func handleScanSystem(apiClient *client.Client, cfg *config.Config, ackTracker * metricItems := make([]client.MetricsReportItem, 0, len(metrics)) for _, metric := range metrics { item := client.MetricsReportItem{ - PackageType: "system", - PackageName: metric.MetricName, - CurrentVersion: metric.CurrentValue, - AvailableVersion: metric.AvailableValue, - Severity: metric.Severity, - RepositorySource: metric.MetricType, - Metadata: metric.Metadata, + PackageType: "system", + PackageName: metric.MetricName, + CurrentVersion: metric.CurrentValue, + AvailableVersion: metric.AvailableValue, + Severity: metric.Severity, + RepositorySource: metric.MetricType, + Metadata: metric.Metadata, } metricItems = append(metricItems, item) } @@ -311,13 +312,13 @@ func handleScanDocker(apiClient *client.Client, cfg *config.Config, ackTracker * imageItems := make([]client.DockerReportItem, 0, len(images)) for _, image := range images { item := client.DockerReportItem{ - PackageType: "docker_image", - PackageName: image.ImageName, - CurrentVersion: image.ImageID, - AvailableVersion: image.LatestImageID, - Severity: image.Severity, - RepositorySource: image.RepositorySource, - Metadata: image.Metadata, + PackageType: "docker_image", + PackageName: image.ImageName, + CurrentVersion: image.ImageID, + AvailableVersion: image.LatestImageID, + Severity: image.Severity, + RepositorySource: image.RepositorySource, + Metadata: image.Metadata, } imageItems = append(imageItems, item) } diff --git a/aggregator-agent/internal/client/client.go b/aggregator-agent/internal/client/client.go index 80f1f69..acb7bc5 100644 --- a/aggregator-agent/internal/client/client.go +++ b/aggregator-agent/internal/client/client.go @@ -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"` diff --git a/aggregator-agent/internal/config/subsystems.go b/aggregator-agent/internal/config/subsystems.go index 8e44b00..5e9e82c 100644 --- a/aggregator-agent/internal/config/subsystems.go +++ b/aggregator-agent/internal/config/subsystems.go @@ -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, }, } diff --git a/aggregator-agent/internal/orchestrator/storage_scanner.go b/aggregator-agent/internal/orchestrator/storage_scanner.go index f3ce773..ac2ca49 100644 --- a/aggregator-agent/internal/orchestrator/storage_scanner.go +++ b/aggregator-agent/internal/orchestrator/storage_scanner.go @@ -4,7 +4,6 @@ import ( "fmt" "time" - "github.com/Fimeg/RedFlag/aggregator-agent/internal/client" "github.com/Fimeg/RedFlag/aggregator-agent/internal/system" ) diff --git a/aggregator-server/internal/api/handlers/storage_metrics.go b/aggregator-server/internal/api/handlers/storage_metrics.go index 6d99d6b..eeb5f23 100644 --- a/aggregator-server/internal/api/handlers/storage_metrics.go +++ b/aggregator-server/internal/api/handlers/storage_metrics.go @@ -1,16 +1,14 @@ package handlers import ( - "encoding/json" - "fmt" + "log" "net/http" "time" "github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries" "github.com/Fimeg/RedFlag/aggregator-server/internal/models" + "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/gorilla/mux" - "github.com/lib/pq" ) // StorageMetricsHandler handles storage metrics endpoints @@ -25,28 +23,21 @@ func NewStorageMetricsHandler(queries *queries.StorageMetricsQueries) *StorageMe } } -// ReportStorageMetrics handles POST /api/v1/agents/{id}/storage-metrics -func (h *StorageMetricsHandler) ReportStorageMetrics(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - agentIDStr := vars["id"] - - // Parse agent ID - agentID, err := uuid.Parse(agentIDStr) - if err != nil { - http.Error(w, "Invalid agent ID", http.StatusBadRequest) - return - } +// ReportStorageMetrics handles POST /api/v1/agents/:id/storage-metrics +func (h *StorageMetricsHandler) ReportStorageMetrics(c *gin.Context) { + // Get agent ID from context (set by middleware) + agentID := c.MustGet("agent_id").(uuid.UUID) // Parse request body var req models.StorageMetricRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } // Validate agent ID matches if req.AgentID != agentID { - http.Error(w, "Agent ID mismatch", http.StatusBadRequest) + c.JSON(http.StatusBadRequest, gin.H{"error": "Agent ID mismatch"}) return } @@ -68,58 +59,34 @@ func (h *StorageMetricsHandler) ReportStorageMetrics(w http.ResponseWriter, r *h CreatedAt: time.Now(), } - if err := h.queries.InsertStorageMetric(r.Context(), dbMetric); err != nil { + if err := h.queries.InsertStorageMetric(c.Request.Context(), dbMetric); err != nil { log.Printf("[ERROR] Failed to insert storage metric for agent %s: %v\n", agentID, err) - http.Error(w, "Failed to insert storage metric", http.StatusInternalServerError) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert storage metric"}) return } } - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{ + c.JSON(http.StatusOK, gin.H{ "status": "success", "message": "Storage metrics reported successfully", }) } -// GetStorageMetrics handles GET /api/v1/agents/{id}/storage-metrics -func (h *StorageMetricsHandler) GetStorageMetrics(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - agentIDStr := vars["id"] - - // Parse agent ID - agentID, err := uuid.Parse(agentIDStr) - if err != nil { - http.Error(w, "Invalid agent ID", http.StatusBadRequest) - return - } - - // Optional query parameters for pagination/limit - limit := parseIntQueryParam(r, "limit", 100) - offset := parseIntQueryParam(r, "offset", 0) +// GetStorageMetrics handles GET /api/v1/agents/:id/storage-metrics +func (h *StorageMetricsHandler) GetStorageMetrics(c *gin.Context) { + // Get agent ID from context (set by middleware) + agentID := c.MustGet("agent_id").(uuid.UUID) // Get storage metrics - metrics, err := h.queries.GetStorageMetricsByAgentID(r.Context(), agentID, limit, offset) + metrics, err := h.queries.GetStorageMetricsByAgentID(c.Request.Context(), agentID, 100, 0) if err != nil { log.Printf("[ERROR] Failed to retrieve storage metrics for agent %s: %v\n", agentID, err) - http.Error(w, "Failed to retrieve storage metrics", http.StatusInternalServerError) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve storage metrics"}) return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ + c.JSON(http.StatusOK, gin.H{ "metrics": metrics, "total": len(metrics), }) } - -// parseIntQueryParam safely parses integer query parameters with defaults -func parseIntQueryParam(r *http.Request, key string, defaultValue int) int { - if val := r.URL.Query().Get(key); val != "" { - var result int - if _, err := fmt.Sscanf(val, "%d", &result); err == nil && result > 0 { - return result - } - } - return defaultValue -} \ No newline at end of file diff --git a/aggregator-server/internal/database/queries/storage_metrics.go b/aggregator-server/internal/database/queries/storage_metrics.go index 88ba20e..c9e7fcd 100644 --- a/aggregator-server/internal/database/queries/storage_metrics.go +++ b/aggregator-server/internal/database/queries/storage_metrics.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "fmt" - "time" "github.com/Fimeg/RedFlag/aggregator-server/internal/models" "github.com/google/uuid" @@ -136,7 +135,7 @@ func (q *StorageMetricsQueries) GetLatestStorageMetrics(ctx context.Context, age // GetStorageMetricsSummary returns summary statistics for an agent func (q *StorageMetricsQueries) GetStorageMetricsSummary(ctx context.Context, agentID uuid.UUID) (map[string]interface{}, error) { query := ` - SELECT + SELECT COUNT(*) as total_disks, COUNT(CASE WHEN severity = 'critical' THEN 1 END) as critical_disks, COUNT(CASE WHEN severity = 'important' THEN 1 END) as important_disks, @@ -149,19 +148,38 @@ func (q *StorageMetricsQueries) GetStorageMetricsSummary(ctx context.Context, ag AND created_at >= NOW() - INTERVAL '24 hours' ` - var summary map[string]interface{} + var ( + totalDisks int + criticalDisks int + importantDisks int + avgUsedPercent sql.NullFloat64 + maxUsedPercent sql.NullFloat64 + firstCollectedAt sql.NullTime + lastCollectedAt sql.NullTime + ) + err := q.db.QueryRowContext(ctx, query, agentID).Scan( - &summary["total_disks"], - &summary["critical_disks"], - &summary["important_disks"], - &summary["avg_used_percent"], - &summary["max_used_percent"], - &summary["first_collected_at"], - &summary["last_collected_at"], + &totalDisks, + &criticalDisks, + &importantDisks, + &avgUsedPercent, + &maxUsedPercent, + &firstCollectedAt, + &lastCollectedAt, ) if err != nil { return nil, fmt.Errorf("failed to get storage metrics summary: %w", err) } + summary := map[string]interface{}{ + "total_disks": totalDisks, + "critical_disks": criticalDisks, + "important_disks": importantDisks, + "avg_used_percent": avgUsedPercent.Float64, + "max_used_percent": maxUsedPercent.Float64, + "first_collected_at": firstCollectedAt.Time, + "last_collected_at": lastCollectedAt.Time, + } + return summary, nil -} \ No newline at end of file +} diff --git a/aggregator-web/src/components/AgentHealth.tsx b/aggregator-web/src/components/AgentHealth.tsx index 3c3fff8..72067dd 100644 --- a/aggregator-web/src/components/AgentHealth.tsx +++ b/aggregator-web/src/components/AgentHealth.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { RefreshCw, @@ -266,9 +266,9 @@ export function AgentHealth({ agentId }: AgentHealthProps) { { value: 20160, label: '2 weeks' }, ]; - - const enabledCount = useMemo(() => subsystems.filter(s => s.enabled).length, [subsystems]); - const autoRunCount = useMemo(() => subsystems.filter(s => s.auto_run && s.enabled).length, [subsystems]); + // Calculate counts directly without useMemo + const enabledCount = subsystems.filter(s => s.enabled).length; + const autoRunCount = subsystems.filter(s => s.auto_run && s.enabled).length; return (
- {enabledCount} enabled • {autoRunCount} auto-running • {subsystems.length} total -
-- Subsystems will be created automatically when the agent checks in. -
-| Subsystem | -Category | -Enabled | -Auto-Run | -Interval | -Last Run | -Next Run | -Actions | -
|---|---|---|---|---|---|---|---|
|
-
- {config.icon}
-
-
-
- {config.name}
- {config.description}
- |
-
- {/* Category */}
- {config.category} | - - {/* Enabled Toggle */} -- - | - - {/* Auto-Run Toggle */} -- - | - - {/* Interval Selector */} -- {subsystem.enabled ? ( - - ) : ( - - - )} - | - - {/* Last Run */} -- {subsystem.last_run_at ? formatRelativeTime(subsystem.last_run_at) : '-'} - | - - {/* Next Run */} -- {subsystem.next_run_at && subsystem.auto_run ? formatRelativeTime(subsystem.next_run_at) : '-'} - | - - {/* Actions */} -- - | -
Overall Status
-- {securityOverview.overall_status === 'healthy' ? 'All systems nominal' : - securityOverview.overall_status === 'degraded' ? `${securityOverview.alerts.length} issue(s)` : - 'Critical issues'} -
-- {getSecurityDisplayName(key)} -
-- {key === 'command_validation' ? - `${subsystem.metrics?.total_pending_commands || 0} pending` : - key === 'ed25519_signing' ? - 'Key valid' : - key === 'machine_binding' ? - `${subsystem.checks?.recent_violations || 0} violations` : - key === 'nonce_validation' ? - `${subsystem.checks?.validation_failures || 0} blocked` : - subsystem.status} -
-- {key === 'nonce_validation' ? - `Nonces: ${subsystem.metrics?.total_pending_commands || 0} | Max: ${checks.max_age_minutes || 5}m | Failures: ${checks.validation_failures || 0}` : - key === 'machine_binding' ? - `Bound: ${checks.bound_agents || 'N/A'} | Violations: ${checks.recent_violations || 0} | Method: Hardware` : - key === 'ed25519_signing' ? - `Key: ${checks.public_key_fingerprint?.substring(0, 16) || 'N/A'}... | Algo: ${checks.algorithm || 'Ed25519'}` : - key === 'command_validation' ? - `Processed: ${subsystem.metrics?.commands_last_hour || 0}/hr | Pending: ${subsystem.metrics?.total_pending_commands || 0}` : - `Status: ${subsystem.status}`} -
-Alerts ({securityOverview.alerts.length})
-Recs ({securityOverview.recommendations.length})
-{Object.keys(securityOverview.subsystems).length}
-Systems
-- {Object.values(securityOverview.subsystems).filter(s => s.status === 'healthy' || s.status === 'enforced').length} -
-Healthy
-{securityOverview.alerts.length}
-Alerts
-- {new Date(securityOverview.timestamp).toLocaleTimeString()} -
-Updated
-Unable to load security status
-