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:
@@ -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;
|
||||
@@ -0,0 +1,84 @@
|
||||
-- Migration: Create separate tables for metrics and docker images
|
||||
-- Purpose: Fix data classification issue where storage/system metrics were incorrectly stored as package updates
|
||||
|
||||
-- Create metrics table for system and storage metrics
|
||||
CREATE TABLE IF NOT EXISTS metrics (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
||||
package_type VARCHAR(50) NOT NULL, -- "storage", "system", "cpu", "memory"
|
||||
package_name VARCHAR(255) NOT NULL,
|
||||
current_version TEXT NOT NULL, -- current usage, value
|
||||
available_version TEXT NOT NULL, -- available space, threshold
|
||||
severity VARCHAR(20) NOT NULL DEFAULT 'low', -- "low", "moderate", "high", "critical"
|
||||
repository_source VARCHAR(255),
|
||||
metadata JSONB DEFAULT '{}',
|
||||
event_type VARCHAR(50) NOT NULL DEFAULT 'discovered',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- Unique constraint to prevent duplicate entries
|
||||
UNIQUE (agent_id, package_name, package_type, created_at)
|
||||
);
|
||||
|
||||
-- Create docker_images table for Docker image information
|
||||
CREATE TABLE IF NOT EXISTS docker_images (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
||||
package_type VARCHAR(50) NOT NULL DEFAULT 'docker_image',
|
||||
package_name VARCHAR(500) NOT NULL, -- image name:tag
|
||||
current_version VARCHAR(255) NOT NULL, -- current image ID
|
||||
available_version VARCHAR(255), -- latest image ID
|
||||
severity VARCHAR(20) NOT NULL DEFAULT 'low', -- "low", "moderate", "high", "critical"
|
||||
repository_source VARCHAR(500), -- registry URL
|
||||
metadata JSONB DEFAULT '{}',
|
||||
event_type VARCHAR(50) NOT NULL DEFAULT 'discovered',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- Unique constraint to prevent duplicate entries
|
||||
UNIQUE (agent_id, package_name, package_type, created_at)
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_metrics_agent_id ON metrics(agent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_metrics_package_type ON metrics(package_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_metrics_created_at ON metrics(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_metrics_severity ON metrics(severity);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_docker_images_agent_id ON docker_images(agent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_docker_images_package_type ON docker_images(package_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_docker_images_created_at ON docker_images(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_docker_images_severity ON docker_images(severity);
|
||||
CREATE INDEX IF NOT EXISTS idx_docker_images_has_updates ON docker_images(current_version, available_version) WHERE current_version != available_version;
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON TABLE metrics IS 'Stores system and storage metrics collected from agents, separate from package updates';
|
||||
COMMENT ON TABLE docker_images IS 'Stores Docker image information and update availability, separate from package updates';
|
||||
|
||||
COMMENT ON COLUMN metrics.package_type IS 'Type of metric: storage, system, cpu, memory, etc.';
|
||||
COMMENT ON COLUMN metrics.package_name IS 'Name of the metric (mount point, metric name, etc.)';
|
||||
COMMENT ON COLUMN metrics.current_version IS 'Current value or usage';
|
||||
COMMENT ON COLUMN metrics.available_version IS 'Available space or threshold';
|
||||
COMMENT ON COLUMN metrics.severity IS 'Severity level: low, moderate, high, critical';
|
||||
|
||||
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 image ID';
|
||||
COMMENT ON COLUMN docker_images.severity IS 'Update severity: low, moderate, high, critical';
|
||||
|
||||
-- Create or replace function to clean old data (optional)
|
||||
CREATE OR REPLACE FUNCTION clean_misclassified_data()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
deleted_count INTEGER := 0;
|
||||
BEGIN
|
||||
-- This function can be called to clean up any storage/system metrics that were
|
||||
-- incorrectly stored in the update_events table before migration
|
||||
|
||||
-- For now, just return 0 as we're keeping the old data for audit purposes
|
||||
RETURN deleted_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Grant permissions (adjust as needed for your setup)
|
||||
-- GRANT ALL PRIVILEGES ON TABLE metrics TO redflag_user;
|
||||
-- GRANT ALL PRIVILEGES ON TABLE docker_images TO redflag_user;
|
||||
-- GRANT USAGE ON SCHEMA public TO redflag_user;
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user