feat: bump to v0.1.23 with security metrics and UI improvements
- Bump agent and server versions to 0.1.23 - Implement security metrics collection (bound agents, command processing, version compliance) - Add dismiss button for timed out commands in agent status - Add config sync endpoint for server->agent configuration updates - Add ignored updates workflow in AgentUpdatesEnhanced (approve/reject workflow) - Swap AgentScanners layout (subsystems top, security bottom) - Replace placeholder security data with database metrics - Add backpressure detection based on pending command ratios
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user