- 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
316 lines
8.6 KiB
Go
316 lines
8.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// DockerReportsHandler handles Docker image reports from agents
|
|
type DockerReportsHandler struct {
|
|
dockerQueries *queries.DockerQueries
|
|
agentQueries *queries.AgentQueries
|
|
commandQueries *queries.CommandQueries
|
|
}
|
|
|
|
func NewDockerReportsHandler(dq *queries.DockerQueries, aq *queries.AgentQueries, cq *queries.CommandQueries) *DockerReportsHandler {
|
|
return &DockerReportsHandler{
|
|
dockerQueries: dq,
|
|
agentQueries: aq,
|
|
commandQueries: cq,
|
|
}
|
|
}
|
|
|
|
// ReportDockerImages handles Docker image reports from agents using event sourcing
|
|
func (h *DockerReportsHandler) ReportDockerImages(c *gin.Context) {
|
|
agentID := c.MustGet("agent_id").(uuid.UUID)
|
|
|
|
// Update last_seen timestamp
|
|
if err := h.agentQueries.UpdateAgentLastSeen(agentID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update last seen"})
|
|
return
|
|
}
|
|
|
|
var req models.DockerReportRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate command exists and belongs to agent
|
|
commandID, err := uuid.Parse(req.CommandID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid command ID format"})
|
|
return
|
|
}
|
|
|
|
command, err := h.commandQueries.GetCommandByID(commandID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "command not found"})
|
|
return
|
|
}
|
|
|
|
if command.AgentID != agentID {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "unauthorized command"})
|
|
return
|
|
}
|
|
|
|
// Convert Docker images to events
|
|
events := make([]models.StoredDockerImage, 0, len(req.Images))
|
|
for _, item := range req.Images {
|
|
event := models.StoredDockerImage{
|
|
ID: uuid.New(),
|
|
AgentID: agentID,
|
|
PackageType: "docker_image",
|
|
PackageName: item.ImageName + ":" + item.ImageTag,
|
|
CurrentVersion: item.ImageID,
|
|
AvailableVersion: item.LatestImageID,
|
|
Severity: item.Severity,
|
|
RepositorySource: item.RepositorySource,
|
|
Metadata: convertToJSONB(item.Metadata),
|
|
EventType: "discovered",
|
|
CreatedAt: req.Timestamp,
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
|
|
// Store events in batch with error isolation
|
|
if err := h.dockerQueries.CreateDockerEventsBatch(events); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to record docker image events"})
|
|
return
|
|
}
|
|
|
|
// Update command status to completed
|
|
result := models.JSONB{
|
|
"docker_images_count": len(req.Images),
|
|
"logged_at": time.Now(),
|
|
}
|
|
|
|
if err := h.commandQueries.MarkCommandCompleted(commandID, result); err != nil {
|
|
fmt.Printf("Warning: Failed to mark docker command %s as completed: %v\n", commandID, err)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "docker image events recorded",
|
|
"count": len(events),
|
|
"command_id": req.CommandID,
|
|
})
|
|
}
|
|
|
|
// GetAgentDockerImages retrieves Docker image updates for a specific agent
|
|
func (h *DockerReportsHandler) GetAgentDockerImages(c *gin.Context) {
|
|
agentIDStr := c.Param("agentId")
|
|
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"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 || pageSize > 100 {
|
|
pageSize = 50
|
|
}
|
|
|
|
offset := (page - 1) * pageSize
|
|
|
|
imageName := c.Query("image_name")
|
|
registry := c.Query("registry")
|
|
severity := c.Query("severity")
|
|
hasUpdatesStr := c.Query("has_updates")
|
|
|
|
// Build filter
|
|
filter := &models.DockerFilter{
|
|
AgentID: &agentID,
|
|
ImageName: nil,
|
|
Registry: nil,
|
|
Severity: nil,
|
|
HasUpdates: nil,
|
|
Limit: &pageSize,
|
|
Offset: &(offset),
|
|
}
|
|
|
|
if imageName != "" {
|
|
filter.ImageName = &imageName
|
|
}
|
|
if registry != "" {
|
|
filter.Registry = ®istry
|
|
}
|
|
if severity != "" {
|
|
filter.Severity = &severity
|
|
}
|
|
if hasUpdatesStr != "" {
|
|
hasUpdates := hasUpdatesStr == "true"
|
|
filter.HasUpdates = &hasUpdates
|
|
}
|
|
|
|
// Fetch Docker images
|
|
result, err := h.dockerQueries.GetDockerImages(filter)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch docker images"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// GetAgentDockerInfo retrieves detailed Docker information for an agent
|
|
func (h *DockerReportsHandler) GetAgentDockerInfo(c *gin.Context) {
|
|
agentIDStr := c.Param("agentId")
|
|
agentID, err := uuid.Parse(agentIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
|
|
return
|
|
}
|
|
|
|
// Get all Docker images for this agent
|
|
pageSize := 100
|
|
offset := 0
|
|
|
|
filter := &models.DockerFilter{
|
|
AgentID: &agentID,
|
|
Limit: &pageSize,
|
|
Offset: &offset,
|
|
}
|
|
|
|
result, err := h.dockerQueries.GetDockerImages(filter)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch docker images"})
|
|
return
|
|
}
|
|
|
|
// Convert to detailed format
|
|
dockerInfo := make([]models.DockerImageInfo, 0, len(result.Images))
|
|
for _, image := range result.Images {
|
|
info := models.DockerImageInfo{
|
|
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)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"docker_images": dockerInfo,
|
|
"is_live": isDockerRecentlyUpdated(result.Images),
|
|
"total": len(dockerInfo),
|
|
"updates_available": countUpdates(dockerInfo),
|
|
})
|
|
}
|
|
|
|
// 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
|
|
parts := strings.Split(imageName, ":")
|
|
if len(parts) > 1 {
|
|
return parts[len(parts)-1]
|
|
}
|
|
return "latest"
|
|
}
|
|
|
|
// Helper function to parse image size from metadata
|
|
func parseImageSize(metadata models.JSONB) int64 {
|
|
// Check if size is stored in metadata
|
|
if sizeStr, ok := metadata["size"].(string); ok {
|
|
if size, err := strconv.ParseInt(sizeStr, 10, 64); err == nil {
|
|
return size
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Helper function to extract labels from metadata
|
|
func extractLabels(metadata models.JSONB) map[string]string {
|
|
labels := make(map[string]string)
|
|
if labelsData, ok := metadata["labels"].(map[string]interface{}); ok {
|
|
for k, v := range labelsData {
|
|
if str, ok := v.(string); ok {
|
|
labels[k] = str
|
|
}
|
|
}
|
|
}
|
|
return labels
|
|
}
|
|
|
|
// Helper function to check if Docker images are recently updated
|
|
func isDockerRecentlyUpdated(images []models.StoredDockerImage) bool {
|
|
if len(images) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Check if any image was updated in the last 5 minutes
|
|
now := time.Now()
|
|
for _, image := range images {
|
|
if now.Sub(image.CreatedAt) < 5*time.Minute {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Helper function to count available updates
|
|
func countUpdates(images []models.DockerImageInfo) int {
|
|
count := 0
|
|
for _, image := range images {
|
|
if image.HasUpdate {
|
|
count++
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|