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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
@@ -3,7 +3,6 @@ package queries
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
||||
"github.com/google/uuid"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
aggregator-server/test-scheduler
Normal file
BIN
aggregator-server/test-scheduler
Normal file
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user