feat: add config sync endpoint and security UI updates

- Add GET /api/v1/agents/:id/config endpoint for server configuration
- Agent fetches config during check-in and applies updates
- Add version tracking to prevent unnecessary config applications
- Clean separation: config sync independent of commands
- Fix agent UI subsystem settings to actually control agent behavior
- Update Security Health UI with frosted glass styling and tooltips
This commit is contained in:
Fimeg
2025-11-03 22:36:26 -05:00
parent eccc38d7c9
commit 38894f64d3
18 changed files with 944 additions and 87 deletions

View File

@@ -164,7 +164,7 @@ func main() {
rateLimiter := middleware.NewRateLimiter()
// Initialize handlers
agentHandler := handlers.NewAgentHandler(agentQueries, commandQueries, refreshTokenQueries, registrationTokenQueries, cfg.CheckInInterval, cfg.LatestAgentVersion)
agentHandler := handlers.NewAgentHandler(agentQueries, commandQueries, refreshTokenQueries, registrationTokenQueries, subsystemQueries, cfg.CheckInInterval, cfg.LatestAgentVersion)
updateHandler := handlers.NewUpdateHandler(updateQueries, agentQueries, commandQueries, agentHandler)
authHandler := handlers.NewAuthHandler(cfg.Admin.JWTSecret, userQueries)
statsHandler := handlers.NewStatsHandler(agentQueries, updateQueries)
@@ -236,6 +236,7 @@ func main() {
agents.Use(middleware.MachineBindingMiddleware(agentQueries, cfg.MinAgentVersion)) // v0.1.22: Prevent config copying
{
agents.GET("/:id/commands", agentHandler.GetCommands)
agents.GET("/:id/config", agentHandler.GetAgentConfig)
agents.POST("/:id/updates", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), updateHandler.ReportUpdates)
agents.POST("/:id/logs", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), updateHandler.ReportLog)
agents.POST("/:id/dependencies", rateLimiter.RateLimit("agent_reports", middleware.KeyByAgentID), updateHandler.ReportDependencies)

View File

@@ -19,16 +19,18 @@ type AgentHandler struct {
commandQueries *queries.CommandQueries
refreshTokenQueries *queries.RefreshTokenQueries
registrationTokenQueries *queries.RegistrationTokenQueries
subsystemQueries *queries.SubsystemQueries
checkInInterval int
latestAgentVersion string
}
func NewAgentHandler(aq *queries.AgentQueries, cq *queries.CommandQueries, rtq *queries.RefreshTokenQueries, regTokenQueries *queries.RegistrationTokenQueries, checkInInterval int, latestAgentVersion string) *AgentHandler {
func NewAgentHandler(aq *queries.AgentQueries, cq *queries.CommandQueries, rtq *queries.RefreshTokenQueries, regTokenQueries *queries.RegistrationTokenQueries, sq *queries.SubsystemQueries, checkInInterval int, latestAgentVersion string) *AgentHandler {
return &AgentHandler{
agentQueries: aq,
commandQueries: cq,
refreshTokenQueries: rtq,
registrationTokenQueries: regTokenQueries,
subsystemQueries: sq,
checkInInterval: checkInInterval,
latestAgentVersion: latestAgentVersion,
}
@@ -1136,3 +1138,44 @@ func (h *AgentHandler) TriggerReboot(c *gin.Context) {
"hostname": agent.Hostname,
})
}
// GetAgentConfig returns current subsystem configuration for an agent
// GET /api/v1/agents/:id/config
func (h *AgentHandler) GetAgentConfig(c *gin.Context) {
idStr := c.Param("id")
agentID, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
}
// Verify agent exists
agent, err := h.agentQueries.GetAgentByID(agentID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
return
}
// Get subsystem configuration from database
subsystems, err := h.subsystemQueries.GetSubsystems(agentID)
if err != nil {
log.Printf("Failed to get subsystems for agent %s: %v", agentID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get subsystem configuration"})
return
}
// Convert to simple format for agent
config := make(map[string]interface{})
for _, subsystem := range subsystems {
config[subsystem.Subsystem] = map[string]interface{}{
"enabled": subsystem.Enabled,
"interval_minutes": subsystem.IntervalMinutes,
"auto_run": subsystem.AutoRun,
}
}
c.JSON(http.StatusOK, gin.H{
"subsystems": config,
"version": time.Now().Unix(), // Simple version timestamp
})
}

View File

@@ -68,13 +68,13 @@ func (h *DockerReportsHandler) ReportDockerImages(c *gin.Context) {
event := models.StoredDockerImage{
ID: uuid.New(),
AgentID: agentID,
PackageType: item.PackageType,
PackageName: item.PackageName,
CurrentVersion: item.CurrentVersion,
AvailableVersion: item.AvailableVersion,
PackageType: "docker_image",
PackageName: item.ImageName + ":" + item.ImageTag,
CurrentVersion: item.ImageID,
AvailableVersion: item.LatestImageID,
Severity: item.Severity,
RepositorySource: item.RepositorySource,
Metadata: models.JSONB(item.Metadata),
Metadata: convertToJSONB(item.Metadata),
EventType: "discovered",
CreatedAt: req.Timestamp,
}
@@ -123,6 +123,8 @@ func (h *DockerReportsHandler) GetAgentDockerImages(c *gin.Context) {
pageSize = 50
}
offset := (page - 1) * pageSize
imageName := c.Query("image_name")
registry := c.Query("registry")
severity := c.Query("severity")
@@ -136,7 +138,7 @@ func (h *DockerReportsHandler) GetAgentDockerImages(c *gin.Context) {
Severity: nil,
HasUpdates: nil,
Limit: &pageSize,
Offset: &((page - 1) * pageSize),
Offset: &(offset),
}
if imageName != "" {
@@ -274,4 +276,13 @@ func countUpdates(images []models.DockerImageInfo) int {
}
}
return count
}
// Helper function to convert map[string]interface{} to models.JSONB
func convertToJSONB(data map[string]interface{}) models.JSONB {
result := make(map[string]interface{})
for k, v := range data {
result[k] = v
}
return models.JSONB(result)
}

View File

@@ -0,0 +1,21 @@
-- Down Migration: Remove metrics and docker_images tables
-- Purpose: Rollback migration 018 - remove separate tables for metrics and docker images
-- Drop indexes first
DROP INDEX IF EXISTS idx_metrics_agent_id;
DROP INDEX IF EXISTS idx_metrics_package_type;
DROP INDEX IF EXISTS idx_metrics_created_at;
DROP INDEX IF EXISTS idx_metrics_severity;
DROP INDEX IF EXISTS idx_docker_images_agent_id;
DROP INDEX IF EXISTS idx_docker_images_package_type;
DROP INDEX IF EXISTS idx_docker_images_created_at;
DROP INDEX IF EXISTS idx_docker_images_severity;
DROP INDEX IF EXISTS idx_docker_images_has_updates;
-- Drop the clean function
DROP FUNCTION IF EXISTS clean_misclassified_data();
-- Drop the tables
DROP TABLE IF EXISTS metrics;
DROP TABLE IF EXISTS docker_images;

View File

@@ -61,7 +61,7 @@ COMMENT ON COLUMN metrics.severity IS 'Severity level: low, moderate, high, crit
COMMENT ON COLUMN docker_images.package_name IS 'Docker image name with tag (e.g., nginx:latest)';
COMMENT ON COLUMN docker_images.current_version IS 'Current image ID';
COMMENT ON COLUMN docker_images.available_version IS 'Latest available image ID';
COMMENT ON COLUMN docker_images.available_version IS 'Latest image ID';
COMMENT ON COLUMN docker_images.severity IS 'Update severity: low, moderate, high, critical';
-- Create or replace function to clean old data (optional)

View File

@@ -3,7 +3,6 @@ package queries
import (
"database/sql"
"fmt"
"time"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/google/uuid"

View File

@@ -3,11 +3,9 @@ package queries
import (
"database/sql"
"fmt"
"time"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/google/uuid"
"github.com/lib/pq"
)
// MetricsQueries handles database operations for metrics

View File

@@ -84,11 +84,48 @@ type BulkDockerUpdateRequest struct {
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
}
// AgentDockerImage represents a Docker image as sent by the agent
type AgentDockerImage struct {
ImageName string `json:"image_name"`
ImageTag string `json:"image_tag"`
ImageID string `json:"image_id"`
RepositorySource string `json:"repository_source"`
SizeBytes int64 `json:"size_bytes"`
CreatedAt string `json:"created_at"`
HasUpdate bool `json:"has_update"`
LatestImageID string `json:"latest_image_id"`
Severity string `json:"severity"`
Labels map[string]string `json:"labels"`
Metadata map[string]interface{} `json:"metadata"`
}
// DockerReportRequest is sent by agents when reporting Docker image updates
type DockerReportRequest struct {
CommandID string `json:"command_id"`
Timestamp time.Time `json:"timestamp"`
Images []DockerImage `json:"images"`
CommandID string `json:"command_id"`
Timestamp time.Time `json:"timestamp"`
Images []AgentDockerImage `json:"images"`
}
// DockerImageInfo represents detailed Docker image information for API responses
type DockerImageInfo struct {
ID string `json:"id"`
AgentID string `json:"agent_id"`
ImageName string `json:"image_name"`
ImageTag string `json:"image_tag"`
ImageID string `json:"image_id"`
RepositorySource string `json:"repository_source"`
SizeBytes int64 `json:"size_bytes"`
CreatedAt string `json:"created_at"`
HasUpdate bool `json:"has_update"`
LatestImageID string `json:"latest_image_id"`
Severity string `json:"severity"`
Labels map[string]string `json:"labels"`
Metadata map[string]interface{} `json:"metadata"`
PackageType string `json:"package_type"`
CurrentVersion string `json:"current_version"`
AvailableVersion string `json:"available_version"`
EventType string `json:"event_type"`
CreatedAtTime time.Time `json:"created_at_time"`
}
// DockerImageUpdate represents a Docker image update from agent scans

Binary file not shown.

Binary file not shown.