diff --git a/aggregator-agent/cmd/agent/main.go b/aggregator-agent/cmd/agent/main.go index f542b8e..f90a77a 100644 --- a/aggregator-agent/cmd/agent/main.go +++ b/aggregator-agent/cmd/agent/main.go @@ -28,7 +28,7 @@ import ( ) const ( - AgentVersion = "0.1.22" // v0.1.22: Machine binding and version enforcement security release + AgentVersion = "0.1.23" // v0.1.23: Real security metrics and config sync ) var ( diff --git a/aggregator-server/cmd/server/main.go b/aggregator-server/cmd/server/main.go index 7bb993c..022e1d5 100644 --- a/aggregator-server/cmd/server/main.go +++ b/aggregator-server/cmd/server/main.go @@ -132,8 +132,8 @@ func main() { userQueries := queries.NewUserQueries(db.DB) subsystemQueries := queries.NewSubsystemQueries(db.DB) agentUpdateQueries := queries.NewAgentUpdateQueries(db.DB) - metricsQueries := queries.NewMetricsQueries(db.DB) - dockerQueries := queries.NewDockerQueries(db.DB) + metricsQueries := queries.NewMetricsQueries(db.DB.DB) + dockerQueries := queries.NewDockerQueries(db.DB.DB) // Ensure admin user exists if err := userQueries.EnsureAdminUser(cfg.Admin.Username, cfg.Admin.Username+"@redflag.local", cfg.Admin.Password); err != nil { diff --git a/aggregator-server/internal/api/handlers/agents.go b/aggregator-server/internal/api/handlers/agents.go index 6c15f16..2d10066 100644 --- a/aggregator-server/internal/api/handlers/agents.go +++ b/aggregator-server/internal/api/handlers/agents.go @@ -1150,7 +1150,7 @@ func (h *AgentHandler) GetAgentConfig(c *gin.Context) { } // Verify agent exists - agent, err := h.agentQueries.GetAgentByID(agentID) + _, err = h.agentQueries.GetAgentByID(agentID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"}) return diff --git a/aggregator-server/internal/api/handlers/docker_reports.go b/aggregator-server/internal/api/handlers/docker_reports.go index 71dfb3a..1fa0a09 100644 --- a/aggregator-server/internal/api/handlers/docker_reports.go +++ b/aggregator-server/internal/api/handlers/docker_reports.go @@ -194,17 +194,24 @@ func (h *DockerReportsHandler) GetAgentDockerInfo(c *gin.Context) { dockerInfo := make([]models.DockerImageInfo, 0, len(result.Images)) for _, image := range result.Images { info := models.DockerImageInfo{ - Name: image.PackageName, - Tag: extractTag(image.PackageName), - ImageID: image.CurrentVersion, - Size: parseImageSize(image.Metadata), - CreatedAt: image.CreatedAt, - Registry: image.RepositorySource, - HasUpdate: image.AvailableVersion != image.CurrentVersion, - LatestImageID: image.AvailableVersion, - Severity: image.Severity, - Labels: extractLabels(image.Metadata), - LastScanned: image.CreatedAt, + ID: image.ID.String(), + AgentID: image.AgentID.String(), + ImageName: extractName(image.PackageName), + ImageTag: extractTag(image.PackageName), + ImageID: image.CurrentVersion, + RepositorySource: image.RepositorySource, + SizeBytes: parseImageSize(image.Metadata), + CreatedAt: image.CreatedAt.Format(time.RFC3339), + HasUpdate: image.AvailableVersion != image.CurrentVersion, + LatestImageID: image.AvailableVersion, + Severity: image.Severity, + Labels: extractLabels(image.Metadata), + Metadata: convertInterfaceMapToJSONB(image.Metadata), + PackageType: image.PackageType, + CurrentVersion: image.CurrentVersion, + AvailableVersion: image.AvailableVersion, + EventType: image.EventType, + CreatedAtTime: image.CreatedAt, } dockerInfo = append(dockerInfo, info) } @@ -217,6 +224,16 @@ func (h *DockerReportsHandler) GetAgentDockerInfo(c *gin.Context) { }) } +// Helper function to extract name from image name +func extractName(imageName string) string { + // Simple implementation - split by ":" and return everything except last part + parts := strings.Split(imageName, ":") + if len(parts) > 1 { + return strings.Join(parts[:len(parts)-1], ":") + } + return imageName +} + // Helper function to extract tag from image name func extractTag(imageName string) string { // Simple implementation - split by ":" and return last part @@ -285,4 +302,14 @@ func convertToJSONB(data map[string]interface{}) models.JSONB { result[k] = v } return models.JSONB(result) -} \ No newline at end of file +} + +// Helper function to convert map[string]interface{} to models.JSONB +func convertInterfaceMapToJSONB(data models.JSONB) models.JSONB { + result := make(map[string]interface{}) + for k, v := range data { + result[k] = v + } + return models.JSONB(result) +} + diff --git a/aggregator-server/internal/api/handlers/metrics.go b/aggregator-server/internal/api/handlers/metrics.go index dac48f9..c79c9c4 100644 --- a/aggregator-server/internal/api/handlers/metrics.go +++ b/aggregator-server/internal/api/handlers/metrics.go @@ -2,7 +2,6 @@ package handlers import ( "fmt" - "log" "net/http" "strconv" "time" @@ -74,7 +73,7 @@ func (h *MetricsHandler) ReportMetrics(c *gin.Context) { AvailableVersion: item.AvailableVersion, Severity: item.Severity, RepositorySource: item.RepositorySource, - Metadata: models.JSONB(item.Metadata), + Metadata: convertStringMapToJSONB(item.Metadata), EventType: "discovered", CreatedAt: req.Timestamp, } @@ -123,6 +122,8 @@ func (h *MetricsHandler) GetAgentMetrics(c *gin.Context) { pageSize = 50 } + offset := (page - 1) * pageSize + packageType := c.Query("package_type") severity := c.Query("severity") @@ -132,7 +133,7 @@ func (h *MetricsHandler) GetAgentMetrics(c *gin.Context) { PackageType: nil, Severity: nil, Limit: &pageSize, - Offset: &((page - 1) * pageSize), + Offset: &offset, } if packageType != "" { @@ -286,4 +287,13 @@ func aggregateSystemMetrics(metrics []models.StoredMetric) *models.SystemMetrics LoadAverage: []float64{0, 0, 0}, LastUpdated: metrics[0].CreatedAt, } +} + +// Helper function to convert map[string]string to models.JSONB +func convertStringMapToJSONB(data map[string]string) models.JSONB { + result := make(map[string]interface{}) + for k, v := range data { + result[k] = v + } + return models.JSONB(result) } \ No newline at end of file diff --git a/aggregator-server/internal/api/handlers/security.go b/aggregator-server/internal/api/handlers/security.go index f330c57..9087cc3 100644 --- a/aggregator-server/internal/api/handlers/security.go +++ b/aggregator-server/internal/api/handlers/security.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "net/http" "time" @@ -112,21 +113,38 @@ func (h *SecurityHandler) CommandValidationStatus(c *gin.Context) { }, } - // Get command metrics + // Get real command metrics if h.commandQueries != nil { - // TODO: Add methods to CommandQueries for aggregate metrics - // For now, we can provide basic status - response["metrics"].(map[string]interface{})["total_pending_commands"] = "N/A" - response["metrics"].(map[string]interface{})["agents_with_pending"] = "N/A" + if totalPending, err := h.commandQueries.GetTotalPendingCommands(); err == nil { + response["metrics"].(map[string]interface{})["total_pending_commands"] = totalPending + } + if agentsWithPending, err := h.commandQueries.GetAgentsWithPendingCommands(); err == nil { + response["metrics"].(map[string]interface{})["agents_with_pending"] = agentsWithPending + } + if commandsLastHour, err := h.commandQueries.GetCommandsInTimeRange(1); err == nil { + response["metrics"].(map[string]interface{})["commands_last_hour"] = commandsLastHour + } + if commandsLast24h, err := h.commandQueries.GetCommandsInTimeRange(24); err == nil { + response["metrics"].(map[string]interface{})["commands_last_24h"] = commandsLast24h + } } // Get agent metrics for responsiveness if h.agentQueries != nil { - // TODO: Add method to count online vs offline agents - // This would help identify if agents are responsive to commands + if activeAgents, err := h.agentQueries.GetActiveAgentCount(); err == nil { + response["checks"].(map[string]interface{})["agent_responsive"] = fmt.Sprintf("%d online", activeAgents) + } } - response["status"] = "healthy" + // Determine if backpressure is active (5+ pending commands per agent threshold) + if totalPending, ok := response["metrics"].(map[string]interface{})["total_pending_commands"].(int); ok { + if agentsWithPending, ok := response["metrics"].(map[string]interface{})["agents_with_pending"].(int); ok && agentsWithPending > 0 { + avgPerAgent := float64(totalPending) / float64(agentsWithPending) + response["checks"].(map[string]interface{})["backpressure_active"] = avgPerAgent >= 5.0 + } + } + + response["status"] = "operational" response["checks"].(map[string]interface{})["command_processing"] = "operational" c.JSON(http.StatusOK, response) @@ -143,6 +161,8 @@ func (h *SecurityHandler) MachineBindingStatus(c *gin.Context) { "min_agent_version": "v0.1.22", "fingerprint_required": true, "recent_violations": 0, + "bound_agents": 0, + "version_compliance": 0, }, "details": map[string]interface{}{ "enforcement_method": "hardware_fingerprint", @@ -151,8 +171,30 @@ func (h *SecurityHandler) MachineBindingStatus(c *gin.Context) { }, } - // TODO: Add metrics for machine binding violations - // This would require logging when machine binding middleware rejects requests + // Get real machine binding metrics + if h.agentQueries != nil { + // Get total agents with machine binding + if boundAgents, err := h.agentQueries.GetAgentsWithMachineBinding(); err == nil { + response["checks"].(map[string]interface{})["bound_agents"] = boundAgents + } + + // Get total agents for comparison + if totalAgents, err := h.agentQueries.GetTotalAgentCount(); err == nil { + // Calculate version compliance (agents meeting minimum version requirement) + if compliantAgents, err := h.agentQueries.GetAgentCountByVersion("0.1.22"); err == nil { + response["checks"].(map[string]interface{})["version_compliance"] = compliantAgents + } + + // Set recent violations based on version compliance gap + boundAgents := response["checks"].(map[string]interface{})["bound_agents"].(int) + versionCompliance := response["checks"].(map[string]interface{})["version_compliance"].(int) + violations := boundAgents - versionCompliance + if violations < 0 { + violations = 0 + } + response["checks"].(map[string]interface{})["recent_violations"] = violations + } + } response["status"] = "enforced" response["checks"].(map[string]interface{})["binding_enforced"] = true @@ -192,6 +234,14 @@ func (h *SecurityHandler) SecurityOverview(c *gin.Context) { // Check Ed25519 signing if h.signingService != nil && h.signingService.GetPublicKey() != "" { overview["subsystems"].(map[string]interface{})["ed25519_signing"].(map[string]interface{})["status"] = "healthy" + // Add Ed25519 details + overview["subsystems"].(map[string]interface{})["ed25519_signing"].(map[string]interface{})["checks"] = map[string]interface{}{ + "service_initialized": true, + "public_key_available": true, + "signing_operational": true, + "public_key_fingerprint": h.signingService.GetPublicKeyFingerprint(), + "algorithm": "ed25519", + } } else { overview["subsystems"].(map[string]interface{})["ed25519_signing"].(map[string]interface{})["status"] = "unavailable" overview["alerts"] = append(overview["alerts"].([]string), "Ed25519 signing service not configured") @@ -200,12 +250,74 @@ func (h *SecurityHandler) SecurityOverview(c *gin.Context) { // Check nonce validation overview["subsystems"].(map[string]interface{})["nonce_validation"].(map[string]interface{})["status"] = "healthy" + overview["subsystems"].(map[string]interface{})["nonce_validation"].(map[string]interface{})["checks"] = map[string]interface{}{ + "validation_enabled": true, + "max_age_minutes": 5, + "validation_failures": 0, // TODO: Implement nonce validation failure tracking + } + overview["subsystems"].(map[string]interface{})["nonce_validation"].(map[string]interface{})["details"] = map[string]interface{}{ + "nonce_format": "UUID:UnixTimestamp", + "signature_algorithm": "ed25519", + "replay_protection": "active", + } - // Check machine binding - overview["subsystems"].(map[string]interface{})["machine_binding"].(map[string]interface{})["status"] = "enforced" + // Get real machine binding metrics + if h.agentQueries != nil { + boundAgents, _ := h.agentQueries.GetAgentsWithMachineBinding() + compliantAgents, _ := h.agentQueries.GetAgentCountByVersion("0.1.22") + violations := boundAgents - compliantAgents + if violations < 0 { + violations = 0 + } - // Check command validation - overview["subsystems"].(map[string]interface{})["command_validation"].(map[string]interface{})["status"] = "operational" + overview["subsystems"].(map[string]interface{})["machine_binding"].(map[string]interface{})["status"] = "enforced" + overview["subsystems"].(map[string]interface{})["machine_binding"].(map[string]interface{})["checks"] = map[string]interface{}{ + "binding_enforced": true, + "min_agent_version": "v0.1.22", + "recent_violations": violations, + "bound_agents": boundAgents, + "version_compliance": compliantAgents, + } + overview["subsystems"].(map[string]interface{})["machine_binding"].(map[string]interface{})["details"] = map[string]interface{}{ + "enforcement_method": "hardware_fingerprint", + "binding_scope": "machine_id + cpu + memory + system_uuid", + "violation_action": "command_rejection", + } + } + + // Get real command validation metrics + if h.commandQueries != nil { + totalPending, _ := h.commandQueries.GetTotalPendingCommands() + agentsWithPending, _ := h.commandQueries.GetAgentsWithPendingCommands() + commandsLastHour, _ := h.commandQueries.GetCommandsInTimeRange(1) + commandsLast24h, _ := h.commandQueries.GetCommandsInTimeRange(24) + + // Calculate backpressure + backpressureActive := false + if agentsWithPending > 0 { + avgPerAgent := float64(totalPending) / float64(agentsWithPending) + backpressureActive = avgPerAgent >= 5.0 + } + + overview["subsystems"].(map[string]interface{})["command_validation"].(map[string]interface{})["status"] = "operational" + overview["subsystems"].(map[string]interface{})["command_validation"].(map[string]interface{})["metrics"] = map[string]interface{}{ + "total_pending_commands": totalPending, + "agents_with_pending": agentsWithPending, + "commands_last_hour": commandsLastHour, + "commands_last_24h": commandsLast24h, + } + overview["subsystems"].(map[string]interface{})["command_validation"].(map[string]interface{})["checks"] = map[string]interface{}{ + "command_processing": "operational", + "backpressure_active": backpressureActive, + } + + // Add agent responsiveness info + if h.agentQueries != nil { + if activeAgents, err := h.agentQueries.GetActiveAgentCount(); err == nil { + overview["subsystems"].(map[string]interface{})["command_validation"].(map[string]interface{})["checks"].(map[string]interface{})["agent_responsive"] = fmt.Sprintf("%d online", activeAgents) + } + } + } // Determine overall status healthyCount := 0 diff --git a/aggregator-server/internal/config/config.go b/aggregator-server/internal/config/config.go index 86ebd20..d99658c 100644 --- a/aggregator-server/internal/config/config.go +++ b/aggregator-server/internal/config/config.go @@ -86,7 +86,7 @@ func Load() (*Config, error) { cfg.CheckInInterval = checkInInterval cfg.OfflineThreshold = offlineThreshold cfg.Timezone = getEnv("TIMEZONE", "UTC") - cfg.LatestAgentVersion = getEnv("LATEST_AGENT_VERSION", "0.1.22") + cfg.LatestAgentVersion = getEnv("LATEST_AGENT_VERSION", "0.1.23") cfg.MinAgentVersion = getEnv("MIN_AGENT_VERSION", "0.1.22") cfg.SigningPrivateKey = getEnv("REDFLAG_SIGNING_PRIVATE_KEY", "") diff --git a/aggregator-server/internal/database/queries/agents.go b/aggregator-server/internal/database/queries/agents.go index 9280f50..b136883 100644 --- a/aggregator-server/internal/database/queries/agents.go +++ b/aggregator-server/internal/database/queries/agents.go @@ -226,6 +226,30 @@ func (q *AgentQueries) GetActiveAgentCount() (int, error) { return count, err } +// GetTotalAgentCount returns the total count of registered agents +func (q *AgentQueries) GetTotalAgentCount() (int, error) { + var count int + query := `SELECT COUNT(*) FROM agents` + err := q.db.Get(&count, query) + return count, err +} + +// GetAgentCountByVersion returns the count of agents by version (for version compliance) +func (q *AgentQueries) GetAgentCountByVersion(minVersion string) (int, error) { + var count int + query := `SELECT COUNT(*) FROM agents WHERE current_version >= $1` + err := q.db.Get(&count, query, minVersion) + return count, err +} + +// GetAgentsWithMachineBinding returns count of agents that have machine IDs set +func (q *AgentQueries) GetAgentsWithMachineBinding() (int, error) { + var count int + query := `SELECT COUNT(*) FROM agents WHERE machine_id IS NOT NULL AND machine_id != ''` + err := q.db.Get(&count, query) + return count, err +} + // UpdateAgentRebootStatus updates the reboot status for an agent func (q *AgentQueries) UpdateAgentRebootStatus(id uuid.UUID, required bool, reason string) error { query := ` diff --git a/aggregator-server/internal/database/queries/commands.go b/aggregator-server/internal/database/queries/commands.go index d4514ee..e1c1dd7 100644 --- a/aggregator-server/internal/database/queries/commands.go +++ b/aggregator-server/internal/database/queries/commands.go @@ -370,6 +370,38 @@ func (q *CommandQueries) CountPendingCommandsForAgent(agentID uuid.UUID) (int, e return count, err } +// GetTotalPendingCommands returns total pending commands across all agents +func (q *CommandQueries) GetTotalPendingCommands() (int, error) { + var count int + query := `SELECT COUNT(*) FROM agent_commands WHERE status = 'pending'` + err := q.db.Get(&count, query) + return count, err +} + +// GetAgentsWithPendingCommands returns count of agents with pending commands +func (q *CommandQueries) GetAgentsWithPendingCommands() (int, error) { + var count int + query := ` + SELECT COUNT(DISTINCT agent_id) + FROM agent_commands + WHERE status = 'pending' + ` + err := q.db.Get(&count, query) + return count, err +} + +// GetCommandsInTimeRange returns count of commands processed in a time range +func (q *CommandQueries) GetCommandsInTimeRange(hours int) (int, error) { + var count int + query := ` + SELECT COUNT(*) + FROM agent_commands + WHERE created_at >= $1 AND status IN ('completed', 'failed', 'timed_out') + ` + err := q.db.Get(&count, query, time.Now().Add(-time.Duration(hours)*time.Hour)) + return count, err +} + // VerifyCommandsCompleted checks which command IDs from the provided list have been completed or failed // Returns the list of command IDs that have been successfully recorded (completed or failed status) func (q *CommandQueries) VerifyCommandsCompleted(commandIDs []string) ([]string, error) { diff --git a/aggregator-web/src/components/AgentScanners.tsx b/aggregator-web/src/components/AgentScanners.tsx index b60c9d9..969d3ff 100644 --- a/aggregator-web/src/components/AgentScanners.tsx +++ b/aggregator-web/src/components/AgentScanners.tsx @@ -228,209 +228,6 @@ export function AgentScanners({ agentId }: AgentScannersProps) { return (
Overall Security Status
-- {securityOverview.overall_status === 'healthy' ? 'All systems nominal' : - securityOverview.overall_status === 'degraded' ? `${securityOverview.alerts.length} active issue(s)` : - 'Critical issues detected'} -
-
- {getSecurityDisplayName(key)}
-
- {getEnhancedSubtitle(key, subsystem.status)} -
-Security Alerts
-Recommendations
-Unable to load security status
-Security monitoring may be unavailable
-Overall Security Status
++ {securityOverview.overall_status === 'healthy' ? 'All systems nominal' : + securityOverview.overall_status === 'degraded' ? `${securityOverview.alerts.length} active issue(s)` : + 'Critical issues detected'} +
+
+ {getSecurityDisplayName(key)}
+
+ {getEnhancedSubtitle(key, subsystem.status)} +
+Security Alerts
+Recommendations
+Unable to load security status
+Security monitoring may be unavailable
+