Files

483 lines
14 KiB
Go

package handlers
import (
"fmt"
"log"
"net/http"
"strconv"
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
"github.com/Fimeg/RedFlag/aggregator-server/internal/logging"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type DockerHandler struct {
updateQueries *queries.UpdateQueries
agentQueries *queries.AgentQueries
commandQueries *queries.CommandQueries
signingService *services.SigningService
securityLogger *logging.SecurityLogger
}
func NewDockerHandler(uq *queries.UpdateQueries, aq *queries.AgentQueries, cq *queries.CommandQueries, signingService *services.SigningService, securityLogger *logging.SecurityLogger) *DockerHandler {
return &DockerHandler{
updateQueries: uq,
agentQueries: aq,
commandQueries: cq,
signingService: signingService,
securityLogger: securityLogger,
}
}
// signAndCreateCommand signs a command if signing service is enabled, then stores it in the database
func (h *DockerHandler) signAndCreateCommand(cmd *models.AgentCommand) error {
// Sign the command before storing
if h.signingService != nil && h.signingService.IsEnabled() {
signature, err := h.signingService.SignCommand(cmd)
if err != nil {
return fmt.Errorf("failed to sign command: %w", err)
}
cmd.Signature = signature
// Log successful signing
if h.securityLogger != nil {
h.securityLogger.LogCommandSigned(cmd)
}
} else {
// Log warning if signing disabled
log.Printf("[WARNING] Command signing disabled, storing unsigned command")
if h.securityLogger != nil {
h.securityLogger.LogPrivateKeyNotConfigured()
}
}
// Store in database
err := h.commandQueries.CreateCommand(cmd)
if err != nil {
return fmt.Errorf("failed to create command: %w", err)
}
return nil
}
// GetContainers returns Docker containers and images across all agents
func (h *DockerHandler) GetContainers(c *gin.Context) {
// Parse query parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
agentID := c.Query("agent")
status := c.Query("status")
filters := &models.UpdateFilters{
PackageType: "docker_image",
Page: page,
PageSize: pageSize,
Status: status,
}
// Parse agent_id if provided
if agentID != "" {
if parsedID, err := uuid.Parse(agentID); err == nil {
filters.AgentID = parsedID
}
}
// Get Docker updates (which represent container images)
updates, total, err := h.updateQueries.ListUpdatesFromState(filters)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch Docker containers"})
return
}
// Get agent information for better display
agentMap := make(map[uuid.UUID]models.Agent)
for _, update := range updates {
if _, exists := agentMap[update.AgentID]; !exists {
if agent, err := h.agentQueries.GetAgentByID(update.AgentID); err == nil {
agentMap[update.AgentID] = *agent
}
}
}
// Transform updates into Docker container format
containers := make([]models.DockerContainer, 0, len(updates))
uniqueImages := make(map[string]bool)
for _, update := range updates {
// Extract container info from update metadata
containerName := update.PackageName
var ports []models.DockerPort
if update.Metadata != nil {
if name, exists := update.Metadata["container_name"]; exists {
if nameStr, ok := name.(string); ok {
containerName = nameStr
}
}
// Extract port information from metadata
if portsData, exists := update.Metadata["ports"]; exists {
if portsArray, ok := portsData.([]interface{}); ok {
for _, portData := range portsArray {
if portMap, ok := portData.(map[string]interface{}); ok {
port := models.DockerPort{}
if cp, ok := portMap["container_port"].(float64); ok {
port.ContainerPort = int(cp)
}
if hp, ok := portMap["host_port"].(float64); ok {
hostPort := int(hp)
port.HostPort = &hostPort
}
if proto, ok := portMap["protocol"].(string); ok {
port.Protocol = proto
}
if ip, ok := portMap["host_ip"].(string); ok {
port.HostIP = ip
} else {
port.HostIP = "0.0.0.0"
}
ports = append(ports, port)
}
}
}
}
}
// Get agent information
agentInfo := agentMap[update.AgentID]
// Create container representation
container := models.DockerContainer{
ID: update.ID.String(),
ContainerID: containerName,
Image: update.PackageName,
Tag: update.AvailableVersion, // Available version becomes the tag
AgentID: update.AgentID.String(),
AgentName: agentInfo.Hostname,
AgentHostname: agentInfo.Hostname,
Status: update.Status,
State: "", // Could be extracted from metadata if available
Ports: ports,
CreatedAt: update.LastDiscoveredAt,
UpdatedAt: update.LastUpdatedAt,
UpdateAvailable: update.Status != "installed",
CurrentVersion: update.CurrentVersion,
AvailableVersion: update.AvailableVersion,
}
// Add image to unique set
imageKey := update.PackageName + ":" + update.AvailableVersion
uniqueImages[imageKey] = true
containers = append(containers, container)
}
response := models.DockerContainerListResponse{
Containers: containers,
Images: containers, // Alias for containers to match frontend expectation
TotalImages: len(uniqueImages),
Total: len(containers),
Page: page,
PageSize: pageSize,
TotalPages: (total + pageSize - 1) / pageSize,
}
c.JSON(http.StatusOK, response)
}
// GetAgentContainers returns Docker containers for a specific agent
func (h *DockerHandler) GetAgentContainers(c *gin.Context) {
agentIDStr := c.Param("agent_id")
agentID, err := uuid.Parse(agentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
return
}
// Parse query parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
status := c.Query("status")
filters := &models.UpdateFilters{
AgentID: agentID,
PackageType: "docker_image",
Page: page,
PageSize: pageSize,
Status: status,
}
// Get Docker updates for specific agent
updates, total, err := h.updateQueries.ListUpdatesFromState(filters)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch Docker containers for agent"})
return
}
// Get agent information
agentInfo, err := h.agentQueries.GetAgentByID(agentID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
return
}
// Transform updates into Docker container format
containers := make([]models.DockerContainer, 0, len(updates))
uniqueImages := make(map[string]bool)
for _, update := range updates {
// Extract container info from update metadata
containerName := update.PackageName
var ports []models.DockerPort
if update.Metadata != nil {
if name, exists := update.Metadata["container_name"]; exists {
if nameStr, ok := name.(string); ok {
containerName = nameStr
}
}
// Extract port information from metadata
if portsData, exists := update.Metadata["ports"]; exists {
if portsArray, ok := portsData.([]interface{}); ok {
for _, portData := range portsArray {
if portMap, ok := portData.(map[string]interface{}); ok {
port := models.DockerPort{}
if cp, ok := portMap["container_port"].(float64); ok {
port.ContainerPort = int(cp)
}
if hp, ok := portMap["host_port"].(float64); ok {
hostPort := int(hp)
port.HostPort = &hostPort
}
if proto, ok := portMap["protocol"].(string); ok {
port.Protocol = proto
}
if ip, ok := portMap["host_ip"].(string); ok {
port.HostIP = ip
} else {
port.HostIP = "0.0.0.0"
}
ports = append(ports, port)
}
}
}
}
}
container := models.DockerContainer{
ID: update.ID.String(),
ContainerID: containerName,
Image: update.PackageName,
Tag: update.AvailableVersion,
AgentID: update.AgentID.String(),
AgentName: agentInfo.Hostname,
AgentHostname: agentInfo.Hostname,
Status: update.Status,
State: "", // Could be extracted from metadata if available
Ports: ports,
CreatedAt: update.LastDiscoveredAt,
UpdatedAt: update.LastUpdatedAt,
UpdateAvailable: update.Status != "installed",
CurrentVersion: update.CurrentVersion,
AvailableVersion: update.AvailableVersion,
}
imageKey := update.PackageName + ":" + update.AvailableVersion
uniqueImages[imageKey] = true
containers = append(containers, container)
}
response := models.DockerContainerListResponse{
Containers: containers,
Images: containers, // Alias for containers to match frontend expectation
TotalImages: len(uniqueImages),
Total: len(containers),
Page: page,
PageSize: pageSize,
TotalPages: (total + pageSize - 1) / pageSize,
}
c.JSON(http.StatusOK, response)
}
// GetStats returns Docker statistics across all agents
func (h *DockerHandler) GetStats(c *gin.Context) {
// Get all Docker updates
filters := &models.UpdateFilters{
PackageType: "docker_image",
Page: 1,
PageSize: 10000, // Get all for stats
}
updates, _, err := h.updateQueries.ListUpdatesFromState(filters)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch Docker stats"})
return
}
stats := models.DockerStats{
TotalContainers: len(updates),
TotalImages: 0,
UpdatesAvailable: 0,
PendingApproval: 0,
CriticalUpdates: 0,
}
// Calculate stats
uniqueImages := make(map[string]bool)
agentsWithContainers := make(map[uuid.UUID]bool)
for _, update := range updates {
// Count unique images
imageKey := update.PackageName + ":" + update.AvailableVersion
uniqueImages[imageKey] = true
// Count agents with containers
agentsWithContainers[update.AgentID] = true
// Count updates available
if update.Status != "installed" {
stats.UpdatesAvailable++
}
// Count pending approval
if update.Status == "pending_approval" {
stats.PendingApproval++
}
// Count critical updates
if update.Severity == "critical" {
stats.CriticalUpdates++
}
}
stats.TotalImages = len(uniqueImages)
stats.AgentsWithContainers = len(agentsWithContainers)
c.JSON(http.StatusOK, stats)
}
// ApproveUpdate approves a Docker image update
func (h *DockerHandler) ApproveUpdate(c *gin.Context) {
containerID := c.Param("container_id")
imageID := c.Param("image_id")
if containerID == "" || imageID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "container_id and image_id are required"})
return
}
// Parse the update ID from container_id (they're the same in our implementation)
updateID, err := uuid.Parse(containerID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid container ID"})
return
}
// Approve the update
if err := h.updateQueries.ApproveUpdate(updateID, "admin"); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to approve Docker update"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Docker update approved",
"container_id": containerID,
"image_id": imageID,
})
}
// RejectUpdate rejects a Docker image update
func (h *DockerHandler) RejectUpdate(c *gin.Context) {
containerID := c.Param("container_id")
imageID := c.Param("image_id")
if containerID == "" || imageID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "container_id and image_id are required"})
return
}
// Parse the update ID
updateID, err := uuid.Parse(containerID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid container ID"})
return
}
// Get the update details to find the agent ID and package name
update, err := h.updateQueries.GetUpdateByID(updateID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "update not found"})
return
}
// For now, we'll mark as rejected (this would need a proper reject method in queries)
if err := h.updateQueries.UpdatePackageStatus(update.AgentID, "docker", update.PackageName, "rejected", nil, nil); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reject Docker update"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Docker update rejected",
"container_id": containerID,
"image_id": imageID,
})
}
// InstallUpdate installs a Docker image update immediately
func (h *DockerHandler) InstallUpdate(c *gin.Context) {
containerID := c.Param("container_id")
imageID := c.Param("image_id")
if containerID == "" || imageID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "container_id and image_id are required"})
return
}
// Parse the update ID
updateID, err := uuid.Parse(containerID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid container ID"})
return
}
// Get the update details to find the agent ID
update, err := h.updateQueries.GetUpdateByID(updateID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "update not found"})
return
}
// Create a command for the agent to install the update
// This would trigger the agent to pull the new image
command := &models.AgentCommand{
ID: uuid.New(),
AgentID: update.AgentID,
CommandType: models.CommandTypeScanUpdates, // Reuse scan for Docker updates
Params: models.JSONB{
"package_type": "docker",
"package_name": update.PackageName,
"target_version": update.AvailableVersion,
"container_id": containerID,
},
Status: models.CommandStatusPending,
Source: models.CommandSourceManual, // User-initiated Docker update
}
if err := h.signAndCreateCommand(command); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create Docker update command"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Docker update command sent",
"container_id": containerID,
"image_id": imageID,
"command_id": command.ID,
})
}